一、前言

  最近依然在看《Java编程思想》这本书,说实话,非常晦涩难懂,除了讲的比较深入外,翻译太烂也是看不懂的一个重要原因。今天在看泛型这一章,也算是有些收获吧,所以写篇博客,记录一下其中比较容易遗忘的一个知识点:在泛型中,extends和super关键字的含义和用法


二、描述

  学过Java的人应该都知道,extends和super这两个关键字的最常见的用法:

  • extends:让一个类继承另外一个类;
  • super:指向父类对象的引用;

  但是在泛型中,这两个关键字都被重载,有了新的含义和用法;


三、解析

 1、extends在泛型中的基本使用

  我们通过一段代码进行讲解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在泛型中使用extends:<T extends Number>
public class Test <T extends Number> {

public static void main(String[] args) {
// 正确使用:Number、Integer、Double、Byte 均属于 Number
Test<Number> t = new Test<>();
Test<Integer> t1 = new Test<>();
Test<Double> t2 = new Test<>();
Test<Byte> t3 = new Test<>();

// 错误使用:String不属于Number
// Test<String> t4 = new Test<>();
}
}

  看上面的代码,我们声明了一个类Test,它有一个泛型T,T的声明为<T extends Number>,这表示什么意思呢?这表明:类Test的泛型,只能是Number类型,或者Number类型的派生类型(子类型);所以,在下面的main方法中,我们将Test类对象的泛型定为NumberIntegerDoubleByte均没有问题,因为它们是Number本身,或者Number的子类型;但是我们将泛型定义为String类型,就会编译错误,因为String不是Number类型的派生类。所以,泛型中extends关键字的作用就是:限定泛型的上边界


 2、Java泛型中的通配符

  上面讲解了泛型中,extends关键字最基本的用法,比较简单,但是在实际的使用中,还有一种更加复杂的用法,就是搭配泛型中的通配符 ? 使用,所以我先来简单的介绍一下泛型中的通配符—— ?

  在泛型中的通配符就是一个问号,标准叫法是无界通配符,它一般使用在参数或变量的声明上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在参数中使用无界通配符
public static void test(List<?> list) {
Object o = list.get(1);
}

public static void main(String[] args) {
List<Integer> list1 = new ArrayList<Integer>();

// 在变量声明中使用无界通配符
List<?> list2 = list1;

test(list1);
test(list2);
}

  泛型中使用无界通配符,表示泛型可以是任意具体的类型,没有限制(基本数据类型除外,基本数据类型不能用作泛型,可以使用基本数据类型的包装类);所以无界通配符给人的感觉就和原生的类型没什么区别,比如就上面这段代码,使用List<?>,和直接使用List,好像是一样的;但是实际上还是有一些区别的,比如看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 在参数中使用无界通配符
public static void test1(List<?> list) {
// 均编译错误,因为使用了无界通配符,编译器无法确定具体是什么类型
// list.add(1111);
// list.add("aaa");
// list.add(new Object());
}
// 在参数中使用原生List
public static void test2(List list) {
// 编译通过,不加泛型时,编译器默认为Object类型
list.add(1111);
list.add("aaa");
list.add(new Object());
}

public static void main(String[] args) {
// 声明两个泛型明确的list集合
List<Integer> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
// 调用使用了<?>的方法
test(list1);
test(list2);
}

  上面这段代码演示了使用通配符和原生类型的区别。在方法test1中,使用了泛型类型为通配符的List,此时,我们将无法使用List的add方法,为什么?我们先看一下add方法的声明:

1
boolean add(E e);

  我们可以看到,add方法的参数类型是一个泛型,可是在test1中,我们在泛型中使用了通配符,这意味着list的泛型可以是任意类型,编译器并不知道它具体是哪种类型,所以不允许我们调用list中的泛型方法。这时候大家可能有点疑问,为什么Object类型也不行呢,Object类型不是所有类型的父类吗。那是因为Java对于原生类型和通配符有不一样的定义,而在语法的设计上要符合这种定义

  在《Java编程思想》上描述了使用通配符泛型和原生类型在定义上的区别:

  • List:表示可以存储任意Object类型的集合;
  • List<?>:表示一个存储某种特定类型的List集合,但是不知道这种特定类型是什么;

  从上面对两种定义的描述,我们可以大致了解通配符与原生类型的区别;当然,具体其实还要更加复杂,但是我现在着重讲的是泛型中的extendssuper关键字,所有这里就不赘述了,上面的讲解主要是为了引出下面的内容。下面开始讲解这两个关键字如何搭配通配符使用。


 3、extends关键字搭配?使用

  上面讲解了extends的一个简单用法,现在来讲解一个更加复杂的用法,就是extends关键字搭配无界通配符?使用。首先还是一样,来看一段代码:

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
public static void test1(List<? extends Number> list) {
Number number = list.get(1);
// 下列均编译错误:list中元素的类型可以是任意Number的子类,所有无法确定list存储的具体是哪一种类型
// list.add(11);
// list.add(new Integer(1));
// list.add(new Double(1));
}

