[Java]泛型

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 本文详细介绍了Java泛型的相关概念和使用方法,包括类型判断、继承泛型类或实现泛型接口、泛型通配符、泛型方法、泛型上下边界、静态方法中使用泛型等内容。作者通过多个示例和测试代码,深入浅出地解释了泛型的原理和应用场景,帮助读者更好地理解和掌握Java泛型的使用技巧。文章还探讨了一些常见的疑惑和误区,如泛型擦除和基本数据类型数组的使用限制。最后,作者强调了泛型在实际开发中的重要性和应用价值。

【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://developer.aliyun.com/article/1631767
出自【进步*于辰的博客

启发博文:《java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一》(转发)。
参考笔记三,P21.1、P42.1。

注:引用启发博文中的两个概念:“类型形参”指泛型声明,“类型实参”指指定泛型具体类型。

1、一个类型判断

instanceOf可用于判断类型,但如下情况不允许,因为已经指定类型实参。

obj instanceOf List<Integer>

PS:我暂不理解其中缘由,但目前可作为泛型的一个使用细节。

2、继承泛型类或实现泛型接口

当继承泛型类或实现泛型接口时,泛型类或泛型接口的类型形参的标识不一定要与其声明时的类型形参一致。若泛型类或泛型接口上带有类型形参,则此类也必须声明此类型形参。如:

声明:class/interface Generic<T> 

继承或实现时:
// E与T是同一个,且前后两个类型形参的标识必须相同
class Car<E> extends/implements Generic<E>class Car extends/implements Generic

3、泛型通配符

3.1 铺垫

大家先看个示例。

public static void main(String[] args) {
   
    List<Integer> list1 = Arrays.asList(2, 0, 2, 3);
    List<String> list2 = Arrays.asList("CHAT", "GPT");
    print(list1);// 编译报错
    print(list2);// 编译报错
}

public static void print(List<Object> list) {
   
    sout list;
}

为何两次调用print()都编译报错?
我的思考:List<Object>、List<Integer>和 List<String>这三个的类型都是List,只是类型实参不同。而 Object 是 Integer 和 String 的父类,为何不能传递?

经查阅资料,我了解到一个新概念:泛型擦除

“泛型擦除”指在通过泛型检查后,将类型实参擦除,并上转为其上界类型Object的一种机制。

因此,List<Integer>和 List<String>中的类型实参会被擦除,并转为Object,而 List<Object>也同样如此。既然如此,为何不能传递?

PS:我猜测这是Java规定,因为任何类型都可转为Object,类型不确定。

3.2 概述

List<Object>的确不能接收 List<Integer>和 List<String>,尽管我暂不知其原理,但问题仍要解决。

如何解决上面编译报错的问题?

目前我暂未整理<?>的理论。大家暂且可以这么理解:<?>表示任意类型

示例。

 public static void main(String[] args) {
   
    List<Integer> list1 = Arrays.asList(2, 0, 2, 3);
    List<String> list2 = Arrays.asList("CHAT", "GPT");
    print(list1);// 打印:[2, 0, 2, 3]
    print(list2);// 打印:[CHAT, GPT]
}

public static void print(List<?> list) {
   
    sout list;
}

注意:
<?>表示任意类型。换言之:未知,表示对某个泛型未知。因此,必须放置在类型形参的位置。如:

List<?> list;
Class<?> class;
Map<?, ?> map;

而不能“无中生<?>”,这样就编译报错:

public void print(? obj) {
   }

4、泛型方法

4.1 概述

