泛型是Java SE 5.0中引入的一项特征,自从这项语言特征出现多年来,我相信,几乎所有的Java程序员不仅听说过,而且使用过它。关于Java泛型的教程,免费的,不免费的,有很多。我遇到的最好的教材有:
- The Java Tutorial
- Java Generics and Collections, by Maurice Naftalin and Philip Wadler
- Effective Java中文版(第2版), by Joshua Bloch.
尽管有这么多丰富的资料,有时我感觉,有很多的程序员仍然不太明白Java泛型的功用和意义。这就是为什么我想使用一种最简单的形式来总结一下程序员需要知道的关于Java泛型的最基本的知识。
Java泛型由来的动机
理解Java泛型最简单的方法是把它看成一种便捷语法,能节省你某些Java类型转换(casting)上的操作:
1 |
List<Apple> box = ...; |
2 |
Apple apple = box.get( 0 );
|
上面的代码自身已表达的很清楚:box是一个装有Apple对象的List。get方法返回一个Apple对象实例,这个过程不需要进行类型转换。没有泛型,上面的代码需要写成这样:
1 |
List box = ...; |
2 |
Apple apple = (Apple) box.get( 0 );
|
很明显,泛型的主要好处就是让编译器保留参数的类型信息,执行类型检查,执行类型转换操作:编译器保证了这些类型转换的绝对无误。
相对于依赖程序员来记住对象类型、执行类型转换——这会导致程序运行时的失败,很难调试和解决,而编译器能够帮助程序员在编译时强制进行大量的类型检查,发现其中的错误。
泛型的构成
由泛型的构成引出了一个类型变量的概念。根据Java语言规范,类型变量是一种没有限制的标志符,产生于以下几种情况:
- 泛型类声明
- 泛型接口声明
- 泛型方法声明
- 泛型构造器(constructor)声明
泛型类和接口
如果一个类或接口上有一个或多个类型变量,那它就是泛型。类型变量由尖括号界定,放在类或接口名的后面:
1 |
public interface List<T> extends Collection<T> {
|
2 |
... |
3 |
} |
简单的说,类型变量扮演的角色就如同一个参数,它提供给编译器用来类型检查的信息。
Java类库里的很多类,例如整个Collection框架都做了泛型化的修改。例如,我们在上面的第一段代码里用到的List接口就是一个泛型类。在那段代码里,box是一个List<Apple>对象,它是一个带有一个Apple类型变量的List接口的类实现的实例。编译器使用这个类型变量参数在get方法被调用、返回一个Apple对象时自动对其进行类型转换。
实际上,这新出现的泛型标记,或者说这个List接口里的get方法是这样的:
1 |
T get( int index);
|
get方法实际返回的是一个类型为T的对象,T是在List<T>声明中的类型变量。
泛型方法和构造器(Constructor)
非常的相似,如果方法和构造器上声明了一个或多个类型变量,它们也可以泛型化。
1 |
public static <t> T getFirst(List<T> list)
|
这个方法将会接受一个List<T>类型的参数,返回一个T类型的对象。
例子
你既可以使用Java类库里提供的泛型类,也可以使用自己的泛型类。
类型安全的写入数据…
下面的这段代码是个例子,我们创建了一个List<String>实例,然后装入一些数据:
1 |
List<String> str = new ArrayList<String>();
|
2 |
str.add( "Hello " );
|
3 |
str.add( "World." );
|
如果我们试图在List<String>装入另外一种对象,编译器就会提示错误:
1 |
str.add( 1 ); // 不能编译
|
类型安全的读取数据…
当我们在使用List<String>对象时,它总能保证我们得到的是一个String对象:
1 |
String myString = str.get( 0 );
|
遍历
类库中的很多类,诸如Iterator<T>,功能都有所增强,被泛型化。List<T>接口里的iterator()方法现在返回的是Iterator<T>,由它的T next()方法返回的对象不需要再进行类型转换,你直接得到正确的类型。
1 |
for (Iterator<String> iter = str.iterator(); iter.hasNext();) {
|
2 |
String s = iter.next(); |
3 |
System.out.print(s); |
4 |
} |
使用foreach
“for each”语法同样受益于泛型。前面的代码可以写出这样:
1 |
for (String s: str) {
|
2 |
System.out.print(s); |
3 |
} |
这样既容易阅读也容易维护。
自动封装(Autoboxing)和自动拆封(Autounboxing)
在使用Java泛型时,autoboxing/autounboxing这两个特征会被自动的用到,就像下面的这段代码:
1 |
List<Integer> ints = new ArrayList<Integer>();
|
2 |
ints.add( 0 );
|
3 |
ints.add( 1 );
|
4 |
5 |
int sum = 0 ;
|
6 |
for ( int i : ints) {
|
7 |
sum += i; |
8 |
} |
然而,你要明白的一点是,封装和解封会带来性能上的损失,所有,通用要谨慎的使用。
子类型
在Java中,跟其它具有面向对象类型的语言一样,类型的层级可以被设计成这样:
在Java中,类型T的子类型既可以是类型T的一个扩展,也可以是类型T的一个直接或非直接实现(如果T是一个接口的话)。因为“成为某类型的子类型”是一个具有传递性质的关系,如果类型A是B的一个子类型,B是C的子类型,那么A也是C的子类型。在上面的图中:
- FujiApple(富士苹果)是Apple的子类型
- Apple是Fruit(水果)的子类型
- FujiApple(富士苹果)是Fruit(水果)的子类型
所有Java类型都是Object类型的子类型。
B类型的任何一个子类型A都可以被赋给一个类型B的声明:
1 |
Apple a = ...; |
2 |
Fruit f = a; |
泛型类型的子类型
如果一个Apple对象的实例可以被赋给一个Fruit对象的声明,就像上面看到的,那么,List<Apple> 和 a List<Fruit>之间又是个什么关系呢?更通用些,如果类型A是类型B的子类型,那C<A> 和 C<B>之间是什么关系?
答案会出乎你的意料:没有任何关系。用更通俗的话,泛型类型跟其是否子类型没有任何关系。
这意味着下面的这段代码是无效的:
1 |
List<Apple> apples = ...; |
2 |
List<Fruit> fruits = apples; |
下面的同样也不允许:
1 |
List<Apple> apples; |
2 |
List<Fruit> fruits = ...; |
3 |
apples = fruits; |
为什么?一个苹果是一个水果,为什么一箱苹果不能是一箱水果?
在某些事情上,这种说法可以成立,但在类型(类)封装的状态和操作上不成立。如果把一箱苹果当成一箱水果会发生什么情况?
1 |
List<Apple> apples = ...; |
2 |
List<Fruit> fruits = apples; |
3 |
fruits.add( new Strawberry());
|
如果可以这样的话,我们就可以在list里装入各种不同的水果子类型,这是绝对不允许的。
另外一种方式会让你有更直观的理解:一箱水果不是一箱苹果,因为它有可能是一箱另外一种水果,比如草莓(子类型)。
这是一个需要注意的问题吗?
应该不是个大问题。而程序员对此感到意外的最大原因是数组和泛型类型上用法的不一致。对于泛型类型,它们和类型的子类型之间是没什么关系的。而对于数组,它们和子类型是相关的:如果类型A是类型B的子类型,那么A[]是B[]的子类型:
1 |
Apple[] apples = ...; |
2 |
Fruit[] fruits = apples; |
可是稍等一下!如果我们把前面的那个议论中暴露出的问题放在这里,我们仍然能够在一个apple类型的数组中加入strawberrie(草莓)对象:
1 |
Apple[] apples = new Apple[ 1 ];
|
2 |
Fruit[] fruits = apples; |
3 |
fruits[ 0 ] = new Strawberry();
|
这样写真的可以编译,但是在运行时抛出ArrayStoreException异常。因为数组的这特点,在存储数据的操作上,Java运行时需要检查类型的兼容性。这种检查,很显然,会带来一定的性能问题,你需要明白这一点。
重申一下,泛型使用起来更安全,能“纠正”Java数组中这种类型上的缺陷。
现在估计你会感到很奇怪,为什么在数组上会有这种类型和子类型的关系,我来给你一个《Java Generics and Collections》这本书上给出的答案:如果它们不相关,你就没有办法把一个未知类型的对象数组传入一个方法里(不经过每次都封装成Object[]),就像下面的:
1 |
void sort(Object[] o);
|
泛型出现后,数组的这个个性已经不再有使用上的必要了(下面一部分我们会谈到这个),实际上是应该避免使用。
通配符
在本文的前面的部分里已经说过了泛型类型的子类型的不相关性。但有些时候,我们希望能够像使用普通类型那样使用泛型类型:
- 向上造型一个泛型对象的引用
- 向下造型一个泛型对象的引用
向上造型一个泛型对象的引用
例如,假设我们有很多箱子,每个箱子里都装有不同的水果,我们需要找到一种方法能够通用的处理任何一箱水果。更通俗的说法,A是B的子类型,我们需要找到一种方法能够将C<A>类型的实例赋给一个C<B>类型的声明。
为了完成这种操作,我们需要使用带有通配符的扩展声明,就像下面的例子里那样:
1 |
List<Apple> apples = new ArrayList<Apple>();
|
2 |
List<? extends Fruit> fruits = apples;
|
“? extends”是泛型类型的子类型相关性成为现实:Apple是Fruit的子类型,List<Apple> 是 List<? extends Fruit> 的子类型。
向下造型一个泛型对象的引用
现在我来介绍另外一种通配符:? super。如果类型B是类型A的超类型(父类型),那么C<B> 是 C<? super A> 的子类型:
1 |
List<Fruit> fruits = new ArrayList<Fruit>();
|
2 |
List<? super Apple> = fruits;
|
为什么使用通配符标记能行得通?
原理现在已经很明白:我们如何利用这种新的语法结构?
? extends
让我们重新看看这第二部分使用的一个例子,其中谈到了Java数组的子类型相关性:
1 |
Apple[] apples = new Apple[ 1 ];
|
2 |
Fruit[] fruits = apples; |
3 |
fruits[ 0 ] = new Strawberry();
|
就像我们看到的,当你往一个声明为Fruit数组的Apple对象数组里加入Strawberry对象后,代码可以编译,但在运行时抛出异常。
现在我们可以使用通配符把相关的代码转换成泛型:因为Apple是Fruit的一个子类,我们使用? extends 通配符,这样就能将一个List<Apple>对象的定义赋到一个List<? extends Fruit>的声明上:
1 |
List<Apple> apples = new ArrayList<Apple>();
|
2 |
List<? extends Fruit> fruits = apples;
|
3 |
fruits.add( new Strawberry());
|
这次,代码就编译不过去了!Java编译器会阻止你往一个Fruit list里加入strawberry。在编译时我们就能检测到错误,在运行时就不需要进行检查来确保往列表里加入不兼容的类型了。即使你往list里加入Fruit对象也不行:
1 |
fruits.add( new Fruit());
|
你没有办法做到这些。事实上你不能够往一个使用了? extends的数据结构里写入任何的值。
原因非常的简单,你可以这样想:这个? extends T 通配符告诉编译器我们在处理一个类型T的子类型,但我们不知道这个子类型究竟是什么。因为没法确定,为了保证类型安全,我们就不允许往里面加入任何这种类型的数据。另一方面,因为我们知道,不论它是什么类型,它总是类型T的子类型,当我们在读取数据时,能确保得到的数据是一个T类型的实例:
1 |
Fruit get = fruits.get( 0 );
|
? super
使用 ? super 通配符一般是什么情况?让我们先看看这个:
1 |
List<Fruit> fruits = new ArrayList<Fruit>();
|
2 |
List<? super Apple> = fruits;
|
我们看到fruits指向的是一个装有Apple的某种超类(supertype)的List。同样的,我们不知道究竟是什么超类,但我们知道Apple和任何Apple的子类都跟它的类型兼容。既然这个未知的类型即是Apple,也是GreenApple的超类,我们就可以写入:
1 |
fruits.add( new Apple());
|
2 |
fruits.add( new GreenApple());
|
如果我们想往里面加入Apple的超类,编译器就会警告你:
1 |
fruits.add( new Fruit());
|
2 |
fruits.add( new Object());
|
因为我们不知道它是怎样的超类,所有这样的实例就不允许加入。
从这种形式的类型里获取数据又是怎么样的呢?结果表明,你只能取出Object实例:因为我们不知道超类究竟是什么,编译器唯一能保证的只是它是个Object,因为Object是任何Java类型的超类。
存取原则和PECS法则
总结 ? extends 和 the ? super 通配符的特征,我们可以得出以下结论:
- 如果你想从一个数据类型里获取数据,使用 ? extends 通配符
- 如果你想把对象写入一个数据结构里,使用 ? super 通配符
- 如果你既想存,又想取,那就别用通配符。
这就是Maurice Naftalin在他的《Java Generics and Collections》这本书中所说的存取原则,以及Joshua Bloch在他的《Effective Java》这本书中所说的PECS法则。
Bloch提醒说,这PECS是指”Producer Extends, Consumer Super”,这个更容易记忆和运用。
相关推荐
Java泛型的用法及T.class的获取过程解析 Java泛型是Java编程语言中的一种重要特性,它允许开发者在编写代码时指定类型参数,从而提高代码的灵活性和可读性。本文将详细介绍Java泛型的用法 及T.class的获取过程解析...
Java 泛型详解 Java 泛型是 Java SE 5.0 中引入的一项特征,它允许程序员在编译时检查类型安全,从而减少了 runtime 错误的可能性。泛型的主要优点是可以Reusable Code,让程序员编写更加灵活和可维护的代码。 ...
Java泛型是Java编程语言中的一个强大特性,它允许我们在定义类、接口和方法时指定类型参数,从而实现代码的重用和类型安全。在Java泛型应用实例中,我们可以看到泛型如何帮助我们提高代码的灵活性和效率,减少运行时...
Java泛型机制详解 Java泛型是Java语言中的一种机制,用于在编译期检查类型安全。Java泛型的出现解决了Java早期版本中类型安全检查的缺陷。Java泛型的好处是可以在编译期检查类型安全,避免了运行时的...
综上所述,虽然Java泛型在编译后会进行类型擦除,但通过上述技巧,我们仍然能够在运行时获得关于泛型类实例化类型的一些信息。在实际开发中,这些方法可以帮助我们编写更加灵活和安全的代码。在示例文件`GenericRTTI...
Java泛型是Java编程语言中的一个关键特性,它在2004年随着Java SE 5.0的发布而引入,极大地增强了代码的类型安全性和重用性。本篇文章将深入探讨Java泛型的发展历程、核心概念以及其在实际开发中的应用。 1. **发展...
Java泛型是Java编程语言中的一个关键特性,它在2004年随着JDK 5.0的发布被引入。这个特性极大地提高了代码的类型安全性和可读性,减少了在运行时出现ClassCastException的可能性。SUN公司的Java泛型编程文档,包括...
下面我们将详细探讨Java泛型接口的相关知识点。 1. **泛型接口的定义** 泛型接口的定义方式与普通接口类似,只是在接口名之后添加了尖括号`<T>`,其中`T`是一个类型参数,代表某种未知的数据类型。例如: ```java...
下面我们将深入探讨Java泛型方法的概念、语法以及使用示例。 **一、泛型方法概念** 泛型方法是一种具有类型参数的方法,这些类型参数可以在方法声明时指定,并在方法体内部使用。与类的泛型类似,它们提供了编译时...
Java泛型是Java编程语言中的一个强大特性,它允许在定义类、接口和方法时使用类型参数,从而实现参数化类型。这使得代码更加安全、可读性更强,并且能够减少类型转换的必要。在“java泛型的内部原理及更深应用”这个...
这是一个使用JAVA实现的泛型编程,分为两部分,第一部分创建泛型类,并实例化泛型对象,得出相加结果。 第二部分用户自行输入0--4,选择要进行的加减乘除运算或退出,再输入要进行运算的两个数,并返回运算结果及...
Java 泛型是一种强大的工具,它允许我们在编程时指定变量的类型,提供了编译时的类型安全。然而,Java 的泛型在运行时是被擦除的,这意味着在运行时刻,所有的泛型类型信息都会丢失,无法直接用来创建对象或进行类型...
"Java 泛型学习" Java 泛型是 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的...
Java 泛型使用详细分析 Java 泛型是 Java 语言中的一种类型系统特性,允许开发者在编译期检查类型安全,以避免在运行时出现类型相关的错误。在本文中,我们将详细介绍 Java 泛型的使用方法和实现原理。 一、泛型的...
Java泛型是Java编程语言中的一个关键特性,它在2004年随着Java SE 5.0的发布而引入,极大地增强了代码的类型安全性和重用性。本篇文章将深入探讨Java泛型的发展历程、核心概念以及其在实际开发中的应用。 1. **发展...
#### 一、什么是Java泛型? Java泛型(Generics)是一种在编译时确保类型安全的机制,它允许程序员编写类型安全的通用类或方法,而无需进行显式的类型转换。在Java 1.5引入泛型之前,集合类(如`ArrayList`)只能...