Java泛型:赋予灵活性的利器

本文最后更新于:1 年前

人生如诗,不在于辞藻的华丽,而在于情感的真挚与深邃,每一行都是心灵的独白。

破冰

  • 🥇 推荐阅读:

🌭 我真的了解泛型吗? - 掘金 (juejin.cn)

思维碰撞

简单概述

  • 简单总结下泛型相关内容,后续找时间再系统学习(2023/10/27 晚)
    • 泛型类
    • 泛型接口
    • 泛型方法:返回值、入参位置、方法体中
  • 我对泛型的理解:

泛型很好理解:我们经常讲到一个对象实例的就是以类作为模板创建的,那么也可以讲一个普通类可以以泛型类作为模板;那么泛型是用来干嘛的呢,我们为什么要使用泛型呢?其实,所有的泛型类在编译后生成的字节码与普通类无异,因为在编译前,所有泛型类型就被擦除了。所以我们可以把泛型看作一个语法糖,将类型转换的校验提前在编译时,减少类型转换错误的发生,使编写的程序更加具有健壮性

🍖 AI 总结(2023/10/13 晚)

泛型是 Java 语言中的一项强大的特性,它允许在编译时指定类、接口或方法的参数类型,从而在编译阶段就能够进行类型检查。这样可以减少类型转换的错误,并提高代码的安全性和可读性。

通过使用泛型,我们可以在编译时捕捉到一些类型错误,而不是在运行时才发现,这样可以避免一些潜在的 bug。泛型还可以增加代码的可重用性和灵活性,因为泛型类、接口和方法可以用于多种不同的类型,而无需针对每一种类型单独编写或重复编写相似的代码。

总的来说,通过使用泛型,我们可以在编写 Java 代码时更好地约束和使用类型信息,减少类型错误,提高代码的可读性和健壮性。

👏 这一批代码比较全面的展示了泛型的各种使用场景了
  • 泛型接口 EndA
1
2
3
4
5
6
7
8
package entity.c;

public interface EndA<T> {
void fun_1(T t);

<R> void fun_2(T t, R r);
}

  • 泛型类 EndB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package entity.c;

/**
* @author 邓哈哈
* 2023/4/9 21:51
* Function:
* Version 1.0
*/

public class EndB<T> {
public <R> EndB(T t, R r) {
}

}

  • 泛型类 EndC<T, E> extends EndB implements EndA
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
package entity.c;

/**
* @author 邓哈哈
* 2023/4/9 21:39
* Function:
* Version 1.0
*/

public class EndC<T, E> extends EndB<T> implements EndA<E> {
@Override
public void fun_1(E t) {

}

@Override
public <R> void fun_2(E e, R r) {

}

public <R> void fun_3(T t, R r) {

}

public <R> EndC(T t, R r) {
super(t, r);
}
}


  • 泛型的使用
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
package entity.c;

/**
* @author 邓哈哈
* 2023/4/9 21:50
* Function: 这一批代码比较全面的展示了泛型的各种使用场景了
* 接下来介绍泛型使用中的诸多语法和注意事项:
* 泛型类, 泛型接口, 泛型类
* 定义泛型接口 EndA, 内含抽象方法 fun_1 fun_2待实现;
* 定义泛型类 EndB, 内含构造器
* 定义泛型类 EndC, 继承EndB, 实现EndA
* 注意:
* 泛型类 EndC实现了 fun_1, fun_2方法, 自定义了 fun_3方法 和 有参构造器
* 实例化泛型类, 要声明泛型类中的泛型
* EndB 声明 T, EndC 声明 T, E
* <p>
* <p>
* <p>
* Version 1.0
*/

public class Result {
public static void main(String[] args) {
EndB<String> endB = new EndB<>("", 18);

EndC<String, Double> EndC_1 = new EndC<>("", 10);

EndC<Integer, Double> EndC_2 = new EndC<>(10, 10);
}
}

转载内容

