前言:
本文基本是转载的CSDN博主「林二月er」的原创文章
代码是自己敲的
原文链接:https://blog.csdn.net/weixin_45395059/article/details/126006369
一、泛型概述
1.什么是泛型?为什么要使用泛型?
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参列表,普通方法的形参列表中,每个形参的数据类型是确定的,而变量是一个参数。在调用普通方法时需要传入对应形参数据类型的变量(实参),若传入的实参与形参定义的数据类型不匹配,则会报错。
那参数化类型
是什么?以方法的定义为例,在方法定义时,将方法签名中的形参的数据类型
也设置为参数(也可称之为类型参数),在调用该方法时再从外部传入一个具体的数据类型和变量。
泛型的本质是为了将类型参数化, 也就是说在泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
2.泛型的使用场景
在 ArrayList 集合中,可以放入所有类型的对象,假设现在需要一个只存储了 String 类型对象的 ArrayList 集合。
代码如下:
1 | @ Test |
- 上面代码没有任何问题,在遍历 ArrayList 集合时,只需将 Object 对象进行向下转型成 String 类型即可得到 String 类型对象。
但是如果在添加String对象时不小心添加了一个Interger类型的对象,会发生什么?
1 | public static void main(String[] args) { |
这里会报错,提示整型不能强转为字符型
那如何可以避免上述异常的出现?即我们希望当我们向集合中添加了不符合类型要求的对象时,编译器能直接给我们报错,而不是在程序运行后才产生异常。这个时候便可以使用泛型
了。
使用泛型的代码如下:
1 | public static void main(String[] args) { |
- < String > 是一个泛型,其限制了 ArrayList 集合中存放对象的数据类型只能是 String,当添加一个非 String 对象时,编译器会直接报错。这样,我们便解决了上面产生的 ClassCastException 异常的问题(这样体现了泛型的类型安全检测机制)。
3.泛型概述小结
- 与使用 Object 对象代替一切引用数据类型对象这样简单粗暴方式相比,泛型使得数据类型的类别可以像参数一样由外部传递进来。它提供了一种扩展能力,更符合面向对象开发的软件编程宗旨。
- 当具体的数据类型确定后,泛型又提供了一种类型安全检测机制,只有数据类型相匹配的变量才能正常的赋值,否则编译器就不通过。所以说,泛型一定程度上提高了软件的安全性,防止出现低级的失误。
- 泛型提高了程序代码的可读性。在定义泛型阶段(类、接口、方法)或者对象实例化阶段,由于 < 类型参数 > 需要在代码中显式地编写,所以程序员能够快速猜测出代码所要操作的数据类型,提高了代码可读性。
- 泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法,下面将正式介绍泛型的相关知识。
二、泛型类
1.泛型类的定义
类型参数用于类的定义中,则该类被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map等。
泛型类的基本语法:
1 | class 类名称 <泛型标识> { |
尖括号 <> 中的 泛型标识被称作是
类型参数
,用于指代任何数据类型。泛型标识是任意设置的(如果你想可以设置为 Hello都行),Java 常见的泛型标识以及其代表含义如下:
1 | T :代表一般的任何类。 |
示例:
1 | public class Generic<T> { |
- 在泛型类中,类型参数定义的位置有三处,分别为:
1.非静态的成员属性类型
2.非静态方法的形参类型(包括非静态成员方法和构造器)
3.非静态的成员方法的返回值类型
- 泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数
1 | public class Test<T> { |
泛型类中的类型参数的确定是在创建泛型类对象的时候(例如 ArrayList< Integer >)
而静态变量和静态方法在类加载时已经初始化,直接使用类名调用;在泛型类的类型参数未确定时,静态成员有可能被调用,因此泛型类的类型参数是不能在静态成员中使用的。
静态泛型方法中可以使用自身的方法签名中新定义的类型参数(即泛型方法,后面会说到),而不能使用泛型类中定义的类型参数。
1 | public class Test2<T> { |
- 泛型类不只接受一个类型参数,它还可以接受多个类型参数。
1 | public class MultiType <E,T> { |
2.泛型类的使用
在创建泛型类的对象时,必须指定类型参数 T 的具体数据类型,即尖括号 <> 中传入的什么数据类型,T 便会被替换成对应的类型。如果 <> 中什么都不传入,则默认是 < Object >。
假设有一个泛型类如下:
1 | public class Generic<T> { |
当创建一个 Generic< T > 类对象时,会向尖括号 <> 中传入具体的数据类型。
1 | class Test01{ |
传入 String 类型时,原泛型类可以想象它会自动扩展,其类型参数会被替换。
1 | public class Generic { |
- 可以发现,泛型类中的
类型参数 T
被 <> 中的 String 类型全部替换了。 - 使用泛型的上述特性便可以在集合中限制添加对象的数据类型,若集合中添加的对象与指定的泛型数据类型不一致,则编译器会直接报错,这也是泛型的类型安全检测机制的实现原理。
三、泛型接口
泛型接口和泛型类的定义差不多,基本语法如下:
1 | public interface 接口名<类型参数> { |
具体示例:
1 | public interface FanxingInterfaceTest<T> { |
重要!泛型接口中的类型参数,在该接口被继承或者被实现时确定。解释如下:
(1)定义一个泛型接口如下:
- 注意:在泛型接口中,静态成员也不能使用泛型接口定义的类型参数。
1 | interface IUsb<U, R>{ |
(2)定义一个接口 IA 继承了 泛型接口 IUsb,在 接口 IA 定义时必须确定泛型接口 IUsb 中的类型参数。
1 | interface IA extends IUsb<String, Double>{ |
1 | // 当去实现 IA 接口时,因为 IA 在继承 IUsu 接口时,指定了类型参数 U 为 String,R 为 Double |
(3)定义一个类 BB 实现了 泛型接口 IUsb,在 类 BB 定义时需要确定泛型接口 IUsb 中的类型参数。
1 | // 实现接口时,需要指定泛型接口的类型参数 |
(4)定义一个类 CC 实现了 泛型接口 IUsb 时,若是没有确定泛型接口 IUsb 中的类型参数,则默认为 Object。
1 | class CC implements IUsb{ |
5)定义一个类 DD 实现了 泛型接口 IUsb 时,若是没有确定泛型接口 IUsb 中的类型参数,也可以将 DD 类也定义为泛型类,其声明的类型参数必须要和接口 IUsb 中的类型参数相同。
1 | class DD <U, R> implements IUsb<U,R>{ |
四、泛型方法
1.泛型方法的定义
当在一个方法签名中的返回值前面声明了一个 < T > 时,该方法就被声明为一个
泛型方法
。< T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。当然,泛型方法中也可以使用泛型类中定义的泛型参数
。
基本语法:
1 | public <类型参数> 返回类型 方法名(类型参数 变量名) { |
(1)只有在方法签名中声明了< T >的方法才是泛型方法,仅使用了泛型类定义的类型参数的方法并不是泛型方法。
示例:
1 | public class Test<U> { |
(2)泛型方法中可以同时声明多个类型参数。
1 | public <T,S,U> T testMethod2(T t, S s, U u){ |
(3)泛型方法中也可以使用泛型类中定义的泛型参数。
1 | public class TestMethod<U> { |
(4)特别注意的是:泛型类中定义的类型参数和泛型方法中定义的类型参数是相互独立的,它们一点关系都没有。
1 | public class Test<T> { |
上面代码中,Test< T > 是泛型类,testMethod() 是泛型类中的普通方法,其使用的类型参数是与泛型类中定义的类型参数。
而 testMethod1() 是一个泛型方法,他使用的类型参数是与方法签名中声明的类型参数。
虽然泛型类中定义的类型参数标识和泛型方法中定义的类型参数标识都为< T >,但它们彼此之间是相互独立的。也就是说,泛型方法始终以自己声明的类型参数为准。
- 注意事项:
- < T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。
- 为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。
- 与泛型类的类型参数定义一样,此处泛型方法中的 T 可以写为
任意标识
,常见的如 T、E、K、V 等形式的参数常用于表示泛型。
补充一点:将静态方法声明为泛型方法
前面在泛型类的定义中提到,在静态成员中不能使用泛型类定义的类型参数,但我们可以将静态成员方法定义为一个泛型方法。
1 | public class Test2<T> { |
2.泛型方法的使用
泛型类,在创建类的对象的时候确定类型参数的具体类型;
泛型方法,在调用方法的时候再确定类型参数的具体类型。
泛型方法签名中声明的类型参数只能在该方法里使用,而泛型接口、泛型类中声明的类型参数则可以在整个接口、类中使用。
当调用泛型方法时,根据外部传入的实际对象的数据类型,编译器
就可以判断出类型参数 T
所代表的具体数据类型。
1 | public class FanxingFunction{ |
- 不难发现,当调用泛型方法时,根据传入的实际对象,
编译器
会判断出类型形参 T 所代表的具体数据类型。
3.泛型方法中的类型判断
在调用泛型方法的时候,可以显式地指定类型参数,也可以不指定。
- 当泛型方法的形参列表中有多个类型参数时,在不指定类型参数的情况下,方法中声明的的类型参数为泛型方法中的几种类型参数的共同父类的最小级,直到 Object。
- 在指定了类型参数的时候,传入泛型方法中的实参的数据类型必须为指定数据类型或者其子类。
示例:
1 | public class Test { |
五、类型擦除
1.什么是类型擦除
泛型的本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间擦除代码中的所有泛型语法并相应的做出一些类型转换动作。
换而言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段。
看一个例子,假如我们给 ArrayList 集合传入两种不同的数据类型,并比较它们的类信息。
1 | public class Test01 { |
- 在这个例子中,我们定义了两个 ArrayList 集合,不过一个是 ArrayList< String>,只能存储字符串。一个是 ArrayList< Integer>,只能存储整型对象。我们通过 arrayString 对象和 arrayInteger 对象的 getClass() 方法获取它们的类信息并比较,发现结果为true。
- 明明我们在 <> 中传入了两种不同的数据类型,按照上文所说的,它们的类型参数 T 不是应该被替换成我们传入的数据类型了吗,那为什么它们的类信息还是相同呢? 这是因为,在编译期间,所有的泛型信息都会被擦除, ArrayList< Integer > 和 ArrayList< String >类型,在编译后都会变成ArrayList< Objec t>类型。
再看一个例子,假设定义一个泛型类如下:
1 | public class Caculate<T> { |
在该泛型类中定义了一个属性 num,该属性的数据类型是泛型类声明的类型参数 T ,这个 T 具体是什么类型,我们也不知道,它只与外部传入的数据类型有关。将这个泛型类反编译:
1 | public class Caculate { |
- 可以发现编译器
擦除
了 Caculate 类后面的泛型标识 < T >,并且将 num 的数据类型替换为 Object 类型,而替换了 T 的数据类型我们称之为原始数据类型
。
那么是不是所有的类型参数被擦除后都以 Object 类进行替换呢?
- 答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换;而有一种情况则不是,那就是使用到了 extends 和 super 语法的
有界类型参数
(即泛型通配符
,后面我们会详细解释)。
再看一个例子,假设定义一个泛型类如下:
1 | public class Caculate<T extends Number> { |
将其反编译:
1 | public class Caculate { |
- 可以发现,使用到了 extends 语法的类型参数 T 被擦除后会替换为 Number 而不再是 Object。
- extends 和 super 是一个限定类型参数边界的语法,extends 限定 T 只能是 Number 或者是 Number 的子类。 也就是说,在创建 Caculate 类对象的时候,尖括号 <> 中只能传入 Number 类或者 Number 的子类的数据类型,所以在创建 Caculate 类对象时无论传入什么数据类型,Number 都是其父类,于是可以使用 Number 类作为 T 的原始数据类型,进行类型擦除并替换。(这一部分涉及到了泛型通配符,在下面还会具体介绍)
2.类型擦除的原理
假如我们定义了一个 ArrayList< Integer > 泛型集合,若向该集合中插入 String 类型的对象,不需要运行程序,编译器就会直接报错。这里可能有小伙伴就产生了疑问:
不是说泛型信息在编译的时候就会被擦除掉吗?那既然泛型信息被擦除了,如何保证我们在集合中只添加指定的数据类型的对象呢?
换而言之,我们虽然定义了 ArrayList< Integer > 泛型集合,但其泛型信息最终被擦除后就变成了 ArrayList< Object > 集合,那为什么不允许向其中插入 String 对象呢?
Java 是如何解决这个问题的?
- 其实在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 < T > 的数据类型,并记录下来,然后再对代码进行编译,
编译的同时进行类型擦除
;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换。
可以把泛型的类型安全检查机制和类型擦除想象成演唱会的验票机制:以 ArrayList< Integer> 泛型集合为例。
当我们在创建一个 ArrayList< Integer > 泛型集合的时候,ArrayList 可以看作是演唱会场馆,而< T >就是场馆的验票系统,Integer 是验票系统设置的门票类型;
当验票系统设置好为< Integer >后,只有持有 Integer 门票的人才可以通过验票系统,进入演唱会场馆(集合)中;若是未持有 Integer 门票的人想进场,则验票系统会发出警告(编译器报错)。
在通过验票系统时,门票会被收掉(类型擦除),但场馆后台(JVM)会记录下观众信息(泛型信息)。
进场后的观众变成了没有门票的普通人(原始数据类型)。但是,在需要查看观众的信息时(操作对象),场馆后台可以找到记录的观众信息(编译器会自动将对象进行类型转换)。
示例:
1 | public class GenericType { |
擦除 ArrayList< Integer > 的泛型信息后,get() 方法的返回值将返回 Object 类型,但编译器会自动插入 Integer 的强制类型转换。也就是说,编译器把 get() 方法调用翻译为两条字节码指令:
- 对原始方法 get() 的调用,返回的是 Object 类型;
- 将返回的 Object 类型强制转换为 Integer 类型;
代码:
1 | Integer n = arrayInteger.get(0);// 这条代码底层如下: |
3.类型擦除小结
泛型信息(包括泛型类、接口、方法)只在代码编译阶段存在,在代码成功编译后,其内的所有泛型信息都会被擦除,并且类型参数 T 会被统一替换为其原始类型(默认是 Object 类,若有 extends 或者 super 则另外分析);
在泛型信息被擦除后,若还需要使用到对象相关的泛型信息,编译器底层会自动进行类型转换(从原始类型转换为未擦除前的数据类型)
六、泛型通配符
1.泛型的继承
在介绍泛型通配符之前,先提出一个问题,在 Java 的多态中,我们知道可以将一个子类对象赋值给其父类的引用,这也叫向上转型
。
举例如下:
1 | //ArrayList 是 List的子类 |
上面的代码很好地体现了Java的多态的特性
在 Java 标准库中的集合 ArrayList< T > 类实现了 List< T >接口,其源码大致如下:
1 | public class ArrayList<T> implements List<T> {...} |
那现在我们思考一个问题,在 ArrayList< T > 泛型集合中,当传入 < T > 中的数据类型相同时,是否还能将一个 ArrayList< T > 对象赋值给其父类的引用 List< T >。
代码如下:
1 | List<String> list = new ArrayList<String>(); |
- 上面的代码没有问题, 即 ArrayList< T > 对象可以向上转型为 List< T >,但两者传入 < T > 中的数据类型必须相同。
继续思考一个问题,已知 Integer 类是 Number 类的子类,那如果 ArrayList<> 泛型集合中,在 <> 之间使用向上转型
,也就是将 ArrayList< Integer > 对象赋值给 List< Number > 的引用,是否被允许呢?
示例:
1 | List<Number> list = new ArrayList<Integer>(); //编译错误 |
- 上面代码会报错,我们发现并不能把 ArrayList< Integer > 对象赋值给 List< Number >的引用,甚至不能把 ArrayList< Integer > 对象赋值给 ArrayList< Number >的引用。
这也说明了在一般泛型中,不能向上转型
。
这是为什么?如果我们假设 ArrayList< Integer >可以向上转型为 ArrayList< Number >
观察下面的代码:
1 | //创建一个ArrayList<Integer>集合 |
当我们把一个 ArrayList< Integer > 向上转型为 ArrayList< Number > 类型后,这个 ArrayList< Number > 集合就可以接收 Float 对象了,因为 Float 类是 Number 类的子类。
但是,ArrayList< Number > 实际上和 ArrayList< Integer > 是同一个集合,而在泛型的定义中, ArrayList< Integer > 集合是不可以接收 Float 对象的。这是因为,在使用 get() 方法获取集合元素的时候,编译器会自动将 Float 对象强转成 Integer 对象,而这会产生 ClassCastException 异常。
正因如此,编译器为了避免发生这种错误,根本就不允许把 ArrayList< Integer >对象向上转型为 ArrayList< Number >;换而言之, ArrayList< Integer > 和 ArrayList< Number > 两者之间没有继承关系。
2.泛型通配符的引入
我们上面讲到了泛型的继承关系,ArrayList< Integer > 不是 ArrayList< Number > 的子类。
(1)先看一个问题:假设我们定义了一个 Pair< T >类,如下:
1 | public class Pair<T>(){ |
(2)然后,我们针对 Pair< Number >类型写了一个静态方法,它接收的参数类型是 Pair< Number >:
1 | public class PairHelper { |
(3)在测试类中创建一个 Pair< Number > 对象,并调用 addPair() 方法。
1 | class Test{ |
(4)上面的代码正常编译运行。但我们发现,在实际创建 Pair< Number > 对象的时候,我们传入的实参 (1, 2) 实际上是 Integer 类型;那我们是否可以直接创建一个 Pair< Integer > 对象,并将其传给 add() 方法呢?
代码:
1 | public class Main { |
- 编译器会直接报错,
原因是 Pair< Integer > 并不是 Pair< Number > 的子类
,而 addPair() 方法的形参数据类型为 Pair< Number >。因此, Pair< Integer > 对象不能传给 addPair() 方法。
那有没有办法使得 addPair() 方法可以接收 Pair< Integer > 对象?总不能重新定义一个新的 addPair() 方法来处理 Pair< Integer > 对象吧,这显然与 Java 中的多态理念相违背。
因此我们需要一个在逻辑上可以表示为 Pair< Integer > 和 Pair< Number > 这两者的父类引用类型,由此,泛型通配符便应运而生。
3.什么是泛型通配符
在现实编码中,确实有这样的需求,希望泛型能够处理
某一类型范围内
的类型参数,比如某个泛型类和它的子类,为此 Java 引入了泛型通配符
这个概念。
泛型通配符有 3 种形式:
- <?> :被称作无限定的通配符。
- <? extends T> :被称作有上界的通配符。
- <? super T> :被称作有下界的通配符
接下来分别介绍三种通配符:
4.上界通配符<? extends T>
上界通配符 <? extends T>
:T 代表了类型参数的上界,<? extends T>
表示类型参数的范围是 T 和 T 的子类。需要注意的是: <? extends T>
也是一个数据类型实参,它和 Number、String、Integer 一样都是一种实际的数据类型。
(1)在泛型的继承中我们说到,ArrayList< Integer > 和 ArrayList< Number > 之间不存在继承关系。而引入上界通配符的概念后,我们便可以在逻辑上将 ArrayList<? extends Number> 看做是 ArrayList< Integer > 的父类,但实质上它们之间没有继承关系。
举例如下:
1 | ArrayList<? extends Number> list = new ArrayList<Integer>(); //编译正确 |
- 逻辑上可以将 ArrayList<? extends Number> 看做是 ArrayList< Integer > 的父类,因此,在使用了上界通配符 <? extends Number> 后,便可以将 ArrayList< Integer > 对象
向上转型
了。
(2)ArrayList<? extends Number> 可以代表 ArrayList< Integer >、ArrayList< Float >、… 、ArrayList< Number >中的某一个集合,但我们不能指定 ArrayList<? extends Number> 的数据类型。(这里有点难理解)
举个例子:
1 | ArrayList<? extends Number> list = new ArrayList<>(); |
可以这样理解,ArrayList<? extends Number> 集合表示了:我这个集合可能是 ArrayList< Integer > 集合,也可能是 ArrayList< Float > 集合,… ,还可能是 ArrayList< Number > 集合;但到底是哪一个集合,不能确定;程序员也不能指定。
所以,在上面代码中,创建了一个 ArrayList<? extends Number> 集合 list,但我们并不能往 list 中添加 Integer、Float 等对象,这也说明了 list 集合并不是某个确定了数据类型的集合。
思考:那既然 ArrayList<? extends Number> 可以代表 ArrayList< Integer > 或 ArrayList< Float >,为什么不能向其中加入 Integer、Float 等对象呢?
- 其原因是 ArrayList<? extends Number> 表示的是一个未知类型的 ArrayList 集合,它可以代表 ArrayList< Integer >或 ArrayList< Float >… 等集合,但却不能确定它到底是 ArrayList< Integer > 还是 ArrayList< Float > 集合。
- 因此,泛型的特性决定了不能往 ArrayList<? extends Number> 集合中加入 Integer 、 Float 等对象,以防止在获取 ArrayList<? extends Number> 集合中元素的时候,产生 ClassCastException 异常
那为什么还需要引入上界统配符
的概念?—- 答:是为了拓展方法形参中类型参数的范围。
(1)在泛型通配符的引入
部分,我们提出了一个问题,有没有办法使得 addPair(Pair< Number> p) 方法接收 Pair< Integer > 对象?而在有了上界通配符的概念后,这个问题便有了解决办法,就是将 addPair() 方法改写。
代码如下:
1 | // 改写前 |
改写 addPair() 方法,用 <? extends Number> 替换了 < Number > ,由于 Pair< Integer > 可以向上转型为 Pair<? extends Number> ,所以调用 addPair() 方法时,我们便可以传入 Pair< Integer > 对象了。
除了可以传入 Pair< Integer > 对象,我们还可以传入 Pair< Double > 对象,Pair< BigDecimal > 对象等等,因为 Double 类和 BigDecimal 类也都是 Number 的子类
<? extends T>d的用法
上面说到,我们无法确定 ArrayList<? extends Number> 具体是什么数据类型的集合,因此其 add() 方法会受限(即不能往集合中添加任何数据类型的对象);但是可以往集合中添加 null,因为 null 表示任何类型。
我们可以调用 get() 方法从集合中获取元素,并赋值给集合中的最高父类 Number (即 <? extends T> 的上界)。
(1)上界通配符 <? extends T> 的正确用法:
1 | public class Test { |
在 printIntVal() 方法中,其形参为 ArrayList<? extends Number>,因此,可以给该方法传入 ArrayList< Integer >、ArrayList< Float > 等集合。
需要注意的是:在 printIntVal() 方法内部,必须要将传入集合中的元素赋值给Number 对象,而不能赋值给某个子类对象; 是因为根据 ArrayList<? extends Number> 的特性,并不能确定传入集合的数据类型(即不能确定传入的是 ArrayList< Integer > 还是 ArrayList< Float >)
- 假设在 printIntVal() 方法中存在下面代码:
1 | Integer intNum = (Integer) number; |
若是传入集合为 ArrayList< Float >,则必然会产生ClassCastException 异常
(2)上界通配符 <? extends T> 的错误用法:
1 | public class Test { |
- 在 ArrayList<? extends Number> 集合中,不能添加任何数据类型的对象,只能添加空值 null,因为 null 可以表示任何数据类型。
总结:
一句话总结:使用 extends 通配符表示可以读,不能写。
5. 下界通配符 <? super T>
下界通配符 <? super T>:T 代表了类型参数的下界,<? super T>表示类型参数的范围是 T 和 T 的超类,直至 Object。需要注意的是: <? super T> 也是一个数据类型实参,它和 Number、String、Integer 一样都是一种实际的数据类型。
(1)ArrayList<? super Integer> 在逻辑上表示为 Integer 类以及 Integer 类的所有父类,它可以代表 ArrayList< Integer>、ArrayList< Number >、 ArrayList< Object >中的某一个集合,但实质上它们之间没有继承关系。
举个例子:
1 | public class GenericType { |
逻辑上可以将 ArrayList<? super Integer> 看做是 ArrayList< Number > 的父类,因此,在使用了下界通配符 <? super Integer> 后,便可以将 ArrayList< Number > 对象向上转型了。
(2)ArrayList<? super Integer> 只能表示指定类型参数范围中的某一个集合,但我们不能指定 ArrayList<? super Integer> 的数据类型。(这里有点难理解)
看一个例子:
1 | public class GenericType { |
这里奇怪的地方出现了,为什么和ArrayList<? extends Number> 集合不同, ArrayList<? super Number> 集合中可以添加 Number 类及其子类的对象呢?
其原因是, ArrayList<? super Number> 的下界是 ArrayList< Number > 。因此,我们可以确定 Number 类及其子类的对象自然可以加入 ArrayList<? super Number> 集合中; 而 Number 类的父类对象就不能加入 ArrayList<? super Number> 集合中了,因为不能确定 ArrayList<? super Number> 集合的数据类型。
5.1 <? super T> 的用法
(1)下界通配符 <? super T> 的正确用法:
1 | public class Test { |
与带有上界通配符的集合ArrayList<? extends T>的用法不同,带有下界通配符的集合ArrayList<? super Number> 中可以添加 Number 类及其子类的对象;ArrayList<? super Number>的下界就是ArrayList
(2)下界通配符 <? super T> 的错误用法:
1 | public class Test { |
public static void fillNumList(ArrayList<? super Number> list) {
list.add(new Integer(0));// 编译正确
list.add(new Float(1.0));// 编译正确
// 遍历传入集合中的元素,并赋值给 Number 对象;会编译错误
for (Number number : list) {
System.out.print(number.intValue() + " ");
System.out.println();
}
// 遍历传入集合中的元素,并赋值给 Object 对象;可以正确编译
// 但只能调用 Object 类的方法,不建议这样使用
for (Object obj : list) {
System.out.println(obj);使用
}
}
}
注意,ArrayList<? super Number> 代表了 ArrayList< Number >、 ArrayList< Object > 中的某一个集合,而 ArrayList< Integer > 并不属于 ArrayList<? super Number> 限定的范围,因此,不能往 fillNumList() 方法中传入 ArrayList< Integer > 集合。
并且,不能将传入集合的元素赋值给 Number 对象,因为传入的可能是 ArrayList< Object > 集合,向下转型可能会产生ClassCastException 异常。
不过,可以将传入集合的元素赋值给 Object 对象,因为 Object 是所有类的父类,不会产生ClassCastException 异常,但这样的话便只能调用 Object 类的方法了,不建议这样使用。
5.3 <? super T> 小结
一句话总结:使用 super 通配符表示可以写,不能读。
6.无限定通配符 <?>
我们已经讨论了 extends T>和 super T>作为方法参数的作用。实际上,Java 的泛型还允许使用无限定通配符>,即只定义一个?符号。
无界通配符>:? 代表了任何一种数据类型,能代表任何一种数据类型的只有 null。需要注意的是: > 也是一个数据类型实参,它和 Number、String、Integer 一样都是一种实际的数据类型。
注意:Object 本身也算是一种数据类型,但却不能代表任何一种数据类型,所以 ArrayList< Object > 和 ArrayList> 的含义是不同的,前者类型是 Object,也就是继承树的最高父类,而后者的类型完全是未知的;ArrayList> 是 ArrayList< Object > 逻辑上的父类。
(1)ArrayList<?> 在逻辑上表示为所有数据类型的父类,它可以代表 ArrayList< Integer>、ArrayList< Number >、ArrayList< Object >中的某一个集合,但实质上它们之间没有继承关系。
举例如下:
1 | public class GenericType { |
上述代码是可以正常编译运行的,因为 ArrayList> 在逻辑上是 ArrayList< Integer > 的父类,可以安全地向上转型。 **(2)ArrayList> 既没有上界也没有下界,因此,它可以代表所有数据类型的某一个集合,但我们不能指定 ArrayList<?> 的数据类型。**
举例如下:
1 | public class GenericType { |
ArrayList> 集合的数据类型是不确定的,因此我们只能往集合中添加 null;而我们从 ArrayList> 集合中取出的元素,也只能赋值给 Object 对象,不然会产生ClassCastException 异常(原因可以结合上界和下界通配符理解)。
(3)大多数情况下,可以用类型参数 < T > 代替 <?> 通配符。
举例如下:
1 | static <?> void isNull(ArrayList<?> list) { |
// 替换如下:
1 | static <T> void isNull(ArrayList<T> list) { |
7.<? extends T>与<? super T> 对比
(1)对于<? extends 类型>,编译器将只允许读操作,不允许写操作。即只可以取值,不可以设值。
(2)对于<? super 类型>,编译器将只允许写操作,不允许读操作。即只可以设值(比如 set 操作),不可以取值(比如 get 操作)。
以上两点都是针对于源码里涉及到了类型参数的方法而言的。比如对于 List 而言,不允许的写操作有 add 方法,因为它的方法签名是boolean add(E e);,此时这个形参 E 就变成了一个涉及了通配符的类型参数;
而不允许的读操作有 get 方法,因为它的方法签名是E get(int index);,此时这个返回值 E 就变成了一个涉及了通配符的类型参数。
作为方法形参,<? extends T> 类型和 <? super T> 类型的区别在于:
extends T> 允许调用读方法T get()获取 T 的引用,但不允许调用写方法 set(T)传入 T 的引用(传入 null 除外)。 super T> 允许调用写方法set(T)传入 T 的引用,但不允许调用读方法 T get()获取 T 的引用(获取 Object 除外)。 先记住上面的结论,我们来看 Java 标准库的 Collections 类定义的 copy() 方法。 **(1)copy() 方法的作用是把一个 List 中的每个元素依次添加到另一个 List 中。它的第一个形参是 List super T>,表示目标 List,第二个形参是 List extends T>,表示源 List。** 代码如下:1 | public class Collections { |
1 | public class Collections { |
1 | // 将 List<Integer> 复制到 List<Number> |
1 | public class Collections { |
表示了非限定通配符,因为 <?> 可以用任意数据类型来替代。
4、List<? extends T> 和 List <? super T> 之间有什么区别 ?
这和上一题有联系,有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符。
这两个 List 的声明都是限定通配符的例子,List<? extends T> 可以接受任何继承自T 的类型的 List,而 List<? super T> 可以接受任何T 的父类构成的 List。
例如:List<? extends Number> 可以接受 List< Integer > 或 List< Float >;List <? super Number> 可以接受 List< Object > 但不能接受 List< Integer >。
5、如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?
编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用 T,E,K,V 等被广泛认可的类型占位符。泛型方法的例子请参阅 Java 集合类框架,最简单的情况下,一个泛型方法可能会像这样:
1 | public class TestMethod<U> { |
6、Java 中如何使用泛型编写带有类型参数的类?
这是上一道题的延伸,面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,而且要使用 JDK 中采用的类型占位符。举例如下:
1 | public class Generic<T> { |
// 泛型构造方法形参 key 的类型也为 T,T 的类型由外部传入
public Generic(T key) {
this.key = key;
}
// 泛型方法 getKey 的返回值类型为 T,T 的类型由外部指定
public T getKey(){
return key;
}
7、编写一段泛型程序来实现 LRU 缓存?
对于喜欢 Java 编程的人来说这相当于是一次练习。提示,LinkedHashMap 可以用来实现固定大小的 LRU 缓存,当 LRU 缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap 提供了一个称为 removeEldestEntry() 的方法,该方法会被 put() 和 putAll() 调用来删除最老的键值对。
8、你可以把 List< String > 传递给一个接受 List< Object > 参数的方法吗?
对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来 String 是 Object 的子类,所以 List< String > 应当可以向上转型为 List< Object > 。但是事实并非如此, List< String > 与 List< Object > 之间没有继承关系,真这样做的话会导致编译错误。
1 | List<Object> objectList; |
9、Array 中可以用泛型吗?
这可能是 Java 泛型面试题中最简单的一个了,当然前提是你要知道 Array 事实上并不支持泛型,这也是为什么《 Effective Java》 一书中建议使用 List 来代替 Array,因为 List 可以提供编译期的类型安全保证,而 Array 却不能。
10、Java 中 List< Object > 和原始类型 List 之间的区别?
原始类型和 < Object > 之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对泛型类型 < Object > 进行检查。< Object > 通过使用 Object 作为类型参数,可以告知编译器可以接收任何数据类型的对象,比如 String 或 Integer。 这道题的考察点在于对泛型中原始类型的正确理解。
它们之间的第二点区别是,你可以把任何泛型类型传递给接收原始类型 List 的方法,但却不能把 List< String > 传递给 List< Object > 的方法,因为会产生编译错误。举例如下:
1 | public class Test { |
fillNumList(list);// 编译正确
fillObjList(list);// 编译错误
}
public static void fillList(List list) {
…
}
public static void fillObjList(List<Object> list) {
...
}
11、Java 中 List<?> 和 List< Object > 之间的区别是什么?
这道题跟上一道题看起来很像,实质上却完全不同。List> 是一个不确定的未知类型的 List,而 List< Object > 是一个确定的 Object 类型的 List。 List> 在逻辑上是所有 List< T > 的父类,你可以把 List< String >、 List< Integer > 等集合赋值给 List<?> 的引用;而 List< Object > 只代表了自己这个泛型集合类,只能把 List< Object > 赋值给 List< Object > 的引用,但是 List< Object > 集合中可以加入任意类型的数据,因为 Object 类是最高父类。 举例如下:
1 | List > listOfAnyType; |
12、Java 中 List< String > 和原始类型 List 之间的区别。
该题类似于“List< Object > 和原始类型 List 之间的区别”。泛型数据类型是类型安全的,而且其类型安全是由编译器保证的,但原始类型 List 却不是类型安全的。你不能把 String 之外的任何其它类型的对象存入 List< String > 中,而你可以把任何类型的对象存入原始 List 中。
使用泛型数据类型你不需要进行类型转换,但是对于原始类型,你则需要进行显式的类型转换。举例如下:
1 | List listOfRawTypes = new ArrayList(); |
1 | String item = (String) listOfRawTypes.get(0);// 获取元素时需要显式的类型转换 |
总结
博主花了一周多的时间,对 Java 泛型的知识进行详细地整理总结。但是受限于个人的知识水平,导致篇幅可能过长,再加上排版的问题,不知道小伙伴们的最后的观感如何。
但是终于终于把 Java 泛型的知识整理完了(可能还有些遗漏,后面会慢慢补充的)。其中需要完善的地方,等过段时间再回来完善,如果文章中有什么错误和需要修改的地方,还望小伙伴们提出和批评指正,博主一定会尽快更改。谢谢大家!
参考
1.https://blog.csdn.net/weixin_45395059/article/details/126006369
?super>?>