Java泛型整理


Java泛型

面试题:泛型的考点,问题我不太记得了,还是想 < Integer> 和 < String> 的对象是否相等

Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符? (gitee.io)

java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一 - little fat - 博客园 (cnblogs.com)

Java泛型深入理解_void-CSDN博客_java泛型

Java 泛型 中 super 怎么 理解?与 extends 有何不同? - 知乎 (zhihu.com)

需要了解的:

  • Java泛型作用,使用
  • 类型擦除
  • 通配符,父类子类上下边界

简介

本质:参数化类型,也就是将数据类型变成一个参数

Java的泛型是伪泛型,因为在编译期间所有的泛型信息都会被擦掉,也就是类型擦除

类型擦除

泛型只在编译阶段有效。看下面的代码:

1
2
3
4
5
6
7
8
9
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();

Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();

if(classStringArrayList.equals(classIntegerArrayList)){
Log.d("泛型测试","类型相同");
}

输出结果:D/泛型测试: 类型相同

通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加==类型检查和类型转换的方法==。也就是说,泛型信息不会进入到运行时阶段。

对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型

使用

一般有三种使用方式:泛型类,泛型接口,泛型方法。

泛型的类型参数只能是类类型,不能是简单类型。

1. 泛型类

类声明

1
2
3
4
5
6
7
8
// T可以使用任意标识,见下面
// 实例化泛型类的时候,必须指定T具体类型
// 类名<泛型>
public class Generic<T>{
private T key;

// 构造,set,get等
}

类实例化:

1
Generic<Integer> g = new Generic<Integer>();

2. 泛型接口

其实和泛型类差不多啦。

接口声明:

1
2
3
public interface Generator<T>{
public T method();
}

接口实现,可以不指定类型

1
2
3
4
5
6
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method(){
return null;
}
}

接口实现,指定类型

1
2
3
4
5
6
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method(){
return "hello";
}
}

注意:这里实现类中的 T,与接口中的 T 不是同一个了,原来接口中的T在这都变成 String 了。

3. 泛型方法

方法声明:

1
2
3
4
5
public static <E> void printArray(E[] inputArray){
for(E e : inputArray){
System.out.printf("%s",e);
}
}

方法使用:

1
2
3
4
5
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray );
printArray( stringArray );

需要注意的几点:

  • 只有在返回类型之前声明了 <T> 的才是泛型方法
  • 静态方法无法访问类上定义的泛型,如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法

举例:

只有在返回类型之前声明了 <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
public class Generic<T>{     
private T key;

public Generic(T key) {
this.key = key;
}

//我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
//所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey(){
return key;
}
//这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}

/**
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
* 如:public <T,K> K showKeyName(Generic<T> container){
* ...
* }
*/

常见通配符

  • ? 不确定的Java类型
  • T (type) 具体的一个Java类型
  • K V (key,value) 代表Java键值中的 Key 与 Value
  • E (element) 代表 Element

泛型限定通配符/上下边界

  • 限定通配符:
    • 上边界:<? extends Number> ,只能是 Number 的子类
      上边界extends
    • 下边界:<? super Integer>,只能是 Integer 的父类
      下边界super
  • 非限定通配符:<?> 代表所有类型

举例:

为泛型添加上边界,即传入的类型实参必须是指定类型的子类型:

声明:

1
2
3
public void showKeyValue1(Generic<? extends Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}

使用:

1
2
3
4
5
6
7
8
9
10
11
Generic<String> generic1 = new Generic<String>("11111");
Generic<Integer> generic2 = new Generic<Integer>(2222);
Generic<Float> generic3 = new Generic<Float>(2.4f);
Generic<Double> generic4 = new Generic<Double>(2.56);

//这一行代码编译器会提示错误,因为String类型并不是Number类型的子类
//showKeyValue1(generic1);

showKeyValue1(generic2);
showKeyValue1(generic3);
showKeyValue1(generic4);

上下边界要添加在声明上,而不是参数上:

1
2
3
4
5
6
7
//在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界,即在泛型声明的时候添加
//public <T> T showKeyName(Generic<T extends Number> container),编译器会报错:"Unexpected bound"
public <T extends Number> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
T test = container.getKey();
return test;
}

泛型通配符

IntegerNumber 的一个子类

泛型类 Generic<Integer>Generic<Number> 的子类吗?能用 Generic<Integer> 传入 Generic<Number> 吗?

答案是不行的。

首先声明一个泛型方法:

1
2
3
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}

然后进行 Integer 与 Number 的测试:

1
2
3
4
5
6
7
8
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

showKeyValue(gNumber);

// showKeyValue(gInteger);