曾经年少轻狂的我,以为自己已经轻松拿捏 Java 泛型,直到后来学习 Java 8 新特性时,被泛型轻松反拿捏!那时的心情,正如标题一样,自我怀疑:“我真的了解泛型吗?” 答案很明显,不了解!要不是有 eclipse/idea 这类编辑器工具提供编译检查的话,真让我手写代码话,可能会写出让人笑掉大牙的代码。于是,再次回头来学习一下泛型知识。

泛型由来

很久很久以前。。。

记得小时候,奶奶提了一篮子鸡蛋去赶集卖鸡蛋,结果把几个老鸭蛋也卖了。原来是不经意间把几个鸭蛋混在鸡蛋一起了,幸好没有把鹅蛋也当做鸡蛋卖了,鹅蛋可比鸡蛋贵很多哦。于是,为了便于区分,爷爷就专门用竹子编织了几个不同的篮子专门用于盛放鸡蛋、鸭蛋、鹅蛋。用代码体现,如下:

  • 装鸡蛋的篮子
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
arduino复制代码/**
* 蛋
*/
class Egg {}

/**
* 鸡蛋
*/
class ChickenEgg extends Egg {}

/**
* 装鸡蛋的篮子
*/
class BasketForChickenEgg {
int capacity = 30;
ChickenEgg[] container = new ChickenEgg[capacity];
int size;

public void add(ChickenEgg chickenEgg) {
if (size >= capacity) {
throw new RuntimeException("basket is full!");
}
container[size++] = chickenEgg;
}

public ChickenEgg get() {
if (size >= 0) {
return container[size--];
}
return null;
}
}
  • 装鸭蛋的篮子
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
arduino复制代码/**
* 蛋
*/
class Egg {}

/**
* 鸭蛋
*/
class DuckEgg extends Egg {}

/**
* 装鸭蛋的篮子
*/
class BasketForDuckEgg {
int capacity = 30;
DuckEgg[] container = new DuckEgg[capacity];
int size;

public void add(DuckEgg duckEgg) {
if (size >= capacity) {
throw new RuntimeException("basket is full!");
}
container[size++] = duckEgg;
}

public DuckEgg get() {
if (size >= 0) {
return container[size--];
}
return null;
}
}
  • 装鹅蛋的篮子
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
arduino复制代码/**
* 蛋
*/
class Egg {}

/**
* 鹅蛋
*/
class GooseEgg extends Egg {}

/**
* 装鸭蛋的篮子
*/
class BasketForGooseEgg {
int capacity = 30;
GooseEgg[] container = new GooseEgg[capacity];
int size;

public void add(GooseEgg gooseEgg) {
if (size >= capacity) {
throw new RuntimeException("basket is full!");
}
container[size++] = gooseEgg;
}

public GooseEgg get() {
if (size >= 0) {
return container[size--];
}
return null;
}
}

从上面的代码可以看出,几个装蛋的篮子类,除了蛋的类型不一样,其他的变量、方法实现逻辑都一样。那时候 java 还没有泛型,哪怕只有一个变量的类型不一样,也得写出不同类来区分,比如后面家里养了鸽子,得编织一个专门盛放鸽子蛋的篮子,再后来家里养了鸵鸟(这个有点夸张了,哈哈),又得编织一个专门盛放鸵鸟蛋的篮子。这样一直下去的话,就必须编织成百上千个不同类型的篮子了。

此时,你肯定会说,直接搞一个专门盛放蛋的篮子,不管什么蛋都可以装,如下:

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
arduino复制代码/**
* 蛋
*/
class Egg {}

/**
* 装蛋的篮子
*/
class BasketForEgg {
int capacity = 30;

Egg[] container = new Egg[capacity];

int size;

public void add(Egg egg) {
if (size >= capacity) {
throw new RuntimeException("basket is full!");
}
container[size++] = egg;
}

public Egg get() {
if (size >= 0) {
return container[size--];
}
return null;
}
}

如上,这个篮子有点特殊,可以装各种蛋,如:鸽子蛋、鹌鹑蛋、鸡蛋。。。但是,小时候的我,非常调皮,经常偷偷的把鸽子蛋和鹌鹑蛋捡到一起,由于这两种蛋大小、颜色都非常相似,奶奶拿到街上去卖的时候,就不好分辨了,很容易卖错价钱!

