文章

Java泛型

Java泛型

泛型

基本概念

什么是泛型?为什么要使用泛型?

泛型,即 “ 参数化类型 “。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。 那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。 也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

泛型,它的意思是把具体的类型泛化,编码的时候 (编译器) 用符号来指代类型,在使用的时候(运行时),再确定它的类型。

泛形的基本术语

以 ArrayList 为例:<> 念着typeof

  1. ArrayList 中的 E 称为 类型参数变量
  2. ArrayList 中的 Integer 称为 实际类型参数
  3. 整个称为 ArrayList 泛型类型
  4. 整个 ArrayList 称为 参数化的类型(ParameterizedType)

泛型基础

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。(如果一个类多处都要用到同一个泛型,这时可以把泛形定义在类上 (即类级别的泛型))

注意:静态方法不能使用类定义的泛形,而应单独定义泛形。

泛型类的最基本写法:

1
2
3
4
5
class 类名称 <泛型标识可以随便写任意标识号标识指定的泛型的类型>{
  private 泛型标识 /*(成员变量类型)*/ var; 
    // .....
  }
}

一个最普通的泛型类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{ 
    //key这个成员变量的类型为T,T的类型由外部指定  
    private T key;

    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}
  1. 泛型的类型参数只能是类类型,不能是简单类型。
  2. 不能对确切的泛型类型使用 instanceof 操作。如下面的操作是非法的,编译时会出错。
1
2
if(ex_num instanceof Generic<Number>) {   
}

泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:

1
2
3
4
//定义一个泛型接口
public interface Generator<T> {
    public T next();
}

当实现泛型接口的类,未传入泛型实参时:

1
2
3
4
5
6
7
8
9
10
11
/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

当实现泛型接口的类,传入泛型实参时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
 * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
 */
public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

泛型方法

泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。

Java 程序中的 普通方 法、构造方法静态方法 中都可以使用泛型。
方法使用泛形前,必须对泛形进行声明,

在调用泛型方法的时候,可以指定泛型,也可以不指定泛型。

  1. 在不指定泛型的情况下,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到 Object。
  2. 在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*不指定泛型的时候*/
Integer add = add(1, 3);
Number add1 = add(2.4, 3);
int i = add(1, 2); //这两个参数都是Integer,所以T为Integer类型
Number f = add(1, 1.2F);//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Number
Object o = add(1, "asd");//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Object

/*指定泛型的时候*/
int a = 泛型擦除.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类
int b = 泛型擦除.<Integer>add(1, 2.2F);//编译错误,指定了Integer,不能为Float
Number c = 泛型擦除.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Floa

// 这是一个简单的泛型方法
public static <T> T add(T x, T y) {
    return y;
}

泛型数组

在 Java 中是 “ 不能创建一个确切的泛型类型的数组 “ 的。

下面的这个例子是不可以的:

1
 List<String>[] ls = new ArrayList<String>[10];// 编译出错

而使用通配符创建泛型数组是可以的,如下面这个例子:

1
2
3
List<?>[] ls = new ArrayList<?>[10];  

List<String>[] ls3 = new ArrayList[10]; // 编译警告:Uncheck assignment

泛型特性

泛型与多态

1
2
3
4
5
6
TextView textView = new Button(context);
// 👆 1. 这是多态。

List<Button> buttons = new ArrayList<Button>();
List<TextView> textViews = buttons;
// 👆 2. 多态用在这里会报错 incompatible types: List<Button> cannot be converted to List<TextView>
  1. Button 是继承自 TextView 的,根据 Java 多态的特性,第一处赋值是正确的。
  2. 但是到了 List 的时候 IDE 就报错了,这是因为 Java 的泛型本身具有「不可变性 Invariance」,Java 里面认为 List 和 List 类型并不一致,也就是说,子类的泛型(List)不属于泛型(List)的子类。

泛型不支持多态;Java 的泛型类型会在编译时发生类型擦除,为了保证类型安全,不允许这样赋值。

在 Java 里用数组做类似的事情,是不会报错的,这是因为数组并没有在编译时擦除类型:

1
TextView[] textViews = new TextView[10];

但是在实际使用中,我们的确会有这种类似的需求,需要实现上面这种赋值。ava 提供了「泛型通配符」 ? extends? super 来解决这个问题。

泛型擦除

Java 的泛型是伪泛型,因为,在编译期间,所有的泛型信息都会被擦除掉(类型擦除(type erasure))。
Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