当初在阅读启发博文时,由于是第一次接触“泛型方法”这个概念,着实摸不着头脑,好半天才弄懂。我归纳了两点:

  1. 泛型类或泛型接口的类型形参可理解为==全局泛型==,而在方法上声明的泛型可理解为==局部泛型==。(大家注意“声明”二字)
    这是什么意思?大家以全局变量局部变量的特性来理解就懂了。
  2. 引入泛型方法的目的是什么?
    首先,大家先回忆一下“局部变量”有什么特性,第一,局部变量的生命周期仅在其声明或定义的当前方法内;第二,局部变量不受全局变量限制和影响。同样,局部泛型也具备这两个特性。
    例如,你需要定义一个方法来实现某种功能,那么,形参的参数类型、参数个数等等是不是都可以任意,对吧?此时你需要去考虑全局变量吗?比如:它们的名字是不是一样?它们的类型是不是一样?等等。。。需要考虑吗?当然不需要!!就是这个道理。

结论:

方法的类型形参是独立于泛型类或泛型接口的类型形参存在的。
全局泛型的类型实参由实例化时指定(指定具体类,如:Generic<Car>),而局部泛型的类型实参由方法调用时实参的类型决定。

有点绕口啊,大家继续往下看就明白了。

举个栗子。

class Test {
   
    class TestGeneric<T> {
   
        public <E> void print(E e) {
   
            sout e;// 打印:10
            sout e instanceof Integer;// 打印:true
        }
    }

    public static void main(String[] args) {
   
        TestGeneric<String> g1 = new Test().new TestGeneric();
        g1.print(10);
    }
}

TestGeneric 只声明了一个泛型<T>,那print()内使用的泛型<E>是哪来的?独立声明一个泛型,这就是泛型方法。

泛型<E>独立于类 TestGeneric 存在,与泛型<T>无关。何为“无关”?即:

<E>可以与<T>同名,而它们的类型实参可以不同。

看示例。<T>的类型实参由实例化时指定,为String
<E>的类型实参由print()调用时的实参类型决定,数字10的类型是什么?是Integer。故<E>的类型实参为Integer

如果把E重命名为T也是一样的,局部泛型不受全局泛型限制

4.2 一个疑惑

大家看到这里肯定有一个疑惑:泛型方法的确可以独立于泛型类或泛型接口声明泛型,但我把泛型<E>置于类 TestGeneric 上声明也可以实现同样的功能。那泛型方法有什么用?没错,的确一样。

不过,我假设一种情况。有100个地方使用了泛型类 TestGeneric,且有66个地方都指定了类型实参。而我现在需要在类 TestGeneric 内定义一个方法来实现某种功能,而这个方法需要使用另一个泛型,如何解决?

如果不用泛型方法,而是将新增泛型<E>声明到类 TestGeneric 上(即<T, E>),那结果是什么?==我需要修改66个地方的类型实参列表==(给<E>也指定类型实参)。这就是泛型方法的作用。

5、泛型上下边界

先说结论:

上边界的本质是为类型形参定义一个父类或超类,而下边界的本质是为类型形参定义一个子孙类
从而实现==限制类型实参选择范围==的作用。

上下边界一共有3个定义位置,以下我会一一进行解释。

为了便于大家理解和阐述,我定义了 A、B、C 三个类,这3个类是依次继承的关系,将用作类型实参。
下图是这3个类的继承关系图。
在这里插入图片描述

5.1 全局泛型处定义

待测试类:

class TestBound<T extends A> {
   }

测试:

TestBound<A> t1 = new TestBound();// 编译通过
TestBound<B> t2 = new TestBound();// 编译通过
TestBound<C> t3 = new TestBound();// 编译通过

示例中定义<T>的上边界是A,实例化时指定的类型实参分别是ABC,都编译通过。

因此,关键字extends的作用是将类型实参的范围限制为上边界的“子孙类”。

5.2 局部泛型处定义

待测试泛型方法:

public <T extends A> void testExtends(T t) {
   }

测试:

TestBound t1 = new TestBound();
t1.testExtends(new A());// 编译通过---------a
t1.testExtends(new B());// 编译通过---------b
t1.testExtends(new C());// 编译通过---------c

示例中定义<T>的上边界是A,根据上文可知:局部泛型的类型实参由方法调用时实参的类型决定。a/b/c 三处的实参的类型分别是ABC,都编译通过。