对比咱们曾经写过的业务代码,有没有发现和装蛋的篮子故事很相似:有某些类的实现算法、逻辑几乎相同,只是个别参数或变量的类型不同,但是根据业务需要,我们不得不写很多这些极其相似的类或者方法,如果有成千上万这样的不同类型的变量,就得写成千上万个非常类似的类,这就导致了非常严重的代码冗余问题了。

JDK1.5 以后。。。

于是,为了解决这一问题,我们把这样的在通用模板中,存在变化的的东西提取出来,用参数化表示,即用 T 来表示。如,BasketForEgg就是一个模板类,只需要指定这个 T 为 ChickenEgg,就可以得到 Basket类,指定 T 为 DuckEgg 便可得到 Basket类,这样就不用写成千上万个类啦,只需要一个模板类就搞定了,换句话说就是通过通用模板类,达到制作完全不同类的目的。如下图所示:

img

通过 BasketForEgg这个模板类,制作出了 BasketForEgg、BasketForEgg、BasketForEgg三个不同的类。虽然,目前 java 在运行期间会存在擦除,这只是 java 实现泛型的一种机制,但是在逻辑上(编译层面) 我们把它们当做三个完全不同的类,你能用 BasketForEgg basket = new BasketForEgg()吗?显然不能,编译不通过。所以在有了泛型之后,要把类名和泛型参数绑在一起来看,才能算一个独立的类,如 BasketForEgg是一个类,而不是 BasketForEgg 类。

一句话总结:泛型类是类的模版,类是对象实例的模版! 有了泛型之后,再也不用写那么多重复的类了!

【Effective Java】的作者:泛型类有点像普通类的工厂,其实就是一个意思啦

泛型定义

泛型即参数化类型,通常用 T 来占位,如需多个就用多个大写字母来占位,如 E、K、V 等,通过填充不同类型的参数,便能得到填充类型的类,常用在类、接口、方法上。如 BasketForEgg就是一个泛型类,而 Egg、DuckEgg 这些都是具体的类。jdk1.5 后,几乎所有的集合类都支持了泛型,常见的开源框架也纷纷支持泛型,jdk8 中 Stream 流更是把泛型的应用体现得淋漓尽致。

泛型分类

根据泛型参数 T,所在的位置不同,可以分为泛型接口、泛型类、泛型方法, 其实接口和类是一个意思,也可以分为两种泛型方法、泛型类/接口。

泛型类

泛型参数 T 写在类名的后面,通过尖括号表示,写在类上面的泛型参数 T,它的作用域自然就是整个类了。那一般什么时候是去填充这个 T 的具体类型呢?常见的两种方式:

  1. 在对这个泛型类实例化时填充 T 的具体类型
  2. 在子类继承这个泛型类时填充 T 的具体类型
  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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
scala复制代码/**
* 装蛋的篮子
*/
class BasketForEgg<T> {
int capacity = 30;

Object[] container = new Object[capacity];

int size;

public void add(T t) {
if (size >= capacity) {
throw new RuntimeException("basket is full!");
}
container[size++] = t;
}

public T get() {
if (size >= 0) {
return (T) container[size--];
}
return null;
}

public static void main(String[] args) {
//实例化对象时,填充具体的泛型类型,这里以鸭蛋为例
BasketForEgg<DuckEgg> duckEggBasket = new BasketForEgg<DuckEgg>();

//装一个鸭蛋,正常
duckEggBasket.add(new DuckEgg());

//编译器报错,因为指定了泛型类型为DuckEgg,就只能装鸭蛋了
duckEggBasket.add(new ChickenEgg());
}

/**
* 子类在继承泛型类时,指定具体的泛型类型
*/
class SubBasketForEgg extends BasketForEgg<DuckEgg> {}

/**
* 子类在继承泛型类时,若未填充具体的类型,则子类也是一个泛型类
*/
class SubBasketForEgg2<T> extends BasketForEgg<T> {}
}

泛型接口

泛型参数 T 写在类接口名的后面,通过尖括号表示。同泛型类基本一致,泛型参数 T 作用域在整个接口中,在某个具体的类实现泛型接口时,填充具体的泛型类型;在子类接口继承泛型接口时,可以填充具体的参数类型,或者不填充,该子类接口也可以是泛型接口。如下:

  • 装东西的篮子
