Java的“泛型”特性,你以为自己会了?(万字长文)

}

Java的“泛型”特性,你以为自己会了?(万字长文)

问题迎刃而解,至于普通的泛型方法和静态的泛型方法是一样的使用,只不过是一个数据类一个属于类的实例的,在使用上区别不大(但是需要注意的是如果在泛型类中 静态泛型方法是不能使用类泛型中的泛型类型的,这个在下文的泛型类中会详细介绍的)。

最后再来看下构造方法

public class Father {

public Father(T t) {

}

}

然后假设他有一个子类是这样子的

class Son extends Father {

public Son(T t) {

super(t);

}

}

这里强调一下,因为在 Father 类中是没有无参构造器的,取而代之的是一个有参的构造器,只不过这个构造方法是一个泛型的方法,那这样子的子类必然需要显示的指明构造器了。

  • 通过泛型方法获取集合中的元素测试

既然说泛型是在申明的时候类型不是重点,只要事情用的时候确定就可以下,那你看下面这个怎么解释?

Java的“泛型”特性,你以为自己会了?(万字长文)

此时想往集合中添加元素,却提示这样的错误,连编译都过不了。这是为什么?

因为此时集合 List 的 add 方法,添加的类型为 T,但是很显然 T 是一个泛型,真正的类型是在使用时候才能确定的,但是 在 add 的并不能确定 T 的类型,所以根本就无法使用 add 方法,除非 list.add(null),但是这却没有任何意义

4. 泛型类

=======

先来看一段这样的代码,里面的使用到了多个泛型的方法,无需关注方法到底做了什么

public class GenericClassTest{

public static void main(String[] args) {

//首先定义一个Integer类型的数组

Integer[] arrInt = {1, 2, 3, 4, 5, 6, 7, 8, 9};

//将第 1 个和第 9 个位置的元素进行交换

new GenericClassTest().changeT(arrInt, 0, 8);

System.out.println("arrInt = " + Arrays.asList(arrInt));

List list = Arrays.asList(“a”, “b”);

testIter(list);

}

/**

  • @param t 参数类型 T

  • @param firstIndex 第一个下标

  • @param secondIndex 第二个下标

  • @param 表示定义了一个类型 为 T 的类型,否则没人知道 T 是什么,编译期也不知道

*/

private void changeT(T[] t, int firstIndex, int secondIndex) {

T tmp = t[firstIndex];

t[firstIndex] = t[secondIndex];

t[secondIndex] = tmp;

}

/**

  • 遍历集合

  • @param list 集合

  • @param 表示定义了一个类型 为 T 的类型,否则没人知道 T 是什么,编译期也不知道

*/

private static void testIter(List list) {

for (T t : list) {

System.out.println("t = " + t);

}

}

}

可以看到里面的  是不是每个方法都需要去申明一次,那要是 100 个方法呢?那是不是要申明 100 次的,这样时候泛型类也就应用而生了。那泛型类的形式是什么样子的呢?请看代码

public class GenericClazz{

//这就是一个最基本的泛型类的样子

}

下面我们将刚刚的代码优化如下,但是这里不得不说一个很基础,但是却很少有人注意到的问题,请看下面的截图中的文字描述部分。

Java的“泛型”特性,你以为自己会了?(万字长文)

为什么实例方法可以,而静态方法却报错?

  1. 首先告诉你结论:静态方法不能使用类定义的泛型,而是应该单独定义泛型

  2. 到这里估计很多小伙伴就瞬间明白了,因为静态方法是通过类直接调用的,而普通方法必须通过实例来调用,类在调用静态方法的时候,后面的泛型类还没有被创建,所以肯定不能这么去调用的

所以说这个泛型类中的静态方法直接这么写就可以啦

/**

  • 遍历集合

  • @param list 集合

*/

private static void testIter(List list) {

for (K t : list) {

System.out.println("t = " + t);

}

}

  • 多个泛型类型同时使用

我们知道 Map 是键值对形式存在,所以如果对 Map 的 Key 和 Value 都使用泛型类型该怎么办?一样的使用,一个静态方法就可以搞定了,请看下面的代码

public class GenericMap {

private static <K, V> void mapIter(Map<K, V> map) {

for (Map.Entry<K, V> kvEntry : map.entrySet()) {

K key = kvEntry.getKey();

V value = kvEntry.getValue();

System.out.println(key + “:” + value);

}

}

public static void main(String[] args) {

Map<String, String> mapStr = new HashMap<>();

mapStr.put(“a”, “aa”);

mapStr.put(“b”, “bb”);

mapStr.put(“c”, “cc”);

mapIter(mapStr);

System.out.println(“======”);

Map<Integer, String> mapInteger = new HashMap<>();

mapInteger.put(1, “11”);

mapInteger.put(2, “22”);

mapInteger.put(3, “33”);

mapIter(mapInteger);

}

}

Java的“泛型”特性,你以为自己会了?(万字长文)

到此,泛型的常规的方法和泛型类已经介绍为了。

5. 通配符

=======