可见,局部泛型上边界的作用与全局泛型相同。

5.3 泛型通配符处定义

5.3.1 介绍

(注:为什么在上述全局泛型和局部泛型处,我不提及“下边界”?)(第5.5项的第2点有答案)

常见的有两种形式:

  1. 在单独使用<?>时,定义泛型上下边界;
  2. 在局部泛型与<?>连用时,定义泛型上下边界。

在上述的阐述中,只解释了<T extends xx>这种格式,其表示“上边界”,而“下边界”的定义是<T super xx>,这是什么意思?

解释<T super xx>

其实在学习“泛型上下边界”时,我就有个疑惑:上文所有对泛型上下边界的定义,都是通过extends关键字实现的,这仅仅是“上边界”,那“下边界”在哪?

我写过一些关于 Java-API 的文章,在解析API时,我注意到了super这个关键字。尽管我暂且没有找到关于<? super xx>这种格式的文章,但经过测试,得出了答案。

示例:
待测试类:

class TestBound<T> {
   
    public void testSuper(List<? super T> list) {
   }
}

测试:

TestBound<C> t1 = new TestBound();// 定义全局泛型 T 的类型实参为 C
List<C> list1 = new ArrayList<>();
t1.testSuper(list1);// 编译通过
List<B> list2 = new ArrayList<>();
t1.testSuper(list2);// 编译通过
List<A> list3 = new ArrayList<>();
t1.testSuper(list3);// 编译通过

结合上文extends关键字的示例,就可以很容易看出super关键字的作用。

因此,关键字super的作用是将类型实参的范围限制为下边界的“父类或超类”。

5.3.2 单独使用<?>

上面概述中的示例就是在单独使用<?>时,定义上下边界。

5.3.3 局部泛型与<?>连用时

例如:

public <T> void show(List<? extends/super T> list) {
   }

这种情形我开始也不理解,何出此言?
<?>表示可接收任意类型,而<T>的类型实参由调用show()时实参的类型决定,也是任意类型。

那问题来了:这两者连用,<?>还有何意义?<? extends/super T>这种格式到底是什么意思?
连用有何作用? 我解析过java.util.Collections的底层,此类中的很多方法都采用了<? extends/super T>这种形式来声明参数。

具体说明见下述示例。(为了便于排版,这可能会影响大家阅读,见谅。)

5.4 上下边界的说明示例

5.4.1 示例1

在这里插入图片描述
看到这个方法,我有了一个大胆的猜测:

<? extends/super T>这种格式单个使用,只能表示类型实参可为任意类型,没有限制作用。至于到底<?><T>哪个没有发挥作用、亦或者都有作用,无从得知,但肯定有一个多余是真的。
但若是两个一起使用(如上图),就可以实现将形参类型限制在同一体系的作用。(“同一体系”指存在继承关系)

就如图中方法copy(),其作用是将所有元素从一个列表复制到另一个列表。即为“复制”,那么,destsrc必须要在同一体系才能实现复制。

大家看我在那篇文章中写的示例:
在这里插入图片描述

这是java.lang.Integer类的API截图:
在这里插入图片描述

可见,Number类是Integer类的父类,
回说copy(),第一个参数dest对应的类型是List<Number>,第2个参数src对应的类型是List<Integer>
那么,此时<T>的类型实参就是Integer,这便印证了我的猜测。
当然,若 Integer 类是间接实现于 Number 接口(假设中间的继承类是xx),则<T>的类型实参就是xx

5.4.2 示例2

在这里插入图片描述
此方法的作用是使用指定元素替换指定列表中的所有元素。即为“替换”,则指定元素的类型与列表中元素的类型也必须在同一体系。与第1个例子同理。
回说fill()<? super T>表示<?>的类型实参是<T>的类型实参的父类或超类,而第2个形参的类型是T,这样就实现了限制功能。

