泛型(或泛型)在语法和预期用例(例如容器类)方面都与C ++中的模板表面上相似。 但是相似之处只是肤浅的-Java语言中的泛型几乎完全在编译器中实现,该编译器执行类型检查和类型推断,然后生成普通的非泛型字节码。 这种实现技术称为擦除 (编译器在其中使用通用类型信息来确保类型安全,但随后在生成字节码之前先将其擦除),这会带来一些令人惊讶的,有时甚至是令人困惑的后果。 尽管泛型是Java类中类型安全方面的一大进步,但学习使用泛型几乎肯定会在此过程中提供一些抓头(有时是诅咒)的机会。
注意:本文假定您熟悉JDK 5.0中泛型的基础知识。
泛型不是协变的
尽管您可能会认为将集合视为数组的抽象很有帮助,但它们具有一些集合没有的特殊属性。 Java语言中的数组是协变的-这意味着,如果Integer
扩展Number
(这样做),那么Integer
不仅是Number
,而且Integer[]
也是Number[]
,您可以自由传递或在需要Number[]
地方分配一个Integer[]
。 (更正式地讲,如果Number
是Integer
的超类型,则Number[]
是Integer[]
的超类型。)您可能会认为泛型类型也是如此List<Number>
是List<Integer>
的超类型。 List<Integer>
,您可以在期望List<Number>
地方传递List<Integer>
。 不幸的是,它不能那样工作。
事实证明,这样做有一个很好的理由:它会破坏安全性泛型应该提供的类型。 想象您可以将List<Integer>
分配给List<Number>
。 然后,以下代码将允许您将不是Integer
放入List<Integer>
:
List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));
因为ln
是List<Number>
,所以向其添加Float
似乎是完全合法的。 但是,如果ln
用li
别名,那么它将破坏li
定义中隐含的类型安全保证-它是整数列表,这就是为什么泛型不能协变的原因。
更多协方差问题
数组是协变的,而泛型不是的事实的另一个结果是,您不能实例化泛型类型的数组( new List<String>[3]
是非法的),除非type参数是无界通配符( new List<?>[3]
是合法的)。 让我们看看如果允许您声明泛型类型的数组会发生什么:
List<String>[] lsa = new List<String>[10]; // illegal
Object[] oa = lsa; // OK because List<String> is a subtype of Object
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[0] = li;
String s = lsa[0].get(0);
最后一行将引发ClassCastException
,因为您已经设法将List<Integer>
塞入应该为List<String>
。 因为数组协方差将允许您颠覆泛型的类型安全性,所以不允许实例化泛型类型的数组(类型参数为无界通配符的类型,如List<?>
除外)。
施工延误
由于进行擦除, List<Integer>
和List<String>
是同一类,并且编译器在编译List<V>
时仅生成一个类(与C ++不同)。 其结果是,编译器不知道是什么类型由代表V
编译时List<V>
类,所以你不能做某些事情有一个类型参数( V
在List<V>
类中)如果您知道要表示的类,则可以执行List<V>
定义。
因为运行时无法从List<Integer>
告诉List<String>
(在运行时,它们都只是List
),所以构造类型由通用类型参数标识的变量是有问题的。 运行时类型信息的缺乏给通用容器类和想要创建防御性副本的通用类带来了问题。
考虑泛型类Foo
:
class Foo<T> {
public void doSomething(T param) { ... }
}
假设doSomething()
方法要在输入时对param
参数进行防御性复制? 您没有太多选择。 您想像这样实现doSomething()
:
public void doSomething(T param) {
T copy = new T(param); // illegal
}
但是您不能使用类型参数来访问构造函数,因为在编译时,您不知道正在构造什么类,因此不知道哪些构造函数可用。 无法使用泛型来表达诸如“ T
必须具有复制构造函数”(甚至是无参数构造函数)的约束,因此无法访问由泛型类型参数表示的类的构造函数。
那clone()
呢? 假设Foo
被定义为使T
扩展Cloneable
:
class Foo<T extends Cloneable> {
public void doSomething(T param) {
T copy = (T) param.clone(); // illegal
}
}
不幸的是,您仍然不能调用param.clone()
。 为什么? 因为clone()
在Object
具有受保护的访问权限,并且要调用clone()
,您必须通过对覆盖了clone()
的类进行公开引用来对其进行调用。 但是不知道T
是否将public重新声明clone()
,因此也可以进行克隆。
构造通配符引用
好的,因此您不能将引用复制到在编译时其类完全未知的类型。 那通配符类型呢? 假设您要对类型为Set<?>
的参数进行防御性复制。 您知道Set
有一个复制构造函数。 您还被告知最好在不知道集合内容类型的情况下使用Set<?>
代替原始类型Set
,因为这种方法可能会发出较少的未经检查的转换警告。 所以你试试这个:
class Foo {
public void doSomething(Set<?> set) {
Set<?> copy = new HashSet<?>(set); // illegal
}
}
不幸的是,即使您知道存在这样的构造函数,也无法使用通配符类型参数调用通用构造函数。 但是,您可以这样做:
class Foo {
public void doSomething(Set<?> set) {
Set<?> copy = new HashSet<Object>(set);
}
}
这种构造不是最明显的,但是它是类型安全的,并且可以执行您认为new HashSet<?>(set)
会执行的操作。
构造数组
您将如何实现ArrayList<V>
? ArrayList
类应该管理V
的数组,因此您可能希望ArrayList<V>
的构造函数创建V
的数组:
class ArrayList<V> {
private V[] backingArray;
public ArrayList() {
backingArray = new V[DEFAULT_SIZE]; // illegal
}
}
但是此代码不起作用-您无法实例化类型参数表示的类型的数组。 编译器不知道V
真正代表什么类型,因此它无法实例化V
的数组。
Collections类使用一个丑陋的技巧来解决此问题,该问题会在编译Collections类时生成未经检查的转换警告。 ArrayList
的实际实现的构造函数如下所示:
class ArrayList<V> {
private V[] backingArray;
public ArrayList() {
backingArray = (V[]) new Object[DEFAULT_SIZE];
}
}
为什么这个代码不会产生ArrayStoreException
时backingArray
被访问? 毕竟,您不能将Object
数组分配给String
数组。 好吧,因为泛型是通过擦除实现的, backingArray
的类型实际上是Object[]
,因为Object
是V
的擦除。 这意味着该类实际上确实希望backingArray
是Object
的数组,但是编译器会进行额外的类型检查以确保其仅包含V
类型的对象。 因此,这种方法将工作,但它的丑陋,并没有真正的东西来模拟(泛型化的Collection框架的连作者这么说-请参阅相关主题 )。
另一种方法是将backingArray
声明为Object
的数组,并将其backingArray
为V[]
。 您仍然会得到未经检查的转换警告(就像您使用前面的方法一样),但是它将使一些未声明的假设(例如backingArray
不应逃避ArrayList
的实现的事实)变得更加清晰。
没走的路
最好的方法是将类文字( Foo.class
)传递到构造函数中,以便实现可以在运行时知道T
的值。 不采用这种方法的原因是为了向后兼容-那么新生成的集合类将与Collections框架的先前版本不兼容。
在这种方法下, ArrayList
如下:
public class ArrayList<V> implements List<V> {
private V[] backingArray;
private Class<V> elementType;
public ArrayList(Class<V> elementType) {
this.elementType = elementType;
backingArray = (V[]) Array.newInstance(elementType, DEFAULT_LENGTH);
}
}
可是等等! 调用Array.newInstance()
时,那里仍然有一个丑陋,未经检查的类型Array.newInstance()
。 为什么? 同样,这是因为向后兼容。 Array.newInstance()
的签名为:
public static Object newInstance(Class<?> componentType, int length)
而不是类型安全的:
public static<T> T[] newInstance(Class<T> componentType, int length)
为什么将Array
生成这种方式? 同样,令人沮丧的答案是保持向后兼容性。 要创建基本类型的数组,例如int[]
,请从适当的包装器类中调用带有TYPE
字段的Array.newInstance()
(对于int
,您将Integer.TYPE
作为类文字传递)。 使用Class<T>
而不是Class<?>
的参数生成Array.newInstance()
可以更安全地使用引用类型,但是将无法使用Array.newInstance()
创建一个实例。基本数组。 也许将来,将为引用类型提供newInstance()
的替代版本,因此您可以同时使用它。
您可能会看到一种模式在这里浮现-与泛型相关的许多问题或妥协并不是泛型本身的问题,而是保持与现有代码向后兼容的要求的副作用。
生成现有的类
转换现有的库类以使其与泛型一起正常工作并非易事,但通常情况下,向后兼容并不是免费的。 我已经讨论了两个示例,其中向后兼容性限制了类库的泛化。
如果不存在向后兼容性,则可能会以其他方式生成的另一种方法是Collections.toArray(Object[])
。 传递给toArray()
的数组有两个目的-如果集合足够小以适合提供的数组,则内容将简单地放在该数组中。 否则,将使用反射创建相同类型的新数组以接收结果。 如果从头开始重写Collections框架,则Collections.toArray()
的参数可能不是数组,而是类文字:
interface Collection<E> {
public T[] toArray(Class<T super E> elementClass);
}
因为Collections框架作为良好类设计的一个例子而被广泛模仿,所以值得注意的是其设计受向后兼容性约束的领域,因此不会盲目地模仿那些方面。
最初,通常使混淆的Collections API的一个元素是containsAll()
, removeAll()
和retainAll()
的签名。 您可能期望remove()
和removeAll()
的签名为:
interface Collection<E> {
public boolean remove(E e); // not really
public void removeAll(Collection<? extends E> c); // not really
}
但这实际上是:
interface Collection<E> {
public boolean remove(Object o);
public void removeAll(Collection<?> c);
}
为什么是这样? 同样,答案在于向后兼容性。 x.remove(o)
的接口协定表示“如果x
包含o
,请将其删除;否则,什么也不做。” 如果x
是泛型集合,则o
不必与x
的type参数类型兼容。 如果将removeAll()
泛型化为仅在其参数为类型兼容的情况下才可调用( Collection<? extends E>
),则在泛型变为非法之前合法的某些代码序列将如下所示:
// a collection of Integers
Collection c = new HashSet();
// a collection of Objects
Collection r = new HashSet();
c.removeAll(r);
如果上面的片段以明显的方式生成(将c
设为Collection<Integer>
,将r
设为Collection<Object>
),则如果removeAll()
的签名要求其参数为Collection<? extends E>
则上面的代码将无法编译Collection<? extends E>
Collection<? extends E>
,而不是无操作。 泛化类库的主要目标之一是不破坏或更改现有代码的语义,因此必须使用比它们弱的类型约束来定义remove()
, removeAll()
, retainAll()
和containsAll()
可能是从头开始为泛型重新设计了它们。
在泛型之前设计的现有类的语义可能会抵制“显而易见的”生成方法。 在这些情况下,您可能会做出类似此处所述的折中,但是从头开始设计新的通用类时,了解Java类库中的哪些惯用语是向后兼容的结果非常有价值,因此不会对它们进行不适当的仿真。
擦除的含义
因为泛型几乎完全在Java编译器中实现,而不是在运行时中实现,所以在生成字节码时,几乎所有与泛型类型有关的类型信息都已被“擦除”。 换句话说,在检查了程序的类型安全性之后,编译器生成的代码几乎与没有泛型,强制类型转换之类的所有代码相同。 与C ++不同, List<Integer>
和List<String>
是同一类(尽管它们是不同的类型,但List<?>
两个子类型都是一个区别-在JDK 5.0中,此区别比该语言的先前版本更重要) 。
擦除的含义之一是,一个类不能同时实现Comparable<String>
和Comparable<Number>
,因为它们实际上都是同一接口,并指定相同的compareTo()
方法。 想要声明一个与String
和Number
DecimalString
比的DecimalString
类似乎是明智的,但是对于Java编译器,您将尝试两次声明相同的方法:
public class DecimalString implements Comparable<Number>, Comparable<String> { ... } // nope
删除的另一个结果是,将强制类型转换或instanceof
与泛型类型参数一起使用没有任何意义。 以下代码根本不会提高代码的类型安全性:
public <T> T naiveCast(T t, Object o) { return (T) o; }
编译器只会发出未检查的转换警告,因为它不知道强制转换是否安全。 该naiveCast()
方法不会做实际上在所有的任何铸造- T
将简单地用其删除(替换Object
),以及传入的对象将转换为Object
-不想要的结果。
擦除还负责上述构造问题-您不能创建泛型类型的对象,因为编译器不知道要调用哪个构造函数。 如果您的泛型类需要构造其类型由泛型类型参数指定的对象,则其构造函数应采用类文字( Foo.class
)并将其存储,以便可以使用反射创建实例。
摘要
泛型在Java语言的类型安全方面迈出了一大步,但是泛型工具的设计以及类库的泛化并非没有妥协。 扩展虚拟机指令集以支持泛型被认为是不可接受的,因为这可能使JVM供应商难以更新其JVM。 因此,采用了可以完全在编译器中实现的擦除方法。 类似地,在泛化Java类库时,保持向后兼容性的需求对如何泛化类库提出了许多限制,从而导致了一些令人困惑和令人沮丧的构造(如Array.newInstance()
)。 这些不是泛型本身的问题,而是语言发展和兼容性的实用性。 但是它们会使仿制药在学习和使用时更加混乱和令人沮丧。
翻译自: https://www.ibm.com/developerworks/java/library/j-jtp01255/index.html