1
2
3
4
5
6
7
csharp复制代码/**
* 装东西的篮子接口:泛型接口
*/
interface Basket<T> {
void add(T t);
T get();
}
  • 装水果的篮子
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
typescript复制代码/**
* 装水果的篮子:在实现篮子接口时,填充具体的泛型类型
* 这里要把Basket<Fruit>看成一个整体,BasketForEgg实现的是Basket<Fruit>接口,
* 而不是Basket接口
*/
class BasketForFruit implements Basket<Fruit> {
int capacity = 30;

Object[] container = new Object[capacity];

int size;

@Override
public void add(Fruit fruit) {
if (size >= capacity) {
throw new RuntimeException("basket is full!");
}
container[size++] = fruit;
}

@Override
public Fruit get() {
if (size >= 0) {
return (Fruit) container[size--];
}
return null;
}


public static void main(String[] args) {
Basket basket = new BasketForFruit();
basket.add(new Fruit());
}
}

class Fruit {

}
  • 装蛋的篮子
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
arduino复制代码/**
* 装蛋的篮子:在实现篮子接口时,指定填充的泛型类型
* 注意这里要把Basket<Egg>看成一个整体,BasketForEgg实现的是Basket<Egg>接口,
* 而不是Basket接口
*/
class BasketForEgg implements Basket<Egg> {
int capacity = 30;

Object[] container = new Object[capacity];

int size;

public void add(Egg egg) {
if (size >= capacity) {
throw new RuntimeException("basket is full!");
}
container[size++] = egg;
}

public Egg get() {
if (size >= 0) {
return (Egg) container[size--];
}
return null;
}

public static void main(String[] args) {
BasketForEgg duckEggBasket = new BasketForEgg();
duckEggBasket.add(new DuckEgg());
}
}

泛型方法

泛型方法,是指泛型参数作用与方法上,一般是在调用方法时,通过入参或者接收返回值的引用指定具体的类型。这里以方法所在的类是非泛型类,只有方法为泛型方法的前提下为例。

1
2
3
4
csharp复制代码public <T> void print() {
List<T> list = new ArrayList<>();
System.out.println(list.size());
}

非泛型类中的泛型方法,一般 放在返回值类型之前,public 权限修饰符、static 静态修饰符之后,表示该方法是一个泛型方法

泛型方法的 T 作用的位置有可以有三处:方法体(方法内部逻辑中)、返回值、入参。可以只作用于一处,也可以两两接结合,甚至三处同时都有,具体场景分析,如下。

  • 占位符 T 只在入参位置不参与方法体逻辑,也不影响返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csharp复制代码public class GenericMethodDemo {
/**
* 只有入参传入了T, 方法体和返回值都未用到T,所以不管传入List<String>
* 还是List<Integer>,不影响此方法
*/
public static <T> void method1(List<T> list) {
System.out.println(list.size());
}
/**
* 把上面的泛型方法改成此种非泛型方法,因为没有必要
*/
public static void method11(List<?> list) {
System.out.println(list.size());
}

}
  • T 只出现在方法体中,入参和返回值都没有 T
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csharp复制代码/**
* 占位符T在方法体中有用到,入参和返回值都没有用到
*/
public static <T> int method2() {
List<T> list = new ArrayList<T>();
return list.size();
}

/**
* 我们用占位符T,目的就是想在调用方法时填充这个T,method2中这个T毫无意义
* 所以可以直接改成非泛型方法,像method3这样
*/
public static int method3() {
List<?> list = new ArrayList<>();
return list.size();
}
  • 返回值中有 T,入参和方法体中没有 T
1
2
3
4
csharp复制代码//编译不同过,方法体都没有T,怎么会返回T呢,所以不存在这种说法
public static <T> T method4() {
return 1;
}
  • 入参没有 T,方法体和返回值都有 T
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csharp复制代码/**
* 这里只有方法体中和返回值中有T,那么这个T什么时候填充的?
*/
public static <T> List<T> method5() {
List<T> list = new ArrayList<>();
return list;
}

