JDK 5.0(也称为Java 5.0或“ Tiger”)对Java语言进行了一些重大更改。 最重要的变化是增加了泛型类型 (泛型)-支持使用实例化时指定的抽象类型参数定义类。 泛型具有增加大型程序的类型安全性和可维护性的巨大潜力。
泛型与JDK 5.0中的其他一些新语言功能协同交互,包括增强的for
循环(有时称为foreach或for / in循环),枚举和自动装箱。
本教程说明了将泛型添加到Java语言的动机,详细介绍了泛型类型的语法和语义,并提供了在类中使用泛型的介绍。
本教程适用于希望学习泛型新语言支持的中级和高级Java开发人员。 假定读者熟悉用Java语言开发接口和类以及基本的面向对象设计技术。
泛型语言功能仅在JDK 5.0和更高版本中可用。 如果您正在开发基于早期JDK版本的软件,则在迁移到JDK 5.0或更高版本之前,不能在代码中使用泛型功能。
您必须具有一个JDK 5.0开发环境,才能使用泛型。 您可以从Sun Microsystems网站免费下载JDK 5.0 。
泛型或泛型是对Java语言的类型系统的扩展,以支持创建可以通过类型进行参数化的类。 您可以将类型参数视为使用参数化类型时要指定的类型的占位符,就像形式化方法参数是在运行时传递的值的占位符一样。
可以在集合框架中看到仿制药的动机。 例如, Map
类允许您将任何类的条目添加到Map
,即使在给定的map中仅存储某种类型的对象(例如String
是很常见的用例。
因为Map.get()
被定义为返回Object
,所以通常必须将Map.get()
的结果Map.get()
转换为期望的类型,如以下代码所示:
Map m = new HashMap();
m.put("key", "blarg");
String s = (String) m.get("key");
要使程序编译,必须将get()
的结果强制转换为String
,并希望结果确实是String
。 但是可能有人在此映射中存储了String
以外的内容,在这种情况下,上面的代码将抛出ClassCastException
。
理想情况下,您想捕捉一下m
是一个将String
键映射到String
值的Map
的概念。 这样一来,您就可以消除代码中的强制类型转换,同时获得一层额外的类型检查,这可以防止某人在集合中存储错误类型的键或值。 这就是泛型为您所做的。
向Java语言添加泛型是一项重大改进。 不仅对语言,类型系统和编译器进行了重大更改以支持通用类型,而且还对类库进行了全面改进,以便使许多重要的类(例如Collections框架)都成为通用类。 这带来了许多好处:
输入安全性。 泛型的主要目标是提高Java程序的类型安全性。 通过了解使用泛型类型定义的变量的类型范围,编译器可以在更大程度上验证类型假设。 没有泛型,这些假设仅存在于程序员的头脑中(或者,如果幸运的话,在代码注释中)。
Java程序中一种流行的技术是定义其元素或键是通用类型的集合,例如“ String
列表”或“从String
到String
映射”。 通过捕获变量声明中的其他类型信息,泛型使编译器可以强制执行这些其他类型约束。 现在可以在编译时捕获类型错误,而不是在运行时显示为ClassCastException
。 将类型检查从运行时移到编译时,可以帮助您更轻松地发现错误并提高程序的可靠性。
消除演员阵容。 泛型的一个附带好处是,您可以从源代码中消除许多类型转换。 这使代码更具可读性,并减少了出错的机会。
尽管减少的转换需求减少了使用泛型类的代码的冗长性,但声明泛型类型的变量会相应地增加冗长性。 比较以下两个代码示例。
此代码不使用泛型:
List li = new ArrayList();
li.put(new Integer(3));
Integer i = (Integer) li.get(0);
此代码使用泛型:
List li = new ArrayList();
li.put(new Integer(3));
Integer i = li.get(0);
在简单程序中只使用一次泛型类型的变量不会导致冗长的净节省。 但是对于许多使用通用类型变量的大型程序,节省下来的费用开始增加了。
潜在的性能提升。 泛型为进一步优化创造了可能性。 在泛型的初始实现中,编译器将相同的强制类型转换插入生成的字节码中,程序员在没有泛型的情况下会指定这些类型。 但是,更多类型信息可供编译器使用这一事实允许在将来的JVM版本中进行优化。
由于实现了泛型的方式,(几乎)不需要更改JVM或类文件即可支持泛型类型。 所有工作都在编译器中完成,该编译器生成的代码类似于您在没有泛型的情况下编写的代码(带有强制转换),并且对其类型安全性更有信心。
泛型类型的许多最佳示例来自Collections框架,因为泛型使您可以对存储在集合中的元素指定类型约束。 考虑一下使用Map
类的示例,该类涉及某种程度的乐观,即Map.get()
返回的结果实际上将是String
:
Map m = new HashMap();
m.put("key", "blarg");
String s = (String) m.get("key");
如果有人在地图中放置了String
以外的内容,则上述代码将引发ClassCastException
。 泛型允许您表达类型约束,即m
是将String
键映射到String
值的Map
。 这样一来,您就可以消除代码中的强制类型转换,同时获得另一层类型检查,以防止某人在集合中存储错误类型的键或值。
以下代码示例显示了JDK 5.0中Collections框架中Map
接口定义的一部分:
public interface Map {
public void put(K key, V value);
public V get(K key);
}
请注意该接口的两个附加功能:
K
和V
的规范,表示在声明Map
类型的变量时将指定的类型的占位符 get()
, put()
和其他方法的方法签名中使用K
和V
为了获得使用泛型的好处,在定义或实例化Map
类型的变量时,必须提供K
和V
具体值。 您可以通过相对简单的方式执行此操作:
Map m = new HashMap();
m.put("key", "blarg");
String s = m.get("key");
使用通用版本的Map
,您不再需要将Map.get()
的结果Map.get()
为String
,因为编译器知道get()
将返回String
。
您不会在使用泛型的版本中保存任何击键; 实际上,它比使用强制转换的版本需要更多的输入。 通过使用泛型类型可以节省更多的类型安全性。 因为编译器对将放入Map
的键和值的类型有更多的了解,所以类型检查从执行时间转移到编译时间,从而提高了可靠性并加快了开发速度。
向Java语言中添加泛型的一个重要目标是保持向后兼容性。 尽管已泛化了JDK 5.0中标准类库中的许多类,例如Collections框架,但是使用Collections类(例如HashMap
和ArrayList
现有代码将在JDK 5.0中未经修改地继续工作。 当然,不利用泛型的现有代码不会获得泛型的其他类型安全优势。
定义通用类或声明通用类的变量时,可以使用尖括号指定形式类型参数 。 形式和实际类型参数之间的关系类似于形式和实际方法参数之间的关系,不同之处在于类型参数表示类型,而不是值。
泛型类中的类型参数几乎可以在任何可以使用类名的地方使用。 例如,以下是java.util.Map
接口的定义的摘录:
public interface Map {
public void put(K key, V value);
public V get(K key);
}
Map
接口有两种类型的参数化-键类型K
和值类型V
现在,将(没有泛型)接受或返回Object
在其签名中使用K
或V
,从而指示了Map
规范基础上的其他类型约束。
在声明或实例化通用类型的对象时,必须指定类型参数的值:
Map map = new HashMap();
请注意,在此示例中,您必须指定两次类型参数-一次声明变量map
的类型,第二次选择HashMap
类的参数化,以便实例化正确类型的实例。
当编译器遇到Map
类型的变量时,它知道K
和V
现在绑定到String
,因此它知道在该变量上Map.get()
的结果将具有String
类型。
除异常类型,枚举或匿名内部类以外的任何类都可以具有类型参数。
建议的命名约定是将大写单字母名称用作类型参数。 这与C ++约定(请参阅附录A:与C ++模板的比较 )不同,并且反映了大多数泛型类将具有少量类型参数的假设。 对于常见的通用模式,建议的名称为:
List
, Set
的内容或Map
的值 与泛型类型混淆的一个常见原因是假设它们像数组一样是协变的。 他们不是。 这是说List
不是 List
的超类型的一种奇特的说法。
如果A扩展了B,则A的数组也将是B的数组,并且您可以在需要B[]
地方自由提供A[]
:
Integer[] intArray = new Integer[10];
Number[] numberArray = intArray;
上面的代码有效,因为Integer
是 Number
,并且Integer
数组是 Number
数组。 但是,对于泛型则不是这样。 以下代码无效:
List intList = new ArrayList();
List numberList = intList; // invalid
最初,大多数Java程序员都发现这种缺乏协方差的烦人甚至是“破损”的东西,但是有充分的理由。 如果您可以将List
分配给List
,则以下代码将违反泛型应该提供的类型安全性:
List intList = new ArrayList();
List numberList = intList; // invalid
numberList.add(new Float(3.1415));
因为intList
和numberList
是别名,所以上面的代码(如果允许)将允许您将Integers
放入intList
。 但是,有一种方法可以编写可以接受通用类型系列的灵活方法,如您在下一个面板中所看到的。
假设您有以下方法:
void printList(List l) {
for (Object o : l)
System.out.println(o);
}
上面的代码在JDK 5.0上编译,但是如果尝试使用List
调用它,则会收到警告。 发生警告的原因是,您将泛型类型( List
)传递给仅承诺将其视为List
(所谓的raw type )的方法,这可能会损害使用泛型的类型安全性。
如果您尝试编写如下方法,该怎么办:
void printList(List
它仍然不会编译,因为List
不是 List
(如上一节所述, 泛型类型不是协变的 )。 确实很烦人-现在您的通用版本比原始的非通用版本有用!
解决方法是使用通配符类型 :
void printList(List> l) {
for (Object o : l)
System.out.println(o);
}
上面代码中的问号是类型通配符。 它的发音为“未知”(如“未知列表”)。 List>
是任何常规List
的超类型,因此您可以将List
, List
或List
自由地传递给>>
printList()
。
上一节“ 类型通配符 ”介绍了类型通配符,它使您可以声明类型为List>
变量。 这样的List
可以做什么? 非常方便的是,您可以从中检索元素,但不能向其中添加元素。 这样做的原因不是编译器知道哪些方法修改了列表,哪些没有。 正是(大多数)变异方法比非变异方法需要更多的类型信息。 以下代码可以正常工作:
List li = new ArrayList();
li.add(new Integer(42));
List> lu = li;
System.out.println(lu.get(0));
为什么这样做? 编译器不知道lu
的List
的type参数的值。 但是,编译器足够聪明,可以执行某种类型推断。 在这种情况下,它推断未知类型参数必须扩展Object
。 (这个特殊的推论并不是很大的飞跃,但是编译器可以做出一些令人印象深刻的类型推论,正如您稍后将看到的那样(在List.get()
details中 )。因此,它可以让您调用List.get()
并将返回类型推论为Object
。
另一方面,以下代码不起作用:
List li = new ArrayList();
li.add(new Integer(42));
List> lu = li;
lu.add(new Integer(43)); // error
在这种情况下,编译器无法对lu
的List
的类型参数进行足够强的推断,以确保将Integer
传递给List.add()
是类型安全的。 因此,编译器将不允许您执行此操作。
以免使您仍然认为编译器对哪些方法更改列表的内容以及哪些方法不更改有一些概念,请注意,以下代码将起作用,因为它不依赖于编译器是否必须了解有关清单类型参数的任何信息。 lu
:
List li = new ArrayList();
li.add(new Integer(42));
List> lu = li;
lu.clear();
您已经看到(在Type参数中 ),可以通过在其定义中添加形式类型参数列表来使类通用。 无论方法定义的类是否是通用的,方法都可以通用。
通用类跨多个方法签名强制执行类型约束。 在List
,类型参数V
出现在get()
, add()
, contains()
等的签名中。当您创建Map
类型的变量时,您将声明跨类型约束方法。 您传递给put()
的值将与get()
返回的类型相同。
同样,在声明泛型方法时,通常这样做是因为要在该方法的多个参数之间声明类型约束。 例如,根据以下代码中ifThenElse()
方法的第一个参数的布尔值,它将返回第二个或第三个参数:
public T ifThenElse(boolean b, T first, T second) {
return b ? first : second;
}
注意,您可以调用ifThenElse()
而不用明确告诉编译器您想要T
值。 不需要明确告诉编译器T的值是什么。 它只知道它们必须全部相同。 编译器允许您调用以下代码,因为编译器可以使用类型推断来推断用String
代替T
满足所有类型约束:
String s = ifThenElse(b, "a", "b");
同样,您可以致电:
Integer i = ifThenElse(b, new Integer(1), new Integer(2));
但是,编译器不允许以下代码,因为没有任何类型可以满足所需的类型约束:
String s = ifThenElse(b, "pi", new Float(3.14));
您为什么选择使用通用方法,而不是将T
类型添加到类定义中? 在至少两种情况下,这是有道理的:
T
的类型约束确实是方法的局部约束时,这意味着不存在将相同类型T
用于同一类的另一个方法签名的约束。 通过将通用方法的类型参数设置为方法的局部方法,可以简化封闭类的签名。 在上一节的示例Generic Methods中 ,类型参数V
是无约束或无界的类型。 有时您需要在类型参数上指定其他约束,而仍不能完全指定它。
考虑示例Matrix
类,该示例使用由Number
类限制的类型参数V
:
public class Matrix { ... }
编译器将允许您创建Matrix
或Matrix
类型的变量,但是如果您尝试定义Matrix
类型的变量,则会发出错误。 类型参数V
据说受 Number
。 在没有类型限制的情况下,假定类型参数受Object
。 这就是为什么上一节中的示例Generic方法允许List.get()
在List>
上调用时返回Object
,即使编译器不知道类型参数V
的类型。
至此,您已经准备好编写一个简单的泛型类。 到目前为止,泛型类的最常见用例是容器类(例如Collections框架)或值持有者类(例如WeakReference
或ThreadLocal
。 让我们写一个类似于List
的类,它充当容器,使用泛型来表达Lhist
所有元素都具有相同类型的约束。 为了简化实现, Lhist
使用固定大小的数组来存储值,并且不接受空值。
Lhist
类将具有一个类型参数V
,这是Lhist
中值的Lhist
,并将具有以下方法:
public class Lhist {
public Lhist(int capacity) { ... }
public int size() { ... }
public void add(V value) { ... }
public void remove(V value) { ... }
public V get(int index) { ... }
}
要实例化Lhist
,您只需在声明一个时指定type参数,并指定所需的容量:
Lhist stringList = new Lhist(10);
在实现Lhist
类时,您将遇到的第一个绊脚石是构造函数。 您想这样实现:
public class Lhist {
private V[] array;
public Lhist(int capacity) {
array = new V[capacity]; // illegal
}
}
这似乎是分配后备数组的自然方法,但是很遗憾,您不能这样做。 原因很复杂; 稍后您将在“血腥细节”中涉及擦除的主题时,将对它们有所了解 。 做你想要的事情的方式是丑陋的和违反直觉的。 构造函数的一种可能实现是此方法(它使用Collections类采用的方法):
public class Lhist {
private V[] array;
public Lhist(int capacity) {
array = (V[]) new Object[capacity];
}
}
或者,您可以使用反射实例化数组。 但是,这样做需要将附加参数传递给构造函数- 类文字 ,例如Foo.class
。 类字面量也将在后面的Class
实施Lhist
的其余方法要容易Lhist
。 这是Lhist
类的完整实现:
public class Lhist {
private V[] array;
private int size;
public Lhist(int capacity) {
array = (V[]) new Object[capacity];
}
public void add(V value) {
if (size == array.length)
throw new IndexOutOfBoundsException(Integer.toString(size));
else if (value == null)
throw new NullPointerException();
array[size++] = value;
}
public void remove(V value) {
int removalCount = 0;
for (int i=0; i 0) {
array[i-removalCount] = array[i];
array[i] = null;
}
}
size -= removalCount;
}
public int size() { return size; }
public V get(int i) {
if (i >= size)
throw new IndexOutOfBoundsException(Integer.toString(i));
return array[i];
}
}
请注意,您使用的形式类型参数V
中,将接受或返回方法V
,但是你没有方法或字段什么任何想法V
了,因为它是不知道的通用代码。
Lhist
类 使用Lhist
类很容易。 要定义整数Lhist
,只需在声明和构造函数中为type参数提供实际值:
Lhist li = new Lhist(30);
编译器知道li.get()
返回的任何值都将是Integer
类型,并且将强制传递给li.add()
或li.remove()
都将是Integer
。 除了构造函数的怪异实现方式之外,您无需做任何特别的事情即可使Lhist
成为通用类。
到目前为止,Java类库中最大的泛型支持使用者是Collections框架。 正如容器类是C ++中模板的主要动机(请参阅附录A:与C ++模板的比较 )(尽管它们随后已被使用得多)一样,提高Collection类的类型安全性是C语言中泛型的主要动机。 Java语言。 Collections类还充当泛型使用方式的模型,因为它们演示了泛型类型的几乎所有标准技巧和习惯用法。
所有标准收集接口均已生成-Collection Collection
, List
, Set
和Map
。 同样,集合接口的实现使用相同的类型参数生成,因此HashMap
实现Map
等。
Collections类还使用泛型的许多“技巧”和习惯用法,例如上限和下限通配符。 例如,在接口Collection
, addAll
方法的定义如下:
interface Collection {
boolean addAll(Collection extends V>);
}
此定义将通配符类型参数与有界类型参数结合在一起,使您可以将Collection
的内容添加到Collection
。
如果类库将addAll()
定义为采用Collection
,则将无法将Collection
的内容添加到Collection
。 与其将addAll()
的参数限制为包含与要添加的集合完全相同的类型的集合,还可以使传递给addAll()
的集合中的元素更合适,使其更合理。添加到您的收藏中。 有界类型使您可以这样做,而有界通配符的使用使您摆脱了组成另一个在其他任何地方都不会使用的占位符名称的要求。
作为生成类可以如何更改其语义的微妙示例(如果您不小心的话),请注意Collection.removeAll()
的参数类型为Collection>
,而不是Collection extends V>
Collection extends V>
。 这是因为可以将混合类型的集合传递给removeAll()
,并且更严格地定义removeAll
将会改变方法的语义和实用性。 这说明生成一个现有类比定义一个新的泛型类要困难得多,因为您必须小心不要更改该类的语义或破坏现有的非泛型代码。
除了Collections类之外,Java类库中的其他几个类还充当值的容器。 这些类包括WeakReference
, SoftReference
和ThreadLocal
。 它们都针对容器的值类型进行了泛化,因此WeakReference
是对T类型的对象的弱引用,而ThreadLocal
是对ThreadLocal
类型的线程局部变量的句柄。
泛型类型最常见,最直接的用法是容器类,例如Collections类或引用类(例如WeakReference
。) Collection
中的type参数的含义在直观上很明显- “所有都是V类型的值的集合。” 同样, ThreadLocal
有一个明显的解释-“类型为T的线程局部变量”。 但是,泛型规范中的任何内容都与遏制无关。
在诸如Comparable
或Class
类的类型中,类型参数的含义更为细微。 有时,例如在Class
,类型变量主要用于帮助编译器进行类型推断。 有时,就像在神秘的Enum
,可以在类层次结构的结构上施加约束。
Comparable
接口已被泛化,以便实现Comparable
的对象声明可以与之进行比较的类型。 (通常,这是对象本身的类型,但有时可能是超类。)
public interface Comparable {
public boolean compareTo(T other);
}
因此, Comparable
接口包含类型参数T
,这是实现Comparable
的类可以与之比较的对象的类型。 这意味着,如果要定义实现Comparable
的类(例如String
,则不仅必须声明该类支持比较,而且还必须声明可比较的对象,通常它本身就是:
public class String implements Comparable { ... }
现在考虑二进制max()
方法的实现。 您想要采用两个相同类型的参数,两个参数必须是Comparable
,并且必须彼此可Comparable
。 幸运的是,如果您使用泛型方法和有界类型参数,那将相对简单:
public static > T max(T t1, T t2) {
if (t1.compareTo(t2) > 0)
return t1;
else
return t2;
}
在这种情况下,您将定义一个泛型方法,该方法在类型T
泛化,并且您必须对其进行扩展(实现) Comparable
。 这两个参数都必须是T
类型,这意味着它们是同一类型,支持比较并且可以相互比较。 简单!
更好的是,编译器将在调用max()
时使用类型推断来确定T
含义。 因此,以下调用有效,而无需完全指定T
:
String s = max("moo", "bark");
编译器将确定T
的预期值为String
,并将进行相应的编译和类型检查。 但是,如果您尝试使用未实现Comparable
的类X
参数调用max()
,则编译器将不允许这样做。
Class
已经泛化了,但是首先让许多人感到困惑。 Class
类型参数T
的含义是什么? 事实证明,这是被引用的类实例。 怎么可能? 那不是通函吗? 即使没有,为什么还要这样定义呢?
在以前的JDK中, Class.newInstance()
方法的定义返回Object
,然后您可能会将其转换为另一种类型:
class Class {
Object newInstance();
}
但是,使用泛型,您可以使用更特定的返回类型定义Class.newInstance()
方法:
class Class {
T newInstance();
}
如何创建Class
类型的实例? 与非泛型代码一样,您有两种方法:调用方法Class.forName()
或使用类文字X.class
。 Class.forName()
定义为返回Class>
。 另一方面,类文字X.class
被定义为具有Class
类型,因此String.class
具有Class
类型。
将Foo.class
设置为Class
类型有什么好处? 最大的好处是,它可以通过类型推断的魔力提高使用反射的代码的类型安全性。 另外,您无需将Foo.class.newInstance()
为Foo
。
考虑一种从数据库中检索一组对象并返回JavaBeans对象集合的方法。 您可以通过反射实例化和初始化创建的对象,但这并不意味着类型安全性必须完全超出范围。 考虑以下方法:
public static List getRecords(Class c, Selector s) {
// Use Selector to select rows
List list = new ArrayList();
for (/* iterate over results */) {
T row = c.newInstance();
// use reflection to set fields from result
list.add(row);
}
return list;
}
您可以像这样简单地调用此方法:
List l = getRecords(FooRecord.class, fooSelector);
编译器将从FooRecord.class
类型为Class
的事实推断出getRecords()
的返回类型。 您可以使用类文字来构造新实例,并向编译器提供类型信息以供其用于类型检查。
Collection
接口包括一种用于将集合的内容复制到调用方指定类型的数组中的方法:
public Object[] toArray(Object[] prototypeArray) { ... }
toArray(Object[])
的语义是,如果传递的数组足够大,则应使用它存储结果; 否则,将使用反射分配相同类型的新数组。 通常,仅将数组作为参数传递以提供所需的返回类型是一种廉价的技巧,但是在添加泛型之前,这是将类型信息传递给方法的最方便的方法。
使用泛型,您可以采用一种更直接的方法。 而不是像上面那样定义toArray()
,通用的toArray()
可能看起来像这样:
public T[] toArray(Class returnType)
调用这样的toArray()
方法很简单:
FooBar[] fba = something.toArray(FooBar.class);
尚未更改Collection
接口以使用此技术,因为这会破坏许多现有的collection实现。 但是,如果从头开始使用泛型重新构建Collection
,则几乎可以肯定会使用此惯用法来指定其希望返回值是哪种类型。
Enum
枚举是JDK 5.0中Java语言的其他新增功能之一。 当使用enum
关键字声明枚举时,编译器会在内部为您生成一个扩展Enum
的类,并为每个枚举值声明静态实例。 因此,如果您说:
public enum Suit {HEART, DIAMOND, CLUB, SPADE};
编译器将在内部生成一个名为Suit
的类,该类扩展了java.lang.Enum
并具有名为HEART
, DIAMOND
, CLUB
和SPADE
常量( public static final
)成员,每个成员均为Suit
类。
像Class
一样, Enum
是一个泛型类。 但是与Class
不同,它的签名稍微复杂一些:
class Enum> { . . . }
这到底是什么意思? 这不是导致无限递归吗?
让我们逐步进行。 类型参数E
在Enum
各种方法中使用,例如compareTo()
或getDeclaringClass()
。 为了使这些类型安全,必须在Enum
类上泛化Enum
类。
那么, extends Enum
部分呢? 那也有两个部分。 第一部分说,作为Enum
类型参数的类本身必须是Enum
子类型,因此您不能声明X类来扩展Enum
。 第二部分说,任何扩展Enum
类都必须将自身作为类型参数传递。 即使Y扩展了Enum
,也不能声明X扩展Enum
。
总而言之, Enum
是一个参数化类型,只能为其子类型实例化,然后这些子类型将继承依赖于该子类型的方法。 ew! 幸运的是,对于Enum
,编译器将为您完成工作,并且正确的事情发生了。
数百万行现有代码使用Java类库中已泛化的类,例如Collections框架, Class
和ThreadLocal
。 重要的是,JDK 5.0中的改进不会破坏所有代码,因此编译器允许您使用泛型类而无需指定其类型参数。
当然,“旧方法”比新方法安全性低,因为您绕过了编译器准备为您提供的类型安全性。 如果您尝试将List
传递给接受List
的方法,它将起作用,但是编译器将发出警告,提示类型安全性可能会丢失(所谓的“未检查的转换”警告)。
没有类型参数的泛型类型,例如声明为List
类型而不是List
的变量,称为原始类型 。 原始类型是与参数化类型的任何实例兼容的分配,但是这样的分配将生成未检查的转换警告。
为了消除某些未检查的转换警告,假设您尚未准备好生成所有代码,则可以改用通配符类型参数。 使用List>
而不是List
。 List
是原始类型; List>
是具有未知类型参数的泛型类型。 编译器将对它们进行不同的处理,并可能发出较少的警告。
在任何情况下,编译器在生成字节码时都会生成强制类型转换,因此在任何情况下生成的字节码都不会比没有泛型时的安全性低。 如果您通过使用原始类型或使用类文件玩游戏来破坏类型安全性,则将获得与没有泛型时相同的ClassCastException
或ArrayStoreException
。
为了帮助从原始集合类型迁移到通用集合类型,Collections框架添加了一些新的集合包装器,以为某些类型安全错误提供预警。 就像Collections.unmodifiableSet()
工厂方法用不允许进行任何修改的Set
包装现有Set
一样, Collections.checkedSet()
(还包括checkedList()
和checkedMap()
)工厂方法会创建包装器或视图类这样可以防止您将错误类型的变量放入集合中。
所有checkedXxx()
方法均以类文字作为参数,因此它们可以(在运行时)检查是否允许修改。 典型的实现如下所示:
public class Collections {
public static Collection
checkedCollection(Collection c, Class type ) {
return new CheckedCollection(c, type);
}
private static class CheckedCollection implements Collection {
private final Collection c;
private final Class type;
CheckedCollection(Collection c, Class type) {
this.c = c;
this.type = type;
}
public boolean add(E o) {
if (!type.isInstance(o))
throw new ClassCastException();
else
return c.add(o);
}
}
}
Perhaps the most challenging aspect of generic types is erasure , which is the technique underlying the implementation of generics in the Java language. Erasure means that the compiler basically throws away much of the type information of a parameterized class when generating the class file. The compiler generates code with casts in it, just as programmers did by hand before generics. The difference is that the compiler has first validated a number of type-safety constraints that it could not have validated without generic types.
The implications of implementing generics through erasure are considerable and, at first, confusing. Although you cannot assign a List
to a List
because they are different types, variables of type List
and List
are of the same class! To see this, try evaluating this expression:
new List().getClass() == new List().getClass()
The compiler generates only one class for List
. By the time the bytecode for List
is generated, little trace of its type parameter remains.
When generating bytecode for a generic class, the compiler replaces type parameters with their erasure . For an unbounded type parameter (
), its erasure is Object
. For an upper-bounded type parameter (
), its erasure is the erasure of its upper bound (in this case, Comparable
). For type parameters with multiple bounds, the erasure of its leftmost bound is used.
If you inspected the generated bytecode, you would not be able to tell the difference between code that came from List
and List
. The type bound T
is replaced in the bytecode with T
's upper bound, which is usually Object
.
Erasure has a number of implications that might seem odd at first. For example, because a class can implement an interface only once, you cannot define a class like this:
// invalid definition
class DecimalString implements Comparable, Comparable { ... }
In light of erasure, the above declaration simply does not make sense. The two instantiations of Comparable
are the same interface, and they specify the same compareTo()
method. You cannot implement a method or an interface twice.
Another, much more annoying implication of erasure is that you cannot instantiate an object or an array using a type parameter. This means you can't use new T()
or new T[10]
in a generic class with a type parameter T
. The compiler simply does not know what bytecode to generate.
There are some workarounds for this issue, generally involving reflection and the use of class literals ( Foo.class
), but they are annoying. The constructor in the Lhist
example class displayed one such technique for working around the problem (see Implementing the constructor ), and the discussion of toArray()
(in Replacing T[] with Class
Another implication of erasure is that it makes no sense to use instanceof
to test if a reference is an instance of a parameterized type. The runtime simply cannot tell a List
from a List
, so testing for (x instanceof List
doesn't make any sense.
Similarly, the following method won't increase the type safety of your programs:
public T naiveCast(T t, Object o) { return (T) o; }
The compiler will simply emit an unchecked warning, because it has no idea whether the cast is safe or not.
The addition of generic types has made the type system in the Java language more complicated. Previously, the language had two kinds of types -- reference types and primitive types. For reference types, the concepts of type and class were basically interchangeable, as were the terms subtype and subclass .
With the addition of generics, the relationship between type and class has become more complex. List
and List
are distinct types, but they are of the same class. Even though Integer
extends Object
, a List
is not a List
, and it cannot be assigned or even cast to List
.
On the other hand, now there is a new weird type called List>
, which is a supertype of both List
and List
. And there is the even weirder List extends Number>
. The structure and shape of the type hierarchy got a lot more complicated. Types and classes are no longer mostly the same thing.
As you learned earlier (see Generic types are not covariant ), generic types, unlike arrays, are not covariant. An Integer
is a Number
, and an array of Integer
is an array of Number
. Therefore, you can freely assign an Integer[]
reference to a variable of type Number[]
. But a List
is not a List
, and for good reason -- the ability to assign a List
to a List
could subvert the type checking that generics are supposed to provide.
This means that if you have a method argument that is a generic type, such as Collection
, you cannot pass a collection of a subclass of V
to that method. If you want to give yourself the freedom to do so, you must use bounded type parameters, such as Collection
(or Collection extends V>
.)
You can use generic types in most situations where you could use a nongeneric type, but there are some restrictions. For example, you cannot declare an array of a generic type (except if the type arguments are unbounded wildcards). The following code is illegal:
List[] listArray = new List[10]; // illegal
Permitting such a construction could create problems, because arrays in Java language are covariant, but parameterized types are not. Because any array type is type-compatible with Object[]
(a Foo[]
is an Object[]
), the following code would compile without warning, but it would fail at runtime, which would undermine the goal of having any program that compiles without unchecked warnings be type-safe:
List[] listArray = new List[10]; // illegal
Object[] oa = listArray;
oa[0] = new List();
String s = lsa[0].get(0); // ClassCastException
If, on the other hand, listArray
were of type List>
, an explicit cast would be required in the last line. Although it would still generate a runtime error, it would not undermine the type-safety guarantees offered by generics (because the error would be in the explicit cast). So arrays of List>
are permitted.
extends
Before the introduction of generics in the Java language, the extends
keyword always meant that a new class or interface was being created that inherited from another class or interface.
With the introduction of generics, the extends
keyword has another meaning. You use extends
in the definition of a type parameter ( Collection
) or a wildcard type parameter ( Collection extends Number>
).
When you use extends
to denote a type parameter bound, you are not requiring a subclass-superclass relationship, but merely a subtype-supertype relationship. It is also important to remember that the bounded type does not need to be a strict subtype of the bound; it could be the bound as well. In other words, for a Collection extends Number>
, you could assign a Collection
(although Number
is not a strict subtype of Number
) as well as a Collection
, Collection
, Collection
, and so on.
In any of these meanings, the type on the right-hand side of extends
can be a parameterized type ( Set
).
So far, you've seen one kind of type bound -- the upper bound . Specifying an upper bound constrains a type parameter to be a supertype of (or equal to) a given type bound, as in Collection extends Number>
. It is also possible, though less common, to specify a lower bound , which you write as Collection super Foo>
. Only wildcards can have lower bounds.
In addition to specifying a type constraint on the type parameter, specifying a bound has another significant effect. If a type T
is known to extend Number
, then the methods and fields of Number
can be accessed through a variable of type T
. It might not be known at compile time what the value of T
is, but it is known at least to be a Number
.
There are some restrictions on which classes can act as type bounds. Primitive types and array types cannot be used as type bounds (but array types can be used as wildcard bounds). Any reference type (including parameterized types) can be used as a type bound.
class C // illegal
class C // illegal
class C //legal
class C >> //legal
class C // legal
One place where you might use a lower bound is in a method that selects elements from one collection and puts them in another. 例如:
class Bunch {
public void add(V value) { ... }
public void copyTo(Collection super V>) { ... }
...
}
The copyTo()
method copies all the values from the Bunch
into a specified collection. Rather than specify that it must be a Collection
, you can specify that it be a Collection super V>
, which means copyTo()
can copy the contents of a Bunch
to a Collection
or a Collection
, rather than just a Collection
.
The other common case for lower bounds is with the Comparable
interface. Rather than specifying:
public static > T max(Collection c) { ... }
You can be more flexible in what types you accept:
public static > T max(Collection c) { ... }
This way, you can pass a type that is comparable to its supertype, in addition to a type that is comparable to itself, for some additional flexibility. This becomes valuable for classes that extend classes that are already Comparable
:
public class Base implements Comparable { ... }
public class Child extends Base { }
Because Child
already implements Comparable
(which it inherits from the superclass Base
), you can pass it to the second example of max()
above, but not the first.
A type parameter can have more than one bound. This is useful when you want to constrain a type parameter to be, say, both Comparable
and Serializable
. The syntax for multiple bounds is to separate the bounds with an ampersand:
class C & Serializable>
A wildcard type can have a single bound -- either an upper or a lower bound. A named type parameter can have one or more upper bounds. A type parameter with multiple bounds can be used to access the methods and fields of each of its bounds.
In the definition of a parameterized class, the placeholder names (such as V
in Collection
) are referred to as type parameters . They have a similar role to that of formal arguments in a method definition. In a declaration of a variable of a parameterized class, the type values specified in the declaration are referred to as type arguments . These have a role similar to actual arguments in a method call. So given the definition:
interface Collection { ... }
and the declaration:
Collection cs = new HashSet();
the name V (which can be used throughout the body of the Collection
interface) is called a type parameter. In the declaration of cs
, both usages of String
are type arguments (one for Collection
and the other for HashSet
.)
There are some restrictions on when you can use type parameters. Most of the time, you can use them anyplace you can use an actual type definition. 但是也有例外。 You cannot use them to create objects or arrays, and you cannot use them in a static context or in the context of handling an exception. You also cannot use them as supertypes ( class Foo
), in instanceof
expressions, or as class literals.
Similarly, there are some restrictions on which types you can use as type arguments. They must be reference types (not primitive types), wildcards, type parameters, or instantiations of other parameterized types. So you can define a List
(reference type), a List>
(wildcard), or a List
(instantiation of other parameterized types). Inside the definition of a parameterized type with type parameter T, you could also declare a >
List
(type parameter.)
The addition of generic types is a major change to both the Java language and the Java class libraries. Generic types (generics) can improve the type safety, maintainability, and reliability of Java applications, but at the cost of some additional complexity.
Great care was taken to ensure that existing classes will continue to work with the generified class libraries in JDK 5.0, so you can get started with generics as quickly or as slowly as you like.
The syntax for generic classes bears a superficial similarity to the template facility in C++. However, there are substantial differences between the two. For example, a generic type in Java language cannot take a primitive type as a type parameter -- only a reference type. This means that you can define a List
, but not a List
. (However, autoboxing can help make a List
behave like a List
of int.)
C++ templates are effectively macros; when you use a C++ template, the compiler expands the template using the provided type parameters. The C++ code generated for List
differs from the code generated for List
, because A and B might have different operator overloading or inlined methods. And in C++, List
and List
are actually two different classes.
Generic Java classes are implemented quite differently. Objects of type ArrayList
and ArrayList
share the same class, and only one ArrayList
class exists. The compiler enforces type constraints, and the runtime has no information about the type parameters of a generic type. This is implemented through erasure , explained in The gory details .
翻译自: https://www.ibm.com/developerworks/java/tutorials/j-generics/j-generics.html