具体见 泛型擦除.md 章节

泛型通配符

[PECS]泛型中的通配符super T和extend T区别(协变、逆变).md 章节

? 无限定的通配符

相当于 ? extends Object,表示对类型没有什么限制,可以把?看成所有类型的父类

1
2
ArrayList<T> al=new ArrayList<T>(); 指定集合元素只能是T类型
ArrayList<?> al=new ArrayList<?>();集合元素可以是任意类型

? extend 上界,协变

上界;协变;? extend T 只能 get,只能向外提供数据,相对于外界来说是作为生产者(Producer extends)

? super 下界,逆变

下界;逆变;? super T 只能 add,只能从外界获取数据,相对于外界来说是作为消费者(Consumer super)

反射获取泛型真实类型

当我们对一个泛型类进行反射时,需要的到泛型中的真实数据类型,来完成如 json 反序列化的操作。此时需要通
过 Type 体系来完成。Type 接口包含了一个实现类 (Class) 和四个实现接口,他们分别是:

  1. TypeVariable 泛型类型变量,可以泛型上下限等信息;
  2. ParameterizedType 具体的泛型类型,可以获得元数据中泛型签名类型 (泛型真实类型)
  3. GenericArrayType 当需要描述的类型是泛型类的数组时,比如 List[],Map[],此接口会作为 Type 的实现。
  4. WildcardType  通配符泛型,获得上下限信息

TypeVariable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class TestType <K extends Comparable & Serializable, V> {
    K key;
    V value;
    public static void main(String[] args) throws Exception {
            // 获取字段的类型
            Field fk = TestType.class.getDeclaredField("key");
            Field fv = TestType.class.getDeclaredField("value");
            TypeVariable keyType = (TypeVariable)fk.getGenericType();
            TypeVariable valueType = (TypeVariable)fv.getGenericType();
            // getName 方法
            System.out.println(keyType.getName()); // K
            System.out.println(valueType.getName()); // V
            // getGenericDeclaration 方法
            System.out.println(keyType.getGenericDeclaration()); // class com.test.TestType
            System.out.println(valueType.getGenericDeclaration()); // class com.test.TestType
            // getBounds 方法
            System.out.println("K 的上界:"); // 有两个
            for (Type type : keyType.getBounds()) { // interface java.lang.Comparable
            System.out.println(type); // interface java.io.Serializable
            }
            System.out.println("V 的上界:"); // 没明确声明上界的, 默认上界是 Object
            for (Type type : valueType.getBounds()) { // class java.lang.Object
            System.out.println(type);
        }
    }
}

ParameterizedType

1
2
3
4
5
6
7
8
9
10
11
12
public class TestType {
    Map<String, String> map;
    public static void main(String[] args) throws Exception {
        Field f = TestType.class.getDeclaredField("map");
        System.out.println(f.getGenericType()); // java.util.Map<java.lang.String, java.lang.String>
        ParameterizedType pType = (ParameterizedType) f.getGenericType();
        System.out.println(pType.getRawType()); // interface java.util.Map
        for (Type type : pType.getActualTypeArguments()) {
            System.out.println(type); // 打印两遍: class java.lang.String
        }
    }
}

GenericArrayType

1
2
3
4
5
6
7
8
public class TestType<T> {
    List<String>[] lists;
    public static void main(String[] args) throws Exception {
        Field f = TestType.class.getDeclaredField("lists");
        GenericArrayType genericType = (GenericArrayType) f.getGenericType();
        System.out.println(genericType.getGenericComponentType());
    }
}

WildcardType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TestType {
    private List<? extends Number> a; // 上限
    private List<? super String> b; //下限
    public static void main(String[] args) throws Exception {
        Field fieldA = TestType.class.getDeclaredField("a");
        Field fieldB = TestType.class.getDeclaredField("b");
        // 先拿到范型类型
        ParameterizedType pTypeA = (ParameterizedType) fieldA.getGenericType();
        ParameterizedType pTypeB = (ParameterizedType) fieldB.getGenericType();
        // 再从范型里拿到通配符类型
        WildcardType wTypeA = (WildcardType) pTypeA.getActualTypeArguments()[0];
        WildcardType wTypeB = (WildcardType) pTypeB.getActualTypeArguments()[0];
        // 方法测试
        System.out.println(wTypeA.getUpperBounds()[0]); // class java.lang.Number
        System.out.println(wTypeB.getLowerBounds()[0]); // class java.lang.String
        // 看看通配符类型到底是什么, 打印结果为: ? extends java.lang.Number
        Gson反序列化
        System.out.println(wTypeA);
    }
}

