文章目录
- 泛型
- 泛型概述
- 泛型类
- 泛型接口
- 泛型方法
- 擦除类型
- 泛型通配符
- 面试题
- **Java中的泛型是什么 ? 使用泛型的好处是什么?**
- **Java的泛型是如何工作的 ? 什么是类型擦除 ?**
- **什么是泛型中的限定通配符和非限定通配符 ?**
- **List<? extends T> 和 List <? super T> 之间有什么区别 ?**
- **如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?**
- **Java 中如何使用泛型编写带有类型参数的类?**
- **编写一段泛型程序来实现 LRU 缓存?**
- **你可以把 List< String > 传递给一个接受 List< Object > 参数的方法吗?**
- **Array 中可以用泛型吗?**
- **Java 中 List< Object > 和原始类型 List 之间的区别?**
- **Java 中 List<?> 和 List< Object > 之间的区别是什么?**
- **Java 中 List< String > 和原始类型 List 之间的区别。**
- **Java 中 List< String > 和原始类型 List 之间的区别。**
泛型
泛型概述
什么是泛型
泛型,即“参数化类型”。
一提到参数,最熟悉的就是定义方法时有形参列表;
- 普通方法的形参列表中,每个形参的数据类型是确定的,而变量是一个参数;
- 在调用普通方法时需要传入对应形参数据类型的变量,若传入的实参与形参的数据类型不匹配会报错。
那什么是参数化类型?
- 以方法的定义为例,在方法标签中的形参的数据类型也设置为参数,在调用方法时再从外部传入一个具体的数据类型和变量。
泛型的本质是为了将类型参数化,也就是说再泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入参数(实参)的数据类型如果不匹配,编译器会直接报错
这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型类
泛型类的定义
类型参数用于类的定义中,则该类被称为泛型类。通过泛型可以完成对一组类的操作对外开放的接口。最典型的就是各种容器类,如:List、Set、Map等。
泛型类的基本语法
class className <泛型标识> {
private 泛型标识 /*(成员变量类型)*/ 变量名;
.....
}
}
- <>中的泛型标识被称为是类型参数,用于指代任何数据类型。
- 泛型标识是任意设置的,Java常见的泛型标识以及含义如下:
T :代表一般的任何类。
E :代表 Element 元素的意思,或者 Exception 异常的意思。
K :代表 Key 的意思。
V :代表 Value 的意思,通常与 K 一起配合使用。
S :代表 Subtype 的意思,文章后面部分会讲解示意。
举例
public class Generic<T> {
// key 这个成员变量的数据类型为 T, T 的类型由外部传入
private T key;
// 泛型构造方法形参 key 的类型也为 T,T 的类型由外部传入
public Generic(T key) {
this.key = key;
}
// 泛型方法 getKey 的返回值类型为 T,T 的类型由外部指定
public T getKey(){
return key;
}
}
-
泛型类中,类型参数定义的位置有三处:
- 非静态的成员属性类型
- 非静态方法的形参类型
- 非静态的成员方法的返回值类型
泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数
public class Test<T> {
public static T one; // 编译错误
public static T show(T one){ // 编译错误
return null;
}
}
- 泛型类中的类型参数的确定是在创建泛型类对象的时候(例如 ArrayList< Integer >)。
- 而静态变量和静态方法在类加载时已经初始化,直接使用类名调用;在泛型类的类型参数未确定时,静态成员有可能被调用,因此泛型类的类型参数是不能在静态成员中使用的。
泛型类不只接受一个类型参数,它还可以接受多个类型参数
public class MultiType <E,T> {
E value1;
T value2;
public E getValue1(){
return value1;
}
public T getValue2(){
return value2;
}
}
泛型类的使用
在创建泛型类的对象时,必须指定类型参数 T 的具体数据类型,即尖括号 <> 中传入的什么数据类型,T 便会被替换成对应的类型。如果 <> 中什么都不传入,则默认是 < Object >。
public class Generic<T> {
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
当创建一个 Generic< T > 类对象时,会向尖括号 <> 中传入具体的数据类型。
@ Test
public void test() {
Generic<String> generic = new Generic<>();// 传入 String 类型
// <> 中什么都不传入,等价于 Generic<Object> generic = new Generic<>();
Generic generic = new Generic();
}
传入 String 类型时,原泛型类可以想象它会自动扩展,其类型参数会被替换
拓展如下:
public class Generic {
private String key;
public Generic(String key) {
this.key = key;
}
public String getKey() {
return key;
}
}
- 可以发现,泛型中的类型参数T被<>中的String类型全部替换了。
- 可以使用泛型的上述特性便可以再集合中限制添加对象的数据类型;
- 若集合中添加的对象与指定的泛型数据类型不一致的时候,编译器会直接报错;
- 这就是泛型的类型安全检测机制的实现原理。
泛型接口
基本语法
public interface interfaceName<类型参数> {
...
}
泛型接口中的类型参数,在该接口被继承或者被实现时确定。
定义一个泛型接口
- 在泛型接口中,静态成员也不能使用泛型接口定义的类型参数
interface IUsb<U, R> {
int n = 10;
U name;// 报错! 接口中的属性默认是静态的,因此不能使用类型参数声明
R get(U u);// 普通方法中,可以使用类型参数
void hi(R r);// 抽象方法中,可以使用类型参数
// 在jdk8 中,可以在接口中使用默认方法, 默认方法可以使用泛型接口的类型参数
default R method(U u) {
return null;
}
}
定义一个接口 IA 继承了 泛型接口 IUsb,在 接口 IA 定义时必须确定泛型接口 IUsb 中的类型参数。
// 在继承泛型接口时,必须确定泛型接口的类型参数
interface IA extends IUsb<String, Double> {
...
}
// 当去实现 IA 接口时,因为 IA 在继承 IUsu 接口时,指定了类型参数 U 为 String,R 为 Double
// 所以在实现 IUsb 接口的方法时,使用 String 替换 U,用 Double 替换 R
class AA implements IA {
@Override
public Double get(String s) {
return null;
}
@Override
public void hi(Double d) {
...
}
}
定义一个类 BB 实现了 泛型接口 IUsb,在 类 BB 定义时需要确定泛型接口 IUsb 中的类型参数。
// 实现接口时,需要指定泛型接口的类型参数
// 给 U 指定 Integer, 给 R 指定了 Float
// 所以,当我们实现 IUsb 方法时,会使用 Integer 替换 U, 使用 Float 替换 R
class BB implements IUsb<Integer, Float> {
@Override
public Float get(Integer integer) {
return null;
}
@Override
public void hi(Float afloat) {
...
}
}
泛型方法
泛型方法的定义
在一个方法名中的返回值前面声明了一个< T >时,该方法就被声明为一个泛型方法。
< T >表明该方法声明了一个类型参数T,并且这个类型参数T只能在该方法中使用。
泛型方法中也可以使用泛型类中定义的泛型参数。
public <类型参数> 返回类型 functionName(类型参数 变量名) {
...
}
只有在方法签名中声明了< T >的方法才是泛型方法,仅使用了泛型类定义的类型参数的方法而不是泛型方法。
泛型方法中可以同时声明多个类型参数。
public class TestMethod<U> {
public <T, S> T testMethod(T t, S s) {
return null;
}
}
泛型方法中也可以使用泛型类中定义的泛型参数。
public class TestMethod<U> {
public <T> U testMethod(T t, U u) {
return u;
}
}
泛型类中定义的类型参数和泛型方法中定义的类型参数是相互独立的,它们一点关系都没有。
public class Test<T> {
public void testMethod(T t) {
System.out.println(t);
}
public <T> T testMethod1(T t) {
return t;
}
}
- 上面代码中,Test< T > 是泛型类,testMethod() 是泛型类中的普通方法,其使用的类型参数是与泛型类中定义的类型参数。
- 而 testMethod1() 是一个泛型方法,他使用的类型参数是与方法签名中声明的类型参数。
- 虽然泛型类中定义的类型参数标识和泛型方法中定义的类型参数标识都为< T >,但它们彼此之间是相互独立的。也就是说,泛型方法始终以自己声明的类型参数为准。
静态泛型方法。
前面在泛型类的定义中提到,在静态成员中不能使用泛型类定义的类型参数,但我们可以将静态成员方法定义为一个泛型方法。
public class Test2<T> {
// 泛型类定义的类型参数 T 不能在静态方法中使用
// 但可以将静态方法声明为泛型方法,方法中便可以使用其声明的类型参数了
public static <E> E show(E one) {
return null;
}
}
擦除类型
什么是类型擦除
泛型的本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间擦除代码中的所有泛型语法并相应的做出一些类型转换动作。
泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除
。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段
。
//ArrayList<String>和ArrayList<Integer>
//本质上都是ArrayList<Object>
public class GenericType {
public static void main(String[] args) {
ArrayList<String> arrayString = new ArrayList<String>();
ArrayList<Integer> arrayInteger = new ArrayList<Integer>();
System.out.println(arrayString.getClass() == arrayInteger.getClass());// true
}
}
-
在这个例子中,我们定义了两个 ArrayList 集合,不过一个是 ArrayList< String>,只能存储字符串。一个是 ArrayList< Integer>,只能存储整型对象。我们通过 arrayString 对象和 arrayInteger 对象的 getClass() 方法获取它们的类信息并比较,发现结果为true。
-
明明在 <> 中传入了两种不同的数据类型,按照上文所说的,它们的类型参数 T 不是应该被替换成我们传入的数据类型了吗,那为什么它们的类信息还是相同呢?
-
这是因为,在编译期间,所有的泛型信息都会被擦除, ArrayList< Integer > 和 ArrayList< String >类型,在编译后都会变成ArrayList< Object>类型。
所有的类型参数被擦除后都以 Object 类进行替换呢?
- 答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换;而有一种情况则不是,那就是使用到了 extends 和 super 语法的
有界类型参数
(即泛型通配符
)。
泛型通配符
泛型的继承
在Java的多态中,我们知道可以将一个子类对象赋值给其父类的引用,这也叫向上转型
在 Java 标准库中的集合 ArrayList< T > 类实现了 List< T >接口,其源码大致如下:
public class ArrayList<T> implements List<T> {...}
那现在ArrayList< T >泛型集合中,传入< T >中的数据类型相同时,是否能将ArrayList< T >对象赋值给其父类的引用List< T >吗?
public class GenericType {
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
}
}
- 上面的代码没有问题, 即 ArrayList< T > 对象可以向上转型为 List< T >,但两者传入 < T > 中的数据类型必须相同。
已知Integer 类是 Number 类的子类,那如果 ArrayList<> 泛型集合中,在 <> 之间使用向上转型
,也就是将 ArrayList< Integer > 对象赋值给 List< Number > 的引用,是否被允许呢?
public class GenericType {
public static void main(String[] args) {
List<Number> list01 = new ArrayList<Integer>();// 编译错误
ArrayList<Number> list02 = new ArrayList<Integer>();// 编译错误
}
}
- 上面代码会报错,我们发现并不能把 ArrayList< Integer > 对象赋值给 List< Number >的引用,甚至不能把 ArrayList< Integer > 对象赋值给 ArrayList< Number >的引用。
这也说明了在一般泛型中,不能向上转型
。
这是为什么?如果我们假设 ArrayList< Integer >可以向上转型为 ArrayList< Number >。
public class GenericType {
public static void main(String[] args) {
// 创建一个 ArrayList<Integer> 集合
ArrayList<Integer> integerList = new ArrayList<>();
// 添加一个 Integer 对象
integerList.add(new Integer(123));
// “向上转型”为 ArrayList<Number>
ArrayList<Number> numberList = integerList;
// 添加一个 Float 对象,Float 也是 Number 的子类,编译器不报错
numberList.add(new Float(12.34));
// 从 ArrayList<Integer> 集合中获取索引为 1 的元素(即添加的 Float 对象):
Integer n = integerList.get(1); // ClassCastException,运行出错
}
}
- 当我们把一个 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 > 两者之间没有继承关系。
什么是泛型通配符
在现实编码中,确实有这样的需求,希望泛型能够处理某一类型范围内
的类型参数,比如某个泛型类和它的子类,为此 Java 引入了泛型通配符
这个概念。
泛型通配符有 3 种形式:
- <?> :被称作无限定的通配符。
- <? extends T> :被称作有上界的通配符。
- <? super T> :被称作有下界的通配符。
上界通配符 <?extends T >
定义
上界通配符 <? extends T>
:T 代表了类型参数的上界,<? extends T>
表示类型参数的范围是 T 和 T 的子类。需要注意的是: <? extends T>
也是一个数据类型实参,它和 Number、String、Integer 一样都是一种实际的数据类型。
**1. 在泛型的继承中我们说到,ArrayList< Integer > 和 ArrayList< Number > 之间不存在继承关系。而引入上界通配符的概念后,我们便可以在逻辑上将 ArrayList<? extends Number> 看做是 ArrayList< Integer > 的父类,但实质上它们之间没有继承关系。
public class GenericType {
public static void main(String[] args) {
ArrayList<Number> list01 = new ArrayList<Integer>();// 编译错误
ArrayList<? extends Number> list02 = 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> 的数据类型。
举个例子
public class GenericType {
public static void main(String[] args) {
ArrayList<? extends Number> list = new ArrayList<>();
list.add(new Integer(1));// 编译错误
list.add(new Float(1.0));// 编译错误
}
}
-
可以这样理解,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 异常。
那为什么还需要引入上界统配符
的概念?---- 答:是为了拓展方法形参中类型参数的范围。
下界通配符<? super T>
定义
下界通配符 <? super T>:T 代表了类型参数的下界,<? super T>表示类型参数的范围是 T 和 T 的超类,直至 Object。需要注意的是: <? super T> 也是一个数据类型实参,它和 Number、String、Integer 一样都是一种实际的数据类型。
无限定通配符< ? >
无界通配符<?>:? 代表了任何一种数据类型,能代表任何一种数据类型的只有 null。需要注意的是: <?> 也是一个数据类型实参,它和 Number、String、Integer 一样都是一种实际的数据类型。
注意:Object 本身也算是一种数据类型,但却不能代表任何一种数据类型,所以 ArrayList< Object > 和 ArrayList<?> 的含义是不同的,前者类型是 Object,也就是继承树的最高父类,而后者的类型完全是未知的;ArrayList<?> 是 ArrayList< Object > 逻辑上的父类。
大多数情况下,可以用类型参数 < T > 代替 <?> 通配符。
<? extends T> 与 <? super T>对比
- 对于<? extends 类型>,编译器将只允许读操作,不允许写操作。即只可以取值,不可以设值。
- 对于<? super 类型>,编译器将只允许写操作,不允许读操作。即只可以设值(比如 set 操作),不可以取值(比如 get 操作)。
PECS原则
何时使用 extends,何时使用 super 通配符呢?为了便于记忆,我们可以用 PECS 原则:Producer Extends Consumer Super。
即:如果需要返回 T,则它是生产者(Producer),要使用 extends 通配符;如果需要写入 T,则它是消费者(Consumer),要使用 super 通配符。
面试题
Java中的泛型是什么 ? 使用泛型的好处是什么?
- 泛型是一种参数化类型的机制。它可以使得代码适用于各种数据类型,从而编写更加通用的代码,例如集合框架。
- 泛型是一种编译时类型确认机制。它提供了代码编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时产生
ClassCastException 异常
。
Java的泛型是如何工作的 ? 什么是类型擦除 ?
- 泛型的正常工作是依赖编译器在编译源码的时候,先进行类型检查,然后进行类型擦除并且在类型参数出现的地方插入强制转换的相关指令实现的。
- 类型擦除:编译器在编译时擦除了代码中所有与泛型相关的信息,所以在运行时不存在任何泛型信息。例如 List< String > 类在运行时仅用一个 List 类型来表示。而为什么要进行擦除呢?这是为了避免类型膨胀。
什么是泛型中的限定通配符和非限定通配符 ?
- 限定通配符对类型参数的范围进行了限制。有两种限定通配符,一种是 <? extends T> ,它通过确保泛型类型必须是T 的子类来设定类型参数的上界;另一种是 <?super T>,它通过确保泛型类型必须是T 的父类来设定类型参数的下界。
- 泛型类型必须使用限定范围内的类型来进行初始化,否则会导致编译错误。另一方面 <?> 表示了非限定通配符,因为 <?> 可以用任意数据类型来替代。
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 >。
如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?
- 编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用 T,E,K,V 等被广泛认可的
类型占位符
。泛型方法的例子请参阅 Java 集合类框架,最简单的情况下,一个泛型方法可能会像这样:
public class TestMethod<U> {
public <T, S> T testMethod(T t, S s) {
return null;
}
}
Java 中如何使用泛型编写带有类型参数的类?
public class Generic<T> {
// key 这个成员变量的数据类型为 T, T 的类型由外部传入
private T key;
// 泛型构造方法形参 key 的类型也为 T,T 的类型由外部传入
public Generic(T key) {
this.key = key;
}
// 泛型方法 getKey 的返回值类型为 T,T 的类型由外部指定
public T getKey(){
return key;
}
}
编写一段泛型程序来实现 LRU 缓存?
你可以把 List< String > 传递给一个接受 List< Object > 参数的方法吗?
- 对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来 String 是 Object 的子类,所以 List< String > 应当可以向上转型为 List< Object > 。但是事实并非如此, List< String > 与 List< Object > 之间没有继承关系,真这样做的话会导致编译错误。
Array 中可以用泛型吗?
- 这可能是 Java 泛型面试题中最简单的一个了,当然前提是你要知道 Array 事实上并不支持泛型,这也是为什么《 Effective Java》 一书中建议使用 List 来代替 Array,因为 List 可以提供编译期的
类型安全保证
,而 Array 却不能。
Java 中 List< Object > 和原始类型 List 之间的区别?
-
原始类型和 < Object > 之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对泛型类型 < Object > 进行检查。< Object > 通过使用 Object 作为类型参数,可以告知编译器可以接收任何数据类型的对象,比如 String 或 Integer。 这道题的考察点在于对泛型中原始类型的正确理解。
-
它们之间的第二点区别是,你可以把任何泛型类型传递给接收原始类型 List 的方法,但却不能把 List< String > 传递给 List< Object > 的方法,因为会产生编译错误。
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 类是最高父类。
Java 中 List< String > 和原始类型 List 之间的区别。
-
该题类似于“List< Object > 和原始类型 List 之间的区别”。泛型数据类型是类型安全的,而且其类型安全是由编译器保证的,但原始类型 List 却不是类型安全的。你不能把 String 之外的任何其它类型的对象存入 List< String > 中,而你可以把任何类型的对象存入原始 List 中。
-
使用泛型数据类型你不需要进行类型转换,但是对于原始类型,你则需要进行显式的类型转换。
- 这道题跟上一道题看起来很像,实质上却完全不同。List<?> 是一个不确定的未知类型的 List,而 List< Object > 是一个确定的 Object 类型的 List。
- List<?> 在逻辑上是所有 List< T > 的父类,你可以把 List< String >、 List< Integer > 等集合赋值给 List<?> 的引用;而 List< Object > 只代表了自己这个泛型集合类,只能把 List< Object > 赋值给 List< Object > 的引用,但是 List< Object > 集合中可以加入任意类型的数据,因为 Object 类是最高父类。
Java 中 List< String > 和原始类型 List 之间的区别。
-
该题类似于“List< Object > 和原始类型 List 之间的区别”。泛型数据类型是类型安全的,而且其类型安全是由编译器保证的,但原始类型 List 却不是类型安全的。你不能把 String 之外的任何其它类型的对象存入 List< String > 中,而你可以把任何类型的对象存入原始 List 中。
-
使用泛型数据类型你不需要进行类型转换,但是对于原始类型,你则需要进行显式的类型转换。