目录:
1. 类型擦除带来的约束与局限性
Java泛型(二):泛型和虚拟机(类型擦除)中已经详细说明了Java虚拟机(JVM,Java Virtual Machine)是如何应对泛型数据的——类型擦除机制。这种做法即兼容了泛型出现之前的JDK版本,同时也解决了 JVM 没有泛型类型的对象的问题。
但是,上帝给你打开了一扇门,肯定会给你关上另外一扇窗,没有哪种方法是十全十美的。类型擦除机制虽然很好的解决了 JVM 没有泛型类型对象的问题,但同时也引出了一些新的问题:
- 不能用类型参数代替基本类型
- 运行时类型查询(instanceof)对泛型类型并不适用
- getClass 方法总是返回原始类型
- 不能在静态域或方法中引用类型变量
- 不能实例化参数化类型的数组
- 不能实例化类型变量
// 以下是上述六点局限性的举例
Person<int> // error(第一点)
if (person instanceof Person<String>) // error(第二点)
Person<String> p = (Person<String>) person; // Warning-can only test that p is a Person
// 第三点
Person<String> stringPerson = ...
Person<Double> doublePerson = ...
stringPerson.getClass() == doublePerson.getClass() // result is true
// 第四点
private static T name // error
public static T getName() {...} // error
// 第五点
Person<String>[] person; // ok
new Person<String>[10]; // error
// 第六点
new T(); // error
new T[10] // error
T.class // error
前四点局限性并不难理解,类型擦除之后所有泛型类型都会转化为其所对应的原始类型,如参数 T 被转化为 Object 类型。
-
对于第一点局限性,由于 Object 类无法存储八大基本类型的变量(其子类就更加不可以了),所以不能用类型参数代替基本类型。如果的确有需求,可以使用基本类型对应的包装器类型(wrapper type)替换,如用 Integer 代替 int 类型。
-
对于第二点,由于类型擦除之后只剩下其原始类型,上述第二点对应的代码实际上仅仅测试了 person 对象是否是任意类型的一个 Person,并不能达到判断 person 对象是否为一个 Person< String > 类型对象的目的。为提醒这一风险,试图查询一个对象是否属于某个泛型类型时,倘若使用 instanceof 会得到一个编译器错误,如果使用强制类型转换会得到一个警告。
-
对于第三点,同样是由于类型擦除,getClass 方法总是返回原始类型,故上述第三点代码中得到的结构为 true (两次调用实际都返回了Person.class)。
-
对于第四点,由于静态域和方法是“属于”这个类的,对于一个泛型类,没有指定具体的类型参数之前,它是不可能知道类内部的类型变量具体是什么类型的。因此在静态域或方法中引用类型变量显然是不可行的。
2. 不能实例化参数化类型的数组
正如前面所说,Java 中不能实例化参数化类型的数组。
new Person<String>[10]; // error
可是,为什么不能这么做呢?在 Java 中,对于一个数组,它会记住自己存储的元素类型,如果试图存储其他类型的元素,就会抛出一个 Array-StoreException 异常:
String[] str = new String[10];
str[0] = 1; // error
不过对于泛型类型,类型擦除会使得这种机制失效。
Person<String>[] stringPersonArray = new Person<String>[10];
stringPersonArray[0] = new Person<Double>();
// 能够通过数组存储检査,不过仍会导致一个类型错误。
类型擦除之后,只剩下原始类型 Person,所以在虚拟机看来 Person< String > 和 Person< Double > 是“同一种类型”,所以上述代码中第二行的赋值操作能够通过数组存储检査,不过仍会导致一个类型错误。出于这个原因,Java 不允许创建参数化类型的数组。
当然,可以用一种取巧的方法实现“实例化参数化类型的数组”。可以声明通配类型的数组,然后进行类型转换:
Person<String>[] stringPersonArray = (Person<String>[]) new Person<?>[10];
当然,如果这么做的话,结果将是不安全的。如果在 stringPersonArray[0] 中存储一个Person< Double >, 然后对 table[0].getInformation() 调用一个 String 的方法,会得到一个 ClassCastException 异常。(table[0].getInformation() 是在Peson< T >类中自己定义的一个方法,会返回一个 T 类型的对象)
如果需要收集参数化类型对象,只有一种安全而有效的方法:使用ArrayList:
ArrayList<Person<String>> p = new ArrayList<>();
3. 不能实例化类型变量
在 Java 中,不能使用像 new T(…),newT[…] 或 T.class 这样的表达式中的类型变量。
3.1 不能使用 new T(…)
不能使用 new T(…) 实例化一个对象。类型擦除会将 T 改变成Object 类型,我们的本意肯定不希望调用 new Object()。例如,下面的构造函数就是非法的:
public Person() {
information = new T(); // error
}
在 JDK 1.8 之后,类似上述构造器问题的最好的解决办法是让调用者提供一个构造器表达式。例如:
Person<String> p = Person.makePerson(String::new);
public Person(T information) {
this.information = information;
}
public static <T> Person<T> makePerson(Supplier<T> constr) {
return new Person<>(constr.get());
}
makePerson 方法接收一个Supplier< T >,这是一个函数式接口, 表示一个无参数而且返回类型为T 的函数。
3.2 不能使用 new T[…]
就像不能 new T(…) 实例化一个对象一样,也不能像这样实例化一个数组。
new T[10]; // error
那么,当某个方法需要返回一个 T 类型的数组时,该怎么办呢?对于这种情况,可以让方法接收一个数组参数,例 ArrayList 中的其中一个 toArray 方法是这么实现的:
/**
* Returns an array containing all of the elements in this list in proper
* sequence (from first to last element); the runtime type of the returned
* array is that of the specified array. If the list fits in the
* specified array, it is returned therein. Otherwise, a new array is
* allocated with the runtime type of the specified array and the size of
* this list.
*
* <p>If the list fits in the specified array with room to spare
* (i.e., the array has more elements than the list), the element in
* the array immediately following the end of the collection is set to
* <tt>null</tt>. (This is useful in determining the length of the
* list <i>only</i> if the caller knows that the list does not contain
* any null elements.)
*
* @param a the array into which the elements of the list are to
* be stored, if it is big enough; otherwise, a new array of the
* same runtime type is allocated for this purpose.
* @return an array containing the elements of the list
* @throws ArrayStoreException if the runtime type of the specified array
* is not a supertype of the runtime type of every element in
* this list
* @throws NullPointerException if the specified array is null
*/
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
此方法需要接收一个数组参数。如果数组足够大,就使用这个数组;否则,用 a 的运行时类型构造一个足够大的新数组。
ArrayList<String> list = new ArrayList<>();
String[] strings = list.toArray(new String[10]);
3.3 不能使用 T.class
表达式 T.class 是不合法的,因为它会擦除为 Object.class。
继续借助 3.1 中的例子,除了让调用者提供一个构造器表达式之外,是否可以通过反射实现同样的功能呢?答案是可以的,但是遗憾的是,不能直接调用:
information = T.class.newInstance();
必须像下面这样设计以便得到一个 Class 对象:
public static <T> Person<T> makePerson(Class<T> c) {
try {
return new MyPerson<>(c.newInstance());
} catch (Exception e) {
return null;
}
}
此方法可以这样调用:
Person<String> p = Person.makePerson(String.class);
注意,Class 类本身就是泛型的。例如,String.class 是一个 Class< String > 的实例(事实上,它是唯一的实例)。