泛型简介
泛型实际上就是类型参数。即事先不确定参数类型,而在调用时传入对应参数类型才确定其类型。
在泛型出现之前,一般是使用多态来实现,即使用Object
来接受所有参数,确定就是接收到参数后需要强转。
定义泛型类
泛型类(generic type)就是一个或多个类型变量的类。
例如:
1 | package test.mw.ExceptionTest; |
在Pair类中引入了一个参数类型T。这里用尖括号<>进行定义。可以按如下格式进行实例化。三种方法都可以,但是第一种比较常用。
1 | Pair pair1 = new Pair<String>("1", "2"); |
同样的,我们可以,通过
1 | public class Pair<T, U>{ |
来定义多个类型泛型。
泛型方法
同样的,我们可以定义带有泛型的方法。例如:
1 | class ArrayAlg{ |
调用
1 | String[] arr = new String[]{"1", "2", "3", "4", "5"}; |
注意这里的格式与类中稍有不同,需要按:
1 | [限定符] [修饰符...] [<泛型,...>] [返回类型-可以为前面的泛型] [name]([parameters ...]){ |
类型变量的限定
我们可以通过以下格式来限定泛型继承了某个类或者实现了某个接口。
1 | public class Pair<T extends Class1> {} |
也可以通过以下格式来限定继承了多个父类或者实现了多个接口。(类和接口都是用extends)
1 | public class Pair<T extends Class1 & Interface1> {} |
泛型代码和虚拟机
虚拟机中并没有泛型对象-所有的对象都是普通类,即其实际上是一个编译器的语法糖。其最主要的特点在于类型擦除。
类型擦除
在Java编译器中,所有的泛型都将被擦除为最低限定类。最低限定类指符合上述限定规则的类型。
例1:
1 | public class Pair<T> { |
会被擦除为:
1 | public class Pair{ |
这是因为T没有限制。
例2:
1 | public class Pair<T extends Float> { |
会被擦除为:
1 | public class Pair{ |
这是因为T被限定为继承了Float的类。所以被擦除为Float
。
例3:
1 | public class Pair<T extends Serializable & Comparable> { |
会被擦除为
1 | public class Pair{ |
当有多个限定时规则如下:
- 将类型转换为第一个超类(接口)。
- 在必要时需要向
Comparable
接口插入强制类型转换。
所以应该尽量把包含方法的接口方法在第一个,可以减少强制转换。提高效率。
转换泛型表达式
由于编译器对泛型做了擦除。所以实际上编译器在接受到泛型的位置加上了强制类型转换。如:
1 | Pair<Integer, Integer> pair = new Pair(1, 2); |
实际上会被转化为
1 | Pair pair = new Pair(1, 2); |
因为这里pair.getFirst()
返回的实际上是Object
。
泛型方法与桥方法
泛型方法也一样会被擦除类型,与类一样的规则。如:
1 | public staic <T> T getMid(T... a){ |
会被擦除为:
1 | public staic Object getMid(Object... a){ |
一般来说是不会存在问题,但是如果出现以下情况:
原Pair类:
1 | public class Pair<T extends Serializable & Comparable> { |
继承类:
1 | public class DateInterval extends Piar<LocalDate>{ |
在类型擦除之后,会编为如下代码:
1 | public class DateInterval extends Piar{ |
但是由于父类原始的setSecond
参数类型为T,则会被擦除为Object
,所以实际上这个类中会包含两个setSecond
方法
public void setSecond(Object second)
:来自擦除后的父类。public void setSecond(LocalDate second)
:来自擦除后的本身类。
那么考虑如下代码:
1 | var interval = new DateInterval(...); |
这里我们将子类赋值给父类。而父类由于是个泛型类,所以其setSecond(T second)
会被擦除为setSecond(Object second)
。
我们实际上是想调用子类的public void setSecond(LocalDate second)
方法。但是由于父类并没有这个重载方法,所以会调用父类的public void setSecond(Object second)
方法。这样就不符合我们的预期了。
所以编译器创建了所谓的桥方法。这个桥方法会去调用子类的方法DataInterval.setSecond(LocalData);
。
但是考虑如下情况:
当子类重载了父类的方法。
1 | class DateInterval extends Pair<LocalDate>{ |
那么在子类中,经过擦除之后,会有以下这个两个方法:
LocalDate getSecond()
Object getSecond()
虽然我们在编写代码时并允许这样的情况存在,因为方法名和参数共同构成了方法的方法签名用于识别这个方法。
但由于这是编译器生成的,所以虚拟机能够正确处理这种情况。
所以实际需要记住以下的特点:
- 虚拟机中没有泛型,只有普通的累和方法。
- 所有的类型参数都会被替换为它们的限定类型。
- 会合成桥方法来保持多态。
- 为保持类型安全性,必要时会插入强制类型转换。
限制与局限性
1. 不能用基本类型实例化类型参数
不能用基本类型实例化类型参数。即没有Pair<double>
,只有Pair<Double>
。其主要原因还是在于类型擦除。擦除之后,Pair类含有Object类型的字段,而Object不能存储基本类型。但是也并会有很大的影响,因为每种基本类型都有其包装类型。
2. 类型查询不能检查出同一类别泛型的不同
即类型查询只产生原始类型。
所以,instanceof
不能用于检查泛型类型,如:
1 | if(pair1 instanceof Pair<String>){}; //Cannot perform instanceof check against parameterized type Pair<String>. Use the form Pair<?> instead since further generic type information will be erased at runtime |
3. 不能创建参数化类型的数组
以下代码会报错
1 | var table = new Pair<String>[10]; //Cannot create a generic array of Pair<String> |
其原因也在于类型擦除,如果允许这种机制的存在,那么擦除后table将是一个Object[]
,但我们期望的是Pair<String>
类型,则类型检查失效,因此禁用这样的声明。
注意:声明这种类型是允许的,但是使用其初始化是不允许的。
要想存储泛型数组有以下两种方法:
先使用通配类型来初始化,在强转为对应类型:
1
var table = (Pair<String>[]) new Pair<?>[10];
但这样并不安全,因为这样同样会失去对泛型的类型检查。
使用
ArrayList
来存储泛型对象:ArrayList<Pair<String>>
4. Varargs警告
上面提到了不能创建泛型类数组,但是当我们向方法中传递可变长参数时,方法内部实际得到的是一个对应参数类型的数组。那么如果传递的是泛型类的对象,则违背了第3条规定。此时虚拟机是允许这种方式的存在,但是会产生一个Varargs
警告。
Type safety: Potential heap pollution via varargs parameter pairs
有两种方法可以来抑制这个错误:
- 增加注解
@SuppressWarning("unchecked")
- 增加注解
@SafeVarargs
5. 不能实例化类型变量
不能在类似new T(...)
的表达式中使用类型变量。
在Java8之后,最好的解决办法是让调用者提供一个构造器表达式。如:
1 | Pair<String> p = Pair.makePair(String::new); |
makePair
接受一个Supplier<T>
,这是一个函数式接口,表示一个无参数而且返回类型为T 的函数:
1 | public static <T> Pair<T> makePair(Supplier<T> constr){ |
在Java8之前,一般是通过反射Constructor.new Instance
来实现。
但是不能通过T.class.getConstructor().newInstance()
来构建,**因为T会被擦除为Object
**。
一个示例的函数应当如下所示:
1 | public static <T> Pair<T> makePair(Class<T> cl){ |
可以这样调用:
1 | Pair<String> p = Pair.makePair(String.class); |
6. 不能构造泛型数组
不能实例化泛型变量,则同样的,也不能构造泛型数组。
即T[] mm = new T[2];
这样是不合法的。
类型擦除会使其总构造出Object[]
。
这里同样可以
- 使用提供构造器
String[]::new
。 - 使用反射
Array.newInstance
来实现。
7. 泛型类的静态上下文中类型变量无效
即不能在静态字段或方法中引用类型变量。即
1 | public class C1<T>{ |
8. 不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类的对现象。实际上,泛型类扩展Throwable
甚至都是不合法的。
如扩展Throwable
:
1 | public class Problem<T> extends Exception{} //ERROR--can`t extend Throwable |
抛出含有泛型的Throwable
1 | public static <T extends Throwable> void doWork(Class<T> t){ |
不过,在异常规范中使用类型变量是允许的。即以下方法是合理的:
1 | public static <T extends Throwable> void d{ |
9. 可以取消对检查型异常的检查
Java异常处理的一个基本原则是,必须为所有检查型异常提供一个处理器。不过可以利用泛型取消这个机制。
10. 注意擦除后的冲突
当泛型类型被擦除后,不允许创建引发冲突的条件。
假定Piar类增加一个equals
方法,如下:
1 | public class Pair<T>{ |
当T为String时,则在类型擦除之前,其会有两个equals
方法:
boolean equals(String var)
:定义在Pair中blooean equals(Object var)
:从Object中继承
但是在擦除之后就会有两个相同的blooean equals(Object)
方法,这就产生了冲突。
所以,泛型还增加了另外一个原则:如果两个接口类型是同一接口的不同参数化,一个类或类型变量就不能同时作为这两个接口类型的子类。即如下代码就是错误的:
1 | class Employee implements Comparable<Employee>{...}; |
其原因在于,又可能会与合成的桥方法产生冲突。
实现Comparable<X>
的类将会获得一个桥方法:
1 | public int comparaTo(Object other){return compareTo((X) other);} |
不能对不同的类型x有两个这样的方法。
泛型类型的继承规则
泛型中的继承规则很简单,就是类型参数完全与继承链无关。
例如,ArrayList<Double>
与ArrayList<Number>
完全没有关系。
而ArrayList<Double>
是AbstractList<Double>
的一个子类。(ArrayList<E>
是AbstractList<E>
的子类)
ArrayList<Double>
是对原ArrayList<E>
的一个继承。
通配符类型
概念
由于Java中的泛型规定的很严格,所以设计了“通配符类型”来缓解。
子类型限定
在通配符类型中,允许类型参数发生变化,比如:
1 | Pair<? extends Employee> pair1 = new Pair<Employee>(); |
表示任何泛型Pair
类型,只要其为Employee或者其子类即可。如Pair<Manager>
。
记得前面说的,泛型参数中不允许其子类多态存在。即:
1 | public class Pair<Employee>{ |
我们不能这么定义Pair
1 | Pair<Employee> pair1 = new Pair<Manager>(); |
但是现在可以使用通配符来实现该功能:
1 | Pair<? extends Employee> = new Pair<Manager>(); |
所以从继承的角度来说,
Pair<Manager>
和Pair<Employee>
继承自Pair<? extends Employee>
;
Pair<? extends Employee>
继承自Pair<T>
;
但是其缺点在于其不能调用含通配类型的参数的方法,如:
1 | Pair<? extends Employee> pari1= new Pair<Manager>(); |
这是因为在编译器中,setFirst
接受的参数是一个继承自Employee
,但不知道具体是什么类型。所以它拒绝接受任何特定的类型。毕竟?不能匹配。(也就是说:编译器并不知道具体代码中的继承链,?仅仅是占位)
但是getFirst
是可以执行的,因为其将getFirst
的返回值赋值给一个Employee
是完全合法的。
超类型限定
超类型限定限定指限定?必须为某一个类的父类。如:
1 | ? super Manager |
指定?必须为Sub的父类。
则这里的行为与子类限定相反,**?不能作为方法的返回值,但是可以作为方法的参数。**其道理也恰好相反。
setFirst
方法中,编译器无法知道其具体类型,所以不能接受参数类型为Employee
或者Object
的方法调用。只能传递Manager类的对象,或者某个子类型。另外,如果调用getFirst
方法,不能保证返回对象的类型。只能将其赋值给一个Object
。
总结
带有子类型限定符的泛型通配符允许你获取一个泛型对象;而带有超类型限定符的泛型通配符允许你写入一个对象。
其主要原因我们需要理解一个点就是:?仅仅是一个类的限定代指,他不会翻译为任何具体的类,也就是说编译器不会判断你传入的这个类是否是XXX的子类或者父类,其只会按照可以安全转换的方式来执行。
比如子类限定,? extends Employee
,这里的?仅仅是限定一个Employee
子类的代指。而且编译器不会判定我们传入的类型是否是其子类。所以即使我们传入Manager
,也不会被接受,因为根本不会判断。但是get方法返回的?可以确定为Employee的子类,所以可以在在外部被接收到。
父类限定也是一样,记住?不代表任何具体的类,其只是一个限定的代指,编译器不会为其做任何判定。
无限定通配符
除了上面的限定通配符,无限定通配符也是存在的:Pair<?>
。其可以看成Pair<? extends Object>
。因为所有的对象都是基于Object
,所以也相当于无限定。
这相当于是子类限定,所以其set方法仍然是无法调用的。而get方法会返回一个Object
对象。
所以虽然无限定通配符Pair<?>
与泛型类型Pair<T>
形式上相似,但实际上是不相同的。
通配符捕获
所谓通配符捕获,即解决无法在方法中获取含有通配符的类的通配符类型的问题,具体例子:
1 | public void swap(Pair<? extends Employee> p) |
上面的代码是错误的,因为?
不能用来声明变量。(就像之前说的,它不代表任何一个具体的类,其只是一个限定)
但是我们要实现这样功能又该怎么做呢?
可以用一个辅助类
1 | public static <T> void swapHelper(Paor<T> p){ |
然后再swap中调用helper类:
1 | public void swap(Pair<? extends Employee> p) |
因为我们将p传入helper中,而helper是一个普通泛型类,所以其可以捕捉T的类型。
当然这个例子看起来有点蠢,但只是一个例子。
反射和泛型
如果通过反射去分析泛型参数,那么将不会得到太多信息,因为其已经被编译器擦除了,而反射是工作在虚拟机中的。
泛型Class类
Class类实际是一个泛型类。如:String.class
实际上是一个Class<String>
类的对象。(也是唯一的对象)
使用Class<T>参数进行类型匹配
匹配泛型方法中Class<T>
参数的类型变量有时会很有用。比如下面的例子:
1 | public static <T> Pair<T> makePair(Class<T> c) throws InstantiataionException, IllegalAccesssException{ |
在外部,我们就可以这样调用:
1 | makePair(Empployee.class); |
即通过Class<T>
反射来新建特定类的对象。
虚拟机中的泛型类型信息
Java泛型的突出特点之一是在虚拟机中擦除泛型类型。但实际上,在擦除过后,其类型仍然保留对泛型的微弱信息。例如,原始的Pair类知道其是源于泛型类Pair<T>
,虽然并不知道T具体为什么类型。
我们可以通过反射java.lang.reflect
包中的接口Type
来查看一些信息:
- Class类:描述具体类型。
TypeVariable
接口:描述类型变量(如T extends Comparable<? super T>
)WildcardType
接口:描述通配符(如? super T
)ParameteriszedType
接口:描述泛型类或接口类型(如Comparable<? super T>
)GenericArrayType
接口:描述泛型数组(如T[]
)
以上的接口都继承自Type
接口,而Class<T>
是Type
的实现。
类型字面量-重要
如上一节介绍的,我们可以通过反射来获取泛型的相关信息,因此,我们也可以通过这种方式来捕获泛型类型,假设定义了一个泛型类<T>,我们可以通过以下示例的方式来捕获其类型接口Type
来进行判断和处理。
1 | class TypeLiteral<T>{ |
注意在调用时需要这么调用:
1 | var type = new TypeLiteral<ArrayList<Integer>>(){}; |
注意这里的格式是在new之后加了一个大括号,这种格式有以下几种类型:
创建其匿名内部类,如
new Parent(){}
实际上创建了一个Parent
的子类。{}内部可以重写其方法。值得注意的是,我们可以用{{init}}
来表示初始化块。实际之前也介绍过,其等价于:1
2
3
4
5public class XXX extends Parent{
{
//init
}
}在实际中我们可以通过继承
ArrayList
或者HashMap
然后用初始化块的方式来快速为其赋值(但注意,这里实际用的就是对应类型的子类,而不是其本身,虽然方法都继承了过来)。如:1
2
3
4
5
6
7
8
9
10Map<String,Object> study = new HashMap<String,Object>(){{
put("name","java");
put("id","1");
}};
List<Integer> list = new ArrayList<Integer>(){{
add(1);
add(2);
add(3);
}};实例化接口,同样的,我们可以通过这种方式来实现一个接口,比如最常见的,
Runable
接口:1
2
3
4
5
6
7//直接开启一个线程
new Thread(new Runnable() {
public void run() {
System.out.println("线程开启!");
}
}).start();
这里有一篇关于这种写法的 文章,其内容大致是正确的,但是有些表述不太准确。供参考。