​ 最近终于开始总结自己理解的东西,终于有时间写写自己对一些知识的认识。文笔不好,还望各位包含。

本预先资料来源于Oracle官方文档Java™ 教程-Java Tutorials

官方文档:https://docs.oracle.com/javase/tutorial/java/generics/index.html

中文翻译:https://pingfangx.github.io/java-tutorials/java/generics/types.html

下面我以问题的方式,阐明对泛型深入浅出的认识:

1.泛型是什么?

​ 泛型是JDK5引入的一种参数化类型特性。JDK7及以上,泛型的菱形可以推断参数化类型:把类型当参数一样传递。数据类型只能是引用类型。

举个栗子:Plate中的T是类型参数;Plate的Banana是实际类型参数;

​ Plate整个称为泛型类型;Plate整个称为参数化的类型;

2.为什么使用泛型,使用泛型的好处?

  • 1.代码更健壮(只要在编译器没有警告,运行期就不会出现ClassCastException);
  • 2.代码更简洁(不用强转)
  • 3.代码更灵活,可复用。

3.泛型包含哪些?

​ 泛型只有三种情况:泛型接口,泛型类,泛型方法

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
//泛型接口
public interface Plate<T> {
public void set(T t);
public T get();
}

//泛型类
public class AiPlate<T> {
private T t;
public AiPlate(T t){
this.t = t;
}

public void set(T t){
this.t = t;
}

public T get(){
return t;
}

//泛型方法
public <U> void addFruit(U u) {
}
}

4.泛型限定有哪些?

?无限定 ; extends 限定上界;super 限定下界 后面会以一个通熟易懂的口诀演示记住他

1
2
3
4
5
6
7
8
9
10
11
Class A {}

Class B {}

Interface C{}

Interface D{}

public class ClassTest<T extends A & C & D>{} //this is ok 可以实现多个接口

public class ClassTest<T extends A & B & C & D> //error, 因为java 只支持单继承

在这个体系中,上界通配符 “Plate<? extends Fruit>” 覆盖下图中蓝色的区域。

img

Plate是Plate的基类,但是不是Plate的基类,对应上面例子,Plate覆盖的下图中红色的区域

img

5. JAVA泛型的原理?什么是泛型擦除机制?

​ Java的泛型是JDK5新引入的特性,为了向下兼容,虚拟机其实是不支持泛型,所以Java实现的是一种伪泛型机制,也就是说Java在编译期擦除了所有的泛型信息,这样Java就不需要产生新的类型到字节码,所有的泛型类型最终都是一种原始类型,在Java运行时根本就不存在泛型信息。

​ 泛型的擦除机制:如果有父类被擦除成父类,否则被擦除成Object。如果有继承,擦除之后会生成桥接方法,来解决类型擦除后保留泛型类型的多态性;桥方法在调用父类方法前,会将object进行强转成父类。

​ 从 .java文件编译到.class文件会产生泛型擦除的残留,保留了定义的格式,方便分析字节码。类的常量池保留了泛型信息,可以通过反射API拿出type类型信息。

6. Java编译器具体是如何擦除泛型的?

  • step 1. 检查泛型类型,获取目标类型
  • step 2. 擦除类型变量,并替换为限定类型

​ 如果泛型类型的类型变量没有限定(),则用Object作为原始类型

​ 如果有限定(),则用XClass作为原始类型

​ 如果有多个限定(T extends XClass1&XClass2),则使用第一个边界XClass1作为原始类

  • step 3. 在必要时插入类型转换以保持类型安全
  • step 4. 生成桥方法以在扩展时保持多态性

7.使用了泛型,后留下很多“后遗症”

7.1 泛型类型变量不能使用基本数据类型

​ 比如没有ArrayList,只有ArrayList.当类型擦除后,ArrayList的原始类中的类型变量(T)替换成Object,但Object类型不能存放int值

7.2 不能使用instanceof 运算符

​ 因为擦除后,ArrayList只剩下原始类型,泛型信息String不存在了,所有没法使用instanceof。

1
2
3
4
5
ArrayList<String> strings = new ArrayList<>();

if(strings instanceof ArrayList<?>){} //可以 被擦除之后的ArrayList本来就是未知的,所以这个可以

if(strings instanceof ArrayList<String>) //不可以
7.3 泛型在静态方法和静态类中的问题

​ 因为泛型类中的泛型参数的实例化在定义泛型类型对象(比如ArrayList)的时候指定的,而静态成员是不需要使用对象来调用的,所有对象都没创建,编译器不知道如何确定这个泛型参数是什么。

7.4 泛型类型中的方法可能会冲突

​ 因为擦除后两个equals方法变成一样的了。擦除之后T变成Object,而Object中的equals是默认被实现了的。所以重复了。

1
2
3
4
5
6
7
public boolean equals(T t){ 
return super equals(t)
}

public boolean equals(Object obj){
return super equals(obj)
}
7.5 没法创建泛型实例

​ 因为类型不确定,没法直接new对象。

1
2
3
public static <E> void append(List<E> list){
E elem = new E(); //不行,因为不知道E的具体类型
}

但是下面这种方式是可以的,通过反射来new对象;

1
2
3
4
public static <E> void append(List<E> list, Class<E> cls) throws Exception{
E elem = cls.newInstance(); // OK
list.add(elem);
}
7.6 没有泛型数组

答:因为数组可以协变,擦除后就没法满足数组协变的原则。

T[] arr = new T[10] //不可以 不知道T代表那个类