public static void main(String[] args) {
//这里在引用接收的时候填充了T,这使得同一个方法返回多种不同类型的值
List<String> list = method5();
list.add("aa");
List<Integer> list1 = method5();
list1.add(1);

}

这种方式有什么意义呢,通过一个方法可以快速得到指定泛型的 List

  • 方法入参有 T,返回值有 T,方法体没有 T
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码public static void main(String[] args) {
//通过传入的参数来确定T的类型
List<String> list = Arrays.asList("aa", "bb");
String s = method6(list);

List<Integer> list1 = Arrays.asList(11, 22);
Integer integer = method6(list1);

}

/**
* 方法入参有T, 返回值有T,方法体中没有T
*/
public static <T> T method6(List<T> list) {
return list.get(0);
}
  • 入参有 T,方法体有 T,但是返回值没有 T
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
ini复制代码/**
* 方法入参有T, 返回没有T,方法体有T
*/
public static <T> int method7(List<T> list) {
T t1 = list.get(0);
T t2 = list.get(1);

Map<String, T> map = new HashMap<>();
map.put("aaa", t1);
map.put("bbb", t2);
return map.size();
}

/**
* method8和method7区别,好像都一样,但是语义不一样
*/
public static int method8(List<?> list) {
Object t1 = list.get(0);
Object t2 = list.get(1);

Map<String, Object> map = new HashMap<>();
map.put("aaa", t1);
map.put("bbb", t2);
return map.size();
}

/**
* method9
*/
public static <T, K> boolean method9(List<T> list, List<K> list1) {
T t1 = list.get(0);
T t2 = list.get(1);

Map<String, Object> map = new HashMap<>();
map.put("aaa", t1);
map.put("bbb", t2);

K k1 = list1.get(0);
K k2 = list1.get(1);
K k3 = list1.get(2);

Map<String, Object> map1 = new HashMap<>();
map1.put("ccc", k1);
map1.put("ddd", k1);
map1.put("eee", k3);

return map.size() > map1.size();
}

如上代码所示,对比 method7 和 method8 发现,方法入参有 T,方法体内用到了 T,返回值没有 T,这里就不会在乎 T 传入的是什么类型了,都不会影响方法体的执行,如果把 List 改成 List<?>也不会有任何影响,唯一要变化的就是如 method8 中,把 T 换成 Object,方法体依然正常执行,但是这样从某种意义上来说破坏了语义,例如我明明传给你的是装鸭蛋的篮子(List), 你却把装鸭蛋的篮子变成了装蛋的篮子(List), 这不是又混淆了吗?所以这里语义上要求不能变,就需要时要具体的泛型 T,而不是通配符“ ?”。

再例如 method9 方法中,有两个入参,其实都改成 List<?>也不是不可以,只是语义不清晰,所以还是使用泛型方法比较好,虽说方法体执行结果都一样,但是代码语义清晰,更方便阅读、理解

总结:方法入参有 T,方法体用到了 T,但是返回值无 T,无论 List 填充什么类型都不影响具体的执行结果,只是在语义上有差别

针对此种类型,举个 JDK 中的栗子:

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
csharp复制代码interface Stream<T> {
//这里T因为接口上已经有了,先忽略,只看泛型方法中的R、A
<R, A> R collect(Collector<? super T, A, R> collector);
}

public final class Collectors {

public static Collector<CharSequence, ?, String> joining() {
return new CollectorImpl<CharSequence, StringBuilder, String>(
StringBuilder::new, StringBuilder::append,
(r1, r2) -> { r1.append(r2); return r1; },
StringBuilder::toString, CH_NOID);
}

public static Collector<CharSequence, ?, String> joining(CharSequence delimiter) {
return joining(delimiter, "", "");
}

public static <T> Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },
CH_ID);
}

}

//现在jdk8下用得最常用的Stream流API,它的collect方法是一个泛型方法,占位符有R、A,入参也有R和A,但是返回值只有R,没有A,根据上面所讲的,A这个泛型参数不在乎它具体是什么类型,即使方法体有用到A,返回值只有R,那么在转入Collector时,这个A就可以用?来代替。