Gson 反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static class Response<T> {
    T data;
    int code;
    String message;

    @Override
    public String toString() {
        return "Response{" +
                "data=" + data +
                ", code=" + code +
                ", message='" + message + '\'' +
                '}';
    }

    public Response(T data, int code, String message) {
        this.data = data;
        this.code = code;
        this.message = message;
    }
}

static class Data {
    String result;

    public Data(String result) {
        this.result = result;
    }

    @Override
    public String toString() {
        return "Data{" +
                "result=" + result +
                '}';
    }
}

public class Test {

    public static void main(String[] args) {
        Response<Data> dataResponse = new Response(new Data("数据"), 1, "成功");
        Gson gson = new Gson();
        String json = gson.toJson(dataResponse);
        System.out.println(json);
        //为什么TypeToken要定义为抽象类?
        Response<Data> resp = gson.fromJson(json, new TypeToken<Response<Data>>() {
        }.getType());
        System.out.println(resp.data.result);
    }
}

在进行 GSON 反序列化时,存在泛型时,可以借助 TypeToken 获取 Type 以完成泛型的反序列化。
但是为什么 TypeToken 要被定义为抽象类呢?

因为只有定义为抽象类或者接口,这样在使用时,需要创建对应的实现类,此时确定泛型类型,编译才能够将泛型
signature 信息记录到 Class 元数据中。

虚拟机是如何实现泛型的?

泛型思想早在 C++ 语言的模板(Template)中就开始生根发芽,在 Java 语言处于还没有出现泛型的版本时,只能通过 Object 是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。,由于 Java 语言里面所有的类型都继承于 java.lang.Object,所以 Object 转型成任何对象都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个 Object 到底是个什么类型的对象。在编译期间,编译器无法检查这个 Object 的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多 ClassCastException 的风险就会转嫁到程序运行期之中。

泛型技术在 C#和 Java 之中的使用方式看似相同,但实现上却有着根本性的分歧,C#里面泛型无论在程序源码中、编译后的 IL 中(Intermediate Language,中间语言,这时候泛型是一个占位符),或是运行期的 CLR 中,都是切实存在的,List<int>与 List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。

Java 语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的 Java 语言来说,ArrayList<int>与 ArrayList<String>就是同一个类,所以泛型技术实际上是 Java 语言的一颗语法糖,Java 语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。

由于 Java 泛型的引入,各种场景(虚拟机解析、反射等)下的方法调用都可能对原有的基础产生影响和新的需求,如在泛型类中如何获取传入的参数化类型等。因此,JCP 组织对虚拟机规范做出了相应的修改,引入了诸如 Signature、LocalVariableTypeTable 等新的属性用于解决伴随泛型而来的参数类型的识别问题,Signature 是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名 [3],这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。修改后的虚拟机规范要求所有能识别 49.0 以上版本的 Class 文件的虚拟机都要能正确地识别 Signature 参数。
另外,从 Signature 属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的 Code 属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。

泛型擦除介绍

Java 的泛型是伪泛型,因为,在编译期间,所有的泛型信息都会被擦除掉(类型擦除(type erasure))。

  1. 泛型信息只存在代码编译阶段,在进⼊ JVM 之前,与泛型关的信息都会被擦除掉
  2. 在类型擦除的时候,如果泛型类⾥的类型参数没有指定上限,则会被转成 Object 类型,如果指定了上限,则会被转换成对应的类型上限。
  3. Java 中的泛型基本上都是在编译器这个层次来实现的。⽣成的 Java 字节码中是不包含泛型中的类型信息的。使⽤泛型的时候加上的类型参数,会在编译器编译的时候擦除掉,这个过程就称为类型擦除

泛型擦除

Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

如在代码中定义的 List

案例 1:

1
2
3
4
5
6
7
8
9
public class Test4 {
	public static void main(String[] args) {
		ArrayList<String> arrayList1=new ArrayList<String>();
		arrayList1.add("abc");
		ArrayList<Integer> arrayList2=new ArrayList<Integer>();
		arrayList2.add(123);
		System.out.println(arrayList1.getClass()==arrayList2.getClass()); // true
	}
}

