0 前言
本文主要是对参考资料的一些整理,以及一些个人见解,难免会有理解错误的地方,欢迎大家指正。
1 概述
Java语言于1995年发布第一个版本,自1991年Sun(1982-2009,被Oracle收购)成立Green项目进行开发以来,花费近四年时间。Java语言主要脱胎于C++语言(诞生于贝尔实验室并于1983年开始发行),在发展过程中与众多面向对象语言(Object-Oriented Language)相互影响,尤其是C#语言(Microsoft于2000年开始发行),泛型是Java1.5发布的一个重要feature。
2 背景
简化代码,增强安全性
举个例子,在日常开发中,接口响应的结果一般包含code,msg和data三个部分。在引入泛型之前,可以写成下面这种形式:
public class Response {
private int code;
private String msg;
private Object data;
// 省略Getter、Setter
}
我们知道不同的接口会对应不同类型的data,那么代码中必然充斥着各种类型转换:
Respnse rsp = new Response();
// 模拟传值
rsp.setData(new Account());
Account account = (Account)rsp.getData();
这会造成什么后果呢?在实现类型转换的过程中,可能出现的转型异常迫使我们每次类型转换前都要进行类型判断,无形中增加了相当多的冗余代码。
3 泛型类和泛型方法
泛型(generics)并不是Java独有的,C++,C#等语言也都支持。
3.1 泛型类
让我们将Response类通过泛型来实现
public class Response<T> {
private int code;
private String msg;
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
...
}
只需在类名尖括号处声明一个类型参数T(T是类型标志,可以是其他字母),当给类传入具体类型如Integer时,类中所有的T都相当于替换为Integer。
具体使用如下:
// 增加泛型声明
Respnse<Account> rsp = new Response<>();
// 模拟传值
rsp.setData(new Account());
// !!! rsp.setData(new Order())无法编译通过,因为它和指定类型Account不一致
// 此处无需强制转换
Account account = rsp.getData();`
可以看到我们调用getData()时无需转型,直接就得到了Account对象,看来Java编译器默默地替我们做了一些微小的工作。
3.2 泛型方法
除了泛型类,还有泛型接口,它的用法跟泛型类相差无几,都是要在名称的尖括号处声明泛型参数。
public interface Generator<T>{
T next();
}
Java可以让某个方法实现泛型,这种情况只需在该方法签名上声明类型参数即可,无需在创建类时声明泛型参数。
public class GenericMethod {
public <K,V> void f(K k,V v) {
System.out.println(k.getClass().getSimpleName());
System.out.println(v.getClass().getSimpleName());
}
public static void main(String[] args) {
GenericMethod gm = new GenericMethod();
gm.f(new Integer(0),new String("generic"));
}
}
打印结果
Integer
String
4 泛型的本质
只要提到泛型,那么肯定会带出类型擦除的问题。类型擦除究竟是怎么回事呢,我们通过几个实验来讨论一下。
4.1 泛型的运行时类型
Response<Account> rsp1 = new Response<>();
Response<Order> rsp2 = new Response<>();
System.out.println(rsp1.getClass().getSimpleName());
System.out.println(rsp2.getClass().getSimpleName());
打印结果
Response
Response
看起来类型参数 Account、Order 在运行时并未体现出现。这表明JVM不会持有泛型参数的信息。
4.2 泛型是如何实现的
Response<Account> rsp = new Response<>();
rsp.setData(new Account());
Account account = rsp.getData();
对上述代码进行反编译,结果如下
Response<Account> rsp = new Response();
rsp.setData(new Account());
// class文件新增转型代码
Account account = (Account)rsp.getData();
原来是编译器自动为字节码文件新增了转型代码。所以泛型内部实现仍未摆脱转型的工作。既然还是存在转型, 那么我们就好奇rsp.getData()的实际类型是什么呢?下面查看一下类型参数的运行时类型。
Response<Account> rsp = new Response<>();
rsp.setData(new Account());
Field[] fields = rsp.getClass().getDeclaredFields();
for (Field f:fields) {
System.out.println(String.format("Field name [%s],type is [%s]",f.getName(),f.getType().getName()));
}
打印结果
Field name [code],type is [int]
Field name [msg],type is [java.lang.String]
Field name [data],type is [java.lang.Object]
可以看到 Response#data 的类型在运行时为 Object,这跟未引入泛型前的思路一致。那么setData(T data)中的参数呢?
Method[] methods = rsp.getClass().getDeclaredMethods();
for (Method m : methods) {
System.out.println(m);
}
打印结果
public java.lang.Object generics.Response.getData()
public void generics.Response.setData(java.lang.Object)
可以证明泛型类中的泛型参数T都将替换为 Object 类型,只有我们需要得到T的引用时,编译器才会为我们自动插入转型代码。这也就是所谓的类型擦除。
4.3 与其他语言泛型实现的比较
- C++,对于每种类型都会在编译前生成一份代码,例如
Response<Account>,Response<Order>会分别生成类似Response_Account,Response_Order的代码,这称为编译时多态技术(还有运行时多态技术)。它的特点是如果存在100种不同类型的Response,就会生成100份代码。 - Java,通过类型擦除实现。所谓的类型检查就是在边界处(对象进入和离开的地方),检查类型是否符合某种约束。包括以下几个条件:
- 赋值语句的左右两边类型必须兼容。
- 方法调用的实参和形参必须兼容。
- return表达式的类型和方法定义的返回值必须兼容。
- 以及多态类型检查,向上转型可以直接通过,但向下转型必须强制转换(前提是有继承关系)。
Java泛型的自动转型是通过编译器插入checkcast指令完成的。类型擦除导致所有的类型参数都以Object存在,在运行时根本就无法确定这个对象到底是什么类型,例如Response<Account>,Response<Order>都会被擦除为和Response<Object>等价。所以,你不能把T真正的当作一个类型来使用,new T()这样的语句是不会编译通过的。注意,这里的‘无法确定’说的是对象的引用,对象的类型永远是确定的,对于一个Account对象,它在堆内存中的类型是不会变的,可以通过RTTI获取到它的真实类型。
- C#,在编译时将泛型编译成元数据,在运行时根据需要生成相应类型的特化代码。既不会像c++那样生成多份代码,又不会像java那样,丢失了泛型的类型。基本做到了两全其美。
// 增加上界
public class Response<T extends Account> {
private int code;
private String msg;
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
Field name [code],type is [int]
Field name [msg],type is [java.lang.String]
Field name [data],type is [generics.Account]
public generics.Account generics.Response.getData()
public void generics.Response.setData(generics.Account)
我们泛型参数指定了上限 Account,那么泛型擦除会自动将参数替换为对应的上限类型。未指定上限时直接替换为超类 Object。对于 ?super Account,上限是 Object
5 泛型和数组
在讲通配符之前,我们先看一个多态的例子,了解一下Java是如何自动转型的。
5.1 多态
class A {
public String show(D obj) {
return ("A and D");
}
public String show(A obj) {
return ("A and A");
}
}
class B extends A {
public String show(A obj) {
return ("B and A");
}
public String show(B obj) {
return ("B and B");
}
}
class C extends B {}
class D extends B {}
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();
System.out.println(a1.show(b)); // A and A
System.out.println(a1.show(c)); // A and A
System.out.println(a1.show(d)); // A and D
System.out.println(a2.show(b)); // B and A
System.out.println(a2.show(c)); // B and A
System.out.println(a2.show(d)); // A and D
System.out.println(b.show(b)); // B and B
System.out.println(b.show(c)); // B and B
System.out.println(b.show(d)); // A and D
对于System.out.println(a2.show(b)):
a2 是 A 的一个引用,只能调用 A 中声明过的方法。 但 A 中不存在一个参数是 B 类型的方法,将 b 向上转型得到 A,A 中存在一个show( A obj)方法,看来应该输出 "A and A"。
但这是错的,因为 a2 实际上是指向 B 的,B 覆写了这个方法,那么最终调用的是B#show(A obj)。最终输出 "B and A"。
对于System.out.println(b.show(d)): B 中不存在一个show(D obj)方法,但B 的父类存在这个方法
所以最终输出 "A and D" 。
对于 a2.show(a2):a2被作为参数传递时,它的类型为 A。所以 a2.show(a2) 会调用 A#show(A obj) 方法,又因为a2的实际类型为B并且B覆写了这个方法。最终输出 "B and A"。
5.2 数组与协变性
数组作为最为重要的一种数据结构,Java底层众多实现都要依赖数组。
public class Fruit {
}
public class Apple extends Fruit {
}
public class Orange extends Fruit {
}
对于以上继承关系的类,我们看如下的代码
Fruit[] fruits = new Apple[10]; // 编译通过,运行通过。
fruits[0] = new Fruit(); // 编译通过,运行抛异常【java.lang.ArrayStoreException】
fruits[0] = new Orange(); // 编译通过,运行抛异常【java.lang.ArrayStoreException】
对第一行代码而言,Fruit和Apple存在继承关系不代表Fruit[]和Apple[]也存在同样的关系,但Java允许这么做,即类的继承关系延伸到了数组,这就是数组的协变性。
第二行代码运行时才抛异常,表明数组会记住内部元素的具体类型,并且在运行时做检查,这里数组的具体类型时Apple,但却试图存储Orange类型,所以一定会报错。
为什么号称强类型的Java要引用这种不太安全的设计呢,James Gosling回答说,他们希望有一个处理数组的通用方法,这个方法签名能够接收所有的数组元素。比如
public static boolean equals(Object[] a, Object[] b) {
...
}
引入数组协变后,这个目的也就达到了。后来Java引入泛型,方法可以传入类型参数,这个特性也就没有意义,考虑到兼容问题这个特性依然保留着。
5.3 泛型与不变性
与数组的协变性不同,泛型是不变的,也就是说下面的代码编译报错
List<Apple> appleList = new ArrayList<Apple>();
List<Fruit> fruitList = appleList; // 错误: 不兼容的类型: List<Apple>无法转换为List<Fruit>
为什么要这么设计呢?首先我们知道Java泛型是为避免ClassCastException异常的,如果我们让泛型支持协变,那么看下面的代码
fruitList.add(new Orange()); // Orange作为Fruit的子类型,ok
Apple apple = appleList.get(0); // 取出的对象实际类型是Orange,泛型却试图转型为Apple,一定会抛ClassCastException异常
综上,为了保证泛型的正确转型,泛型是不变的。
5.4 泛型数组
首先明确一点,Java是不允许直接创建泛型数组的。下面的代码会编译失败
List<Fruit>[] fruitList= new ArrayList<Fruit>[10]; // idea直接报错,Error java:创建泛型数组
Java为什么要这么约束呢,这一个Java泛型设计点原则有关
如果一段代码在编译时系统没有产生“[unchecked]未经检查的转换”警告,则程序在运行时不会引发ClassCastException异常。 换句话说,直接创建泛型数组是有可能抛出未经检查的异常。那么什么情况下会发生这种事呢,看下面代码
List<String>[] stringListArray = new ArrayList<String>[10]; // 假设可以创建泛型数组
Object[] objectArray = stringListArray; // 数组协变,Object[]可接受任何类型的数组
List<Integer> intList = Arrays.asList(42);
objectArray[0] = intList; // 任何对象都可赋给Object
String s = stringListArray[0].get(0);
分析最后一行代码String s = stringListArray[0].get(0);
stringListArray[0]是一个List对象,List取出元素前,编译器会通过checkcast指令进行类型检查,如果没有完成类型检查,则会报出类似ClassCastException。
这里想得到String类型,但它的实际类型却是Integer,抛出ClassCastException,违背Java泛型设计原则。
那么问题出在哪儿呢?我们的泛型约束呢?为何形同虚设。最关键的地方是第二行代码,数组协变使泛型数组摆脱了严格的类型检查,这个漏洞可以允许你添加任何对象,泛型变得毫无意义,而数组协变特性作为历史遗留问题暂时不可能移除,所以Java决定不允许创建泛型数组是合理的。
5.5 创建泛型数组
如果我们确实需要创建泛型数组,是可以实现的。
- 泛型类数组
public static class Fruit<T> {}
Fruit<Integer>[] fruitArray; // 声明
//fruitArray = (Fruit<Integer>[]) new Object[10]; // ClassCastException,[Ljava.lang.Object; cannot be cast to [LFruit;
fruitArray = (Fruit<Integer>[]) new Fruit[10]; // 关键代码,创建一个原生类型的数组并转型。
System.out.println(fruitArray.getClass().getSimpleName()); // 打印:Fruit[]
fruitArray[0] = new Fruit<>(); // 插入new Object(),new Double()均会报错
Fruit<Integer> fruit = fruitArray[0]; // 此处也无需我们转型
- 类型参数数组
public static class Fruit<T> {
private T[] categories;
public Fruit(int size) {
categories = (T[])new Object[size];
}
public void put(int index, T item) {
categories[index] = item;
}
public T get(int index) { return categories[index]; }
public T[] all() { return categories; }
public static void main(String[] args) {
Fruit<String> fruit = new Fruit<>(10);
fruit.put(0,"banana"); // ok
System.out.println(fruit.get(0)); // ok
String[] categories = fruit.all(); // ClassCastException:[Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
}
}
因为泛型的擦除机制,T[]的实际类型是Object[]。all()方法将Object[]转型为String[]当然会报错。
不是说擦除吗,这个String类型怎么还存在?因为编译器已经将它写入了字节码中,当调用all()方法时,checkcast指令检测到异常会抛ClassCastException。
为什么构造器中 (T[])new Object[size]不会抛异常呢,因为类型擦除,相当于 (Object[])new Object[size]
注意:如果Fruit该为Fruit,那么 (T[])new Object[size]相当于 (String[])new Object[size],肯定会抛ClassCastException
如果将Fruit类改造成下面的形式呢
public static class Fruit<T> {
private Object[] categories;
public Fruit(int size) {
categories = new Object[size];
}
public void put(int index, T item) {
categories[index] = item;
}
public T get(int index) {
return (T) categories[index];
}
public T[] all() {
return (T[]) categories;
}
}
调用它的all()方法还是会抛ClassCastException异常,因为数组的类型还是Object[]
- 类型标志
public static class Fruit<T> {
private T[] categories;
public Fruit(Class<T> clz,int size) {
categories = (T[]) Array.newInstance(clz,size);
}
public void put(int index, T item) {
categories[index] = item;
}
public T get(int index) {
return categories[index];
}
public T[] all() {
return categories;
}
public static void main(String[] args) {
Fruit<String> fruit = new Fruit<>(String.class,10);
fruit.put(0, "banana");
System.out.println(fruit.get(0));
String[] categories = fruit.all();
}
}
推荐
6 泛型的有界类型
6.1 <? extends T>
以List<? extends Fruit>为例,不能添加,只能取值。
不知道它的具体类型,只知道它属于Fruit的子类
- 如果你添加一个
Orange类,错!它可以指向List<Apple>。 - 你可能想到添加
Fruit总可以吧,错!它可以指向List<Apple>,等你取出这个对象时,Fruit转型为Apple会抛ClassCastException异常的。
但是你可以取值,因为取出的所有对象都可以向上转型为Fruit。
依我来看,
List<? extends Fruit>的上界类型表示的是一种能力,想要表达的是它既可以指向List<Apple>,也可以指向List<Orange>,但不会指代某一具体类型。就好比我喜欢喝饮料,可乐也行,雪碧也行,但你不能把饮料窄化成某一种具体的饮料。
6.2 <? super T>
以List<? super Apple>为例,可以添加,也可以取值,但取出的都是Object。
不知道它的具体类型,只知道它属于Apple的某个父类。
- 一开始我以为可以添加
Apple的任意父类对象,但这是错的。why,这个父类是不确定的,你添加Fruit,但它可以指向List<Food>。 - 你可以添加
Apple及其子类,因为不管它是什么类型,Apple类总可以向上转成这种类型。
虽然你并不清楚它的具体类型,但你可以保证它是一个Object
6.2 <?>
这玩意儿既不能读也不能取,貌似没什么作用。但它可以直观的提醒我们,这个参数的用途跟它的具体类型没有关系,比如List<?>,可能只是计算它的size。
7 结语
参考资料:
java泛型 泛型的内部原理:类型擦除以及类型擦除带来的问题