泛型定义
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
eg: Generic<T>{}
一些常用的泛型类型变量:
E:元素(Element),多用于java集合框架
K:关键字(Key)
N:数字(Number)
T:类型(Type)
V:值(Value)
为什么有泛型
- auto cast
适用于多种数据类型执行相同的代码(代码复用)
- type checking at compile time
泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)
类型擦除(type erasure)
使用泛型加上的类型参数在编译阶段会去掉,这个过程就成为类型擦除。也就是说泛型只在编译阶段有效,在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段,生成的字节码文件是不包括泛型信息的。总结成一句话,泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
Java 5中初次引入泛型时,需要它们尽量保持与现存JVM的后向兼容性。为了达到这一目的, ArrayList<String>
和 ArrayList<Integer>
的运行时表示是相同的。这被称作泛型多态 (generic polymorphism)的消除模式(erasure model)。
实际举个例子看下字节码
1
2
3
4
5
6
7
8
|
public class GenericDemo1 {
static <E> void compare(E e1, E e2) {
}
static <E extends Comparable<E>> void compare(E e1, E e2) {
}
}
|
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
|
static <E extends java.lang.Object> void compare(E, E);
descriptor: (Ljava/lang/Object;Ljava/lang/Object;)V
flags: ACC_STATIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 e1 Ljava/lang/Object;
0 1 1 e2 Ljava/lang/Object;
LocalVariableTypeTable:
Start Length Slot Name Signature
0 1 0 e1 TE;
0 1 1 e2 TE;
Signature: #19 // <E:Ljava/lang/Object;>(TE;TE;)V
static <E extends java.lang.Comparable<E>> void compare(E, E);
descriptor: (Ljava/lang/Comparable;Ljava/lang/Comparable;)V
flags: ACC_STATIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 15: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 e1 Ljava/lang/Comparable;
0 1 1 e2 Ljava/lang/Comparable;
LocalVariableTypeTable:
Start Length Slot Name Signature
0 1 0 e1 TE;
0 1 1 e2 TE;
Signature: #22 // <E::Ljava/lang/Comparable<TE;>;>(TE;TE;)V
|
可以看方法的描述符,已经转成具体的类型了。
泛型的使用
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法
泛型类
泛型用于类的定义中,被称为泛型类。通过泛型可以完成一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。
泛型类示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//重点:在实例化泛型类时,必须指定T的具体类型,此处T相当于形参,实际使用的时候会传入实参,可以理解成泛型类的声明,泛型类中的T可以当实际类型使用。
//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
public class Generic<T>{
//key这个成员变量的类型为T,T的类型由外部指定
private T key;
public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
this.key = key;
}
public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
return key;
}
}
|
定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。
看一个例子:
1
2
3
4
5
6
7
8
9
|
Generic generic = new Generic("111111");
Generic generic1 = new Generic(4444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);
Log.d("泛型测试","key is " + generic.getKey());
Log.d("泛型测试","key is " + generic1.getKey());
Log.d("泛型测试","key is " + generic2.getKey());
Log.d("泛型测试","key is " + generic3.getKey());
|
注意:
- 泛型的类型参数只能是类类型,不能是简单类型。
- 不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译器会报错:“Unknown class”
if(ex_num instanceof Generic<Number>){ }
泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:
1
2
3
4
|
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
|
- 当实现泛型接口的类,未传入泛型实参时
1
2
3
4
5
6
7
8
9
10
11
|
/**
* 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
* 即:class FruitGenerator<T> implements Generator<T>{
* 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
*/
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
|
- 当实现泛型接口的类,传入泛型实参时:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/**
* 传入泛型实参时:
* 定义一个类实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
* 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
* 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
*/
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
|
泛型方法
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/**
* 泛型方法的基本介绍
* @param tClass 传入的泛型实参
* @return T 返回值为T类型
* 说明:
* 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}
|
1
2
|
Object obj = genericMethod(Class.forName("com.test.test"));
|
泛型方法的基本用法
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
65
66
67
68
69
70
71
72
73
74
75
|
public class GenericTest {
//这个类是个泛型类,在上面已经介绍过
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
//我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
//所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey(){
return key;
}
/**
* 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
* 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
public E setKey(E key){
this.key = keu
}
*/
}
/**
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
* 如:public <T,K> K showKeyName(Generic<T> container){
* ...
* }
*/
public <T> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
//当然这个例子举的不太合适,只是为了说明泛型方法的特性。
T test = container.getKey();
return test;
}
//这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
//这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
//同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
public void showKeyValue2(Generic<?> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
/**
* 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
* 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
* 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
public <T> T showKeyName(Generic<E> container){
...
}
*/
/**
* 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
* 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
* 所以这也不是一个正确的泛型方法声明。
public void showkey(T genericObj){
}
*/
public static void main(String[] args) {
}
}
|
泛型类中的泛型方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class GenerateTest<T>{
public void show_1(T t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
//由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
public <E> void show_3(E t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
public <T> void show_2(T t){
System.out.println(t.toString());
}
}
|
泛型方法与可变参数
1
2
3
4
5
|
public <T> void printMsg( T... args){
for(T t : args){
Log.d("泛型测试","t is " + t);
}
}
|
1
|
printMsg("111",222,"aaaa","2323.4",55.55);
|
静态方法与泛型
静态方法有一种情况需要注意一下,那就是在泛型类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class StaticGenerator<T> {
....
....
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show(T t){..},此时编译器会提示错误信息:
"StaticGenerator cannot be refrenced from static context"
*/
public static <T> void show(T t){
}
}
|
泛型方法使用原则
泛型方法能使方法独立于类而产生变化,以下两点需要注意:
- 如果使用泛型方法能将整个类泛型化那么就应该使用泛型方法。
- static方法无法访问泛型类的泛型参数,如果要使用泛型能力,就必须使其成为泛型方法。
协变与逆变
定义
逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类,A≥B表示A是B的父类)
-
f(⋅)是 协变(covariant) 的,当A≤B时有f(A)≤f(B)成立;
比如类型转换关系是数组,当Apple ≤ Fruit,Apple[] ≤ Fruit
比如类型转换关系是泛型的上边界,当Apple ≤ Fruit,List<? extends Apple> ≤ List<? extends Fruit>
-
f(⋅)是 逆变(contravariant) 的,当A≤B时有f(A) ≥ f(B)成立;
比如类型转换关系是泛型的上边界,当Apple ≤ Fruit,List<? super Apple> ≥ List<? extends Fruit>
-
f(⋅)是 不变(invariant) 的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
比如类型转换关系是普通泛型,当Apple ≤ Fruit,List<Apple>
和 List<Fruit>
这两个泛型类没有继承关系
数组的类型信息是会一直存在的,在编译期和运行时都可以做检查。但是在使用泛型时,类型信息在编译期被擦除了,运行时也就无从检查。因此,泛型将这种错误检测放在编译期。
类型通配符
由于泛型不是协变的,所以Generic<Integer>
不能被看作为Generic<Number>
的子类。同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是没有继承关系的。
编译器的逻辑其实是这样的,Integer is a Number, Generic<Integer> is not a Generic<Number>
。
类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参。再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。可以解决当具体类型不确定的时候,使用通配符? 代替。当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。
注意 ? 和 Object 的区别
- ?表示某一种类型实参,比如
List<?> list
, 集合里只能存放某一种类型的元素,List<Object>
list, 则可以存放多种类型的元素,既可以存放Integer,也可以存放String。
- 对于
List<?>
就不能向其内部添加元素。除了添加null之外,因为null是所有类类型的默认初始值,所有类类型都可以识别null。
List<?>
不允许添加,但是允许读取元素,使用Object来接收
Object o = list.get(0);
?
的用法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class GenericDemo {
public static void main(String[] args) {
List<Integer> list1 = Lists.newArrayList(1, 2, 3);
List<String> list2 = Lists.newArrayList("a", "b", "c");
// 1. 作为方法参数使用, List<?> 是所有List<Class类型>的父类,可以用来接收像List<String>、List<Integer>的对象。
print(list1);
print(list2);
// 2. 方法体内声明一个变量,用来接收List<?>的子类 也就是List<Integer>、List<String>等对象。
List<?> list = list1;
list.forEach(System.out::println);
list = list2;
list.forEach(System.out::println);
}
static void print(List<?> list) {
list.forEach(System.out::println);
}
}
|
使用通配符可以实现泛型类的协变和逆变,请看下面两节。
协变
<? extends A>
像上面这样使用子类通配符就完成了协变,看一个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class Demo4 {
public static void main(String[] args) {
List<? extends Fruit> extendsFruits = Lists.newArrayList(new Apple());
// 添加
extendsFruits.add(new Apple()); // 编译错误 ?extends Fruit can not be applied to Apple
// 读取
Fruit fruit = extendsFruits.get(0); // ok
}
}
class Fruit {
}
class Apple extends Fruit {
}
|
List<? extends Fruit> flist
可以指向List<Apple>
,也可以指向List<Orange>
,java中的泛型不是具化泛型,编译期会将泛型信息擦除,运行时无从检查安全性,所以会在编译期进行类型检查。由于协变过程丢掉了类型信息(? 可以表示Fruit或者任何Fruit的子类,当flist = new ArrayList<Apple>()
?就是Apple,当flist = new ArrayList<Orange>()
就是Orange),编译器拒绝所有不安全的操作。
报错信息已经很明白了,?不能应用于Apple,谁知道它后面还会不会编程Orange呢
逆变
<? super A>
像上面这样使用子类通配符就完成了逆变,看一个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class Demo4 {
public static void main(String[] args) {
List<? super Apple> superApples = Lists.newArrayList(new Apple(), new Fruit());
// 添加
superApples.add(new Apple()); // ok
superApples.add(new Fruit()); // 编译错误 ? extends Apple can not be applied to Fruit
// 读取
Fruit fruit = superApples.get(0); // 编译错误 required:Fruit,Found:? super Apple
Object object = superApples.get(0); // ok
}
}
class Fruit {
}
class Apple extends Fruit {
}
|
? 代表Apple或者Apple的超类,编译器知道向其中添加Apple或Apple的子类型(例如Jonathan)是安全的了。但是,既然Apple是下界,那么向这样的List中添加Fruit是不安全的。同理也读取不了其中的值,因为?表示Apple或者Apple的父类,除非用Object接受。
PECS
什么时候使用extends,什么时候使用super。《Effective Java》给出精炼的描述:producer-extends, consumer-super(PECS)。
想在网上找张现成的图都找不到,太tm懒了

producer和consumer直译不太好理解,大家理解成仓库就行了,实际上也确实是存储地的意思。
生产者的仓库只能读不能写,消费者的仓库只能写度不能读,结合图片很好理解。
框架和库代码中到处都是PECS,让我们来看一个实际的例子,java.util.Collections的copy方法
1
|
public static <T> void copy(List<? extends T> src, List<? super T> dest) {
|
src:来源,对应生产者仓库需要从中读取数据,所以使用子类通配符
dest:出口,对应消费者仓库需要往里面存储数据,所以使用超类通配符
自限定泛型与协变
自限定泛型
前面几个小结阐述了限定类型泛型的作用,即限制类型参数的范围,同时可以使用限定的界限包含的方法。
什么样的是自限定类型呢?即,限定范围就是自身。比如一个实现了Comparable接口的的User类,希望只能和User类的实例进行比较,那么我们需要这样写:
1
2
3
4
5
|
static class User implements Comparable<User>{
public int compareTo(User o) {
//
}
}
|
这样就利用了限定类型的两个用处:1.限定范围;2.可以使用限定类的方法。另外一个点,就是限定了只能是同类型的进行比较。
我们再来看一个更复杂的,jdk里的Enum类定义:
public abstract class Enum<E extends Enum<E>> extends Object implements Comparable<E>, Serializable {}
这是一个更复杂的限定类型,为什么要这么复杂呢?如果我们写成简单的样子行不行?像这样:
public abstract class Enum implements Comparable<Enum>, Serializable {}
这和Enum类的作用有关系,Enum这个抽象类是用来被继承的,所以我们总是在定义和使用Enum的子类,而且我们希望enum Day和enum Month只能自己的枚举值之间进行比较,而不能互相比较。比如Monday和January就不能比较,因为比较是没有意义的。
看明白了这里,我们再来看一下,简化了的Enum能不能达到上述的限制目的?
1
2
3
4
5
6
7
8
9
10
11
|
class Day extends Enum{
}
class Month extends Enum{
}
public satic void main(String[] args){
Day monday = new Day("monday");
Month january = new Month("january");
monday.compareTo(january);// 1
}
|
Java字节码格式并不禁止继承java.lang.Enum,但是javac编译器硬性不让你继承java.lang.Enum。改用Scala编译器轻松继承Enum。
上述1位置的compareTo方法,在简化的Enum情况下是可以的,但是在jdk中复杂的Enum下是不可以的。这也就是为什么jdk里的Enum要定义的这么复杂,目的就是为了限制Enum的子类只能和自己同类的实例进行比较。
再来看《Java编程思想》给出的一个例子
1
2
3
4
5
6
7
8
|
class SelfBounded<T extends SelfBounded<T>> {}
class A extends SelfBounded<A> {}
class B extends SelfBounded<A> {}
// compile error:Type parameter 'B' is not within its bound; should extend 'SelfBounded<B>'
// 编译器已经说得很明显了,因为 class B extends SelfBounded<A> 不满足条件:T extends SelfBounded<T> ,
// 如何才能满足条件呢?将类B的定义改为class B extends SelfBounded<B>,
// 上面的 class A 和 class B 都是满足条件的,因为 class A extends SelfBounded<A>
//class D extends SelfBounded<B> {}
|
SelfBounded类接受泛型参数T,而T由一个边界限定,这个边界就是拥有T作为其参数的SelfBounded
自限定类型的作用:产生参数协变
前文我们讲了数组协变,泛型类的协变,参数协变又是什么呢?
先看一个协变返回类型的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class Base {}
class Derived extends Base {}
interface OrdinaryGetter {
Base get();
}
interface DerivedGetter extends OrdinaryGetter {
// DerivedGetter.get()覆盖了OrdinaryGetter.get()
@Override Derived get();
}
public class CovariantReturnTypes {
void test(DerivedGetter d) {
Derived result1 = d.get(); // 调用的DerivedGetter.get()
Base result2 = d.get(); // 也调用的DerivedGetter.get()
}
}
|
自限定泛型将子类作为其返回值。
1
2
3
4
5
6
7
8
9
10
11
12
|
interface GenericGetter<T extends GenericGetter<T>> {
T get();
}
interface Getter extends GenericGetter<Getter> {}
public class GenericsAndReturnTypes {
void test(Getter g) {
Getter result1 = g.get();
GenericGetter result2 = g.get(); // Also the base type
}
}
|
参数协变:类型实参随着子类类型变化而变化
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
|
abstract class Father<E extends Father<E>> implements Comparable<E> {
private final int ordinal;
protected Father(int ordinal) {
this.ordinal = ordinal;
}
public final Class<E> getDeclaringClass() {
Class<?> clazz = getClass();
Class<?> zuper = clazz.getSuperclass();
return (zuper == Enum.class) ? (Class<E>) clazz : (Class<E>) zuper;
}
public final int compareTo(E o) {
Father<?> other = o;
Father<E> self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal;
}
}
class Child1 extends Father<Child1> {
public Child1(int ordinal) {
super(ordinal);
}
}
class Child2 extends Father<Child2> {
protected Child2(int ordinal) {
super(ordinal);
}
}
public class Demo4 {
public static void main(String[] args) {
Child1 c11 = new Child1(11);
Child1 c12 = new Child1(12);
Child2 c21 = new Child2(21);
c11.compareTo(c12);
c11.compareTo(c21); // 编译错误 Child1 can not be applied to Child2
}
}
|
自限定类型的本质就是:基类使用子类类型作为其参数类型。当子类继承父类时,使用自己的类型作为参数类型就能实现特定的意义(比如上面代码中自己和自己比较)。这意味着泛型基类变成了一种所有子类的公共功能模版,注意公共模板这个用词,一般不适用模板来做什么,只会使用子类,比如jdk中的枚举Enum也是模板类,jdk更彻底让编译器直接禁用了。
小结
自限定类型的用法
class SelfBounded<T extends SelfBounded<T>> {}
自限定类型的作用
自限定类型的作用在于产生参数协变
泛型嵌套
这个很简单,提一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class GenericDemo {
public static void main(String[] args) {
Map<String, List<String>> map = Maps.newHashMap();
List<String> list = Lists.newArrayList();
list.add("hello");
list.add("world");
map.put("a", list);
Set<Map.Entry<String, List<String>>> set = map.entrySet();
Iterator<Map.Entry<String, List<String>>> iterator = set.iterator();
while (iterator.hasNext()) {
Map.Entry<String, List<String>> entry = iterator.next();
System.out.println(entry.getKey() + "," + entry.getValue());
}
}
}
|
运行结果:
泛型数组
在java中定义数组类型变量时,实际类型不能是泛型数组,静态类型可以是泛型数组
因为这样做会破坏类型安全。核心的问题在于Java范型和C#范型存在根本区别:Java的范型停留在编译这一层,到了运行时,这些范型的信息其实是被抹掉的;而C#的范型做到了MSIL这一层。Java的做法不必修改JVM,减少了潜在的大幅改动和随之而来的风险,也许同时也反映出Java Bytecode规范在设计之初的先天不足;C#则大刀阔斧,连CLR一起改以支持更彻底的范型,换句话说,在范型这一点上,感觉C#更C++一点。
在Java中,Object[]数组可以是任何数组的父类(数组协变),或者说,任何一个数组都可以向上转型成它在定义时指定元素类型的父类的数组,这个时候如果我们往里面放不同于原始数据类型 但是满足后来使用的父类类型的话,编译不会有问题,但是在运行时会检查加入数组的对象的类型,于是会抛ArrayStoreException:
1
2
3
|
Object[] ducks = new Integer[10];
ducks[0] = "hello";
System.out.println(ducks[0]); // 运行时error, java.lang.ArrayStoreException: java.lang.String
|
因为Java的范型会在编译后将类型信息抹掉,这样如果Java允许我们使用类似
1
|
Map<Integer, String>[] mapArray = new Map<Integer, String>[20];
|
这样的语句的话,我们在随后的代码中可以把它转型为Object[]然后往里面放Map<Double, String>实例。这样做不但编译器不能发现类型错误,就连运行时的数组存储检查对它也无能为力,它能看到的是我们往里面放Map的对象,我们定义的<Integer, String>在这个时候已经被抹掉了,于是而对它而言,只要是Map,都是合法的。想想看,我们本来定义的是装Map<Integer, String>的数组,结果我们却可以往里面放任何Map,接下来如果有代码试图按原有的定义去取值,后果是什么不言自明。
一句话总结就是:数组是协变的,泛型是不变的,类型信息会在编译期擦除,如果可以使用泛型数组,将数组向上转型后,不管是在编译时还是在运行时,编译器和虚拟机都检查不了数组存放的类型到底对不对了。
再来举两个小例子
下面这样是不可以的,因为数组的实际类型不能绑定泛型信息
1
|
List<String>[] ls = new ArrayList<>[10];
|
下面这样是可以的,数组的静态类型可以泛型数组,可以在编译期做一些检查
1
2
3
4
5
6
7
|
List<String>[] ls = new ArrayList[10];
ls[0] = Lists.newArrayList("1");
// ls[1] = Lists.newArrayList(1);// 编译期 error, 提示不匹配的类型
Object[] os = ls;
os[1] = Lists.newArrayList(2);
System.out.println(os[0]);
System.out.println(os[1]);
|
所以使用声明类型是泛型数组这样的方法来对编译期做类型检查也是不错的。
注意事项
<? extends A>
和 <T extends A>
- ? 表示类型实参,T表示类型形参
- 要使用T必须先声明,参照泛型类、泛型接口、泛型方法,声明后才可以使用,?表示类型实参,直接可以使用
<T extends Comparable<? super T>>
一定要理解前文提到的逆变,这个问题就很容易解释了。
java.util.Collections
1
2
3
4
5
6
7
8
|
public class Collections {
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
}
|
-
语法分析
1.1. extends对泛型上限进行了限制即T必须是Comparable的子类或者实现类
1.2. <? super T>
表示Comparable<>
中的泛型类型下限为T!
分成两部分理解
T(泛型声明尖括号<>
中第一个T):相当于泛型的声明(泛型接口、泛型类、泛型方法),编译时会将类型实参替换调这个类型形参。声明完后可以将T当做类型实参使用,比如在T的作用域类使用T t; if(t instanceof String){}
这种形式。
? super T
:泛型类逆变,A继承B, 类型转换后,List<? super A> 是 List<? super B>的父类,所以List<? super A>既可以表示List<A>
也可以表示List<B>
为什么会有这种写法<T extends Comparable<? super 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
|
class Father implements Comparable<Father> {
@Override
public int compareTo(Father o) {
return 0;
}
}
class Son extends Father {
}
class Client {
public static <T extends Comparable<T>> void sort1(List<T> list) {
list.sort(null);
}
public static <T extends Comparable<? super T>> void sort2(List<T> list) {
list.sort(null);
}
public static void main(String[] args) {
List<Father> fs = null;
List<Son> ss = null;
sort1(fs); // ok
sort1(ss); // error List<T> can not be applied to List<Son> 理由:不匹配的等价约束,Father and Son
sort2(fs); // ok
sort2(ss); // ok
}
}
|
编译器提示的已经很明确了,因为Father 实现的Comparable<Father>
, 子类继承Father,实现的也是Comparable<Father>
,sort1方法只允许T extends Comparable<T>
这样的List<T>
类实参,为了能比较子类,需要对泛型类List<T>
做逆变,也就是List<? super T>
,现在如果A继承B, 类型转换(List)后,List<? super A> 是 List<? super B>的父类,所以List<? super A>既可以表示List<A>
也可以表示List<B>
。
下面不用看了,只是用来纪念一段曲折的学习史。
让我们从头开始捋
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
List<?> 和 List<ClassA>、List<ClassB>
? 是类型实参,表示所有的Class实例,也即可以表示ClassA、ClassB等。
List<?> 相当于 List<ClassA> 和List<ClassB> 等 的父类, 父类可以用来接收子类, of course,List<?> 也可以用来接收List<ClassA> 和List<ClassB> 等
为什么会有List<?>这种写法
用前面打印列表举例
print(List<Number> list);
List<Integer> list = null;
print(list); // 出错,编译器认为Integer是Number的子类,但是List<Integer>不是List<Number>的子类,也就是泛型不具有协变。
要定义打印列表方法还得重载print,可不可以有个泛型类既是List<Number>的父类也是List<Integer>的父类
List<?>就表示List<具体类型>的父类。
但是 ? 表示的范围太大了,这里只想表示Number及Number子类的泛型 父类,这就引出了泛型边界。
|
1
2
3
4
5
6
7
8
9
10
11
|
List<?>和List<? extends ClassA>、List<? super ClassA>
可以使用类比的手段表示上面三者的区别
? 表示所有具体类型,像是String、Integer等等,可以想象成(负无穷, 正无穷)
?extends ClassA 表示对?加以限制,现在?只能表示ClassA或者ClassA的子类 (负无穷, ClassA]
注意:如果A是接口<? extends A> ?表示A的所有实现类;
如果A是子类<? extends A> ? 表示A或者A的所有子类
?extends ClassA 表示对?加以限制,现在?只能表示ClassA或者ClassA的父类 [ClassA, 正无穷)
上一个例子中?表示的范围太大了,可以使用List<? extends Number>
? 可以表示Number或者Number的子类
List<? extends Number> 表示 List<Number>或者List<Integer>等的父类
List<? super Integer> 表示 List<Integer> 或者 List<Number>的父类。
|
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
<T extends Comparable<? super T>>
由于Comparable是接口,T表示Comparable的一个实现类
// 1. <T extends Comparable>
static class A implements Comparable {
@Override
public int compareTo(Object o) {
return 0;
}
}
<T extends Comparable> T是Comparable<Object>的实现类, 上面A符合要求
符合要求是指在类似下面这种写法时,可以用来表示A,也就是参数化类型T可以接受实参A
public static <T extends Comparable> void sort(List<T> list) {
list.sort(null);
}
或者
class Test<T extends Comparable>{}
// 2. <T extends Comparable<T>>
先来看<T1 extends Comparable<T2>> 表示T1是Comparable<T2>的实现类。
同理 <T extends Comparable<T>> 表示T 是Comparable<T>的实现类。
static class A implements Comparable<A> {
@Override
public int compareTo(A o) {
return 0;
}
}
<T extends Comparable<T>> A是Comparable<A>的实现类, 上面A符合要求
符合要求是指在类似下面这种写法时,可以用来表示A,也就是参数化类型T可以接受实参A
public static <T extends Comparable<T>> void sort(List<T> list) {
list.sort(null);
}
或者
class Test<T extends Comparable<T>>{}
// 3. <T extends Comparable<? super T>>
假设A有子类AA
public class GenericDemo1 {
public static void main(String[] args) {
Test<A> test1 = new Test<>();
// 下面这行报错,提示类型越界
// Type parameter 'AA' is not within its bound; should implement 'java.lang.Comparable<AA>'
// Test<AA> test2 = new Test<>();
}
static class Test<T extends Comparable<T>> {
}
static class A implements Comparable<A> {
@Override
public int compareTo(A o) {
return 0;
}
}
static class AA extends A {
}
}
上面例子表明A 符合 要求,因为A 实现了 Comparable<A>,A 是 Comparable<A>的实现类
但是A的子类AA不符合要求, 因为AA继承A,但是实现的是Comparable<A> 也就是
AA是Comparable<A>的实现类, AA 不是Comparable<AA>的实现类 // 很重要
从语义上也好理解,T可以和T进行比较,T不能和T的父类比较,但我们都知道比较的字段肯定是放在父类上的,子类肯定可以和父类进行比较。
剧透一下? super T, ?表示T的父类,这下子类就可以和父类进行比较了。我们看下具体的分析流程。
聚焦一下问题, Test<T extends Comparable<T>>只接受A,不接受AA
如何改造Test<T extends Comparable<T>>,使得T既接受A也接受AA
这个问题就回到了第二块的内容List<?> 和 List<? extends Number>、List<? super Integer> 这里的AA相当于Integer。
我们可以使用Test<T extends Comparable<?>>这种写法,现在既能接受A也接受AA
A 是 Comparable<A>的实现类
AA 是 Comparable<A> 的实现类
但是存在像List<?>一样的问题,没有限制
对问题建模,Test<T extends Comparable<?>>,如何让?既表示T又表示T的父类, 显然用super即可解决
Test<T extends Comparable<? super T>>
现在new Test<A>和new Test<AA>都是合法的了。
因为T extends Comparable<? super T> 现在可以表示 AA是 Comparable<A>的实现类了。 也即 ? spuer AA 可以表示A
// 小结
还是那句话,T是类型形参,要被类型实参所取代,使用边界限制也就是对类型实参做筛选,上例中的AA是实参,
T extends Comparable 和 Comparable<? super T> 都是边界限制
判断AA满足不满足要求, 因为AA extends A,A 是Comparable<A>的实现类,所以AA也是Comparable<A>的实现类。
? super AA 可以表示A
所以AA 满足要求。
|
- 来看一段代码辅助理解
<T extends Comparable<T>>
和<T extends Comparable<? super 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
|
static class Father implements Comparable<Father> {
int age;
public Father(int age) {
this.age = age;
}
@Override
public int compareTo(Father o) {
return this.age - o.age;
}
}
static class Son extends Father {
public Son(int age) {
super(age);
}
}
static class A<E extends Comparable<E>> {
}
static class B<E extends Comparable<? super E>> {
}
static <E extends Comparable<E>> int compare(E e1, E e2) {
return e1.compareTo(e2);
}
static <E extends Comparable<? super E>> int compare2(E e1, E e2) {
return e1.compareTo(e2);
}
|
1
2
|
A<Father> a1 = new A<>(); // 1
A<Son> a2 = new A<>(); // 2
|
上面第二行编译报错,提示 Type parameter 'com.eh.ftd.dsa.ds.impl.Generic.Son' is not within its bound; should implement 'java.lang.Comparable<com.eh.ftd.dsa.ds.impl.Generic.Son>'
出错信息表明类型参数Son越界,应该实现Comparable<Son>
。由此我们可以推断Father implements Comparable<Father>
,子类继承父类,样式为Son implements Comparable<Father>
。编译器认为这个不在限制范围<E extends Comparable<E>>
内所以会报错。
使用<E extends Comparable<? super E>>
这种写法的作用是,父类实现了Comparable<父类>
,子类没有实现Comparable<子类>
但是子类实现了Comparable<父类>
,<? super E>
中的?既可以表可以父类,也可以表示子类。所以现在可以使用B<Son> b = new B<>();
这种写法。
注意
1
2
3
|
Son s1 = new Son(1);
Son s2 = new Son(2);
System.out.println(compare(s1, s2));
|
上述代码运行正确,如上所述参数Son应该也越界才是,说明编译器检测时对泛型方法自动做了一层转换。泛型方法<E extends Comparable<E>> int compare(E e1, E e2)
和<E extends Comparable<? super E>> int compare
是完全等价的, 编译器会报错compare(E,E) clashes with compare(E,E); both methods have same erasure
,也就是两个方法由于有相同的泛型类型(类型擦除)而产生了冲突。
建议泛型方法和泛型类保持一致,都采用E extends Comparable<? super E>>
这种写法。
参考
Java泛型详解
java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一
Java中<? extends T>和<? super T>的理解
Java泛型 自限定类型(Self-Bound Types)详解
java为什么不支持泛型数组? - 胖君的回答 - 知乎