Java基础-泛型
1.概述
Java泛型(generics)是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter)。声明的类型参数在使用时用具体的类型来替换。 可用于泛型类、泛型接口、泛型方法。
Java的泛型是JDK5新引入的特性,为了向下兼容,虚拟机其实是不支持泛型,所以Java实现的是一种伪泛型机制,也就是说Java在编译期擦除了所有的泛型信息,这样Java就不需要产生新的类型到字节码,所有的泛型类型最终都是一种原始类型,在Java运行时根本就不存在泛型信息。
如何理解上面的定义呢,类型参数是什么?
说起参数,都会想到定义方法时要定义形参,调用方法时要传入实参。当一个方法可以适配多种类型的参数,
可以将形参设为Object类型,等处理完毕后再强制转换为想要的类型,错误的类型转换会在运行时导致程序奔溃。
比如:
static void testGenerics() {
List list = new ArrayList();
String a = "A";
Integer b = 2;
list.add(a);
list.add(b);
for (Object o : list) {
String tmp = (String) o;
System.out.println(tmp);
}
}
将Integer转Object再转为String类型,编译不会报错,但运行时会报错。
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
我们希望在写代码时尽早发现这类问题,在使用阶段可以像指定实参一样指定类型,泛型在使用阶段指订参事类型,在编译阶段就避免了错误的强制转换。
boolean add(E e);
上面的参数类型E是在初始化List时指定的,在编译阶段就会检查是否出错。
参数存在于方法中,类和接口中存一系列相关的方法,这就引出了泛型应用的三种常见使用方式泛型类、泛型接口、泛型方法。
使用泛型可以让代码更健壮,编译期检查类型转换的错误。
省略了强制类型转换,代码更简洁。代码可以更加灵活可以复用。
2.相关概念
泛型参数列表:<K,V>
列表中大写字母随意定。
原始类型:ArrayList
泛型类型:ArrayList<E>
类型参数:E
参数化类型:ArrayList<Integer>
实际参数类型:Integer
类型绑定:<T extends Fruit&Serializable>
T 是Fruit的子类并且实现了Serializable接口。
通配符:?
限定通配符的上边界:<? extends Number >
限定通配符的下边界:<? super Integer >
桥接:在泛型擦除过程中,父类中的泛型方法中的类型被擦除成Object类型,而子类中进行复写的方法会保留具体的泛型类型,本来要进行复写的方法出现不同的入参,变成重载了。桥接方法就是将父类中擦除泛型的方法在其内部调用子类同名方法的实现,就保证了复写。
协变:
逆变:
3.泛型应用
3.1 泛型方法
定义泛型方法,只需要将泛型参数列表<K,V>
置于返回值之前。
public class GenericsMethod {
public <K, V> V genericsMet(K input) {
return (V) input;
}
}
以上就是普通的泛型方法,指定输入类型为K,返回类型为V ,K和V的具体类型要到使用时才可以确定。
public static void main(String[] args) {
GenericsMethod genericsMethod = new GenericsMethod();
int a = genericsMethod.genericsMet(3);
String b = genericsMethod.genericsMet("dd");
//String c = genericsMethod.genericsMet(3);
}
入参为3 是Integer类型,则K为Integer类型;承接返回值的a是Integer类型,所以V为Integer类型,如果a为String类型那么V为String类型。
入参列表中的泛型类型由实参类型决定,而返回值中的泛型类型由承接返回值的变量类型决定。
3.2 泛型类
定义泛型类,只需要在定义类时将泛型参数列表置于类名称之后。
public class GenericsClass<T> {
private T args; // 定义泛型类型的成员变量
public GenericsClass(T args) { // 泛型构造方法的参数类型也为T
this.args = args;
}
public T getArgs() { // 注意该方法不是泛型方法
return args;
}
public void setArgs(T args) {
this.args = args;
}
}
以上泛型参数T的实际类型可以通过定义GenericsClass实例对象时确定,如下:
// 创建引用时指定泛型参数类型为Integer
GenericsClass<Integer> stringGenericsClass = new GenericsClass<Integer>();
// 在new对象时可以不指定返程参数类型
GenericsClass<String> stringGenericsClass1 = new GenericsClass<>();
在创建stringGenericsClass引用的时候确定了T为Integer,new 对象的过程指定的泛型参数要和创建引用时用的泛型参数一致。
当然创建引用时也可以不指定泛型参数,此时在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。
GenericsClass stringGenericsClass = new GenericsClass<>(23);
stringGenericsClass.setArgs(24);
stringGenericsClass.setArgs("abcd");
GenericsClass stringGenericsClass1 = new GenericsClass<>("abc");
stringGenericsClass1.setArgs(25);
stringGenericsClass1.setArgs(new Person());
3.3 泛型接口
定义泛型接口,只需要在定义接口时,在接口名称后添加泛型参数列表。这和泛型类定义一样。
public interface GenericsInterface<T,V> {
V transform(T arg);
}
- 在使用泛型接口时,可以在实现类中继续使用泛型参数,不明确泛型类型。
// 继续使用接口中的泛型参数,将具体类型的指定留到该类使用阶段。
public class GenericsInterfaceImpl<K,V> implements GenericsInterface<K,V> {
@Override
public V transform(K arg) {
return null;
}
}
- 当然也可以在接口中指定泛型类型,此时实现类无需添加泛型参数列表,明确泛型类型。
// 在实现接口时就指定<K,V> 的具体类型。
public class GenericsInterfaceImpl implements GenericsInterface<Integer,String> {
@Override
public String transform(Integer arg) {
return null;
}
}
3.4 限定类型
以上泛型变量都是派生自Object类,所以在泛型方法的内部实现中,T变量只能使用Object自带的方法。这大大局限了泛型变量T能实现的功能。
如何为泛型变量T添加更多能力呢??? 这就是类型绑定的作用,通过为T绑定更多接口或者父类,T就可以使用这些接口和父类中的方法。
类型绑定使用extends
关键字,首先明确这和继承是不一样的。
定义绑定:<T extends Comparable>
多重绑定:<T extends Car & Comparable & Serializable>
多重绑定时如果有实体类,则只能有一个实体类,后面可以配合多个接口类型,实体类型必须排在最前面。
示例1:绑定接口
// 先定义一个接口
public interface Comparable<T> {
public int compareTo(T o);
}
// 在方法泛型参数中绑定该Comparable接口
public <G extends Comparable< G >> G max(G... input) {
G maxNode = input[0];
for (G tmp : input) {
if (maxNode.compareTo(tmp) < 0) {
maxNode = tmp;
}
}
return maxNode;
}
以上泛型参数类型G就可以使用compareTo方法。
示例2:绑定类
// 先定义一个基类
public class Car {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// 定义两个继承类
class ACar extends Car {
public ACar() {
setName("ACar");
}
@Override
public String getName() {
return "AType" + super.getName();
}
}
class BCar extends Car {
public BCar() {
setName("BCar");
}
@Override
public String getName() {
return "BType" + super.getName();
}
}
定义一个绑定类的泛型方法。
public static <T extends Car> String getCarName(T car) {
return car.getName();
}
调用泛型方法。
String aCarName = getCarName(new ACar());
String bCarName = getCarName(new BCar());
System.out.println(aCarName);
System.out.println(bCarName);
3.5 泛型通配符
通配符只能在创建泛型类的引用中使用,用于填充泛型T,不能用在泛型定义过程中。
不能再泛型方法中使用通配符。
无边界通配符<?>
? 通配符的意义就是它是一个未知的符号,可以是代表任意的类。 通配符是用来在创建引用时填充泛型T的。
// 在创建泛型引用时,需要明确类型,后面接对应泛型类实例。
GenericsClass<String> stringGenericsClass2 = new GenericsClass<>("abc");
GenericsClass<Integer> stringGenericsClass3 = new GenericsClass<Integer>(23);
// 使用通配符?的引用,可以接多种泛型类实例。
GenericsClass<?> stringGenericsClass = new GenericsClass<>(23);
stringGenericsClass = new GenericsClass<String>("abc");
stringGenericsClass = new GenericsClass<Integer>(33);
// 不指定泛型类型时,同样可以接多种泛型类实例。
GenericsClass stringGenericsClass1 = new GenericsClass<>("abc");
stringGenericsClass1 = new GenericsClass<String>("abc");
stringGenericsClass1 = new GenericsClass<Integer>(23);
因为一个泛型类,如果省略了填充类型,默认填充的是无边界通配符。
限定通配符的上边界 <? extends Number > 上边界只能读不能写
<? extends Number >
限定了通配符的上边界,限定了上边界的泛型变量,只能接受上边界及其子类的泛型实例。
// 定义一个继承体系
public class Person {}
public class Employee extends Person {}
public class Manager extends Employee {}
public class CTO extends Manager{}
这里我们看下具体使用
public static void topLimitDemo(){
// 泛型上界规定了list只能持有T为Manager及其子类的容器实例。
List<? extends Manager> list;
// list 无法指向T为 Person 或 Employee的容器实例。
// list = new ArrayList<Person>();
// list = new ArrayList<Employee>();
// list 只能指向Manager及其子类的容器实例。
list = new ArrayList<Manager>();
list = new ArrayList<CTO>();
}
规定了上边界的泛型变量只能读不能存。
// 存入元素
// list.add(new Manager());
// list.add(new CTO());
// 读取时因为子类对象向上转型为Manager所以是可以成功的。
Manager tmp = list.get(0);
无法存入元素
单看list只知道它指向的是Manager及其子类的容器实例,但无法确定具体类型,所以add时编译器无法确定是否能正确转型。 假设list指向的是new ArrayList();是无法将一个Manager对象转为CTO对象,所以无法添加成功。
可以读取元素
读取元素是按向上转型的,读取时因为子类对象向上转型为Manager所以是可以成功的。
限定通配符的下边界:<? super Integer > 下边界只能写不能读
<? super Integer >
限定了通配符的下边界,限定了下边界的泛型变量,只能接受下边界及其父类的泛型实例。
// 泛型下界规定了list只能持有T为Manager及其父类的容器实例。
List<? super Manager> list;
// list 只能持有Manager及其父类的容器实例。
list = new ArrayList<Person>();
list = new ArrayList<Employee>();
list = new ArrayList<Manager>();
// list 无法指向T为Manager子类的容器实例。
//list = new ArrayList<CTO>();
上面可以看出同一个list 可以接受T为Manager及其父类的容器实例。
可以存入元素
// 存入元素只能存入Manager或者Manager子类的元素。
list.add(new CTO());
list.add(new Manager());
// 无法存入父类元素
// list.add(new Employee());
// list.add(new Person());
无法读取元素,这里指编译器无法判断得到实例元素的具体类型,只会被认定为Object。
// 读取时无法得到容器内元素具体类型,返回为Object类型
Object o = list.get(0);
// CTO cto = list.get(0);
// Manager manager = list.get(1);
小结:
- 如果你想从一个数据类型里获取数据,使用 ? extends 通配符(能取不能存)
此时如果要插入通配符适配的类型,容器会接收 - 如果你想把对象写入一个数据结构里,使用 ? super 通配符(能存不能取)
- 如果你既想存,又想取,那就别用通配符。
使用通配符 一个目的 灵活的转型 API
4.泛型实现原理
要理解泛型的实现原理得先从类型擦除(Type Erasure)讲起
4.1 类型擦除
java的泛型基本上完全在编译器中实现,用于编译器执行类型检查和类型判断,然后生成普通的非泛型的字节码,这种实现技术为“擦除”(erasure) 。
什么意思呢????
就是虽然你写的代码里面可以任意指定泛型T,但是生成的字节码里面是没有你指定的T啊V啊之类的。因为T和V之类的大写字母就不是JVM中定义和合法类型,如果在字节码运行的部分保留这些东西,那运行就会出错。
如何理解呢?看下面的例子
示例1:
public static void main(String[] args) {
Class a = new ArrayList<String>().getClass();
Class b = new ArrayList<Integer>().getClass();
System.out.println(a == b);
// out: true
}
明明是两个不同的泛型容器,但实际生成的字节码是相同的,如下:
public static void main(String[] args) {
Class a = (new ArrayList()).getClass();
Class b = (new ArrayList()).getClass();
System.out.println(a == b);
}
在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。
泛型擦除保证了其不会在运行时出现。
既然是编译时期处理的,我们就首先要学会两个命令。
编译源代码:javac OriginClass.java
反编译生成的字节码文件:javap -v GeneralClass.class
-c 是生成分解的方法,-v 是显示更详细的信息。
当然我们可以通过Idea的插件ASM Bytecode Viewer来直接查看java文件生成的字节码,以下主要通过该方式来进行比较。
泛型类和普通类生成字节码的不同
普通类
public class OriginClass {}
生成的字节码
// class version 52.0 (52)
// access flags 0x21
public class com/zero/genericsdemo02/demo02/OriginClass {
// compiled from: OriginClass.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/zero/genericsdemo02/demo02/OriginClass; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
}
泛型类
public class GeneralClass<T> {}
生成的字节码
// class version 52.0 (52)
// access flags 0x21
// signature <T:Ljava/lang/Object;>Ljava/lang/Object;
// declaration: com/zero/genericsdemo02/demo02/GeneralClass<T>
public class com/zero/genericsdemo02/demo02/GeneralClass {
// compiled from: GeneralClass.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/zero/genericsdemo02/demo02/GeneralClass; L0 L1 0
// signature Lcom/zero/genericsdemo02/demo02/GeneralClass<TT;>;
// declaration: this extends com.zero.genericsdemo02.demo02.GeneralClass<T>
MAXSTACK = 1
MAXLOCALS = 1
}
对比以上两部分字节码我们可以发现,字节码中可运行的部分几乎一模一样。有差异的地方是泛型类的字节码中多了几行注释。
// signature <T:Ljava/lang/Object;>Ljava/lang/Object;
// declaration: com/zero/genericsdemo02/demo02/GeneralClass<T>
我们源代码中自己些的注释是不会被编译进字节码中的,而这两个注释实际上是Java为了支持泛型,在编译时将源代码中的泛型信息以属性的形式添加在常量池中,ASM等生成的动态字节码会以注释的形式展示。
就是被抹除的泛型信息会存在常量池中。通过javap -v GeneralClass.class
命令就可以看到其在常量池中的位置。
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()V
#2 = Class #13 // com/zero/genericsdemo02/demo02/GeneralClass
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 Signature
#9 = Utf8 <T:Ljava/lang/Object;>Ljava/lang/Object;
#10 = Utf8 SourceFile
#11 = Utf8 GeneralClass.java
#12 = NameAndType #4:#5 // "<init>":()V
注意这里的Signature属性,
Signature:存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。
java虚拟机规范中为了响应在泛型类中如何获取传入的参数化类型等问题,引入了signature,LocalVariableTypeTable等新的属性来记录泛型信息,所以所谓的泛型类型擦除,仅仅是对方法的code属性中的字节码进行擦除,而原数据中还是保留了泛型信息的,这些信息被保存在class字节码的常量池中,使用了泛型的代码调用处会生成一个signature签名字段,signature指明了这个常量在常量池的地址,这样我们就找到了参数化类型。这样我们也知道 现在就明白了泛型擦除不是擦除全部
我们拆解下这个属性
<T:Ljava/lang/Object;>
这部分代表泛型类中<T>
说明T被擦除成Object类型。
Ljava/lang/Object;
这部分代表返回类型为Object。
【TODO】 这里后期得学习JVM之后再回顾。
泛型方法
public class GeneralClass<L, K> {
public <E> String test(E input) {
return "";
}
}
生成的部分字节码
// access flags 0x1
// signature <E:Ljava/lang/Object;>(TE;)Ljava/lang/String;
// declaration: java.lang.String test<E>(E)
public test(Ljava/lang/Object;)Ljava/lang/String;
L0
LINENUMBER 6 L0
LDC ""
ARETURN
L1
LOCALVARIABLE this Lcom/zero/genericsdemo02/demo02/GeneralClass; L0 L1 0
// signature Lcom/zero/genericsdemo02/demo02/GeneralClass<TL;TK;>;
// declaration: this extends com.zero.genericsdemo02.demo02.GeneralClass<L, K>
LOCALVARIABLE input Ljava/lang/Object; L0 L1 1
// signature TE;
// declaration: input extends E
MAXSTACK = 1
MAXLOCALS = 2
从中可以看出,源代码中泛型方法已经擦除了泛型类型,将入参擦除成了Object类型。但将泛型方法信息保存在Signature中。
示例2:
我们知道java在编译阶段进行类型检查,向一个Integer容器添加String是无法通过编译的。
List<Integer> list = new ArrayList<Integer>();
list.add(33);
// list.add("abc"); 无法通过编译
try {
Method method = list.getClass().getMethod("add", Object.class);
method.invoke(list,"abc");
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(list); // out:[33, abc]
因为填充的泛型类型Integer在字节码中被擦除了,被替换为Object;在运行时只要是一个Object类型就可以add进List。反射是在运行时调用方法,跳过了编译器的类型检查,所以可以将String添加到一个指明类型是Integer的List中。
小结:
类型擦除的过程
首先找到用来替换类型参数的具体类,一般是Object,如果指定了类型参数的上界则使用上界类型。将代码中的类型参数都替换成具体类,去掉类型声明,如:T get()
变成Object get() 去掉<>
。最后由于类型擦除后少了部分方法,则需要生成一些桥接方法作为补充。
1. 检查泛型类型,获取目标类型
2. 擦除类型变量,并替换为限定类
如果泛型类型的类型变量没有限定(<T>),则用Object作为原始类型
如果有限定(<T extends XClass>),则用边界类型XClass作为原始类型
如果有多个限定(T extends XClass1&XClass2),则使用第一个边界XClass1作为原始类
3. 在必要时插入类型转换以保持类型安全
4. 生成桥方法以在扩展时保持多态性
4.2 桥方法
因为 java 在编译源码时, 会进行 类型擦除, 导致泛型类型被替换限定类型(无限定类型就使用 Object
). 因此为保持继承和重载的多态特性, 编译器会生成 桥方法.
什么是桥方法? 下面看个例子就清楚了。
public class Car<T> {
// 车装的货物
private T goods;
public T getGoods() {
return goods;
}
public void setGoods(T goods) {
this.goods = goods;
}
}
public class Truck extends Car<String>{
@Override
public void setGoods(String goods) {
}
@Override
public String getGoods() {
return super.getGoods();
}
}
因为类型擦除,所以父类的泛型T被替换成Object,本来继承重载了
下面是class文件反编译得出。
public class Truck extends Car{
public Truck() { }
// 子类中方法
public void setGoods(String goods) {
}
public String getGoods() {
return (String)super.getGoods();
}
// 父类中方法,底层调用了子类中的方法,这就是 桥方法
public volatile void setGoods(Object obj) {
setGoods((String)obj);
}
public volatile Object getGoods() {
return getGoods();
}
}
父类中的setGoods方法,底层依旧调用的是子类中的setGoods方法。
类型擦除的缺陷
- 泛型类型由于类型擦除,源代码中添加的类型信息都被移除了,所以有些运行期的操作无法实现,比如:转型,instanceof 和 new。
public class Erased<T> {
private static final int SIZE = 100;
public static void f(Object arg) {
//编译不通过
if (arg instanceof T) {}
//编译不通过
T var = new T();
//编译不通过
T[] array = new T[SIZE];
//编译不通过
T[] array = (T) new Object[SIZE];
}
}
- 泛型类型变量不能使用基本类型。
Erased 是无法通过编译的,因为基本类型无法放置到对象类型中。 - 静态成员变量不能使用泛型类型
因为泛型类中的泛型参数是在实例化时确定的。
4.静态成员方法可以使用泛型类型
因为泛型方法使用的泛型参数不是泛型类的泛型参数 ,而是在静态方法调用时指定的泛型参数类型。
5.泛型类型会导致方法冲突。
因为泛型方法中的泛型参数会被擦除成边界类型或者Object,如果同同名的方法入参为边界类型或者Object,就会导致生成的字节码中,code部分出现相同的方法。
6.Java中没有泛型数组
类型判断解决办法
通过使用定义一个工具类,使用Class的isInstance可以解决泛型实例类型比较的问题。
public class ClassType<T> {
Class<T> typeClass;
public ClassType(Class<T> typeClass) {
this.typeClass = typeClass;
}
public boolean isInstance(Object obj) {
return typeClass.isInstance(obj);
}
}
使用方式如下
public static void main(String[] args) {
ClassType<Employee> classType =new ClassType<>(Employee.class);
System.out.println(classType.isInstance(new Manager()));
System.out.println(classType.isInstance(new Employee()));
System.out.println(classType.isInstance(new Person()));
}
4.3 协变、逆变
逆变和协变需要从java中继承机制说起,子类继承父类那么可以在使用父类的时候使用子类替换。
如果B类是A类的派生类,那么B类的引用可以赋值给A类的引用。
java中使用赋值一般有两个地方,
(1)使用运算符显式赋值
Person person = new Employee();
使用父类型引用person持有子类型实例new Employee() 对象的引用。
(2)函数传参赋值
static void createHat(Person person){
System.out.println(person);
}
createHat(new Employee());
createHat 显示接收一个Person类型参数,我们传入一个子类型实例对象new Employee()同样可以编译通过正常运行。
小结:所以,Java中赋值操作一般左右类型相同,或者引用类型是实例类型的父类。实参是
但还有一类常见的赋值操作并不符合,即数组和容器的赋值。
Person[] peoples = new Employee[5];
List<? extends Person> personList = new ArrayList<Employee>();
List<? super Employee> personList1 = new ArrayList<Person>();
// Employee[] employees = new Person[5];
// List<Person> personList = new ArrayList<Employee>();
// List<Employee> employeeList = new ArrayList<Person>();
不同类型的数组、不同类型的容器为什么可以相互兼容? 这里就要涉及到协变和逆变的概念。
模式定义:假设F(x)是Java中的一种代码模式,x是其中可变的部分。
协变:如果B是A的子类,F(B)也能享受F(A)的待遇,子类实例享受父类待遇,那么F模式是协变的。
逆变:如果B是A的子类,F(A)也能享受F(B)的待遇,父类实例享受子类待遇,那么F模式是逆变的。
不变:如果F(A)和F(B)不享受任何继承待遇,那么F模式是不变的。
Java中的协变和逆变
协变:如果一个父类型容器引用可以持有一个子类型容器实例对象,则称发生了协变。如:
数组
Person[] peoples = new Employee[3];
Person 是Employee的父类,persons引用可以持有Employee数组实例,因为在Java中,数组是自带协变的。
虽然数组是协变的,但实际运行时添加元素到数组中去,依旧会做类型检查;如下面语句编译时不会报错,但在运行时会抛出错误。
Person[] peoples = new Employee[4];
peoples[0] = new Person();// 运行报错,子类实例类型的数组不能添加一个父类对象。
peoples[1] = new Manager();// Employee类型的数组实例可以接受 Employee及其子类Manager对象。
数组的协变设计,没有在编译阶段发现潜在问题,而将错误抛出延迟到了运行阶段,这是其为人诟病的地方。不过数组支持协变后,java.util.Arrays#equals(java.lang.Object[], java.lang.Object[])
这种类型的函数就不需要为每种可能的数组类型去分别实现一次了。数组的协变设计有历史版本兼容性方面的考虑等,Java的每一个设计可能不是最优的,但确实是设计者在当时的情况下可以做出的最好选择。
列表
// List<Person> personList = new ArrayList<Employee>(); 无法编译
泛型容器没有自带协变所以personList不能持有Employee类型的泛型容器。
通过使用泛型通配符上边界,容器类可以实现协变。
// 可以通过编译
List<? extends Person> personList = new ArrayList<Employee>();
但限定了通配符上边界会导致该容器只能读不能写,读到的元素也会被统一为上边界元素。
// 不能写 ,可以添加null
// personList.add(new Person()); 无法编译
// personList.add(new Employee()); 无法编译
// 只能读
Object object = personList.get(0);
Person person = personList.get(0);
Employee employee = (Employee) personList.get(0);
Manager manager = (Manager) personList.get(0);
不能写是因为,List在编译时已经将泛型擦除成Object,运行阶段无法做类型检查,只能根据变量声明在编译阶段进行类型检查,而List<? extends Person>
代表可以容纳任何Person子类,无法得出具体的类型,所以插入任何类型都是不安全的。
读的时候可以将子类实例对象转为上边界类型,转到具体子类需要强制转换。
逆变:如果一个父类型容器引用持有父类型容器实例,可以向其添加子类型实例变量。则称为逆变。
List<? super Employee> a = new ArrayList<Employee>();
List<? super Person> b = new ArrayList<Person>();
a = b;// 子类型可以接受父类型 实例容器
PECS原则 Producer Extends,Consumer Super
因为使用<? extends T>后,如果泛型参数作为返回值,用T接收一定是安全的,也就是说使用这个函数的人可以知道你生产了什么东西;
而使用<? super T>后,如果泛型参数作为入参,传递T及其子类一定是安全的,也就是说使用这个函数的人可以知道你需要什么东西来进行消费。
比如Java8新增的函数接口java.util.function.Consumer#andThen方法就体现了Consumer Super这一原则。
参考:
Java泛型详解, by jamesehng
java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一, by ViVieLeieLei
java基础巩固笔记(2)-泛型, by brianway
Java 泛型进阶, by 于晓飞93
夯实JAVA基本之一——泛型详解(2):高级进阶, by 启舰
java泛型:擦除/桥方法/协变(不要在新代码中使用原生态类型) ---- effective java notes,by soullines
Java进阶知识点:协变与逆变, by 爱养花的码农
https://juejin.cn/post/6910981275742371854#heading-5