2. java 泛型
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?
顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,
操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
2.1 什么是泛型
自JDK1.5之后,java通过泛型解决了容器类型安全这一问题。
-
泛型的本质是参数化类型
也就是说,泛型就是将所操作的数据类型作为参数的一种语法。
-
泛型的作用
-
使用泛型能写出更加通用灵活的代码
-
泛型将代码安全性检查提前到编译期
泛型被加入Java语法中,还有一个最大的原因:解决容器的类型安全,使用泛型后,能让编译器在编译的时候借助传入的类型参数检查对容器的插入,获取操作是否合法,从而将运行时ClassCastException转移到编译时比如:
List dogs =new ArrayList(); dogs.add(new Cat());
在没有泛型之前,这种代码除非运行,否则你永远找不到它的错误。但是加入泛型后,会在编译的时候就检查出来。
List<Dog> dogs=new ArrayList<>(); dogs.add(new Cat());//Error Compile
-
泛型能够省去类型强制转换
在JDK1.5之前,java容器都是通过将类型向上转型为object类型来实现的,因此在从容器中取出来的时候需要手动的强制转换。
Dog dog=(Dog)dogs.get(1);
-
加入泛型后,由于编译器知道了具体的类型,因此编译期会自动进行类型转换,使得代码更加优雅。
2.2 泛型的具体实现
我们可以定义泛型类、泛型方法、泛型接口等,那泛型的底层是怎么实现的呢?
-
泛型的擦除
Java 设计者将泛型完全作为了语法糖加入了新的语法中,什么意思呢?也就是说泛型对于JVM来说是透明的,有泛型的和没有泛型的代码,通过编译器编译后所生成的二进制代码是完全相同的。这个语法糖的实现被称为擦除
-
泛型擦除的过程
泛型是编译时将具体的类型作为类型参数传递给方法、类、接口。
擦除是在代码运行过程中将具体的类型都抹除。
JDK1.5之前需要编写模板代码的地方都是通过object来保存具体的值。比如:
public class Node{ private Object obj; public Object get(){ return obj; } public void set(Object obj){ this.obj=obj; } public static void main(String[] argv){ Student stu=new Student(); Node node=new Node(); node.set(stu); Student stu2=(Student)node.get(); } }
这样的实现能满足绝大多数需求,但是泛型还是有更多方便的地方,最大的一点就是编译期类型检查,于是Java 1.5之后加入了泛型,但是这个泛型仅仅是在编译的时候帮你做了编译时类型的检查,成功编译后生成的.class文件还是一模一样的,这便是擦除。
JDK1.5以后的实现
public class Node<T>{ private T obj; public T get(){ return obj; } public void set(T obj){ this.obj=obj; } public static void main(String[] argv){ Student stu=new Student(); Node<Student> node=new Node<>(); node.set(stu); Student stu2=node.get(); } }
两个版本生成的.class文件
Node:
public Node(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public java.lang.Object get(); Code: 0: aload_0 1: getfield #2 // Field obj:Ljava/lang/Object; 4: areturn public void set(java.lang.Object); Code: 0: aload_0 1: aload_1 2: putfield #2 // Field obj:Ljava/lang/Object; 5: return }
public class Node<T> { public Node(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public T get(); Code: 0: aload_0 1: getfield #2 // Field obj:Ljava/lang/Object; 4: areturn public void set(T); Code: 0: aload_0 1: aload_1 2: putfield #2 // Field obj:Ljava/lang/Object; 5: return }
可以看到泛型就是使用泛型代码的时候,将类型信息传递给具体的泛型代码,而经过编译后,生成的.class文件和原始的代码一模一样,就好像传递过来的类型信息又被擦除了一样。
2.3 泛型语法
Java 的泛型就是一个语法糖,而语法糖最大的好处就是让人方便使用,但是它的缺点也在于如果不剥开这颗语法糖,有很多奇怪的语法就很难理解。
2.3.1 类型边界
泛型最终会擦除为object类型。这样导致的是在编写泛型代码的时候,对泛型元素的操作只能使用object自带的一些方法,但是有时候我们想使用其它类型的方法怎么办?
比如:
public class Node{
private People obj;
public People get(){
return obj;
}
public void set(People obj){
this.obj=obj;
}
public void playName(){
System.out.println(obj.getName());
}
}
代码中需要使用obj.getName()
方法,因此比如规定传入的元素必须是People及其子类,那么这样的方法怎么通过泛型体现出来呢?
答案是:extends,泛型重载了extends关键字,可以通过extends关键字指定最终擦除所替代的类型。
public class Node<T extends People>{
private T obj;
public T get(){
return obj;
}
public void set(T obj){
this.obj=obj;
}
public void playName(){
System.out.println(obj.getName());
}
}
通过extend
关键字,编译器会将最后类型都擦除为People
类型,就好像最开始我们看见的原始代码一样。
2.3.2 泛型与向上转型的概念
先讲一讲几个概念:
- 协变:子类能向父类转换
Animal a = new Cat();
- 逆变:父类能向子类转换
Cat c = (Cat)a;
- 不变:两者均不能转换
对于协变,我们见得最多的就是多态,而逆变常见于强制类型转换。
public static void error(){
Object[] nums=new Integer[3];
nums[0]=3.2;
nums[1]="string"; //运行时报错,nums运行时类型是Integer[]
nums[2]='2';
}
因为数组是协变的,因此Integer[]
可以转换为Object[]
,在编译阶段编译器只知道nums
是Object[]
类型,而运行时nums
则为Integer[]
类型,因此上述代码能够编译,但是运行会报错。
这就是常见的人们所说的数组是协变的。这里带来一个问题,为什么数组要设计为协变的呢?既然不让运行,那么通过编译有什么用?
答案是在泛型还没出现之前,数组协变能够解决一些通用的问题:
public static void sort(Object[] a) {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a);
else
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
/**
* 摘自JDK 1.8 Arrays.equals()
*/
public static boolean equals(Object[] a, Object[] a2) {
//...
for (int i=0; i<length; i++) {
Object o1 = a[i];
Object o2 = a2[i];
if (!(o1==null ? o2==null : o1.equals(o2)))
return false;
}
//..
return true;
}
可以看到,只操作数组本身,而不关心数组中具体保存的元素,或者是不管什么元素,取出来就作为一个Object
存储的时候,只用编写一个Object[]
就能写出通用的数组参数方法。比如:
Arrays.sort(new Student[]{...})
Arrays.sort(new Apple[]{...})
等,但是这样的设计留下来的诟病就是偶尔会出现对数组元素有具体的操作的代码,比如上面的error()
方法。
泛型的出现,是为了保证类型安全的问题,如果将泛型也设计为协变的话,那也就违背了泛型最初设计的初衷,因此在Java中,泛型是不变的,什么意思呢?
List<Number>
和 List<Integer>
是没有任何关系的,即使Integer
是 Number
的子类
也就是对于public static void test(List<Number> nums){...}
方法,是无法传递一个List<Integer>
参数的。
逆变 一般常见于强制类型转换:
Object obj="test";
String str=(String)obj;
原理便是Java 反射机制能够记住变量obj
的实际类型,在强制类型转换的时候发现obj
实际上是一个String
类型,于是就正常的通过了运行。
2.3.3 泛型与向上转型的实现
前面说了这么多,应该关心的问题在于,如何解决既能使用数组协变带来的方便性,又能得到泛型不变带来的类型安全?
答案依然是extends,super关键字与通配符 ?
泛型重载了extends,super关键字来解决通用泛型的表示。
注意:这句话可能比较熟悉,没错,前面说过extends还被用来指定擦除到的具体类型,比如<E extends Fruit>,表示在运行时将E替换为Fruit,注意E表示的是一个具体的类型,但是这里的extend和通配符连续使用<? extends Fruit>这里通配符?表示一个通用类型,它所表示的泛型在编译的时候,被指定的具体的类型必须是Fruit的子类。比如List<? extends Fruit> list= new ArrayList<Apple>,ArrayList<>中指定的类型可以是Apple,Orange等。不要混淆。
- 协变泛型(上界): < ? extends ***>
public static void playFruit(List < ? extends Fruit> list){
//do somthing
}
public static void main(String[] args) {
List<Apple> apples=new ArrayList<>();
List<Orange> oranges=new ArrayList<>();
List<Food> foods =new ArrayList<>();
playFruit(apples);
playFruit(oranges);
//playFruit(foods); 编译错误
}
可以看到,参数List < ? extends Fruit>
所表示是需要一个List<>
,其中尖括号所指定的具体类型必须是继承自Fruit
的。
public static void main(String[] args) throws Exception {
List<? extends Fruit> top = new ArrayList<>();
top.add(null);
//top.add(new Apple());//编译报错
Fruit fruit = top.get(0);
}
上界<? extend Fruit> ,表示所有继承Fruit的子类,但是具体是哪个子类,无法确定,所以调用add的时候,要add什么类型,谁也不知道。但是get的时候,不管是什么子类,不管追溯多少辈,肯定有个父类是Fruit,所以,我都可以用最大的父类Fruit接着,也就是把所有的子类向上转型为Fruit。
-
逆变泛型(下界):<? super ***>
public static void playFruitBase(List < ? super Fruit> list){ //.. } public static void main(String[] args) { List<Apple> apples=new ArrayList<>(); List<Food> foods =new ArrayList<>(); List<Object> objects=new ArrayList<>(); playFruitBase(foods); playFruitBase(objects); //playFruitBase(apples); 编译错误 }
同理,参数
List < ? super Fruit>
所表示是需要一个List<>
,其中尖括号所指定的具体类型必须是Fruit
的父类类型。public static void main(String[] args) throws Exception { List<? super Apple> bottom = new ArrayList<>(); bottom.add(new Apple()); //Apple apple = bottom.get(0);//编译不通过 Apple object = (Apple) bottom.get(0); }
下界<? super Apple>,表示Apple的所有父类,包括Fruit,一直可以追溯到老祖宗Object 。那么当我add的时候,我不能add Apple的父类,因为不能确定List里面存放的到底是哪个父类。但是我可以add Apple及其子类。因为不管我的子类是什么类型,它都可以向上转型为Apple及其所有的父类甚至转型为Object 。但是当我get的时候,Apple的父类这么多,我用什么接着呢,除了Object,其他的都接不住。
所以,归根结底可以用一句话表示,那就是编译器可以支持向上转型,但不支持向下转型。具体来讲,我可以把Apple对象赋值给Fruit的引用,但是如果把Fruit对象赋值给Apple的引用就必须得用cast。
-
无界通用符: ?