在这个例子中,我们定义了两个 ArrayList 数组,不过一个是 ArrayList 泛型类型,只能存储字符串。一个是 ArrayList 泛型类型,只能存储整形。最后,我们通过 arrayList1 对象和 arrayList2 对象的 getClass 方法获取它们的类的信息,最后发现结果为 true。说明泛型类型 String 和 Integer 都被擦除掉了,只剩下了原始类型。

案例 2:

1
2
3
4
5
6
7
8
9
public class Test4 {
	public static void main(String[] args) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
		ArrayList<Integer> arrayList3=new ArrayList<Integer>();
		arrayList3.add(1);//这样调用add方法只能存储整形,因为泛型类型的实例为Integer
		arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
		for (int i=0;i<arrayList3.size();i++) {
			System.out.println(arrayList3.get(i));
		}
	}

在程序中定义了一个 ArrayList 泛型类型实例化为 Integer 的对象,如果直接调用 add 方法,那么只能存储整形的数据。不过当我们利用反射调用 add 方法的时候,却可以存储字符串。这说明了 Integer 泛型实例在编译之后被擦除了,只保留了原始类型,在运行时也可以添加。

类型擦除引起的问题及解决方法

先检查,在编译

Java 编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,在进行编译的。

1
2
3
4
5
public static  void main(String[] args) {
	ArrayList<String> arrayList=new ArrayList<String>();
	arrayList.add("123");
	arrayList.add(123); // 编译错误
}

检查编译的对象和引用传递的问题

1
2
ArrayList<String> arrayList1 = new ArrayList(); // 第一种 情况
ArrayList arrayList2 = new ArrayList<String>();// 第二种 情况

本来类型检查就是编译时完成的。new ArrayList() 只是在内存中开辟一个存储空间,可以存储任何的类型对象。而真正涉及类型检查的是它的引用,因为我们是使用它引用 arrayList1 来调用它的方法,比如说调用 add() 方法。所以 arrayList1 引用能完成泛型类型的检查。

类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test10 {
	public static void main(String[] args) {
		
		ArrayList<String> arrayList1=new ArrayList();
		arrayList1.add("1");//编译通过
		arrayList1.add(1);//编译错误
		String str1=arrayList1.get(0);//返回类型就是String
		
		ArrayList arrayList2=new ArrayList<String>();
		arrayList2.add("1");//编译通过
		arrayList2.add(1);//编译通过
		Object object=arrayList2.get(0);//返回类型就是Object
		
		new ArrayList<String>().add("11");//编译通过
		new ArrayList<String>().add(22);//编译错误
		String string=new ArrayList<String>().get(0);//返回类型就是String
	}
}

自动类型转换

因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?看下 ArrayList 和 get 方法:

1
2
3
4
public E get(int index) {
	RangeCheck(index);
    return (E) elementData[index];
}

看以看到,在 return 之前,会根据泛型变量进行强转。

写了个简单的测试代码:

1
2
3
4
5
6
7
public class Test {
    public static void main(String[] args) {
        ArrayList<Date> list=new ArrayList<Date>();
        list.add(new Date());
        Date myDate=list.get(0);
    }
}

然后反编了下字节码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(java.lang.String[]);
Code:
0: new #16 // class java/util/ArrayList
3: dup
4: invokespecial #18 // Method java/util/ArrayList."<init
:()V
7: astore_1
8: aload_1
9: new #19 // class java/util/Date
12: dup
13: invokespecial #21 // Method java/util/Date."<init>":()
 
16: invokevirtual #22 // Method java/util/ArrayList.add:(L
va/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
22: invokevirtual #26 // Method java/util/ArrayList.get:(I
java/lang/Object;
25: checkcast #19 // class java/util/Date
28: astore_2
29: return

看第 22 ,它调用的是 ArrayList.get() 方法,方法返回值是 Object,说明类型擦除了。然后第 25,它做了一个 checkcast 操作,即检查类型 #19, 在在上面找 #19 引用的类型,他是
9: new #19 // class java/util/Date
是一个 Date 类型,即做 Date 类型的强转。
所以不是在 get 方法里强转的,是在你调用的地方强转的。

类型擦除与多态的冲突和解决方法

现在有这样一个泛型类:

1
2
3
4
5
6
7
8
9
class Pair<T> {
	private T value;
	public T getValue() {
		return value;
	}
	public void setValue(T value) {
		this.value = value;
	}
}

一个子类继承它:

