Java泛型理解

什么是泛型?

我们都知道 Java 中有形参和实参之分,是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数,其本身没有确定的值。在调用函数时,实参将赋值给形参。

而泛型是一种参数化的类型(可以理解为类型形参),它允许在定义类、接口时通过一个「标识」表示类中某个属性的类型或者是某个方法的返回值或参数的类型。这个类型参数将在使用时(例如,继承或实现这个接口、创建对象或调用方法时)确定(即实际传入的类型参数,也称为类型实参)。好处是增强代码的安全性。

以 Java 中的集合为例,如果不指定泛型,那么传入集合中的元素就可以是任意类型的,这就可能带来一些问题,比如我取出集合中的某个元素后,可能要进行「类型转换」,但是如果不指定泛型,那么我是无法知道集合中的元素的什么类型的(默认是 Object 类型),就可能带来 ClassCastException 异常。

泛型的使用场景?

  • 自定义通用返回结果 CommonResult<T> 通过参数 T 可根据具体的返回类型动态指定结果的数据类型
  • DAO 层、自定义的通用泛型方法
  • 比较器(Comparable、Comparator)
    ...

泛型的使用方式有哪几种?

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

泛型方法的一个例子:

java 复制代码
// 创建不同类型的数组:  
Integer[] intArray = {1, 2, 3};  
String[] stringArray = {"hello", "world"};  
printArray(intArray);  
printArray(stringArray);

public static <E> void printArray(E[] inputArray) {  
        for (E element : inputArray) {  
            System.out.printf("%s ", element);  
        }  
        System.out.println();  
}

个人认为使用泛型方法的好处有二:

  1. 允许方法传入多种类型的参数,提高了方法的可扩展性。
  2. 允许方法使用和类不同的泛型参数,比如上面的泛型方法使用的是 E,而该方法所在的类使用的可以是 T。

什么是泛型擦除?为什么要擦除?

泛型擦除就是指 Java 程序在编译期间所有的泛型信息都会被擦除,也就是「类型擦除」。

类型擦除的主要过程如下:1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。2.移除所有的类型参数。

比如编译器会在编译期间将泛型 T 擦除为 Object 类型,或将 T extends xxx 擦除为其限定类型 xxx。

总结:泛型本质上还是「编译时行为」。为了保证引入泛型机制但不创建新的类型,减少 JVM 的运行开销,编译器会通过擦除将泛型类转换为一般类。

举个例子来证明泛型是「编译时行为」:

java 复制代码
List<Integer> list = new ArrayList<>();  
list.add(2);  
// 1. 编译期间直接添加与泛型参数类型不同元素会报错,因为有「泛型校验」
list.add("aa");  
// 2. 运行期间通过反射添加却是可以的
Class<? extends List> clazz = list.getClass();  
Method add = clazz.getDeclaredMethod("add", Object.class);  
add.invoke(list, "kl");  
System.out.println(list);

也正是由于泛型擦除的问题,下面的「方法重载会报错」:

java 复制代码
public static void test(List<String> list1) {  
    System.out.println(1);  
}  
  
public static void test(List<Integer> list1) {  
    System.out.println(1);  
}

因为擦除后大家都是 List 了,默认里面存的就是 Object 类型的元素。

通配符

由于如果泛型类型只能是固定的,在某些场景下使用起来不够灵活。

使用通配符可以解决泛型无法协变的问题。

协变指的就是如果 Child 是 Parent 的子类,那么List<Child> 也应该是List<Parent> 的子类,但是泛型是不支持的。

无界通配符

List<?>List 有区别吗?当然有!

List<?> list 表示 list 是持有某种特定类型的 List,但是不知道具体是哪种类型。因此,我们添加元素进去的时候会报错。

List list 表示 list 是持有的元素的类型是 0bject,因此可以添加任何类型的对象,只不过编译器会有警告信息。

java 复制代码
List<?> list = new ArrayList<>( );
list.add("sss");//报错
List list2 = new ArrayList<>();
list2. add("sss");//警告信息

上/下边界通配符

在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

上边界通配符 extends 要求传入的类型实参必须是指定类型或其子类。

举个例子:

java 复制代码
// 限制必须是 Person 类或其子类
<? extends Person>

下边界通配符 super 则要求传入的类型实参必须是指定类型或其父类。

java 复制代码
// 限制必须是 Person 类或其父类
<? super Person>

使用 <? extends xxx><? super extends xxx> 需要注意的点

G<Father> 和 G<Son> 没有继承关系。

这块可能不太好理解,具体可以看宋红康老师 JavaSE 的视频

泛型中的坑

1.当泛型遇到 catch

泛型的类型参数不能用在 Java 异常处理的 catch 语句中。因为异常处理是由 JVM 在运行时刻来进行的。由于类型信息被擦除,JVM 是无法区分两个异常类型MyException<String>MyException<Integer>

2.当泛型类内包含静态变量

java 复制代码
public class StaticTest{
    public static void main(String[] args){
        GT<Integer> gti = new GT<Integer>();
        gti.var=1;
        GT<String> gts = new GT<String>();
        gts.var=2;
        System.out.println(gti.var);
    }
}
class GT<T>{
    public static int var=0;
    public void nothing(T x){}
}

以上代码输出结果为:2!

由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,它们的静态变量是共享的。

相关推荐
liu_chunhai5 分钟前
设计模式(3)builder
java·开发语言·设计模式
Mr.D学长10 分钟前
毕业设计 深度学习社交距离检测系统(源码+论文)
python·毕业设计·毕设
姜学迁13 分钟前
Rust-枚举
开发语言·后端·rust
wdxylb13 分钟前
解决Python使用Selenium 时遇到网页 <body> 划不动的问题
python
冷白白14 分钟前
【C++】C++对象初探及友元
c语言·开发语言·c++·算法
凌云行者18 分钟前
rust的迭代器方法——collect
开发语言·rust
代码骑士21 分钟前
【一起学NLP】Chapter3-使用神经网络解决问题
python·神经网络·自然语言处理
It'sMyGo22 分钟前
Javascript数组研究09_Array.prototype[Symbol.unscopables]
开发语言·javascript·原型模式
睡觉然后上课33 分钟前
c基础面试题
c语言·开发语言·c++·面试
qing_04060339 分钟前
C++——继承
开发语言·c++·继承