1.概述
1.1 背景
Java推出泛型之前,程序员可以构建一个元素类型为Object的集合,该集合能够存储任意类型对象,而在使用该集合的过程中,需要程序员明确知道存储每个元素的数据类型,否则很容易引发ClassCastException异常
1.2 泛型的概念
Java泛型(Generics)是JDK5中引入的一个新特性,泛型提供了编译时类型安全监测机制,该机制允许我们在编译时检测到非法的类型数据结构。
泛型的本质就是参数化类型,也就是所操作的数据类型被指定为一个参数
1.3 泛型的好处
- 类型安全
- 消除了强制类型的转换
2.泛型类
class 类名称<泛型标识,泛型标识,......> {
private 泛型标识 变量名;
......
}
常用的泛型标识:T、E、K、V
注意事项:
- 泛型类,如果没有指定具体的数据类型,此时类型是Object
- 泛型的类型参数只能是类类型(引用数据类型),不能是基本数据类型
- 泛型类型在逻辑上可以看成是多个不同的类型,但实际上都是相同类型(方法重载时如果只有泛型类型不同,此时不允许编译通过)
3.泛型类派生子类
-
子类也是泛型类,子类和父类的泛型类型要保持一致
class ChildGeneric<T> extends Generic<T>
-
子类不是泛型类,父类要明确泛型的数据类型
class ChildGeneric extends Generic<String>
4.泛型接口
interface 接口名称<泛型标识,泛型标识,......> {
泛型标识 方法名();
......
}
常用的泛型标识:T、E、K、V
泛型接口使用(和泛型类派生子类用法一致):
- 实现类不是泛型类,接口要明确数据类型
- 实现类也是泛型类,实现类和接口的泛型类要保持一致
5.泛型方法
修饰符 <T, E, ......> 返回值类型 方法名(形参列表) {
方法体......
}
常用的泛型标识:T、E、K、V
注意事项:
- public与返回值中间非常重要,可以理解为声明此方法为泛型方法
- 只有声明了的方法才是泛型方法,泛型类中使用了泛型的成员方法并不是泛型方法
- 如果泛型方法的类型标识符和类的泛型标识符重复了,使用的一定是泛型方法的类型
- 参数可以使用泛型可变参:public void print(T... t){}
6.类型通配符
-
泛型通配符一般是使用"?"代替具体的类型实参
-
泛型通配符是类型实参,而不是类型形参
-
List<?> 可以接收任意类型的集合,但是不能向其中添加数据(不能add)
7.泛型通配符上限
类/接口<? extends 实参类型>
要求该泛型的类型,只能是实参类型,或为实参类型的子类类型
注意事项:
-
接受的话数据类型是实参类型(实参类型 item = list.get())
-
List<? extend Animal>可以接收Animal和Animal子类类型的集合。也不能添加,因为类型不确定,如果接受的是Animal的子类,添加的是Animal,这样显然是不行的,所以也就不让添加了
-
List<Integer> list = new ArrayList<>(); List<? extends Cat> list = new ArrayList<>() {{ add(new MiniCat()); }}; 左边有类型,右边无类型。经过类型推断,右边的集合数据类型和左边的保持一致(由反编译字节码得出结论)
8.泛型通配符下限
类/接口 <? super 实参类型>
要求改泛型的类型,只能是实参类型,或为实参类型的父类类型
注意事项:
- 接收的话数据类型是Object类型(Object obj = list.get())
- 可以填充类型(可以add),类型为实参类型或实参子类类型。
9.泛型擦除
泛型是JDK 1.5中才引入的概念,在这之前是没有泛型的。但是,之后版本的泛型代码能够很好的和之前版本的代码兼容,那是因为,泛型信息只存在于代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦除,我们称之为—类型擦除。
- T 会被擦除为 Object
- T extends Number 会被擦除为 Number
- 调用者知道传递的是什么类型所以调用后会做一个强转
// 源码
Map<String, String> map = new HashMap<>();
map.put("hello", "world");
map.put("你好", "世界");
System.out.println(map.get("hello"));
// 反编译class文件
Map map = new HashMap();
map.put("hello", "world");
map.put("你好", "世界");
System.out.println((String)map.get("hello"));
9.1 泛型擦除引发的问题
下面我们看一个稍微有点复杂的例子,首先声明一个接口,然后创建一个实现该接口的类:
public interface Fruit<T> {
T get(T param);
}
public class Apple implements Fruit<Integer> {
@Override
public Integer get(Integer param) {
return param;
}
}
按照之前我们的理解,在进行类型擦除后,应该是这样的:
public interface Fruit {
Object get(Object param);
}
public class Apple implements Fruit {
@Override
public Integer get(Integer param) {
return param;
}
}
但是,如果真是这样的话那么代码是无法运行的,因为虽然Apple
类中也有一个get
方法,但是与接口中的方法参数不一致,也就是说没有覆盖接口中的方法。针对这种情况,编译器会通过添加一个桥接方法来满足语法上的要求,同时保证了基于泛型的多态能够有效。我们反编译上面代码生成的字节码文件:
可以看到,编译后的代码中生成了两个get
方法。参数为Object
的get
方法负责实现Fruit
接口中的同名方法,然后在实现类中又额外添加了一个参数为Integer
的get
方法,这个方法也就是理论上应该生成的带参数类型的方法。最终用接口方法调用额外添加的方法,通过这种方式构建了接口和实现类的关系,类似于起到了桥接的作用,因此也被称为桥接方法,最终,通过这种机制保证了泛型情况下的Java多态性。