1
2
3
4
5
6
7
8
9
10
class DateInter extends Pair<Date> {
	@Override
	public void setValue(Date value) {
		super.setValue(value);
	}
	@Override
	public Date getValue() {
		return super.getValue();
	}
}

在这个子类中,我们设定父类的泛型类型为 Pair<Date>,在子类中,我们覆盖了父类的两个方法,我们的原意是这样的:
将父类的泛型类型限定为 Date,那么父类里面的两个方法的参数都为 Date 类型:

1
2
3
4
5
6
7
8
9
class Pair {
	private Date value;
	public Date getValue() {
		return value;
	}
	public void setValue(Date value) {
		this.value = value;
	}
}

可是由于种种原因,虚拟机并不能将泛型类型变为 Date,只能将类型擦除掉,变为原始类型 Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突。

于是 JVM 采用了一个特殊的方法,来完成这项功能,那就是桥方法

首先,我们用 javap -c className 的方式反编译下 DateInter 子类的字节码,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {
  com.tao.test.DateInter();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method com/tao/test/Pair."<init>"
:()V
       4: return
 
  public void setValue(java.util.Date);  //我们重写的setValue方法
    Code:
       0: aload_0
       1: aload_1
       2: invokespecial #16                 // Method com/tao/test/Pair.setValue
:(Ljava/lang/Object;)V
       5: return
 
  public java.util.Date getValue();    //我们重写的getValue方法
    Code:
       0: aload_0
       1: invokespecial #23                 // Method com/tao/test/Pair.getValue
:()Ljava/lang/Object;
       4: checkcast     #26                 // class java/util/Date
       7: areturn
 
  public java.lang.Object getValue();     //编译时由编译器生成的巧方法
    Code:
       0: aload_0
       1: invokevirtual #28                 // Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法
;
       4: areturn
 
  public void setValue(java.lang.Object);   //编译时由编译器生成的巧方法
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #26                 // class java/util/Date
       5: invokevirtual #30                 // Method setValue:(Ljava/util/Date;   去调用我们重写的setValue方法
)V
       8: return
}

从编译的结果来看,我们本意重写 setValue 和 getValue 方法的子类,竟然有 4 个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是 Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的 setvalue 和 getValue 方法上面的@Oveerride 只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。

不过,要提到一点,这里面的 setValue 和 getValue 这两个桥方法的意义又有不同。

泛型类型变量不能是基本数据类型

不能用类型参数替换基本类型。就比如,没有 ArrayList,只有 ArrayList。因为当类型擦除后,ArrayList 的原始类型变为 Object,但是 Object 类型不能存储 double 值,只能引用 Double 的值。

运行时类型查询

1
2
3
4
5
6
7
ArrayList<String> arrayList = new ArrayList<String>();  // 因为类型擦除之后,ArrayList<String>只剩下原始类型,泛型信息String不存在了。

// 那么,运行时进行类型查询的时候使用下面的方法是错误的
if( arrayList instanceof ArrayList<String>) 

// Java限定了这种类型查询的方式
if( arrayList instanceof ArrayList<?>)

泛型类型的不能实例化

Ref

讲解的很详细

泛型通配符之? super 和? extend

泛型中的通配符

通配符?

用通配符? ,让泛型具备可变性

? extend T 上界通配符/协变

1
2
3
List<Button> buttons = new ArrayList<Button>();
List<TextView> textViews = buttons;
// 👆 多态用在这里会报错 incompatible types: List<Button> cannot be converted to List<TextView>

上面将 buttons 赋值给 textViews 编译器会提示错误,这是因为泛型具有不可变性,List 和 List 是两种不同的类型。

怎么解决?

1
2
3
List<Button> buttons = new ArrayList<Button>();
//  👇
List<? extends TextView> textViews = buttons;

这个 ? extends 叫做 「上界通配符」,可以使 Java 泛型具有「协变性 Covariance」,协变就是允许上面的赋值是合法的。

在继承关系树中,子类继承自父类,可以认为父类在上,子类在下。extends 限制了泛型类型的父类型,所以叫上界。

它有两层意思:

  1. 其中 ? 是个通配符,表示这个 List 的泛型类型是一个未知类型。
  2. extends 限制了这个未知类型的上界,也就是泛型类型必须满足这个 extends 的限制条件,这里和定义 class 的 extends 关键字有点不一样:
    • 它的范围不仅是所有直接和间接子类,还包括上界定义的父类本身,也就是 TextView。
    • 它还有 implements 的意思,即这里的上界也可以是 interface。