另外:Apple extends Fruit

Apple[] 的父类是Fruit[] 这个叫数组的协变。

其中数组协变中:父类可以持有子类,子类持有父类可能会报转化异常。

List 和 List在擦除之后运行期不知道是什么类型了,都是不满足协变原则了;数组是可以协变的,但是list是不会协变的。

另外假如:A extends B

Plate = Plate是不允许的,因为在泛型中,不管AB是什么关系,Plate和Plate没有任何关系。

8. 通配符和泛型的关系?

​ 通配符可以让泛型转化更灵活。

9. 说出以下类型的区别?

1
2
3
4
5
6
Plate 普通的盘子类,编译时不会做类型检查
Plate<Object> 参数化的plate类,他在编译之后,字节码的类中的方法是Object
Plate<?> 非限定通配符下的泛型类,泛型?表示类型未知,等价于Plate<? extends Object>
Plate<T> 泛型Plate类,T是未知类型,编译后会被擦除成Object对象
Plate<? extends T> 限定上界的泛型类
Plate<? super T> 限定下界的泛型类

10.泛型边界的熟记口诀:

  • extends 上界;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
import java.util.ArrayList;
import java.util.List;

/** * extends 上界
* super 下界
* 说明:以下方便记忆的:“上”表示父类,“下”表示子类
* 口诀:
* 上界只可往上读,往上赋值;方法只能传递自己或者子类
* 下界只可向下写,向下赋值;方法只能传递自己或者父类
* * 类关系: apple extends fruit extends food */
public class TestBoundary {

public static void main(String[] args) {
//演示extends赋值的上界
List<Food> foods = new ArrayList<>();
List<Apple> apples = new ArrayList<>();
List<Fruit> fruits = new ArrayList<>();

//fruits = apples; //error 往上赋值需要 extends
List<? extends Fruit> fruits1 = apples; //向上赋值成功

//演示向上读取
Fruit fruit = fruits1.get(0); //ok 上界修饰的可以往上读
Food food = fruits1.get(0); //ok 上界修饰的可以往上读
Object object = fruits1.get(0); //ok 上界修饰的可以往上读

//Apple apple = fruits1.get(0); //error 不能往下读

//演示方法传递
eat1(fruits); //ok 传递自己
eat1(apples); //ok 传递子类
//eat1(foods); //error 传递了父类

//演示Super赋值的下界
List<Food> foodsLower = new ArrayList<>();
List<Apple> applesLower = new ArrayList<>();
List<Fruit> fruitsLower = new ArrayList<>();
//fruitsLower = foodsLower; //error 往下赋值需要super修饰
List<? super Fruit> fruitList = foodsLower; // 向下赋值ok了

//演示向下写
fruitList.add(new Apple(1)); //用下写是ok
//fruitList.add(new Food()); //error 用上写是不行的;

//演示下界方法只能传递自己或者父类
eat2(fruitsLower); //传递自己ok
eat2(foodsLower); // 传递父类ok
//eat2(applesLower); //传递子类error
}

public static <T> void eat1(List<? extends Fruit> fruits){
}

public static <T> void eat2(List<? super Fruit> fruits){
}
}

11.最后说了半天规则,用实例演示以下泛型有什么用处

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
57
58
59
60
61
62
63
64
65
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* 泛型的简单应用
* 从没用泛型慢慢演进 */
public class Test {

//普通不使用泛型的方法。
//有个需求需要把香蕉或者更多水果copy一下,难道需要重新写N个copy方法么?
//答案不是的,出现了泛型copy2
public static void copy1(List<Apple> dest,List<Apple> src){
Collections.copy(dest,src);
}

//现在可以把无数多相同的水果相互copy了,但是又有一个需求了
//现在苹果是确定了的,我想想找一个水果list把苹果放进去,于是就出现了copy3
public static <T> void copy2(List<T> dest,List<T> src){
Collections.copy(dest,src);
}

//现在找到一个水果list放苹果了。但是现在还有一个需求
//就是现在水果list确定了,我要放的水果不确定,于是有了copy4
public static <T> void copy3(List<? super T> dest,List<T> src){
Collections.copy(dest,src);
}

//这个就是Collections.copy的终极用法了。
//public static <T> void copy(@RecentlyNonNull List<? super T> dest, @RecentlyNonNull List<? extends T> src)
public static <T> void copy4(List<? super T> dest,List<? extends T> src){
Collections.copy(dest,src);
}

public static void main(String[] args) {
//新建两个苹果list
List<Apple> apples = new ArrayList<>();
apples.add(new Apple(1));
List<Apple> apples1 = new ArrayList<>();
apples1.add(new Apple(2));

//新建两个香蕉list
List<Banana> bananas = new ArrayList<>();
bananas.add(new Banana(1));
List<Banana> bananas1 = new ArrayList<>();
bananas1.add(new Banana(2));

//新建水果list
List<Fruit> fruits = new ArrayList<>();
fruits.add(new Apple(10)) ; // 先要告诉编译器fruit是放的苹果,否者copy3的dest参数编译器不知道放什么

//普通方法
copy1(apples,apples1);

//使用泛型方法
copy2(bananas,bananas1);

//苹果往水果盘子放
Test.<Apple>copy3(fruits,apples);

//注意这里苹果是确认的了
//水果list确定了放任意水果
Test.<Fruit>copy4(fruits,apples);
}
}

测试代码:https://github.com/oujie123/UnderstandingOfGeneric