通配符 ? 即占位符的意思,也就是在使用期间是无法确定其类型的,只有在将来实际使用的时候指明类型,它有三种形式

  • <?> 无限定的通配符。是让泛型能够接受**未知类型**的数据
  • < ? extends E>有上限的通配符。能接受指定类及其子类类型的数据,E就是该泛型的上边界

  • <? super E>有**下限**的通配符。能接受**指定类及其父类类型**的数据,E就是该泛型的下边界

5.1 通配符之 <?>

============

上面刚刚说到了使用一个类型来表示反省类型是必须要申明的,也即  ,那是不是不申明就不能使用泛型呢?当然不是,这小节介绍的 <?> 就是为了解决这个问题的。

<?> 表示,但是话又说话来了,那既然可以不去指明具体类型,那 ? 就不能表示一个具体的类型也就是说如果按照原来的方式这么去写,请看代码中的注释

Java的“泛型”特性,你以为自己会了?(万字长文)

而又因为任何类型都是 Object 的子类,所以,这里可以使用 Object 来接收,对于 ? 的具体使用会在下面两小节介绍

Java的“泛型”特性,你以为自己会了?(万字长文)

另外,大家要搞明白泛型和通配符不是一回事

5.2 通配符之 <? extend E>

=====================

<? extend E> 表示有上限的通配符,能接受其类型和其子类的类型 E 指上边界,还是写个例子来说明 public class GenericExtend { public static void main(String[] args) { List listF = new ArrayList<>(); List listS = new ArrayList<>(); List listD = new ArrayList<>(); testExtend(listF); testExtend(listS); testExtend(listD); } private static void testExtend(List<? extends Father> list) {} } class Father {} class Daughter extends Father{} class Son extends Father { } 这个时候一切都还是很和平的,因为大家都遵守着预定,反正 List 中的泛型要么是 Father 类,要么是 Father 的子类。但是这个时候如果这样子来写(具体原因已经在截图中写明了) ![Java的“泛型”特性,你以为自己会了?(万字长文)](https://img-blog.csdnimg.cn/img_convert/86d673ff82d32c336ca26e0aa2f913ad.png) 5.3 通配符之 <?super E> =================== <?super E> 表示有下限的通配符。也就说能接受指定类型及其父类类型,E 即泛型类型的下边界,直接上来代码然后来解释 public class GenericSuper { public static void main(String[] args) { List listS = new Stack<>(); List listF = new Stack<>(); List listG = new Stack<>(); testSuper(listS); testSuper(listF); testSuper(listG); } private static void testSuper(List<? super Son> list){} } class Son extends Father{} class Father extends GrandFather{} class GrandFather{} 因为 List<? super Son> list 接受的类型只能是 Son 或者是 Son 的父类,而 Father 和 GrandFather 又都是 Son 的父类,所以以上程序是没有任何问题的,但是如果再来一个类是 Son 的子类(如果不是和 Son 有关联的类那更不行了),那结果会怎么样?看下图,相关重点已经在图中详细说明 ![Java的“泛型”特性,你以为自己会了?(万字长文)](https://img-blog.csdnimg.cn/img_convert/59af8849e24d1ef647b6afeb03e0e9aa.png) 好了,其实泛型说到这里基本就差不多了,我们平时开发能遇到的问题和不常遇见的问题本文都基本讲解到了。最后我们再来一起看看泛型的另一个特性:**泛型擦除**。 6\. 泛型擦除 ======== 先来看下泛型擦除的定义 # 泛型擦除 因为泛型的信息只存在于 java 的编译阶段,编译期编译完带有 java 泛型的程序后,其生成的 class 文件中与泛型相关的信息会被擦除掉,以此来保证程序运行的效率并不会受影响,也就说泛型类型在 jvm 中和普通类是一样的。 别急,知道你看完概念肯定还是不明白什么叫泛型擦除,举个例子 public class GenericWipe { public static void main(String[] args) { List listStr = new ArrayList<>(); List listInt = new ArrayList<>(); List listDou = new ArrayList<>(); System.out.println(listStr.getClass()); System.out.println(listInt.getClass()); System.out.println(listDou.getClass()); } } ![Java的“泛型”特性,你以为自己会了?(万字长文)](https://img-blog.csdnimg.cn/img_convert/5458a8373ff354e910fa2cc0b3d7a038.png) ![Java的“泛型”特性,你以为自己会了?(万字长文)](https://img-blog.csdnimg.cn/img_convert/25955d8eeffd97f8f0e14b90a5478048.png) 这也就是说 java 泛型在生成字节码以后是根本不存在泛型类型的,甚至是在编译期就会被抹去,说来说去好像并没有将泛型擦除说得很透彻,下面我们就以例子的方式来一步一步证明 * 通过反射验证编译期泛型类型被擦除 class Demo1 { public static void main(String[] args) throws Exception { List list = new ArrayList<>(); //到这里是没有任何问题的,正常的一个 集合类的添加元素 list.add(1024); list.forEach(System.out::println); System.out.println("-------通过反射证明泛型类型编译期间被擦除-------"); //反射看不明白的小伙伴不要急,如果想看反射的文章,请留言反射,我下期保证完成 list.getClass().getMethod("add", Object.class).invoke(list, "9527"); for (int i = 0; i < list.size(); i++) { System.out.println("value = " + list.get(i)); } } } ![Java的“泛型”特性,你以为自己会了?(万字长文)](https://img-blog.csdnimg.cn/img_convert/9e954ea5e73f96f25e79d5c71c55ddff.png) 打印结果如下: ![Java的“泛型”特性,你以为自己会了?(万字长文)](https://img-blog.csdnimg.cn/img_convert/80cfe4507950ac38617adb57bbd73d33.png) 但是直接同一个反射似乎并不能让小伙伴们买账,我们为了体验差异,继续写一个例子 class Demo1 { public static void main(String[] args) throws Exception { //List 实际上就是一个泛型,所以我们就不去自己另外写泛型类来测试了 List list = new ArrayList<>(); //到这里是没有任何问题的,正常的一个 集合类的添加元素 list.add(1024); list.forEach(System.out::println); System.out.println("-------通过反射证明泛型类型编译期间被擦除-------"); list.getClass().getMethod("add", Object.class).invoke(list, "9527"); for (int i = 0; i < list.size(); i++) { System.out.println("value = " + list.get(i)); } //普通的类 FanShe fanShe = new FanShe(); //先通过正常的方式为属性设置值 fanShe.setStr(1111); System.out.println(fanShe.getStr()); //然后通过同样的方式为属性设置值 不要忘记上面的List 是 List 是泛型哦!不要连最基本的知识都忘记了 fanShe.getClass().getMethod("setStr", Object.class).invoke(list, "2222"); System.out.println(fanShe.getStr()); } } //随便写一个类 class FanShe{ private Integer str; public void setStr(Integer str) { this.str = str; } public Integer getStr() { return str; } } ![Java的“泛型”特性,你以为自己会了?(万字长文)](https://img-blog.csdnimg.cn/img_convert/1a156ee148030e4ca6d7a5796f3d4a29.png) 测试结果显而易见,不是泛型的类型是不能通过反射去修改类型赋值的。 * 由于泛型擦除带来的自动类型转换 因为泛型的类型擦除问题,导致所有的泛型类型变量被编译后都会被替换为原始类型。既然都被替换为原始类型,那么为什么我们在获取的时候,为什么不需要强制类型转换? ![Java的“泛型”特性,你以为自己会了?(万字长文)](https://img-blog.csdnimg.cn/img_convert/64d84077daaae8ae46a53f684004093a.png) 下面这么些才是一个标准的带有泛型返回值的方法。 public class TypeConvert { public static void main(String[] args) { //调用方法的时候返回值就是我们实际传的泛型的类型 MyClazz1 myClazz1 = testTypeConvert(MyClazz1.class); MyClazz2 myClazz2 = testTypeConvert(MyClazz2.class); } private static T testTypeConvert(Class tClass){ //只需要将返回值类型转成实际的泛型类型 T 即可 return (T) tClass; } } class MyClazz1{} class MyClazz2{} * 由泛型引发的数组问题 名字怪吓人的,实际上说白了就是不能创建泛型数组 ![Java的“泛型”特性,你以为自己会了?(万字长文)](https://img-blog.csdnimg.cn/img_convert/e2836c6faea7740dfd5cdbd5d3210276.png) 看下面的代码 ![Java的“泛型”特性,你以为自己会了?(万字长文)](https://img-blog.csdnimg.cn/img_convert/daeb158cbbcddddc34dbccf936ee5090.png) 为什么不能创建泛型类型的数组? 因为List 和 List 被编译后在 JVM 中等同于List ,所有的类型信息在编译后都等同于List,也就是说编译器此时也是无法区分数组中的具体类型是 Integer类型还是 String 。 ## 最后 **自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。** **深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。** **因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。** ![](https://img-blog.csdnimg.cn/img_convert/be4a1e9bd6c3fb973087f37a1cea42b0.png) ![](https://img-blog.csdnimg.cn/img_convert/992a5b7dee623253a40dfc5e083065ac.png) ![](https://img-blog.csdnimg.cn/img_convert/90ec4e202378f8d6b67d29d2b0bd7718.png) **既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!** [**如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!**](https://bbs.csdn.net/topics/618164986) **由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!** 泛型类型的数组? 因为List 和 List 被编译后在 JVM 中等同于List ,所有的类型信息在编译后都等同于List,也就是说编译器此时也是无法区分数组中的具体类型是 Integer类型还是 String 。 ## 最后 **自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。** **深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。** **因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。** [外链图片转存中...(img-leHopwIo-1715443167887)] [外链图片转存中...(img-q8FQ7nPO-1715443167888)] [外链图片转存中...(img-vYuNYpO9-1715443167888)] **既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!** [**如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!**](https://bbs.csdn.net/topics/618164986) **由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**
  • 20
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值