大家看我在那篇文章中写的示例:
在这里插入图片描述
先看第2个实参10,其类型是Integer,则<T>的类型实参为Integer。而第1个实参类型为List<Object>,则此时<?>接收的类型为Object
Integer 类继承于 Object 类,也印证了我的猜测。

5.4.3 示例3

在这里插入图片描述
此方法与前2个方法都不同,其只有一个形参,可见我在示例1中的猜想有点纰漏。

结论:

<? extends/super T>这种格式单个使用,只能表示类型实参可为任意类型,没有限制作用。而之所以没有限制作用,是因为==没有定义具体的上下边界==。

原因如下。

5.4.4 示例补充说明

1:示例1
<? super T><? extends T>同时存在,这样就限制了2个List类型形参的类型实参都必须属于同一体系。

2:示例2
<? super T><T>同时存在,这样就将<?>的类型实参限制为必须是<T>的类型实参的父类或超类。

3:示例3
<T extends Object & Comparable<? super T><? extends T>同时存在。
分析:

  1. 首先,<T>的类型实参被限制为只能是 Object 类或 Comparable 接口的子类,
  2. 其中的<? super T>,将<?>的类型实参限制为必须是<T>的类型实参的父类或超类;
  3. 然后,<? extends T>是将<?>的类型实参限制为必须是<T>的类型实参的子孙类。

因此,这三项结合在一起,产生的效果是:

max()可接收类型为Collection、类型实参为 Object 类或 Comparable 接口的子类的实参,且 Comparable 接口的类型实参必须是此类型实参的父类或超类。

大家看我在那篇文章中写的示例:
在这里插入图片描述
max()接收的类型是List<Integer>List接口继承于Collection接口,则<?>的类型实参是Integer
而 Integer 类既继承于 Object 类,也实现于 Comparable 接口。因而,编译通过。

这是java.lang.Comparable接口的API截图:
在这里插入图片描述

5.5 上下边界注意事项

1、 <T extends xx>可以表示<T>继承或实现于xx类或接口,因为<T>的类型实参也可以是类或接口;

2、 只有<?>可以定义上下边界,即:<? extends/super xx>;而全局泛型和局部泛型都只能定义上边界,即:<T extends xx>

3、 一种定义上边界的特殊格式。

class Generic<T extends Object & Collection> {
   }

或:

public <T extends Object & Collection> void handler(T t) {
   }

使用&符号将2个父类或超类连接起来,表示定义2个上边界。

注意&前可以是类或接口,而&后只能是接口。

6、关于在静态方法中使用泛型

静态方法无法使用全局泛型。因此,若要在静态方法中使用泛型,则必须定义为静态泛型方法。

关于类加载,详述可查阅博文《Java知识点锦集》的第5项。

因为静态方法加载于类加载的第三过程“初始化”,而全局泛型加载于实例化。

7、一大个疑惑

7.1 发生背景

这是一个泛型方法,业务很简单,可测试时出现了问题。

/**
 * @param origin 数组
 * @param index 索引
 * @return 数组元素
 */
public static <T, U> T at(T[] origin, U index) {
   
    int temp = index.getClass() == Integer.class? (int) index : 0;
    return origin[temp];
}

测试代码:

int[] origin = {
   2, 0, 2, 3};
at(origin, 1);// 编译报错

若顺利执行,应返回0,可实际是编译报错,原因是origin的类型不能是int[]。更准确的说,不能是基本数据类型数组。

为何不能是基本数据类型数组?
为了研究此问题,做如下测试。

class TestGeneric<T> {
   
    // 获取指定数组的第1个元素
    public T at(T[] args) {
   
        return args[0];
    }
}

从此示例可以看出,args不能是基本数据类型数组,即<T>的类型实参不能是基本数据类型,为什么?因为<T>是全局泛型,限制只能是类。

于是,我将此限制也应用于局部泛型。

结论无误,但底层逻辑错了。

泛型也称之为“参数化类型”,类型实参只能是类,而不能是基本数据类型。

换言之,理论如此,根本不需要经过测试进行推论。

