在我们的实际工作中 泛型(Generics) 是无处不在的,我们也写过不少,看到的更多,如,源码、开源框架… 随处可见,但是,我们真正理解泛型吗?理解多少呢?例如:Box
、Box<Object>
、Box<?>
、Box<T>
、Box<? extends T>
、Box<? super T>
之间的区别是什么?本篇文章将会对 泛型(Generics) 进行全面的解析,让我们对泛型有更深入的理解。
本篇文章的示例代码放在 Github 上,所有知识点,如图:
Lucy 喜欢吃🍊(为什么要使用泛型)
首先,通过一个盘子装水果小故事来打开我们的泛型探索之旅(我们为什么要使用泛型),故事场景如下:
Lucy 到 James 家做客,James 需要招待客人,且知道 Lucy 喜欢吃橘子🍊,于是使用水果盘装满了🍊来招待客人
这个场景怎么用代码表现呢,我们来新建几个类,如下:
Fruit:水果类
1 | package entity; |
Apple:苹果类,继承水果类
1 | package entity; |
Orange:橘子类,继承水果类
1 | package entity; |
Plate:水果盘接口
1 | package entity; |
FruitPlate:水果盘类,实现水果盘接口
1 | package entity; |
AiFruitPlate:智能水果盘,实现水果盘接口
1 | package entity; |
Person:人类
1 | package entity; |
Lucy:Lucy类,继承 Person 类,她拥有吃橘子的能力 eat
1 | import entity.Orange; |
James:James类,继承 Person 类,他拥有获取水果盘的能力 getAiFruitPlate
1 | import entity.*; |
Scenario:测试类
1 | import entity.*; |
运行结果,如下:
1 | Lucy like eat Orange 🍊 |
我们可以很明显的看出,使用了泛型之后,不需要类型转换,如果,我们把 scenario1()
方法,稍微改下,如下:
1 | private static void scenario1() { |
编译器不会提示有问题,但是运行之后报错,如下:
1 | Exception in thread "main" java.lang.ClassCastException: entity.Apple cannot be cast to entity.Orange |
而,我们把 scenario2()
(使用了泛型)做出同样的修改,如下:
1 | private static void scenario2() { |
编译器,会提示我们有错误,如图:
通过以上案例,很清晰的知道我们为什么要使用泛型,如下:
- 消除类型转换
- 在编译时进行更强的类型检查
- 增加代码的复用性
泛型类(Generic Class)
泛型类是通过类型进行参数化的类,这样说可能不是很好理解,之后我们用代码演示。
普通类(A Simple Class)
首先,我们来定义一个普通的类,如下:
1 | package definegeneric; |
它的 get
、set
方法接受和返回一个 Object
,所以,我们可以随意的传递任何类型。在编译时无法检查类型的使用,我们可以传入 Integer
且取出 Integer
,也可以传入 String
,从而容易导致运行时错误。
泛型类(A Generic Class)
泛型类的定义格式如下:
1 | class name<T1,T2,...,Tn>{ |
在类名之后的 <>
尖括号,称之为类型参数(类型变量),定义一个泛型类就是使用 <>
给它定义类型参数:T1、T2 … Tn。
然后,我们把 SimpleClass
改成泛型类,如下:
1 | package definegeneric; |
所以的 object
都替换成为 T
,类型参数可以定义为任何的非基本类型,如:class类型、interface类型、数组类型、甚至是另一个类型参数。
调用和实例化泛型类型(nvoking and Instantiating a Generic Type)
要想使用泛型类,必须执行泛型类调用,如:
1 | GenericClass<String> genericClass; |
泛型类的调用类似于方法的调用(传递了一个参数),但是,我们没有将参数传递给方法,而是,将类型参数(String)传递给了 GenericClass
类本身。
此代码不会创建新的 GenericClass
对象,它只是声明了 genericClass
将保存对 String
的引用
要实例化此类,要使用 new
关键字,如:
1 | GenericClass<String> genericClass = new GenericClass<String>(); |
或者
1 | GenericClass<String> genericClass = new GenericClass<>(); |
在 Java SE 7 或更高的版本中,编译器可以从上下文推断出类型参数,因此,可以使用 <>
替换泛型类的构造函数所需的类型参数
类型参数命名规范(Type Parameter Naming Conventions)
我们的类型参数是否一定要写成 T
呢,按照规范,类型参数名称是单个大写字母。
常用的类型参数名称有,如:
类型参数 | 含义 |
---|---|
E | Element |
K | Key |
N | Number |
V | Value |
S,U,V… | 2nd, 3rd, 4th type |
多类型参数(Multiple Type Parameters)
泛型类可以有多个类型参数,如:
1 | public interface MultipleGeneric<K,V> { |
输出结果:
1 | key:per, value:6 |
如上代码,new ImplMultipleGeneric
将 K
实例化为 String
,将 V
实例化为 Integer
,因此, ImplMultipleGeneric
构造函数参数类型分别为 String
和 Integer
,在编写 new ImplMultipleGeneric
代码时,编辑器会自动填写 <>
的值
由于,Java 编译器会从声明 ImplMultipleGeneric
推断出 K
和 V
的类型,因此我们可以简写为,如下:
1 | MultipleGeneric<String, Integer> m1 = new ImplMultipleGeneric<>("per",6); |
泛型接口(Generic Interface)
定义泛型接口和定义泛型类相似(泛型类的技术可同用于泛型接口),如下:
1 | interface name<T1,T2,...,Tn>{ |
我们来定义一个泛型接口,如下:
1 | package definegeneric; |
那么,如何实现一个泛型接口呢,我们使用两种方式来实现泛型接口,如下:
使用泛型类,实现泛型接口,且不指定确切的类型参数,所以,实现的 next()
返回值自动变成 T
1 | package definegeneric.impl; |
使用普通类,实现泛型接口,且指定确切的类型参数为 String
,所以,实现的 next()
返回值自动变成 String
1 | package definegeneric.impl; |
泛型方法(Generic Methods)
泛型方法使用了类型参数的方法,泛型方法比较独立,可以声明在 普通类、泛型类、普通接口、泛型接口中。
泛型方法定义格式,如下:
1 | public <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) |
泛型方法的类型参数列表,在 <>
内,该列表必须在方法返回类型之前;对于静态的泛型方法,类型参数必须在 static
之后,方法返回类型之前。
普通类里定义泛型方法(Generic methods in a Simple Class)
我们在普通类中定义泛型方法,如下:
1 | package methodgeneric; |
methodGeneric.<String>genericMethod("java","dart","kotlin")
通常可以省略掉 <>
的内容,编译器将推断出所需的类型,和调用普通方法一样,如:
1 | methodGeneric.genericMethod("java","dart","kotlin") |
泛型类里定义泛型方法(Generic methods in a Generic Class)
我们在泛型类中定义泛型方法,如下:
1 | package methodgeneric; |
在泛型类中定义泛型方法时,需要注意,泛型类里的泛型参数 <T>
和泛型方法里的泛型参数 <T>
不是同一个。
限定类型参数(Bounded Type Parameters)
我们经常看到类似 public <U extends Number> void inspect(U u)
的代码,<U extends Number>
就是限制类型参数,只对数字进行操作且只接受 Number
或其子类。
要声明一个限定的类型参数,需要在参数类型后加上 extends
关键字,然后是其上限类型(类或接口)。
限定类型参数的泛型类(Generic Class of Bounded Type Parameters)
泛型类也可以使用限定类型参数,如下:
1 | package boundedgeneric; |
限定类型参数的泛型方法(Generic methods of Bounded Type Parameters)
泛型方法也可以使用限定类型参数,如下:
1 | package boundedgeneric; |
多重限定(Multiple Bounds)
限定类型参数,也可以为多个限定,如:
1 | <T extends B1 & B2 & B3> |
多个限定参数,如果其中有类,类必须放在第一个位置,例如:
1 | interface A { ... } |
泛型,继承和子类型(Generics, Inheritance, and Subtypes)
在前面的盘子装水果小故事里我们已经创建好了一些水果类,如下:
1 | public class Fruit { |
他们的继承关系,如图:
众所周知,我们可以把子类赋值给父类,例如:
1 | Apple apple = new Apple(); |
泛型也是如此,我们定义一个水果盘子的泛型类,如下:
1 | public class FruitPlateGen<Fruit> implements Plate<Fruit> { |
所以,是 Fruit
的子类都可以放入水果盘里,如下:
1 | FruitPlateGen<Fruit> fruitPlate = new FruitPlateGen<Fruit>(); |
现在,James 可以获取盘子,如下:
1 | public class James extends Person { |
如是,James 想获取放橘子的盘子,如下:
1 | James james = new James(); |
虽然,Orange
是 Fruit
的子类,但是,FruitPlateGen<Orange>
不是 FruitPlateGen<Fruit>
的子类,所以,不能传递产生继承关系。
泛型类和子类型(Generic Classes and Subtyping)
我们可以通过继承(extends)或实现(implements)泛型类或接口,例如:
1 | private static class ExtendFruitPlate<Orange> extends FruitPlateGen<Fruit> { |
此时,ExtendFruitPlate<Orange>
就是 FruitPlateGen<Fruit>
的子类,James 再去拿盘子,就不会有错误提示:
1 | james.getAiFruitPlateGen(new ExtendFruitPlate<Orange>()); |
通配符(Wildcards)
我们经常看到类似 List<? extends Number>
的代码,?
就是通配符,表示未知类型。
上限通配符(Upper Bounded Wildcards)
我们可以使用上限通配符来放宽对变量的限制,例如,上文提到的 FruitPlateGen<Fruit>
和 FruitPlateGen<Orange>()
就可以使用上限通配符。
我们来改写一下 getAiFruitPlateGen
方法,如下:
1 | public FruitPlateGen getAiFruitPlateGen2(FruitPlateGen<? extends Fruit> plate) { |
这时候,James 想获取放橘子的盘子,如下:
1 | James james = new James(); |
上限通配符 FruitPlateGen<? extends Fruit>
匹配 Fruit
和 Fruit
的任何子类型,所以,我们可以传入 Apple
、Orange
都没有问题。
下限通配符(Lower Bounded Wildcards)
上限通配符将未知类型限定为该类型或其子类型,使用 extends
关键字,而下限通配符将未知类型限定为该类型或其父类型,使用 super
关键字。
我们再来宽展一下 getAiFruitPlateGen
方法,如下:
1 | public FruitPlateGen getAiFruitPlateGen3(FruitPlateGen<? super Apple> plate) { |
这时候,James 只能获取 FruitPlateGen<Fruit>
和 FruitPlateGen<Apple>
的盘子,如下:
1 | James james = new James(); |
下限通配符 FruitPlateGen<? super Apple>
匹配 Apple
和 Apple
的任何父类型,所以,我们可以传入 Apple
、Fruit
。
通配符和子类型(Wildcards and Subtyping)
在 泛型,继承和子类型 章节有讲到,虽然,Orange
是 Fruit
的子类,但是,FruitPlateGen<Orange>
不是 FruitPlateGen<Fruit>
的子类。但是,你可以使用通配符在泛型类或接口之间创建关系。
我们再来回顾下 Fruit
的继承关系,如图:
代码,如下:
1 | Apple apple = new Apple(); |
这个代码是没有问题的,Fruit
是 Apple
的父类,所以,可以把子类赋值给父类。
代码如下:
1 | List<Apple> apples = new ArrayList<>(); |
因为,List<Apple>
不是 List<Fruit>
的子类,实际上这两者无关,那么,它们的关系是什么?如图:
List<Apple>
和 List<Fruit>
的公共父级是 List<?>
。
我们可以使用上下限通配符,在这些类之间创建关系,如下:
1 | List<Apple> apples = new ArrayList<>(); |
下图展示了上下限通配符声明的几个类的关系,如图:
PECS原则(Producer extends Consumer super)
在上文中有 FruitPlateGen
水果盘子的类,我们尝试使用上下限通配符来实例化水果盘,代码如下:
1 | Apple apple = new Apple(); |
上限通配符无法 set
数据,但是,可以 get
数据且只能 get
到其上限 Fruit
,所以,上限通配符可以安全的访问数据。
在来看一下代码,如下:
1 | FruitPlateGen<? super Apple> fruitPlateGen1 = new FruitPlateGen<>(); |
下限通配符可以且只能 set
其下限 Apple
,也可以 get
数据,但只能用 Object
接收(因为Object是所有类型的父类,这是一个特例),所以,下限通配符可以安全的写入数据。
所以,在使用上下限通配符时,可以遵循以下准则:
- 如果你只需要从集合中获得类型T , 使用<? extends T>通配符
- 如果你只需要将类型T放到集合中, 使用<? super T>通配符
- 如果你既要获取又要放置元素,则不使用任何通配符
类型擦除(Type Erasure)
Java 语言使用类型擦除机制实现了泛型,类型擦除机制,如下:
- 编译器会把所有的类型参数替换为其边界(上下限)或 Object,因此,编译出的字节码中只包含普通类、接口和方法。
- 在必要时插入类型转换,已保持类型安全
- 生成桥接方法以在扩展泛型类时保持多态性
泛型类型的擦除(Erasure of Generic Types)
Java 编译器在擦除过程中,会擦除所有类型参数,如果类型参数是有界的,则替换为第一个边界,如果是无界的,则替换为 Object。
我们定义了一个泛型类,代码如下:
1 | public class Node<T> { |
由于类型参数 T
是无界的,因此,Java 编译器将其替换为 Object,如下:
1 | public class Node { |
我们再来定义一个有界的泛型类,代码如下:
1 | public class Node<T extends Comparable<T>> { |
Java 编译器其替换为第一个边界 Comparable
,如下:
1 | public class Node { |
泛型方法的擦除(Erasure of Generic Methods)
Java 编译器同样会擦除泛型方法中的类型参数,例如:
1 | public static <T> int count(T[] anArray, T elem) { |
由于 T
是无界的,因此,Java 编译器将其替换为 Object,如下:
1 | public static int count(Object[] anArray, Object elem) { |
如下代码:
1 | class Shape { ... } |
有一个泛型方法,如下:
1 | public static<T extends Shape> void draw(T shape){ |
Java 编译器将用第一个边界 Shape
替换 T
,如下:
1 | public static void draw(Shape shape){ |
桥接方法(Bridge Methods)
有时类型擦除会导致无法预料的情况,如下:
1 | public class Node<T> { |
类型擦除后,代码如下:
1 | public class Node { |
此时,Node 的方法变为 setData(Object data)
和 MyNode 的 setData(Integer data)
不会覆盖。
为了解决此问题并保留泛型类型的多态性,Java 编译器会生成一个桥接方法,如下:
1 | class MyNode extends Node { |
这样 Node 的方法 setData(Object data)
和 MyNode 生成的桥接方法 setData(Object data)
可以完成方法的覆盖。
泛型的限制(Restrictions on Generics)
为了有效的使用泛型,需要考虑以下限制:
- 无法实例化具有基本类型的泛型类型
- 无法创建类型参数的实例
- 无法声明类型为类型参数的静态字段
- 无法将Casts或instanceof与参数化类型一起使用
- 无法创建参数化类型的数组
- 无法创建,捕获或抛出参数化类型的对象
- 无法重载每个重载的形式参数类型都擦除为相同原始类型的方法
无法实例化具有基本类型的泛型类型
代码如下:
1 | class Pair<K, V> { |
创建对象时,不能使用基本类型替换参数类型:
1 | Pair<int, char> p = new Pair<>(8, 'a'); // error |
无法创建类型参数的实例
代码如下:
1 | public static <E> void append(List<E> list) { |
无法声明类型为类型参数的静态字段
代码如下:
1 | public class MobileDevice<T> { |
类的静态字段是所有非静态对象共享的变量,因此,不允许使用类型参数的静态字段。
无法将Casts或instanceof与参数化类型一起使用
代码如下:
1 | public static <E> void rtti(List<E> list) { |
Java 编译器会擦除所有类型参数,所有,无法验证在运行时使用的参数化类型。
无法创建参数化类型的数组
代码如下:
1 | List<Integer>[] arrayOfLists = new List<Integer>[2]; // error |
无法创建,捕获或抛出参数化类型的对象
代码如下:
1 | class MathException<T> extends Exception { ... } // error |
无法重载每个重载的形式参数类型都 擦除为相同原始类型的方法
代码如下:
1 | public class Example { |
print(Set<String> strSet)
和 print(Set<Integer> intSet)
在类型擦除后是完全相同的类型,所以,无法重载。