下面也是可以的:

1
2
3
List<? extends TextView> textViews = new ArrayList<TextView>(); // 👈 本身
List<? extends TextView> textViews = new ArrayList<Button>(); // 👈 直接子类
List<? extends TextView> textViews = new ArrayList<RadioButton>(); // 👈 间接子类

在使用了上界通配符之后,List 的使用上有没有什么问题:

1
2
3
4
List<? extends TextView> textViews = new ArrayList<Button>();
TextView textView = textViews.get(0); // 👈 get 可以
textViews.add(textView);
//             👆 add 会报错,no suitable method found for add(TextView)

List<? extends TextView> 的泛型类型是个未知类型 ?,编译器也不确定它是啥类型,只是有个限制条件。

  1. get()

由于它满足 ? extends TextView 的限制条件,所以 get 出来的对象,肯定是 TextView 的子类型,根据多态的特性,能够赋值给 TextView,啰嗦一句,赋值给 View 也是没问题的。

  1. add
  • List<? extends TextView> 由于类型未知,它可能是 List,也可能是 List;对于前者,显然我们要添加 TextView 是不可以的
  • 实际情况是编译器无法确定到底属于哪一种,无法继续执行下去,就报错了。

那我干脆不要 extends TextView ,只用通配符 ? 呢?

这样使用 List<?> 其实是 List<? extends Object> 的缩写。

1
2
3
4
5
6
List<Button> buttons = new ArrayList<>();

List<?> list = buttons;
Object obj = list.get(0);

list.add(obj); // 👈 这里还是会报错

和前面的例子一样,编译器没法确定 ? 的类型,所以这里就只能 get 到 Object 对象。

同时编译器为了保证类型安全,也不能向 List<?> 中添加任何类型的对象,理由同上。

由于 add 的这个限制,使用了 ? extends 泛型通配符的 List,只能够向外提供数据被消费,从这个角度来讲,向外提供数据的一方称为「生产者 Producer」。对应的还有一个概念叫「消费者 Consumer」,对应 Java 里面另一个泛型通配符 ? super。

另外一个案例:

1
2
3
List<? extends Number> numbers = new ArrayList<Number>(); // correct
List<? extends Number> numbers2 = new ArrayList<Integer>(); // correct
List<? extends Number> numbers3 = new ArrayList<Object>(); // wrong

? super T 下界通配符/逆变

1
List<? super Button> buttons = new ArrayList<TextView>();

这个 ? super 叫做「下界通配符」,可以使 Java 泛型具有「逆变性 Contravariance」。

  1. 通配符 ? 表示 List 的泛型类型是一个未知类型。
  2. super 限制了这个未知类型的下界,也就是泛型类型必须满足这个 super 的限制条件。
    • super 我们在类的方法里面经常用到,这里的范围不仅包括 Button 的直接和间接父类,也包括下界 Button 本身。
    • super 同样支持 interface。

下面几种情况都是可以的:

1
2
3
List<? super Button> buttons = new ArrayList<Button>(); // 👈 本身
List<? super Button> buttons = new ArrayList<TextView>(); // 👈 直接父类
List<? super Button> buttons = new ArrayList<Object>(); // 👈 间接父类

使用了下界通配符的 List,我们再看看它的 get 和 add 操作:

1
2
3
4
List<? super Button> buttons = new ArrayList<TextView>();
Object object = buttons.get(0); // 👈 get 出来的是 Object 类型
Button button = ...
buttons.add(button); // 👈 add 操作是可以的

使用下界通配符 ? super 的泛型 List,只能读取到 Object 对象,一般没有什么实际的使用场景,通常也只拿它来添加数据,对于 List<? super Button> 从外取数据,也就是消费已有的 List<? super Button>,往里面添加 Button,因此这种泛型类型声明称之为「消费者 Consumer」。

添加元素,可以添加为 T 及 T 的子类。

1
2
3
4
5
List<? super Fruit> fruits2 = new ArrayList<Fruit>();
//        fruits2.add(new Fruit()); // ok 
//        fruits2.add(new Apple()); // ok 
//        fruits2.add(new Orange()); // ok 
//        fruits2.add(new Object()); // 编译错误

小结