// showKeyValue(gInteger)这里编译器会为我们报错:Generic<java.lang.Integer> cannot be applied to Generic<java.lang.Number>

那么要怎么才能让 showKeyValue(gNumber)showKeyValue(gInteger) 都不报错呢?

答案是使用通配符 ? ,将参数改为 Generic<?> obj 即可:

1
2
3
public void showKeyValue1(Generic<?> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}

类型通配符一般是使用 ? 代替具体的类型实参

注意了,此处 ? 是类型实参,而不是类型形参。

再直白点的意思就是,此处的 和Number、String、Integer一样都是一种实际的类型,可以把 看成所有类型的父类,是一种真实的类型。

常见面试题

  1. Java中的泛型是什么 ? 使用泛型的好处是什么?
    泛型是一种参数化类型的机制。它可以使得代码适用于各种类型,从而编写更加通用的代码,例如集合框架。

    泛型是一种编译时类型确认机制。它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现ClassCastException

2、Java的泛型是如何工作的 ? 什么是类型擦除 ?

​ 泛型的正常工作是依赖编译器在编译源码的时候,先进行类型检查,然后进行类型擦除并且在类型参数出现的地方插入强制转换的相关指令实现的。

​ 编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如 List<String> 在运行时仅用一个List类型来表示。为什么要进行擦除呢?这是为了避免类型膨胀。

  1. 什么是泛型中的限定通配符和非限定通配符 ?
    限定通配符对类型进行了限制。

    • 两种限定通配符
      • <? extends T>它通过确保类型必须是T的子类来设定类型的上界
      • <? super T>它通过确保类型必须是T的父类来设定类型的下界
        泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。
    • 另一方面<?>表示了非限定通配符,因为<?>可以用任意类型来替代。
  2. List<? extends T>和List <? super T>之间有什么区别 ?

  3. 如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?

  4. Java中如何使用泛型编写带有参数的类?

  5. 编写一段泛型程序来实现LRU缓存?

  6. 你可以把 List<String> 传递给一个接受 List<Object> 参数的方法吗?

  7. Array中可以用泛型吗?
    Array不支持泛型,而List支持。List可以提供编译期的类型安全保证,而Array却不能。

  8. 如何阻止Java中的类型未检查的警告?
    如果你把泛型和原始类型混合起来使用,例如下列代码,Java 5的javac编译器会产生类型未检查的警告
    ,例如 List<String> rawList = new ArrayList()
    注意: Hello.java使用了未检查或称为不安全的操作;
    这种警告可以使用 @SuppressWarnings("unchecked") 注解来屏蔽。

  9. Java中 List<Object> 和原始类型 List 之间的区别?

    1. 原始类型和带参数类型<Object>之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,通过使用Object作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String或Integer。这道题的考察点在于对泛型中原始类型的正确理解。
    2. 它们之间的第二点区别是,你可以把任何带参数的泛型类型传递给接受原始类型List的方法,但却不能把List<String>传递给接受List<Object>的方法,因为会产生编译错误。
  10. Java中List<?>List<Object> 之间的区别是什么?
    这道题跟上一道题看起来很像,实质上却完全不同。
    List<?> 是一个所有类型的List,而List<Object>其实是Object类型的List
    你可以把List<String>, List<Integer>赋值给List<?>,却不能把List<String>赋值给List<Object>

`List<Object>` 与 `List<?>` 并不等同,`List<Object>` 是`List<?>`的子类。还有不能往`List<?> list`里添加任意对象,除了null。
  1. List<String>和原始类型List之间的区别.
    该题类似于“11. 原始类型和带参数类型之间有什么区别”。
    带参数类型是类型安全的,而且其类型安全是由编译器保证的,但原始类型List却不是类型安全的
    你不能把String之外的任何其它类型的Object存入String类型的List中,而你可以把任何类型的对象存入原始List中。
    使用泛型的带参数类型你不需要进行类型转换,但是对于原始类型,你则需要进行显式的类型转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    List listOfRawTypes = new ArrayList();
    listOfRawTypes.add("abc");
    listOfRawTypes.add(123); //编译器允许这样 - 运行时却会出现异常
    String item = (String) listOfRawTypes.get(0); //需要显式的类型转换
    item = (String) listOfRawTypes.get(1); //抛ClassCastException,因为Integer不能被转换为String

    List<String> listOfString = new ArrayList();
    listOfString.add("abcd");
    listOfString.add(1234); //编译错误,比在运行时抛异常要好
    item = listOfString.get(0); //不需要显式的类型转换 - 编译器自动转换

文章作者: SongX64
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 SongX64 !
  目录