一、前言
最近依然在看《Java编程思想》这本书,说实话,非常晦涩难懂,除了讲的比较深入外,翻译太烂也是看不懂的一个重要原因。今天在看泛型这一章,也算是有些收获吧,所以写篇博客,记录一下其中比较容易遗忘的一个知识点:在泛型中,extends和super关键字的含义和用法。
二、描述
学过Java的人应该都知道,extends和super这两个关键字的最常见的用法:
extends
:让一个类继承另外一个类;super
:指向父类对象的引用;
但是在泛型中,这两个关键字都被重载,有了新的含义和用法;
三、解析
1、extends在泛型中的基本使用
我们通过一段代码进行讲解:
1 | // 在泛型中使用extends:<T extends Number> |
看上面的代码,我们声明了一个类Test,它有一个泛型T,T的声明为<T extends Number>
,这表示什么意思呢?这表明:类Test的泛型,只能是Number类型,或者Number类型的派生类型(子类型);所以,在下面的main方法中,我们将Test类对象的泛型定为Number
、Integer
、Double
、Byte
均没有问题,因为它们是Number本身,或者Number的子类型;但是我们将泛型定义为String类型,就会编译错误,因为String不是Number类型的派生类。所以,泛型中extends关键字的作用就是:限定泛型的上边界;
2、Java泛型中的通配符
上面讲解了泛型中,extends关键字最基本的用法,比较简单,但是在实际的使用中,还有一种更加复杂的用法,就是搭配泛型中的通配符 ?
使用,所以我先来简单的介绍一下泛型中的通配符—— ?;
在泛型中的通配符就是一个问号,标准叫法是无界通配符,它一般使用在参数或变量的声明上:
1 | // 在参数中使用无界通配符 |
泛型中使用无界通配符,表示泛型可以是任意具体的类型,没有限制(基本数据类型除外,基本数据类型不能用作泛型,可以使用基本数据类型的包装类);所以无界通配符给人的感觉就和原生的类型没什么区别,比如就上面这段代码,使用List<?>,和直接使用List,好像是一样的;但是实际上还是有一些区别的,比如看下面这段代码:
1 | // 在参数中使用无界通配符 |
上面这段代码演示了使用通配符和原生类型的区别。在方法test1
中,使用了泛型类型为通配符的List,此时,我们将无法使用List的add方法,为什么?我们先看一下add方法的声明:
1 | boolean add(E e); |
我们可以看到,add方法的参数类型是一个泛型,可是在test1
中,我们在泛型中使用了通配符,这意味着list的泛型可以是任意类型,编译器并不知道它具体是哪种类型,所以不允许我们调用list中的泛型方法。这时候大家可能有点疑问,为什么Object类型也不行呢,Object类型不是所有类型的父类吗。那是因为Java对于原生类型和通配符有不一样的定义,而在语法的设计上要符合这种定义;
在《Java编程思想》上描述了使用通配符泛型和原生类型在定义上的区别:
- List:表示可以存储任意Object类型的集合;
- List<?>:表示一个存储某种特定类型的List集合,但是不知道这种特定类型是什么;
从上面对两种定义的描述,我们可以大致了解通配符与原生类型的区别;当然,具体其实还要更加复杂,但是我现在着重讲的是泛型中的extends
和super
关键字,所有这里就不赘述了,上面的讲解主要是为了引出下面的内容。下面开始讲解这两个关键字如何搭配通配符使用。
3、extends关键字搭配?使用
上面讲解了extends的一个简单用法,现在来讲解一个更加复杂的用法,就是extends关键字搭配无界通配符?使用。首先还是一样,来看一段代码:
1 | public static void test1(List<? extends Number> list) { |
我们通过上面这段代码进行讲解。上面我们定义了一个方法,名字叫test1
,它接收一个参数List<? extends Number> list
,这表示参数是一个List类型,而且这个List类型的泛型不确定,但是只能是Number类型,或者Number类型的子类,所以我们在main方法中创建了四个List对象,泛型分别是Number
、Integer
、Double
、String
,只有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 | public static void test1(List<? super Integer> list) { |
上面的代码中定义了一个方法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编程思想》