【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://blog.csdn.net/m0_69908381/article/details/129773883
出自【进步*于辰的博客】
请先浏览序言!!这对后续阅读一定有所帮助。
由于之前对泛型的学习不够全面,就想找一些博文取取经,其中一位前辈的创文【java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一】(转发)让我受益匪浅!!。
现在回头看,觉得应该写点什么。一是希望能帮助大家更容易地理解和运用泛型;二是对泛型这个知识点进行一次梳理。
当然,前辈们总结的已经很全面,我又怎会班门弄斧。
因此,这篇文章可以说是我阅读完那篇文章后的 读后小结 \color{red}{读后小结} 读后小结。为什么这么说呢?是这样,那位前辈是基于知识分享的角度来撰写那篇文章,更加注重全面、整体,在一些细节上难免有所省略;而我是初次系统地学习泛型这个知识点,为了更好地理解,做了很多测试,又为了让我的阐述更有证明力,就把那些示例都迁移到这篇文章中。简言之, 一一解释、通俗易懂 \color{green}{一一解释、通俗易懂} 一一解释、通俗易懂。
参考笔记三,P21.1、P42.1。
注:向那位前辈的博文借两个概念:泛型声明称之为类型形参,指定泛型具体类型称之为类型实参。
instanceOf
可用于判断类型,但如下情况不允许,因为已经指定类型实参。
obj instanceOf List<Integer>
留言: \color{brown}{留言:} 留言:其实我不理解其中缘由,欢迎各位博友评论区讨论!
当继承泛型类或实现泛型接口时,泛型类或泛型接口的类型形参的标识不一定要与其声明时的类型形参一致。若泛型类或泛型接口上带有类型形参,则此类也必须声明此类型形参。
如:
声明:class/interface Generic<T>
继承或实现时:
// E与T是同一个,且前后两个类型形参的标识必须相同
class Car<E> extends/implements Generic<E>
或
class Car extends/implements Generic
大家先看个示例。
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;
}
为何两次调用 \color{grey}{为何两次调用} 为何两次调用print()
都编译报错? \color{grey}{都编译报错?} 都编译报错?
我的思考:List、List
和 List
这三个的类型都是List
,只是类型实参不同。而 Object 是 Integer 和 String 的父类,为何不能传递?
经查阅资料,我了解到一个新概念: 泛型擦除 \color{red}{泛型擦除} 泛型擦除。
“泛型擦除”指在通过泛型检查后,将类型实参擦除,并上转为其上界类型
Object
的一种机制。
因此,List
和 List
中的类型实参会被擦除,并转为Object
,而 List也同样如此。
既然如此,为何还不能传递?
留言: \color{brown}{留言:} 留言:这也是我始终不解之处,望各位博友不吝赐教!!
List的确不能接收 List
和 List
,尽管我暂不知其原理,但问题仍要解决。
如何解决上面的编译报错问题? \color{grey}{如何解决上面的编译报错问题?} 如何解决上面的编译报错问题?
关于泛型通配符
>
的理论,目前我还不知如何阐述。大家暂且可以这么理解:>
表示任意类型。
引入>
。
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;
}
注意: \color{red}{注意:} 注意:
>
表示任意类型。换言之:未知,表示对某个泛型未知。因此,必须放置在类型形参的位置。
如:
List<?> list;
Class<?> class;
Map<?, ?> map;
而不能“无中生>
”。
public void print(? obj) {}
关于泛型方法,当初我在阅读那位前辈的博文时,由于是第一次接触这个新概念,着实摸不着头脑,好半天才弄懂。我归纳了两点:
结论: \color{red}{结论:} 结论:
方法的类型形参是独立于泛型类或泛型接口的类型形参存在的。
全局泛型的类型实参由实例化时指定(指定具体类,如:Generic
),而局部泛型的类型实参由方法调用时实参的类型决定。
哈哈。。。有点绕口啊,大家继续往下看就明白了。
举个栗子。
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 只声明了一个泛型
,那print()
内使用的泛型
是哪来的? 独立声明一个泛型 \color{red}{独立声明一个泛型} 独立声明一个泛型,这就是泛型方法。
泛型
独立于类 TestGeneric 存在,与泛型
无关。 何为“无关”? \color{grey}{何为“无关”?} 何为“无关”?如:
可以与
同名,而它们的类型实参可以不同。
的类型实参由实例化时指定,为String
;
的类型实参由print()
调用时的实参类型决定,数字10
的类型是什么?是Integer
。故
的类型实参为Integer
。
如果把E
重命名为T
也是一样的,局部泛型不受全局泛型限制。
大家看到这里肯定有一个疑惑:“泛型方法的确可以独立于泛型类或泛型接口声明泛型,但我把泛型
置于泛型类TestGeneric 上声明还不是可以实现同样的功能。那泛型方法有什么用?”
没错,的确一样。
不过,我假设一种情况。有100个地方使用了泛型类 TestGeneric,且有66个地方都指定了类型实参。而我现在需要在类 TestGeneric 内定义一个方法来实现某种功能,而这个方法需要使用另一个泛型,如何解决?
如果不用泛型方法,而是将新增泛型
声明到类 TestGeneric 上(即
),那结果是什么?我需要修改66个地方的类型实参列表(给
也指定类型实参)。这就是泛型方法的作用。
先说结论: \color{red}{先说结论:} 先说结论:
上边界的本质是为类型形参定义一个父类或超类,而下边界的本质是为类型形参定义一个子孙类。
从而实现 限制类型实参选择范围 \color{green}{限制类型实参选择范围} 限制类型实参选择范围的作用。
上下边界一共有3个定义位置,以下我会一一进行解释。
为了便于大家理解和阐述,我定义了 A、B、C 三个类,这3个类是依次继承的关系,将用作类型实参。
下图是这3个类的继承关系图。
待测试类:
class TestBound<T extends A> {}
测试:
TestBound<A> t1 = new TestBound();// 编译通过
TestBound<B> t2 = new TestBound();// 编译通过
TestBound<C> t3 = new TestBound();// 编译通过
示例中定义
的上边界是A
,实例化时指定的类型实参分别是A
、B
、C
,都编译通过。
因此,关键字extends
的作用是将类型实参的范围限制为“上边界”的子孙类。
待测试泛型方法:
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
示例中定义
的上边界是A
,根据上文可知:局部泛型的类型实参由方法调用时实参的类型决定。a/b/c 三处的实参的类型分别是A
、B
、C
,都编译通过。
可见,局部泛型上边界的作用与全局泛型相同。
注:为什么在上述全局泛型和局部泛型处,我不提及“下边界”?(第5.5项的第2点有答案)
常见的有2种形式:
>
时,定义泛型上下边界;>
连用时,定义泛型上下边界。在上述的阐述中,只解释了
这种格式,其表示“上边界”,而“下边界”的定义是
,这是什么意思?
解释 \color{purple}{解释} 解释
。
其实在学习“泛型上下边界”时,我就有个疑惑:上文所有对泛型上下边界的定义,都是通过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
的作用是将类型实参的范围限制为“下边界”的父类或超类。
>
时上面概述中的示例就是在单独使用>
时,定义上下边界。
>
连用时例如:
public <T> void show(List<? extends/super T> list) {}
这种情形我开始也不理解,何出此言?
>
表示可接收任意类型,而
的类型实参由调用show()
时实参的类型决定,也是任意类型。
那问题来了:
这两者连用,>
还有何意义? extends/super T>
这种格式到底是什么意思?
连用有何作用?
我解析过java.util.Collections
类的底层,此类中的很多方法都采用了 extends/super T>
这种形式来声明参数。
具体说明见下述示例。(为了便于排版,这可能会影响大家阅读,见谅。。。)
extends/super T>
这种格式单个使用,只能表示类型实参可为任意类型,没有限制作用。至于到底>
与哪个没有发挥作用、亦或者都有作用,无从得知,但肯定有一个多余是真的。
但若是两个一起使用(如上图),就可以实现 将形参类型限制在同一体系 \color{green}{将形参类型限制在同一体系} 将形参类型限制在同一体系的作用。(“同一体系”指存在继承关系)
就如图中方法copy()
,其作用是将所有元素从一个列表复制到另一个列表。即为“复制”,那么,dest
与src
必须要在同一体系才能实现复制。
可见,Number
类是Integer
类的父类,
回说copy()
,第一个参数dest
对应的类型是List
,第2个参数src
对应的类型是List
。
那么,此时
的类型实参就是Integer
,这便印证了我的猜测。
当然,若 Integer 类是间接实现于 Number 接口(假设中间的继承类是xx
),则
的类型实参就是xx
。
此方法的作用是使用指定元素替换指定列表中的所有元素。即为“替换”,则指定元素的类型与列表中元素的类型也必须在同一体系。与第1个例子同理。
回说fill()
, super T>
表示>
的类型实参是
的类型实参的父类或超类,而第2个形参的类型是T
,这样就实现了限制功能。
大家看我在那篇文章中写的示例:
先看第2个实参10
,其类型是Integer
,则
的类型实参为Integer
。而第1个实参类型为List
,则此时>
接收的类型为Object
。
Integer 类继承于 Object 类,也印证了我的猜测。
此方法与前2个方法都不同,其只有一个形参,可见我在示例1中的猜想有点纰漏。
最终结论 : \color{red}最终结论{}: 最终结论:
extends/super T>
这种格式单个使用,只能表示类型实参可为任意类型,没有限制作用。而之所以没有限制作用,是因为 没有定义具体的上下边界 \color{blue}{没有定义具体的上下边界} 没有定义具体的上下边界。
原因如下。
1、示例1。
super T>
与 extends T>
同时存在,这样就限制了2个List
类型形参的类型实参都必须属于同一体系。
2、示例2。
super T>
与
同时存在,这样就将>
的类型实参限制为必须是
的类型实参的父类或超类。
3、示例3。
与 extends T>
同时存在。
分析: \color{blue}{分析:} 分析:
的类型实参被限制为只能是 Object 类或 Comparable 接口的子类, super T>
,将>
的类型实参限制为必须是
的类型实参的父类或超类; extends T>
是将>
的类型实参限制为必须是
的类型实参的子孙类。因此,这三项结合在一起,产生的效果是:
max()
可接收类型为Collection
、类型实参为 Object 类或 Comparable 接口的子类的实参,且 Comparable 接口的类型实参必须是此类型实参的父类或超类。
大家看我在那篇文章中写的示例:
max()
接收的类型是List
,List
接口继承于Collection
接口,则>
的类型实参是Integer
。
而 Integer 类既继承于 Object 类,也实现于 Comparable 接口。因而,编译通过。
这是java.lang.Comparable
接口的API截图:
1、
可以表示
继承或实现于xx
类或接口,因为
的类型实参也可以是类或接口;
2、 只有>
可以定义上下边界,即: extends/super xx>
;而全局泛型和局部泛型都只能定义上边界,即:
;
3、 一种定义上边界的特殊格式。
class Generic<T extends Object & Collection> {}
或:
public <T extends Object & Collection> void handler(T t) {}
使用&
符号将2个父类或超类连接起来,表示定义2个上边界。
注意: \color{red}{注意:} 注意:&
前可以是类或接口,而&
后只能是接口。
静态方法无法使用全局泛型。因此,若要在静态方法中使用泛型,则必须定义为静态泛型方法。
关于类加载,详述可查阅博文【【java知识点】锦集】的第5项。
因为静态方法加载于类加载的第三过程“初始化”,而全局泛型加载于实例化。
事情起因: 事情起因: 事情起因:
这是一个泛型方法,业务很简单,可测试时出现了问题。
/**
* @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[]
。更准确的说,不能是基本数据类型数组。
为何不能是基本数据类型数组? \color{grey}{为何不能是基本数据类型数组?} 为何不能是基本数据类型数组?
为了研究此问题,做如下测试。
class TestGeneric<T> {
// 获取指定数组的第1个元素
public T at(T[] args) {
return args[0];
}
}
从此示例可以看出,args
不能是基本数据类型数组,即
的类型实参不能是基本数据类型,为什么?因为
是全局泛型,限制只能是类。
于是,我将此限制也应用于局部泛型。
结论无误,但底层逻辑错了。 \color{red}{结论无误,但底层逻辑错了。} 结论无误,但底层逻辑错了。
泛型也称之为“参数化类型”,类型实参只能是类,而不能是基本数据类型。
换言之,理论如此,根本不需要经过测试进行推论。
为何之前我会认为泛型的类型实参可以是基本数据类型? 为何之前我会认为泛型的类型实参可以是基本数据类型? 为何之前我会认为泛型的类型实参可以是基本数据类型?
主要有两个原因。第一,我对泛型的掌握有所欠缺;第二,我的基础功底不够扎实,以致于被误导了。
看这个方法。
<T> void at(T t) {}
请问:t
可以是基本数据类型吗?
当然可以。这就是误导之处,于是我认为T[] t
的t
也可以是基本数据类型数组。
真正的底层逻辑。 \color{red}{真正的底层逻辑。} 真正的底层逻辑。
为什么T t
的t
可以是基本数据类型?
因为在底层,触发了包装类的“自动装箱”机制。如:at(1)
,会在底层自动将
的类型实参适配为Integer
,即:自动装箱int → Integer
。
因此,并不是说类型实参可以是基本数据类型,而是在底层进行了“自动装箱”,将类型实参适配为其包装类。
为什么T[] t
的t
不可以是基本数据类型数组?
同理,研究其底层,拿上面的测试示例。
int[] origin = {2, 0, 2, 3};
at(origin, 1);// 编译报错
从上文可知: 局部泛型的类型实参由方法调用时实参的类型决定 \color{green}{局部泛型的类型实参由方法调用时实参的类型决定} 局部泛型的类型实参由方法调用时实参的类型决定。
假设t
可以是基本数据类型数组(编译通过)。示例中origin
的类型是int[]
,则
的类型实参为int
。由于类型实参只能是类,故
的类型实参为Integer
。
也就是说,在底层会经历int[] → Integer[]
。
虽然 int 类型可以经“自动装箱”封装为 Integer 类,但int[]
不能转换成Integer[]
,此转换不合法。
综上所述,t
不可以是基本数据类型数组。
在解析 Java-API 时,我发现T[] t
的t
可以是基本数据类型数组。需要满足什么条件或有什么规律呢?
大家先看一些示例。
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);// --------√
我自己都迷糊了。。。真是一言难尽,我也不知如何表述。
总结: \color{red}{总结:} 总结:
全局泛型的类型实参可以是类、基本数据类型数组和类数组;局部泛型同样。
大家都熟悉反射,不知道大家有没有注意这个细节:
Class<String> z1 = Integer.class;// 编译报错
为了研究这个问题,我将反编译、反射、类加载、泛型等知识点都考虑其中,却依旧没有答案。不然,我发起了【提问】,向博友们请教。
在大家的回复中,我注意到一个新概念: 泛型擦除 \color{green}{泛型擦除} 泛型擦除。
之前我在解析Java-API 时,遇到过与这个概念类似的描述,只是没注意。关于此概念的理论,在上面【泛型通配符】的阐述中提过,这里就不多赘述,我直接说在此处的应用。
示例中指定了类型实参为
String
,泛型擦除机制会将String
擦除,并转为其上界类型Object
。
一个猜测: 一个猜测: 一个猜测:
Integer.class
的底层是反编译,与Class
类的底层有关,由于此类的底层比较复杂,我暂且未解析,故无法进行说明。
我的猜测:Integer.class
的底层是将 Class 类的类型实参适配为Integer
。
Class
指定类型实参为String
,虽然经泛型擦除会转为Object
,但依旧是字符串。而Integer.class
指定类型实参为Integer
,则赋值转换不合法,故编译报错。
留言: \color{purple}{留言:} 留言:虽然这段解析比较合理,但毕竟是我的猜测,没有理论支撑。有不妥之处,欢迎大家在评论区指正!!
本文中的例子是为了阐述泛型相关思想、方便大家理解而简单举例的,不一定有实用性。泛型,在java中应用非常广泛,比如:开源。掌握了泛型对阅读源码、使用第三方框架都有很大的助力。关于泛型,大家可以自行测试一下,很多地方就都迎刃而解。
本文完结。