varargs_and_heap_pollution

第二十章 可变参数(Varargs)和堆污染(Heap Pollution)

1 简介

Java语言支持可变参数函数调用,即函数调用者可使用任意个数的参数调用同一个函数。当函数被调用时,这些参数可以当作是一个参数数组访问。如下例所示:在VarargsExample类中定义了print静态方法,该方法可接收任意个String类型的参数。实际上,在print()方法中,参数values可当作一个字符串数组处理,这是由Java编译器和Java虚拟机共同完成的。所以,在main方法中调用print方法时,可传入0个、1个或者3个String类型的参数。

public class VarargsExample {
    public static void print (String... values) {  
        for (String oneValue: values)
            System.out.println(oneValue);
    }
    
    public static void main(String[] args) {
        print();  // 可传入0个参数
        print("one");  // 可传入1个参数
        print("one", "two", "three"); // 可传入3个参数
    }
}

Varargs的使用方法很简单,但是,Varargs需要遵守两个规则:

  1. 每个方法最多只能有一个可变参数。
  2. 可变参数只能放在最后一个参数的位置上。这是因为,在函数调用的过程中,最后的参数是放在栈顶的,所以,Java虚拟机能够计算可变参数的个数,进而为其创建数组对象。更详细的Java虚拟机函数调用的处理细节,请查阅相关章节(稍后推出)。

类似的,可变参数函数也可以重载和覆盖,下面是一个函数重载和覆盖的例子。Base类实现两个print方法,一个接收字符串类型的变长参数,另一个接收一个整型参数。在子类Derived中,覆盖了接收变长参数的print方法。在main方法中,第一个print()方法调用的是基类Base的、接收整形对象的print()方法,而第二个print()方法调用的是子类Derived的print()方法。

public class Base {
    public void print (String... values) {  
        for (String oneValue: values)
            System.out.println("Base " + oneValue);
    }

    public void print(Integer i) {
        System.out.println("Base " + i);
    }
}

public class Derived extends Base {
    @Override
    public void print (String... values) {  
        for (String oneValue: values)
            System.out.println("Derived " + oneValue);
    }

    public static void main(String[] args) {
        Derived d = new Derived();
        d.print(Integer.valueOf(1)); // 打印 Base 1
        d.print("one"); // 打印 Derived one
        // 打印
        // Derived one
        // Derived two
        // Derived three
        d.print("one", "two", "three"); 
    }
}

2 堆污染(Heap Pollution)

简单的说,堆污染是指Java堆中的数据遭到了破环。Java是一种强类型语言,每个对象都有其自身的类型,并且引用只能指向其类型的对象或者其子类的对象。Java也不支持指针操作或者内存操作,那么,Java如何可能发生堆污染呢?

堆污染是由于类型擦除(Type Erasure)而产生的。如下面的代码所示:listOfDouble是一个包含双精度浮点数的链表。经编译类型擦除后,它的类型变为List。所以它能够赋值给aList,进而赋值给listOfInteger。但是,当使用listOfDouble时,代码还是认为链表中的数据是Double类型的。当运行时,Java虚拟机会检测到链表中真实的类型却是Integer,所以,会抛出ClassCastException异常。

import java.util.List;
import java.util.ArrayList;

public class HeapPollutionExample {
    public static void main(String[] args) {
        List<Double> listOfDouble = new ArrayList<Double>();
        List aList = listOfDouble;
        List<Integer> listOfInteger = aList;
        aList.add(Integer.valueOf(1));
        System.out.println(listOfDouble.get(0) + 1); // 抛出 ClassCastException
    }
}

在编译上述代码时,编译器会打印如下警告,提示开发人员该代码包含了unchecked或者不安全的操作。

Note: HeapPollutionExample.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details.

类似的情况也可能发生在可变参数上。例如:VarargsExample.print()方法接收可变参数values,每个元素是List<String>。可是经过类型擦除,每个元素的类型变成了List。因此,编译器也会报告上述的警告。

import java.util.List;
import java.util.Arrays;
public class VarargsExample {
    public static void print (List<String>... values) {  
        System.out.println(values.getClass().getName()); // 打印 [LJava.util.List;
    }
    
    public static void main(String[] args) {
        print(Arrays.asList("one", "two", "three"));
    }
}

3 @SafeVarargs标注

有时,这种警告是很难避免的。因为,开发人员使用的代码库(或者第三方库)定义的函数参数可能是可变参数。所以,为了避免编译器反复报告这个警告,Java语言提供了@SafeVarargs标注,表示方法内的代码是可信任的。当编译器检查到了这个标注后,就不会报告上述的警告了。

值得注意的是,@SafeVarargs并不能避免类型异常(或者堆污染)。该标注仅仅用于告知编译器无需再向开发人员报告此问题。

import java.util.List;
import java.util.Arrays;
public class VarargsExample {
    @SafeVarargs
    public static void print (List<String>... values) {  
        System.out.println(values.getClass().getName());
    }
}

3.1 Reifiable Type

这里有一个概念需要简单的讲解一下。Java语言将那些经过类型擦除(Type Erasure)过程之后仍然能全部保留类型信息的类型称为Reifiable Type。Reifiable Type包括基本数据类型(Primitive Data Type),非泛型类或者接口,那些元素是Reifiable Type的数组。总而言之,在大多数情况下,基本数据类型和非泛型类都是Reifiable Type。

当可变参数的类型是非Reifiable Type(Non-Reifiable Type)时,即经类型擦除后,该类型会丢失部分类信息时,该函数可能会引起堆污染(Heap Pollution),编译器会报告unchecked warnings。

3.2 @SafeVarargs VS @SuppressWarnings("unchecked")

使用@SuppressWarnings("unchecked")也能起到类似的效果(抑制编译器报告unchecked警告)。例如,Java编译器不会对下面的代码报告任何警告。可是,@SafeVarargs和@SuppressWarnings是不同的。@SuppressWarnings的生存期是在SOURCE(@Retention(value=SOURCE)),所以,@SuppressWarnings只在编译期可见。其意义是开发人员明确指出这行代码是正确的,无需编译器再次提醒。而@SafeVarargs的生存期是RUNTIME(@Retention(value=RUNTIME)),即@SafeVarargs存在于编译期和运行时。@SafeVarargs更多的是一种函数契约(Method Contract),即:函数的开发人员向函数调用者保证该函数不会破坏堆。有关标注(Annotation)的生存期或者使用方法可参考第十五章标注

import java.util.List;
import java.util.Set;
public class VarargsExample {
    @SafeVarargs
    public static void print (Set<String>... values) {
    }

    @SuppressWarnings("unchecked")
    public static void print (List<String>... values) {
    }
}

6 结语

本章介绍了可变参数的用法。因为类型擦除的原因,有时编译器无法检查出函数内的代码是否安全。因此,在这种情况下,编译器会报告unchecked警告。如果开发人员认为代码是正确的,可以使用@SafeVarargs来抑制编译器的警告。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.