//再看Collecters类中的joining()、toList()方法,返回的恰好就是Collector<CharSequence, ?, String>,用?指定了A。
  • 方法人参、方法体、返回值都有 T,这是最常见的一种方式
1
2
3
4
5
6
7
8
9
10
11
ini复制代码public static <T> T method10(Class<T> clazz) {
T t1 = null;
try {
t1 = clazz.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return t1;
}

泛型引用

在 java 语言中,数据类型可以分为两大类:基础数据类型引用数据类型,那么加上泛型参数后的引用类型,这里姑且叫做泛型引用类型,泛型引用和其他对象引用不同,它通常只能指向它自己类型的实例。

普通的泛型引用

还是以装蛋的篮子为例,这里为了简化代码,使用 jdk 的 List 类代替 Basket,ArrayList这个泛型引用,只能指向装鸭蛋的对象实例,如下:

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
scala复制代码/**
* 装蛋的篮子
*/
public class BasketDemo {

public static void main(String[] args) {
//ArrayList<DuckEgg> 与 ArrayList<ChickenEgg>是两个完全不同的类型
//这里编译报错,ArrayList<DuckEgg>只能指向new ArrayList<DuckEgg>()实例
ArrayList<DuckEgg> duckEggArrayList1 = new ArrayList<ChickenEgg>();

//正常通过
ArrayList<DuckEgg> duckEggArrayList = new ArrayList<DuckEgg>();
}
}

/**
* 蛋
*/
class Egg {}

/**
* 鸡蛋
*/
class ChickenEgg extends Egg {}

/**
* 鸭蛋
*/
class DuckEgg extends Egg {}

/**
* 鹅蛋
*/
class GooseEgg extends Egg {}

那么问题来了,DuckEgg 是 Egg 的子类,那 ArrayList可以指向 new ArrayList()对象实例吗?

img

答案很明显,编译报错!这里再次强调文章开头的观点,ArrayList与 ArrayList在编译器的眼里是两个完全不同的具体类,即使 Egg 是 DuckEgg 的父类,所以这里类型不匹配,编译不通过,不能这么指定。

问题又来了,由于 ArrayList 是实现了 List 接口的,那 List泛型类型可以指向 ArrayList吗?

1
2
3
4
5
typescript复制代码public class BasketDemo {
public static void main(String[] args) {
List<Egg> duckEggArrayList = new ArrayList<Egg>();
}
}

答案是肯定的。因为 ArrayList本身就实现了 List接口,所以在实例化 ArrayList时,填充 E 为 Egg 类型,自然而然 ArrayList就实现了 List接口,使用父类引用指向子类实现,这不就是咱们的多态嘛!这里把 ArrayList看做是一个整体类,List看做一个整体类,ArrayList是实现了 List接口的,所以这里是可以的,也是经常用得最多的方式。

那如何让一个 List这样的引用,既可以指向 ArrayList、又可以指向 ArrayList呢?

通配符“?”泛型引用

为了让一个泛型引用,指向它的多个不同类型的实例,这里需要使用到通配符“?”,通过 List<? extends Egg>表示只要是 Egg 及其子类,它都能认识,如下:

1
2
3
4
5
6
7
8
9
10
typescript复制代码public class BasketDemo {
public static void main(String[] args) {
//反应引用List<? extends Egg>指向了ArrayList<DuckEgg>实例
List<? extends Egg> eggList = new ArrayList<DuckEgg>();
//引用变量eggList又指向了ArrayList<ChickenEgg>实例
eggList = new ArrayList<ChickenEgg>();
//引用变量eggList又指向了ArrayList<GooseEgg>实例
eggList = new ArrayList<GooseEgg>();
}
}

如上代码所示,List<? extends Egg>在这里是一个引用标识,并没有任何具体的 List<? extends Egg>类型,所以也不能直接使用 new 关键字 new 一个 ArrayList<? extends Egg>()实例,它的作用就是扩大泛型引用的范围,它可以指向多个填充不同类型的具体类,如:ArrayList、ArrayList、ArrayList、ArrayList

那么问题来了,像 List<? extends Egg>这样的引用指向的实例对象,可以正常 CRUD 吗?

img

如上代码所示,在往 List<? extends Egg>里面添加一个 new Egg()时,编译器提示错误,这是因为 List<? extends Egg>是一个泛型引用,在使用 eggList 这个引用变量添加元素时,还是会受到泛型引用接口的限定,它可能指向一个装鸭蛋的 list 实例,也可能指向一个装鸡蛋的 list 实例,它到底指向哪个具体的类型,它不知道,所以这里它不敢乱添加,但是它知道我这里面装的就是 Egg 类型,get 出来的一定是 Egg 类型。

这就是常见的上下边界问题,通过这种方式是为了扩大泛型引用的范围。当然还有个List<? super Egg> 泛型引用,和 extends 一样,只不过方向相反。

问题又双叒来了,像这样的 List<? extends Egg>的泛型引用,不能添加元素,不能修改原素,又有什么意义呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码public class GenericTest {
public static void main(String[] args) {
List<DuckEgg> duckEggs = new ArrayList<DuckEgg>();
List<ChickenEgg> chickenEggs = new ArrayList<ChickenEgg>();

iterateList(duckEggs);
iterateList(chickenEggs);
}

/**
* 主要目的用于非修改遍历
*/
public static void iterateList(List<? extends Egg> list) {
for (Egg egg : list) {
System.out.println(egg);
}
}
}

如上代码所示,由于 List<? extends Egg> list 可以指向多个具体的实例,又不能对其进行修改,所以这里的主要目的就是非修改遍历

再看看 List<?>这个泛型引用,这个 ?没有上下边界了,它不知道指向具体哪个类型,所以它泛化的对象就是 Object,在 Java 里面 Object 就是根对象,是一切对象的父对象,所以通过调用 get 方法,得到的必然也是 Object 类型的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码public class GenericTest {
public static void main(String[] args) {
List<DuckEgg> duckEggs = new ArrayList<DuckEgg>();
List<ChickenEgg> chickenEggs = new ArrayList<ChickenEgg>();

iterateList(duckEggs);
iterateList(chickenEggs);
}

/**
* List<?> 更进一步,可以指向填充任意类型的实例对象
*/
public static void iterateList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
}

获取泛型类型

有的时候需要获取到具体的泛型类型来做一些业务判断,或者在研发一些框架时,需要获取到该类填充的具体类型,所以有必要了解下如何获取泛型类型。

  1. 如何获取本类的泛型具体类型?
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
csharp复制代码public class GenericTest {
public static void main(String[] args) {
GenericTest genericTest = new GenericTest();
genericTest.getGenericType();
}
public void getGenericType() {
//这里在编译期指定为鸭蛋,但是运行期,会被擦除,如:BasketForEgg basketForEgg = new BasketForEgg();
//所以这样是拿不到具体的泛型类型的。
//BasketForEgg<DuckEgg> basketForEgg = new BasketForEgg<>();

BasketForEgg<DuckEgg> basketForEgg = new BasketForEgg<>(DuckEgg.class);
System.out.println(basketForEgg.getClazz().getSimpleName()); //DuckEgg

}

/**
* 装蛋的篮子
*/
class BasketForEgg<T> {
//用于获取具体的泛型类型,无其他作用
Class<T> clazz;

public BasketForEgg(Class<T> clazz) {
this.clazz = clazz;
}

public Class<T> getClazz() {
return clazz;
}
}

class Egg{}

class DuckEgg extends Egg{}
}
  1. 如何获取父类或者父接口的泛型类型?
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
scala复制代码public class GenericTest {
public static void main(String[] args) {
GenericTest genericTest = new GenericTest();
genericTest.getGenericType();
genericTest.getGenericType1();
}

/**
* 获取父类泛型类型
*/
public void getGenericType() {
BasketForDuckEgg basketForDuckEgg = new BasketForDuckEgg();
//通过class对象获取父类泛型参数类型,ParameterizedType是Type子类,所以这里需要强转一下
ParameterizedType superclass = (ParameterizedType)basketForDuckEgg.getClass().getGenericSuperclass();
//因为泛型参数可能有多个,所以这里拿到实际的泛型类型为一个数组
Type[] actualTypeArguments = superclass.getActualTypeArguments();
//得到结果:[class com.kang.mybatis.study.sort.GenericTest$DuckEgg]
System.out.println(Arrays.toString(actualTypeArguments));
}

/**
* 获取父接口的泛型类型
*/
public void getGenericType1() {
BasketForChickenEgg basketForChickenEgg = new BasketForChickenEgg();
//通过class对象获取父接口泛型参数类型,存在实现多个接口的情况
Type[] genericInterfaces = basketForChickenEgg.getClass().getGenericInterfaces();
for (Type genericInterface : genericInterfaces) {
ParameterizedType parameterizedType = (ParameterizedType) genericInterface;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
//执行结果就是鸡蛋啦:[class com.kang.mybatis.study.sort.GenericTest$ChickenEgg]
System.out.println(Arrays.toString(actualTypeArguments));
}
}

/**
* 装鸭蛋的篮子
*/
class BasketForDuckEgg extends BasketForEgg<DuckEgg> {

}

/**
* 装鸡蛋的篮子
*/
class BasketForChickenEgg implements Basket<ChickenEgg> {

}

interface Basket<T> {}

/**
* 装蛋的篮子
*/
class BasketForEgg<T> {

}

class Egg{}

class DuckEgg extends Egg{}

class ChickenEgg extends Egg{}
}
  1. 泛型父类如何获取到自己的具体泛型类型呢?
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
scala复制代码public class GenericTest {
public static void main(String[] args) {
BasketForDuckEgg basketForDuckEgg = new BasketForDuckEgg();
basketForDuckEgg.getGenericType();
}

/**
* 装鸭蛋的篮子
*/
static class BasketForDuckEgg extends BasketForEgg<DuckEgg> {
/**
* 通过子类获取填充的父类泛型类型给父类使用
*/
public void getGenericType() {
//BasketForEgg<DuckEgg>想要获取自己的泛型类型,通过其子类来实现
ParameterizedType parameterizedType = (ParameterizedType) this.getClass().getGenericSuperclass();
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
Class clazz = (Class) actualTypeArguments[0];
System.out.println(clazz.getSimpleName());
super.setClazz(clazz);
}
}

/**
* 装蛋的篮子
*/
static class BasketForEgg<T> {
//通过一个class变量来获取子类实现时指定的泛型类型
Class<T> clazz;

/**
* 子类调用
* @param clazz
*/
public void setClazz(Class<T> clazz) {
this.clazz = clazz;
System.out.println(clazz.getName());
}
}

class Egg{}

class DuckEgg extends Egg{}

class ChickenEgg extends Egg{}
}
  1. 总结

    1. 对于直接获取本类的泛型具体类型,通过一个成员变量来保存泛型的具体类型,因为泛型参数是通过 new 关键字来确定的,在运行期泛型已经被擦除了,所以是拿不到自己本类的泛型具体类型的;
    2. 而某个类继承某个泛型类或者实现某个泛型接口时,是可以直接拿到泛型父类/父接口,这是因为在编译期通过 extends、implements 时确定尖括号这个 T 的类型的,编译后会把这个 T 类型保留在字节码里;
    3. 为什么会有获取具体泛型类型这样的需求?在一些开源框架中,随处可见这样的用法,例如 spring 的泛型注入,mybatis、hibernate 等根据泛型类型做 orm 映射,还有咱们在导出 Excel 时,List 也需要用到具体的泛型类型,来确定导出时列名称等。

泛型原理

这个就不用多说了,泛型目前针对 java 来说,就是一种语法糖,用来骗骗编译器的,把类型转化风险提前放在编译期解决。为了兼容老版本的 jdk,实际的泛型会被擦除,所以编译后的 class 和之前的版本没什么区别。

展望未来,说不定以后的 jdk 更新版本中,会保留真实的泛型类型类,同 C++一样。如 ArrayList与 ArrayList就是两个不同的类了呢。


Java泛型:赋予灵活性的利器
https://test.atomgit.net/blog/2023/04/09/Java泛型的理解/
作者
Memory
发布于
2023年4月9日
更新于
2023年10月27日
许可协议