为何之前我会认为泛型的类型实参可以是基本数据类型?
主要有两个原因。第一,我对泛型的掌握有所欠缺;第二,我的基础功底不够扎实,以致于被误导了。
看这个方法。

<T> void at(T t) {
   }

请问:t可以是基本数据类型吗?
当然可以。这就是误导之处,于是我认为T[] tt也可以是基本数据类型数组。

真正的底层逻辑。

1、为什么T tt可以是基本数据类型?

因为在底层,触发了包装类的“自动装箱”机制。如:at(1),会在底层自动将<T>的类型实参适配为Integer,即:自动装箱int → Integer

因此,并不是说类型实参可以是基本数据类型,而是在底层进行了“自动装箱”,将类型实参适配为其包装类。

2、为什么T[] tt不可以是基本数据类型数组?

同理,研究其底层,拿上面的测试示例。

int[] origin = {
   2, 0, 2, 3};
at(origin, 1);// 编译报错

从上文可知:==局部泛型的类型实参由方法调用时实参的类型决定==。

假设t可以是基本数据类型数组(编译通过)。示例中origin的类型是int[],则<T>的类型实参为int。由于类型实参只能是类,故<T>的类型实参为Integer

也就是说,在底层会经历int[] → Integer[]

虽然 int 类型可以经“自动装箱”封装为 Integer 类,但int[]不能转换成Integer[],此转换不合法。

综上所述,t不可以是基本数据类型数组。

7.2 类型实参选择范围

在解析 Java-API 时,我发现T[] tt可以是基本数据类型数组。需要满足什么条件或有什么规律呢?

大家先看一些示例。
1、

class A<T> {
   
    public void show(T t) {
   }
}

// 测试
int a = 2023;
Integer b = 2023;

A<Integer> a1 = new A<>();
a1.show(a);// ------√
a1.show(b);// ------√

// 测试
int[] arr1 = {
   a};

A<int[]> a2 = new A<>();
a2.show(arr1);// ------√

2、

class A<T> {
   
    public void show(T[]t) {
   }
}

// 测试
int[] arr1 = {
   2023};
Integer[] arr2 = {
   2023};

A<Integer> a1 = new A<>();
a1.show(arr1);// ---------×
a1.show(arr2);// ---------√

// 测试
int[][] arr11 = {
   arr1};
Integer[][] arr21 = {
   arr2};

A<int[]> a2 = new A<>();
a2.show(arr11);// ---------√
a2.show(arr21);// ---------×

A<Integer[]> a3 = new A<>();
a3.show(arr11);// ---------×
a3.show(arr21);// ---------√

3、

public <T> void show(T t) {
   }

// 测试
int a = 2023;
int[] arr1 = {
   a};
Integer[] arr2 = {
   a};
int[][] arr11 = {
   arr1};
Integer[][] arr21 = {
   arr2};

show(a);// ------------√
show(arr1);// ---------√
show(arr2);// ---------√
show(arr11);// --------√
show(arr21);// --------√

4、

public <T> void show(T[] t) {
   }

// 测试
int[] arr1 = {
   2023};
Integer[] arr2 = {
   2023};
int[][] arr11 = {
   arr1};
Integer[][] arr21 = {
   arr2};

show(arr1);// ---------×
show(arr2);// ---------√
show(arr11);// --------√
show(arr21);// --------√

我自己都迷糊了。。。真是一言难尽,我也不知如何表述。

总结:

全局泛型的类型实参可以是类、基本数据类型数组和类数组;局部泛型同样。

8、扩展

大家都熟悉反射,不知道大家有没有注意这个细节:

Class<String> z1 = Integer.class;// 编译报错

为了研究这个问题,我将反编译、反射、类加载、泛型等知识点都考虑其中,却依旧没有答案。不然,我发起了【提问】,向博友们请教。

在大家的回复中,我注意到一个新概念:泛型擦除

