一、为什么使用泛型
在java引入泛型之前,java的泛型程序设计是使用继承的向上转型性质实现的,也就是所有的变量让它们都继承自Object类。看以下的一段代码:
这样的话,比如我们可以定义一个ArrayList类,让它保存所有的String类型元素,或者Integer类型元素等等。
例如:ArrayList strList = new ArrayList(); strList.add("HELLO");
ArrayList intList = new ArrayList(); intList.add(2);
但是这个有如下两个缺点:
- 当获取一个值时,必须进行强制类型转换(即向下转型)。例如 String str = (String) strList.get(0);
- 由于可以向list里添加任何类型的对象(比如,对于strList,也可以添加Integer类型,strList.add(2)也不会有任何问题),所以当添加的元素类型不一致,并且在需要后续get时,及其容易出错。(比如,会默认认为strList存储的都是String类型,而实际上strList的第二个元素存储的是Integer,所以当调用:String str = (String) strList.get(1);会报错)
所以基于上述问题,java中又增加了泛型的概念,主要就是帮助在编译时就能够避免上述现象的发生。例如,可以通过:
ArrayList<String> strList = new ArrayList<String>();
来限定strList只能存储String类型的变量。当然,也说过,后面的new ArrayList<String>();中的String是可以省略的,它可以由前面推导出来,即写成如下形式:
ArrayList<String> strList = new ArrayList<>();
二、定义泛型类、泛型方法、泛型接口
可以自定义泛型类、泛型方法与泛型接口。
1. 定义泛型类
只需要在类名后面加个‘<>’,并且在尖括号里面填入泛型参数即可。
注意点:
- 泛型参数可以是1个,如 class ClassName<T>;也可以是多个,如class ClassName<K, V>;对于泛型参数使用哪个字母没有特殊要求,不过一般约定俗称的规则是:首先,必须是大写;第二,使用E表示集合元素的类型,使用K, V表示Map的关键字与值的类型,使用T表示任意的类型;
- 当定义泛型类时,除了可以定义成‘class ClassName<T>’的形式,还可以定义成如‘class ClassName<T extends FatherClassName>’,但注意不可以定义成‘class ClassName<T super SonClassName>’的形式!!!(没有这种定义方式)。
- 对于‘class ClassName<T>’形式,表示只要是Object类型及其子类型都可以传入,换句话说,这种形式,对泛型参数没有任何限制;对于class ClassName<T extends FatherClassName>,表示定义的这个类的泛型参数只能传入FatherClassName类型或者继承自这个类的子类型;
- 对于class ClassName<T extends FatherClassName>形式,不管FatherClassName是类还是接口,都应该用extends,而不应该用implements。
- 针对上述的两种定义形式,在实现对应的泛型类的方法时,究竟T类型能够有哪些默认方法可以使用?这个也很简单,比如class ClassName<T>,就可以把T当做一个Object来用,所以它具有toString(),equals()等方法可供使用;同样的,对于class ClassName<T extends FatherClassName>,那么就可以把T当做FatherClassName类型使用,比如FatherClassName有“computeSum()”的方法,那么T也就可以调用相应的方法。具体可以看下面的一个示例:
class Fruit {
public String sayName() {
return "I am Fruit";
}
}
class Apple extends Fruit {
public String sayName() {
return "I am Apple";
}
}
class Point<T, S extends Fruit> { // x与y不是一个类型,x可以是任意的继承自Object的类型;而y只能是Fruit及其子类型
private T x; // 就可以把T看做Object
private S y; // 就可以把S看做Fruit
public Point() {
}
public Point(T x, S y) {
this.x = x;
this.y = y;
}
public String getX() {
return x.toString(); // 可以把x当做Object类型,所以有toString方法
}
public S getY() {
return y;
}
public String ySayName() { // 可以把y当做Fruit类型,所以有sayName方法
return y.sayName();
}
public boolean xequalsY(T x, S y) {
if (x.getClass() == y.getClass()) { // 由上面两行注释可知,x与y肯定都有getClass方法,并且接下来肯定返回false
return true;
} else {
return false;
}
}
}
public class Test {
public static void main(String[] args) {
Point<Integer, Fruit> point = new Point<>(10, new Apple()); // 10自动被包装成Integer
Point<Float, Apple> point2 = new Point<>(10.3f, new Apple()); //也可以调用成功
// Point<Double, Double> point3 = new Point<>(10.3, 10.3); // 调用失败,第二个泛型参数不能是Double,只能是Fruit或Apple
String x = point.getX(); // 输出x=10
String yName = point.ySayName(); // 输出"I am Apple"
boolean bool = point.xequalsY(10, new Apple()); // 输出false
String x2 = point2.getX(); // 输出x2=10.3
String yName2 = point2.ySayName(); // 输出"I am Apple"
boolean bool2 = point2.xequalsY(10.3f, new Apple()); // 输出false
}
}
2. 定义java泛型方法
需要注意的是,java泛型方法既可以定义在泛型类中,也可以定义在非泛型类中。并且,像上面定义泛型类中的xequalsY、getY都不是泛型方法!!!因为它们用的泛型参数都是泛型类中定义好的参数T、S。
将泛型方法定义在非泛型类中
其中,T... a表示这个函数接收到的是一个可变参数的输入,或者说参数更像是一个T[]的数组。调用方式如下:
另外,可以省略后面尖括号中的String(包括可以省略尖括号),因为后面的可以自动推导出来,因此常用的调用方式为:
同样将T作为一个Object类型使用,那么a就是Object[]。
将泛型方法定义在泛型类中
和上面差不多,只不过类是泛型类,并且方法的泛型参数不能和类的泛型参数一样,例如:
调用如下:
3. 定义泛型接口
泛型接口的定义与泛型类的定义差不多,如:
但需要注意的是,实现这个接口时很容易搞错。需要注意以下两点。
- 当定义实现这个接口的类时,如果泛型接口未传入泛型实参(即还是T,不是String、Integer之类的),则在定义类的时候,需将泛型的声明也一起加到类中(即必须定义一个泛型类)。
- 如果泛型接口传入类型参数时,则定义类时无需将它定义为泛型类,并且在定义这个类重写接口方法时,接口中的所有使用泛型的地方都要替换成传入的实参类型。
interface Generator<T> {
public T next();
}
class FruitGenerator<T> implements Generator<T> { // FruitGenerator定义一个泛型参数,并将这个泛型参数传给这个Interface。千万不能是class FruitGenerator implements Generator<T>,这样会报错!!!因为Generator不知道T是什么
@Override
public T next() { return null;}
}
class FruitGenerator2 implements Generator<String> { // 或者在实现这个接口时,直接给这个接口传入实际的泛型类型也是可以的。
@Override
public String next() {return null; }
}
public class Test {
public static void main(String[] args) {
FruitGenerator<String> fruitGenerator = new FruitGenerator<>();
FruitGenerator2 fruitGenerator2 = new FruitGenerator2();
}
}
三、通配符
假设定义了如下的泛型类:
class Box<T> {
private T data;
public Box() {
}
public Box(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
我们想要知道:对于不同传入的泛型实参,其生成的相应对象实例的类型是不是一样的呢?他们之间具不具有继承等关系呢?(类似于问:Box<Integer>和Box<String>是同一种类型吗?Integer继承了Number类,那么是不是Box<Integer>也是Box<Number>的子类?)
可以看下面一段代码:
public class GenericTest {
public static void main(String[] args) {
Box<String> name = new Box<String>("corn");
Box<Integer> age = new Box<Integer>(712);
Box<Number> age2 = new Box<Number>(721);
System.out.println("name class:" + name.getClass()); // 输出:Box
System.out.println("age class:" + age.getClass()); // 输出:Box
System.out.println("age2 class:" + age2.getClass()); // 输出:Box
System.out.println(name.getClass() == age.getClass()); // true
}
}
其实可以发现,Box<String>、Box<Integer> 、Box<String>其实都是同一个类Box,也就是说,在Java运行时并没有产生新的类,包括Box<Integer>并不是Box<Number>类的子类,泛型参数更多的像是限定了这个类所使用的数据类型。具体的原理参看下节的“擦除”。
再看下面代码:
public class GenericTest {
public static void main(String[] args) {
Box<Number> name = new Box<Number>(99);
Box<Integer> age = new Box<Integer>(712);
getData(name);
//The method getData(Box<Number>) in the type GenericTest is
//not applicable for the arguments (Box<Integer>)
getData(age); // 报错
}
public static void getData(Box<Number> data){
System.out.println("data :" + data.getData());
}
}
我们知道,由于Box<Integer>不是Box<Number>的子类,因此当运行到getData(age);时就会出错(因为不是继承的关系,所以不能向上转型),但是肯定又不想再重复写一个能够接收Box<Integer>的getData方法,所以就有了通配符的产生。看下面代码:
public class GenericTest {
public static void main(String[] args) {
Box<String> name = new Box<String>("corn");
Box<Integer> age = new Box<Integer>(712);
Box<Number> number = new Box<Number>(314);
getData(name);
getData(age);
getData(number);
}
public static void getData(Box<?> data) {
System.out.println("data :" + data.getData());
}
}
通过使用无限定通配符Box<?>既可以使得getData方法传入Box<String>、Box<Integer>等参数。虽然Box<?>也是Box类型,并不是Box<String>、Box<Integer>等的父类型,但这种通配符确实比较特殊,使用它就能让泛型类的泛型参数为任意对象。同样的,还有类型统配符上限和类型通配符下限的概念。比如,对于getData方法,如果只想让它传入的是泛型参数为Number或继承自Number的泛型类,那么就可以定义为如下:
public class GenericTest {
public static void main(String[] args) {
Box<String> name = new Box<String>("corn");
Box<Integer> age = new Box<Integer>(712);
Box<Number> number = new Box<Number>(314);
getData(name); // 错误
getData(age); // 正确
getData(number); // 正确
}
public static void getData(Box<? extends Number> data) {
System.out.println("data :" + data.getData());
}
}
这时发现,getData(name)编译不通过,因为String不是Number的子类。总结一下:
- 无限定通配符 ClassName<?> :对泛型类的泛型参数没有要求,只要是继承自Object的类即可,即只要是任意类即可;
- 类型通配符上限 ClassName<? extends FatherClassName> :传入的泛型类的类型参数必须是FatherClassName类或这个类的子类
- 类型通配符下限 ClassName<? super SonClassName> :传入的泛型类的类型参数必须是SonClassName类或这个类的父类
四、深入理解泛型的“擦除”概念
Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。理解类型擦除对于用好泛型是很有帮助的,尤其是一些看起来“疑难杂症”的问题,弄明白了类型擦除也就迎刃而解了。
- 消除类型参数声明,即删除<>及其包围的部分。
- 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
- 为了保证类型安全,必要时插入强制类型转换代码。
- 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。
1. 擦除类定义中的类型参数
无限制类型擦除
当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如<T>和<?>的类型参数都被替换为Object,如下:
有限制类型擦除
当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object,如下:
2. 擦除方法定义中的类型参数
擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例,如图:
3. 桥接方法和泛型的多态
考虑如下代码:
interface Info<T> {
T info(T var);
}
class BridgeMethodTest implements Info<Integer> {
@Override
public Integer info(Integer var) {
return var;
}
}
按照之前将的擦除原则,那么擦除之后,就是下面这样:
interface Info {
Object info(Object var);
}
class BridgeMethodTest implements Info {
@Override
public Integer info(Integer var) {
return var;
}
}
但是,明显可以看出,这样擦除类型后的代码在语法上是错误的:BridgeMethodTest类中虽然存在一个info方法,但是和Info接口要求覆盖的info方法不一致:参数类型不一致。在这种情况下,Java编译器会自动增加一个所谓的“桥接方法”(bridge method)来满足Java语法的要求,同时也保证了基于泛型的多态能够有效。我们反编译一下BridgeMethodTest.class文件可以看到Java编译器到底是如何做的:
$ javap BridgeMethodTest.class
Compiled from “BridgeMethodTest.java”
public class BridgeMethodTest implements Info<java.lang.Integer> {
public BridgeMethodTest();
public java.lang.Integer info(java.lang.Integer);
public java.lang.Object info(java.lang.Object);
}
可以看出,Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。如何理解“桥接”二字呢?我们进一步反编译BridgeMethodTest看一下:
public class BridgeMethodTest
implements Info
{
public BridgeMethodTest()
{
}
public Integer info(Integer integer)
{
return integer;
}
public volatile Object info(Object obj)
{
return info((Integer)obj);
}
}
info(Object)方法通过调用子类的info(Integer)方法搭起了父类和子类的桥梁,也就是说,info(Object obj)这个方法起到了连接父类和子类的作用,使得Java的多态在泛型情况下依然有效。
当然,我们在使用基于泛型的多态时不必过多的考虑“桥接方法”,Java编译器会帮我们打理好一切。
五、 使用泛型的注意点
1. 不能使用基本类型实例化泛型参数
例如:Box<double>、Box<int>等都是非法的,因为泛型中的泛型参数都是要继承自Object类的,基本类型不是类。因此应改为:Box<Double>、Box<Integer>。
2. 运行时类型查询只适应于原始类型
例如:
if (a instanceof Box<String>)
if (a instanceof Box<T>)
Box<String> p = (Box<String>) a;
上面的这些都是错的,原因在于就根本没有Box<String>、Box<T>这些类,只有Box这个类。
3. 不能实例化类型变量
因为类型擦除后,T就变为Object,我们肯定不不希望创建一个new Object()的对象。如果非要创建一个对象,可以通过反射调用Class.newInstance方法构建,不过细节有点复杂,不再介绍。
4. 不能创建参数化类型的数组
例如:Pair<String>[] table = new Pair<String>[10];这样会产生错误的。
为什么要这样呢?其实主要是为了避免数组里出现类型不一致的元素。
参考:
https://blog.youkuaiyun.com/qq_41286138/article/details/105250938
https://www.jianshu.com/p/c75e1f5e45b7
5. 不能向参数可变的方法传递一个泛型类型的实例
其实和第4点原理一样,因为参数可变的方法相当于就是传一个泛型数组,这是不允许的。例子如下:
6. 泛型类中不能有静态类型的泛型变量和泛型方法
例如:
这个是错误的。(原因暂未知)
7. 不能抛出或捕获泛型类的实例,甚至泛型类扩展Throwable都是非法的