public static void test2(List list) {
Object object = list.get(1);
// 编译通过:原生list可以存储任意Object类型
list.add(11);
list.add(new Integer(1));
list.add(new Double(1));
}

public static void main(String[] args) {
// 注意下列List的泛型
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
List<Double> list3 = new ArrayList<>();
List<Number> list4 = new ArrayList<>();

// 调用使用了泛型的方法
test1(list1); // 编译错误,因为list1的泛型为String,不是Number的子类
test1(list2); // 编译通过,因为list2的泛型为Integer,是Number的子类
test1(list3); // 编译通过,因为list3的泛型为Double,是Number的子类
test1(list4); // 编译通过,因为list4的泛型为Number,是Number的本身
}

  我们通过上面这段代码进行讲解。上面我们定义了一个方法,名字叫test1,它接收一个参数List<? extends Number> list,这表示参数是一个List类型,而且这个List类型的泛型不确定,但是只能是Number类型,或者Number类型的子类,所以我们在main方法中创建了四个List对象,泛型分别是NumberIntegerDoubleString,只有String类型的list作为参数调用test1时,才编译错误,因为String不是Number类型的子类;所以,此处extends的作用是:限定了参数或变量中,泛型的上界

  这种写法有什么好处呢?好处就是:我们确定了泛型的上界,缩小了类型的范围,例如test1中,我们取出List集合中的元素,返回值是一个Number,而不是像test2方法中,返回值是Object类型。这是因为我们使用extends,限制了泛型的类型是Number或其子类,于是编译器就可以知道,这个list中的所有元素,一定属于Number,所有可以用Number接收,也可以调用Number类的方法;但是在test2方法中,没有限定类型上界,所有只能用Object接收;

  那这么写有什么问题呢?也很明显,就是我们在讲通配符时说到的问题:无法调用参数为泛型的方法。我们使用了通配符,同时继承了Number类,根据我们之前说过的定义,List<? extends Number> list表示一个存储特定类型的List集合,且这个类型是Number或者Number的子类,这就是Java给这种参数的定义。所以编译器只能知道,这个list中,元素的大致类型,但是它具体是哪种类型,编译器不知道,所以编译器不允许我们调用任何需要用到这个具体类型信息的方法;比如我们传入一个元素为Byte类型的List,然后再调用add方法,为集合加入一个int值,这显然是不合理的。所以,使用这种参数类型,有时候也可以帮助我们限定某些不应该进行的操作。


 4、super关键字搭配?使用

  super关键字和extends关键字的含义相反,super关键字的作用是:限定了泛型的下界;还是先看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void test1(List<? super Integer> list) {
// 只能通过Object接收
Object object = list.get(1);

// 编译正确,允许以下操作
list.add(111);
list.add(new Integer(1));
// 编译错误,1.5不是Integer
list.add(1.5);
}

public static void main(String[] args) {
// 创建三个用于测试的List集合,泛型不同
List<Integer> list1 = new ArrayList<>();
List<Number> list2 = new ArrayList<>();
List<Double> list3 = new ArrayList<>();

// 调用使用了泛型的方法
test1(list1); // 编译正确,因为list1的泛型为Integer,等价于参数中泛型的下界
test1(list2); // 编译正确,因为list2的泛型为Number,是Integer的基类
test1(list3); // 编译错误,因为list1的泛型为Double,不是Integer的基类
}

  上面的代码中定义了一个方法test1来测试泛型中的super关键字,这个方法的参数类型是List<? super Integer> list,这表示这个方法的参数是一个List集合,而集合的泛型只能是Integer,或者Integer的基类。我们在main方法中定义了三个集合验证这个结论,这三个集合的泛型各不相同,分别使用这三个集合作为参数,调用test1方法。结果,泛型为Integer,以及Number的集合,调用方法成功,而泛型为Double的list编译错误。

  我们看test1中的代码可以发现,与泛型中使用extends关键字不同,使用super关键字可以调用add这个参数为泛型的方法,这是为什么呢?因为我们在泛型中使用了super这个,限定了泛型的下界为Integer,这表示在list这个集合中,所有的元素一定是Integer类型,或者Integer类型的基类型,比如说Number;这表明,我们在集合中添加一个Integer类型的元素,一定是合法的,因为Integer类型的对象,肯定也是一个Integer的基类型的对象(多态);当然,如果Integer还有子类,那也可以在add中传入Integer的子类对象(虽然Integer没有子类);

  但是我们从这个list中取出元素,只能用Object接收,这是为什么呢?因为我们定义了list的泛型下界是Integer,表明list的具体泛型可以是Integer的任何基类,而一个类的基类不止一个,比如Integer继承Number,而Number又继承Object。在这种情况下,编译器并不知道泛型具体是哪一种类型,所以只能用最高类Object进行接收。


四、总结

  上面的内容大致的讲解了一下泛型中extends和super关键字的用法,让人可以有一个简单的认识,但是更多的是我个人的理解。通过看书,我觉得泛型是一个很复杂的东西,仅仅只是看还是不够的,还是需要多多实践,在实践中才能加深理解。


参考

《Java编程思想》