之前我在解析Java-API 时,遇到过与这个概念类似的描述,只是没注意。关于此概念的理论,在上面<?>的阐述中提过,这里就不多赘述,我直接说在此处的应用。

示例中指定了类型实参为String,泛型擦除机制会将String擦除,并转为其上界类型Object

一个猜测:

Integer.class的底层是反编译,与Class<T>的底层有关,由于此类的底层比较复杂,我暂且未解析,故无法进行说明。

我的猜测

Integer.class的底层是将 Class 类的类型实参适配为Integer

Class<String>指定类型实参为String,虽然经泛型擦除会转为Object,但依旧是字符串。而Integer.class指定类型实参为Integer,则赋值转换不合法,故编译报错。

PS:这只是我的个人理解,可能不正确,但现阶段,这有利于自己的学习理解。

9、最后

本文中的例子是为了阐述泛型相关思想、方便大家理解而简单举例的,不一定有实用性,仅是抛砖引玉。

泛型,在Java中应用非常广泛,比如:开源。掌握了泛型对阅读源码、使用第三方框架都有很大的助力。大家可以自行测试一下,很多地方就都迎刃而解。

本文完结。

相关文章
|
4月前
|
安全 Java 编译器
揭秘JAVA深渊:那些让你头大的最晦涩知识点,从泛型迷思到并发陷阱,你敢挑战吗?
【8月更文挑战第22天】Java中的难点常隐藏在其高级特性中,如泛型与类型擦除、并发编程中的内存可见性及指令重排,以及反射与动态代理等。这些特性虽强大却也晦涩,要求开发者深入理解JVM运作机制及计算机底层细节。例如,泛型在编译时检查类型以增强安全性,但在运行时因类型擦除而丢失类型信息,可能导致类型安全问题。并发编程中,内存可见性和指令重排对同步机制提出更高要求,不当处理会导致数据不一致。反射与动态代理虽提供运行时行为定制能力,但也增加了复杂度和性能开销。掌握这些知识需深厚的技术底蕴和实践经验。
86 2
|
2月前
|
存储 安全 Java
🌱Java零基础 - 泛型详解
【10月更文挑战第7天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
15 1
|
2月前
|
Java 语音技术 容器
java数据结构泛型
java数据结构泛型
27 5
|
2月前
|
存储 Java 编译器
Java集合定义其泛型
Java集合定义其泛型
19 1
|
3月前
|
Java 编译器 容器
Java——包装类和泛型
包装类是Java中一种特殊类,用于将基本数据类型(如 `int`、`double`、`char` 等)封装成对象。这样做可以利用对象的特性和方法。Java 提供了八种基本数据类型的包装类:`Integer` (`int`)、`Double` (`double`)、`Byte` (`byte`)、`Short` (`short`)、`Long` (`long`)、`Float` (`float`)、`Character` (`char`) 和 `Boolean` (`boolean`)。包装类可以通过 `valueOf()` 方法或自动装箱/拆箱机制创建。
39 9
Java——包装类和泛型
|
2月前
|
存储 Java 编译器
【用Java学习数据结构系列】初识泛型
【用Java学习数据结构系列】初识泛型
20 2
|
3月前
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
|
2月前
|
安全 Java 编译器
Java基础-泛型机制
Java基础-泛型机制
16 0
|
3月前
|
存储 安全 搜索推荐
Java中的泛型
【9月更文挑战第15天】在 Java 中,泛型是一种编译时类型检查机制,通过使用类型参数提升代码的安全性和重用性。其主要作用包括类型安全,避免运行时类型转换错误,以及代码重用,允许编写通用逻辑。泛型通过尖括号 `&lt;&gt;` 定义类型参数,并支持上界和下界限定,以及无界和有界通配符。使用泛型需注意类型擦除、无法创建泛型数组及基本数据类型的限制。泛型显著提高了代码的安全性和灵活性。
|
2月前
|
Java
【Java】什么是泛型?什么是包装类
【Java】什么是泛型?什么是包装类
18 0