Java 的泛型本身是不支持协变和逆变的。

  1. 可以使用泛型通配符 ? extends 来使泛型支持协变,但是 「只能读取不能修改」,这里的修改仅指对泛型集合添加元素,如果是 remove(int index) 以及 clear 当然是可以的。
  2. 可以使用泛型通配符 ? super 来使泛型支持逆变,但是 「只能修改不能读取」,这里说的不能读取是指不能按照泛型类型读取,你如果按照 Object 读出来再强转当然也是可以的。

这被称为 PECS 法则:「Producer-Extends, Consumer-Super」。

PECS 原则

PECS(Producer Extends Consumer Super)

PE extends(? extends),协变,取

频繁往外取内容的,适合用上界 extends

当只想从集合中获取元素,请把这个集合看成生产者,请使用 <? extends T>,这就是 Producer extends 原则,PECS 原则中的 PE 部分。

CS super(? super),逆变, 存

经常往里插入的,适合用下界 super

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class PECS {

    public static void main(String[] args) {
//       System.out.println("Hello PECS");

        List<Fruit> fruitList = new ArrayList<Fruit>();
        fruitList.add(new Apple());
        fruitList.add(new Orange());
//        getOutFruits(fruitList); // ok
//        getOutFruits2(fruitList); // ok


        List<Apple> apples = new ArrayList<Apple>();
        apples.add(new Apple());
//        getOutFruits(apples); 编译错误
//        getOutFruits((List<Fruit>)apples); // 强制类型转换,同样编译错误,不兼容的类型: List<Apple>无法转换为List<Fruit>
//        getOutFruits2(apples); // ok

        List<? extends Fruit> basket = apples;//按上一个例子,这个是可行的
        for (Fruit fruit : basket) {
            System.out.println(fruit); // ok
        }
//        basket.add(new Apple()); // 编译错误

//       存元素如果用fruits
        List<? extends Fruit> fruits = new ArrayList<>(); //
//        fruits.add(new Fruit()) // 编译错误,认为是List<Fruit>类型
//        fruits.add(new Apple()) // 编译错误,认为是List<Apple>类型
//        fruits.add(new Orange()) // 编译错误,认为是List<Orange>类型
//        List<Fruit>,List<Apple>,List<Orange>是完全不同的类型,用? extends Fruit存元素时编译器就不知道到底是什么类型,编译错误

        // 存取元素用 ? super Fruit,fruits当成是生产者,
        List<? super Fruit> fruits2 = new ArrayList<>();
        fruits2.add(new Fruit());
        fruits2.add(new Apple());
        fruits2.add(new Orange());

    }


    public static void getOutFruits2(List<? extends Fruit> basket) {
        for (Fruit fruit : basket) {
            System.out.println(fruit);
            //...do something other
        }
        System.out.println("--------------");
    }

    public static void getOutFruits(List<Fruit> basket) {
        for (Fruit fruit : basket) {
            System.out.println(fruit);
            //...do something other
        }
        System.out.println("--------------");
    }
}
1
2
3
4
5
6
用了<? extends Fruit>相当于告诉编译器,我们的篮子(集合)是用来处理水果以及水果的子类型。因为子类型有许多,我们并没有告诉编译器是哪个子类型。

编译器在这里遇到的问题是,如果add的是Apple类型时,则basket应该是List<Apple>,如果add是Fruit类型,则basket应该是List<Fruit>。而List<Apple>和List<Fruit>前面已经提过,是2个完全没有关系的类型,
所以编译器不知道是哪个子类型将加入集合,不知道到底是List<Apple>还是List<Fruit>,所以编译器只能报错。(注意,这里讨论的都是类型,而不是对象)

另一方面,编译器已经知道集合里全部都是水果的子类型,所以编译器可以保证取出的数据全部是水果。
1
2
3
4
5
6
用了<? super Apple>相当于告诉编译器,集合接受处理Apple以及Apple的超类型,即Object,Fruit,Apple三个类型。
但编译器并不知道到底是List<Object>,List<Fruit>还是List<Apple>?

编译器只知道,苹果和苹果子类型是可以放进去(也是Fruit的子类型,也是Object的子类型)。这意味着,我们总是可以将一个苹果的子类型放入苹果的超类型的list中。

而取出时的情况是,编译器不知道是按哪个类型取出, 到底是Object,Fruit,Apple中的哪个呢?但是编译器可以选择永远不会错的类型,也就是Object的类型,因为Object是所有类型的超类型。

Ref

讲解的很不错

本文由作者按照 CC BY 4.0 进行授权