Java 基础之泛型
文章目录
Java平台在JDK 5中引入了一个重要的特性 —— 泛型(generics),允许在定义类和接口的时候使用
为什么引入泛型?
泛型引入的目的是为了解决容器(数据结构)的类型安全性,使得编译器在编译时就能发现明显的类型错误,从而避免运行时的转型错误。下面举一个具体的例子:
在引入泛型以前:
|
|
上述代码是合理的,但是为了取出有效的对象,必须进行强制的类型转换,因为List并不知道其中的对象是什么类型,取出是都是Object对象;可以把不同类型的对象放入同一个容器中,但如果做了不合法的转型,会导致运行时错误。(java.lang.ClassCastException)
在引入了泛型后,编译器可以在编译时了解容器中具体存放的对象的类型,确保其它类型的对象无法存入容器中,从而在编译时就发现大多数的类型错误,避免运行时错误:
|
|
如何使用泛型
泛型可以用于类和接口中,其通用的结构如下:
|
|
在定义有参数的类型时,要使用一种不对类型参数作任何假设的方式指定具体的值。上面用类型参数 E 作为占位符,在具体使用的时候由其它的类型(负载类型)替换掉。
在创建泛型实例时,赋值语句的右侧会重复类型参数的值。一般情况下,编译器能够推导出类型参数的值,可以使用菱形句法省略重复的类型值。
|
|
在非泛型的类中也可以使用泛型方法,结构如下:
|
|
上述方法在调用时,可以显示指定类型参数的具体值,如:
|
|
也可以不指定,让编译器自己推断,
1
|
Utils.add(1,2); |
编译器不仅可以从方法接收的参数类型来推断,也可以从方法赋值的目标参数来推断。类型推导在lambda表达式中也经常使用。
类型擦除
Java 泛型中的类型参数只在编译时可见,javac 会去掉类型参数,这个被称为类型擦除(type erasure)。在生成的Java字节代码中是不包含泛型中的类型信息的,但会保留泛型的一些踪迹,在运行时通过反射可以看到(from Java 技术手册)。类型擦除也是Java的泛型实现方式与C++模板机制实现方式之间的重要区别。
Java 5中添加的泛型是一个新增加的语言特性,而类型擦除正是解决后向兼容型的保证。旧的非泛型集合类和新的泛型集合类是可以兼容的:
|
|
这是因为在经过了编译器的类型擦除后,对 JVM 来说看到的都是List。非泛型的List一般成为原始类型。类型擦除机制会导致一些看上去比较奇怪的现象:
|
|
上述代码乍看上去是合法的,但实际上并不能进行编译。因为在类型擦除以后,两个方法的签名都是:
|
|
运行时无法通过签名区分这两个方法,因而Java规范把这种句法列为非法的。
关于类型擦除需要记住的几点是:
泛型类并没有自己独有的Class类对象。比如并不存在
List<String>.class
或是List<Integer>.class
,而只有List.class
。在经过类型擦除后剩下的只有原始类型,无论是List<String>
或是List<Integer>
,对JVM来说都看作List。静态变量是被泛型类的所有实例所共享的。对于声明为
MyClass<T>
的类,访问其中的静态变量的方法仍然是MyClass.myStaticVar
。不管是通过new MyClass<String>
还是new MyClass<Integer>
创建的对象,都是共享一个静态变量。泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型
MyException<String>
和MyException<Integer>
的。对于JVM来说,它们都是MyException
类型的。也就无法执行与异常对应的catch语句。
通配符与上下界
在开始接触泛型的时候,最让人困惑的问题之一是,“ List<String>
是 List<Object>
的子类吗?”。即这样的代码是合理的吗:
|
|
乍看起来,似乎是正确的,因为String是Object的子类,因而集合中任何一个String类型的元素都是有效的Object对象。但是
|
|
既然objects的类型声明为List<Object>
,那么就能将Object实例存入其中;但是,这个实例保存的元素都是字符串,尝试存入的Object对象与其并不兼容。因而这种写法是不合理的,也就是说,“ List<String>
并不是 List<Object>
的子类” 如果想让容器的类型具有父子关系,需要使用未知类型:
|
|
在Java中,有“未知类型”这一明确的概念,使用 <?>
表示。这是一种最简单的Java通配符类型 (wildcard type)。ArrayList<?>
和 ArrayList<T>
是不一样的,前者是变量可以使用的完整的类型,即便它的负载类型是不确定的;ArrayList<?>
和 ArrayList<Object>
也是不一样的,ArrayList<Object>
实际上确定了List中包含的是Object及其子类,在使用的时候都可以通过Object来进行引用,而ArrayList<?>
中所包含的元素类型是不确定。正因为ArrayList<?>
包含的元素类型是不确定的,就不能通过new ArrayList<?>()
的方法来创建一个新的ArrayList对象。在Java语言的规范中,是禁止实例化负载为未知类型的容器的。
虽然对编译器来说,ArrayList<?>
的元素类型是什么,但还是可以通过Object来引用其中的元素(get()返回的类型是Object),因为虽然类型未知,但肯定是Object或者它的子类。
考虑下面的代码:
|
|
试图对一个未知类型的泛型类进行操作的时候,总是会出现编译错误。其原因在于通配符所表示的类型是未知的。
其实,Java的通配符类型并不只有未知类型一种,还存在受限通配符(bounded wildcard)这一概念,也叫做类型参数约束条件,即使用上下界来约束类型参数可以使用哪些类型。如List<? extends Number>说明List中可能包含的元素类型是Number及其子类。而List<? super Number>则说明List中包含的是Number及其父类。当引入了上界之后,在使用类型的时候就可以使用上界类中定义的方法。
- ? extends T - 这里的?表示类型T的任意子类型,包括类型T本身。
- ? super T - 这里的?表示类型T的任意父类型,包括类型T本身。
这两者的使用通常让人感到非常迷惑,比如说,由于 ? extends T 表示类型T和它的任意子类型,那么我们可以说List<? extends Number>
实例可以添加任意为Number子类的元素吗?
|
|
关于 ? extends T 和 ? super T 的使用,有一个简单的原则可以参考,
当你需要从一个数据结构中获取数据时(get),那么就使用 ? extends T;如果你需要存储数据(put)到一个数据结构时,那么就使用 ? super T; 如果你又想存储数据,又想获取数据,那么就不要使用通配符 ? ,即直接使用具体泛型T。
|
|
泛型的类型系统
面向对象的语言中,一个重要的规则是Liskov替换原则, 即可以使用子类替换父类,即在需要父类引用的地方可以传入子类的引用;但反之,如果要将父类引用替换子类引用,就需要使用强制类型转换,虽然编译器并不能保证运行时刻这种转换一定是合法的。
但引入泛型之后,对Java的类型系统带来了一些令人困扰的问题,比如说 ArrayList<Object>
和 ArrayList<String>
的关系,List<Integer>
和Collection<Integer>
的关系等,在Java深度历险(五)——Java泛型一文中的描述很好:
引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于
List<String>
和List<Object>
这样的情况,类型参数String是继承自Object的。而第二种指的是List接口继承自Collection接口。对于这个类型系统,有如下的一些规则: 1. 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即List<String>
是Collection<String>
的子类型,List<String>
可以替换Collection<String>
。这种情况也适用于带有上下界的类型声明。 2. 当泛型类的类型声明中使用了通配符的时候,其子类型可以在两个维度上分别展开。如对Collection<? extends Number>
来说,其子类型可以在Collection这个维度上展开,即List<? extends Number>
和Set<? extends Number>
等;也可以在Number这个层次上展开,即Collection<Double>
和Collection<Integer>
等。如此循环下去,ArrayList<Long>
和HashSet<Double>
等也都算是Collection<? extends Number>
的子类型。 3. 如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。
一个小坑
创建泛型数组在Java中是不被允许的。需要适用类型转换来创建泛型数组。
|
|
-EOF-
参考: Java深度历险(五)——Java泛型 浅谈Java泛型 Java技术手册