教妹学 Java:晦涩难懂的泛型
本篇通过一种趣味化的形式来讲述 Java 的泛型。
00、故事的起源
“二哥,要不我上大学的时候也学习编程吧?”有一天,三妹突发奇想地问我。
“你确定要做一名程序媛吗?”
“我觉得女生做程序员,有着天大的优势,尤其是我这种长相甜美的。”三妹开始认真了起来。
“好像是啊,遇到女生提问,我好像一直蛮热情的。”
“二哥,你不是爱好写作嘛,还是一个 Java 程序员,不妨写个专栏,名字就叫《教妹学 Java》。我高考完就开始跟着你学习编程,还能省下一笔培训费。”三妹看起来已经替我筹划好了呀。
“真的很服气你们零零后,蛮有想法的。刚好我最近在写 Java 系列的专栏,不妨试一试!”
PS:亲爱的读者朋友们,我们今天就从晦涩难懂的“泛型”开始吧!(子标题是三妹提出来的,内容由二哥我来回答)
01、二哥,为什么要设计泛型啊?
三妹啊,听哥慢慢给你讲啊。
Java 在 5.0 时增加了泛型机制,据说专家们为此花费了 5 年左右的时间(听起来很不容易)。有了泛型之后,尤其是对集合类的使用,就变得更规范了。
看下面这段简单的代码。
ArrayList<String> list = new ArrayList<String>();
list.add("沉默王二");
String str = list.get(0);
但在没有泛型之前该怎么办呢?
首先,我们需要使用 Object 数组来设计 Arraylist
类。
class Arraylist {private Object[] objs;private int i = 0;public void add(Object obj) {objs[i++] = obj;}public Object get(int i) {return objs[i];}
}
然后,我们向 Arraylist
中存取数据。
Arraylist list = new Arraylist();
list.add("沉默王二");
list.add(new Date());
String str = (String)list.get(0);
你有没有发现两个问题:
- Arraylist 可以存放任何类型的数据(既可以存字符串,也可以混入日期),因为所有类都继承自 Object 类。
- 从 Arraylist 取出数据的时候需要强制类型转换,因为编译器并不能确定你取的是字符串还是日期。
对比一下,你就能明显地感受到泛型的优秀之处:使用类型参数解决了元素的不确定性——参数类型为 String 的集合中是不允许存放其他类型元素的,取出数据的时候也不需要强制类型转换了。
02、二哥,怎么设计泛型啊?
三妹啊,你一个小白只要会用泛型就行了,还想设计泛型啊?!不过,既然你想了解,那么哥义不容辞。
首先,我们来按照泛型的标准重新设计一下 Arraylist
类。
class Arraylist<E> {private Object[] elementData;private int size = 0;public Arraylist(int initialCapacity) {this.elementData = new Object[initialCapacity];}public boolean add(E e) {elementData[size++] = e;return true;}E elementData(int index) {return (E) elementData[index];}
}
一个泛型类就是具有一个或多个类型变量的类。Arraylist 类引入的类型变量为 E(Element,元素的首字母),使用尖括号 <>
括起来,放在类名的后面。
然后,我们可以用具体的类型(比如字符串)替换类型变量来实例化泛型类。
Arraylist<String> list = new Arraylist<String>();
list.add("沉默王三");
String str = list.get(0);
Date 类型也可以的。
Arraylist<Date> list = new Arraylist<Date>();
list.add(new Date());
Date date = list.get(0);
其次,我们还可以在一个非泛型的类(或者泛型类)中定义泛型方法。
class Arraylist<E> {public <T> T[] toArray(T[] a) {return (T[]) Arrays.copyOf(elementData, size, a.getClass());}
}
不过,说实话,泛型方法的定义看起来略显晦涩。来一副图吧(注意:方法返回类型和方法参数类型至少需要一个)。
现在,我们来调用一下泛型方法。
Arraylist<String> list = new Arraylist<>(4);
list.add("沉");
list.add("默");
list.add("王");
list.add("二");String [] strs = new String [4];
strs = list.toArray(strs);for (String str : strs) {System.out.println(str);
}
最后,我们再来说说泛型变量的限定符 extends
。在解释这个限定符之前,我们假设有三个类,它们之间的定义是这样的。
class Wanglaoer {public String toString() {return "王老二";}
}class Wanger extends Wanglaoer{public String toString() {return "王二";}
}class Wangxiaoer extends Wanger{public String toString() {return "王小二";}
}
我们使用限定符 extends
来重新设计一下 Arraylist
类。
class Arraylist<E extends Wanger> {}
当我们向 Arraylist
中添加 Wanglaoer
元素的时候,编译器会提示错误:Arraylist
只允许添加 Wanger
及其子类 Wangxiaoer
对象,不允许添加其父类 Wanglaoer
。
Arraylist<Wanger> list = new Arraylist<>(3);
list.add(new Wanger());
list.add(new Wanglaoer());
// The method add(Wanger) in the type Arraylist<Wanger> is not applicable for the arguments
// (Wanglaoer)
list.add(new Wangxiaoer());
也就是说,限定符 extends
可以缩小泛型的类型范围。
03、二哥,听说虚拟机没有泛型?
三妹,你功课做得可以啊,连虚拟机都知道了啊。哥可以肯定地回答你,虚拟机是没有泛型的。
啰嗦一句哈。我们编写的 Java 代码(也就是源码,后缀为 .java 的文件)是不能够被操作系统直接识别的,需要先编译,生成 .class 文件(也就是字节码文件)。然后 Java 虚拟机(JVM)会充当一个翻译官的角色,把字节码翻译给操作系统能听得懂的语言,告诉它该干嘛。
怎么确定虚拟机没有泛型呢?我们需要把泛型类的字节码进行反编译——强烈推荐超神反编译工具 Jad !
现在,在命令行中敲以下代码吧(反编译 Arraylist
的字节码文件 Arraylist.class
)。
jad Arraylist.class
命令执行完后,会生成一个 Arraylist.jad 的文件,用文本编辑工具打开后的结果如下。
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Arraylist.javapackage com.cmower.java_demo.fanxing;import java.util.Arrays;class Arraylist
{public Arraylist(int initialCapacity){size = 0;elementData = new Object[initialCapacity];}public boolean add(Object e){elementData[size++] = e;return true;}Object elementData(int index){return elementData[index];}private Object elementData[];private int size;
}
类型变量 <E>
消失了,取而代之的是 Object !
既然如此,那如果泛型类使用了限定符 extends
,结果会怎么样呢?我们先来看看 Arraylist2
的源码。
class Arraylist2<E extends Wanger> {private Object[] elementData;private int size = 0;public Arraylist2(int initialCapacity) {this.elementData = new Object[initialCapacity];}public boolean add(E e) {elementData[size++] = e;return true;}E elementData(int index) {return (E) elementData[index];}
}
字节码文件 Arraylist2.class
使用 Jad 反编译后的结果如下。
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Arraylist2.javapackage com.cmower.java_demo.fanxing;// Referenced classes of package com.cmower.java_demo.fanxing:
// Wangerclass Arraylist2
{public Arraylist2(int initialCapacity){size = 0;elementData = new Object[initialCapacity];}public boolean add(Wanger e){elementData[size++] = e;return true;}Wanger elementData(int index){return (Wanger)elementData[index];}private Object elementData[];private int size;
}
类型变量 <E extends Wanger>
不见了,E 被替换成了 Wanger
。
通过以上两个例子说明,Java 虚拟机会将泛型的类型变量擦除,并替换为限定类型(没有限定的话,就用 Object
)。
04、二哥,类型擦除会有什么问题吗?
三妹啊,你还别说,类型擦除真的会有一些“问题”。
我们来看一下这段代码。
public class Cmower {public static void method(Arraylist<String> list) {System.out.println("Arraylist<String> list");}public static void method(Arraylist<Date> list) {System.out.println("Arraylist<Date> list");}}
在浅层的意识上,我们会想当然地认为 Arraylist<String> list
和 Arraylist<Date> list
是两种不同的类型,因为 String 和 Date 是不同的类。
但由于类型擦除的原因,以上代码是不会通过编译的——编译器会提示一个错误(这正是类型擦除引发的那些“问题”):
Erasure of method method(Arraylist) is the same as another method in type
CmowerErasure of method method(Arraylist) is the same as another method in type
Cmower
大致的意思就是,这两个方法的参数类型在擦除后是相同的。
也就是说,method(Arraylist<String> list)
和 method(Arraylist<Date> list)
是同一种参数类型的方法,不能同时存在。类型变量 String
和 Date
在擦除后会自动消失,method 方法的实际参数是 Arraylist list
。
有句俗话叫做:“百闻不如一见”,但即使见到了也未必为真——泛型的擦除问题就可以很好地佐证这个观点。
05、二哥,听说泛型还有通配符?
三妹啊,哥突然觉得你很适合作一枚可爱的程序媛啊!你这预习的功课做得可真到家啊,连通配符都知道!
通配符使用英文的问号(?)来表示。在我们创建一个泛型对象时,可以使用关键字 extends
限定子类,也可以使用关键字 super
限定父类。
为了更好地解释通配符,我们需要对 Arraylist
进行一些改进。
class Arraylist<E> {private Object[] elementData;private int size = 0;public Arraylist(int initialCapacity) {this.elementData = new Object[initialCapacity];}public boolean add(E e) {elementData[size++] = e;return true;}public E get(int index) {return (E) elementData[index];}public int indexOf(Object o) {if (o == null) {for (int i = 0; i < size; i++)if (elementData[i]==null)return i;} else {for (int i = 0; i < size; i++)if (o.equals(elementData[i]))return i;}return -1;}public boolean contains(Object o) {return indexOf(o) >= 0;}public String toString() {StringBuilder sb = new StringBuilder();for (Object o : elementData) {if (o != null) {E e = (E)o;sb.append(e.toString());sb.append(',').append(' ');}}return sb.toString();}public int size() {return size;}public E set(int index, E element) {E oldValue = (E) elementData[index];elementData[index] = element;return oldValue;}
}
1)新增 indexOf(Object o)
方法,判断元素在 Arraylist
中的位置。注意参数为 Object
而不是泛型 E
。
2)新增 contains(Object o)
方法,判断元素是否在 Arraylist
中。注意参数为 Object
而不是泛型 E
。
3)新增 toString()
方法,方便对 Arraylist
进行打印。
4)新增 set(int index, E element)
方法,方便对 Arraylist
元素的更改。
你知道,Arraylist<Wanger> list = new Arraylist<Wangxiaoer>();
这样的语句是无法通过编译的,尽管 Wangxiaoer 是 Wanger 的子类。但如果我们确实需要这种 “向上转型” 的关系,该怎么办呢?这时候就需要通配符来发挥作用了。
利用 <? extends Wanger>
形式的通配符,可以实现泛型的向上转型,来看例子。
Arraylist<? extends Wanger> list2 = new Arraylist<>(4);
list2.add(null);
// list2.add(new Wanger());
// list2.add(new Wangxiaoer());Wanger w2 = list2.get(0);
// Wangxiaoer w3 = list2.get(1);
list2 的类型是 Arraylist<? extends Wanger>
,翻译一下就是,list2 是一个 Arraylist
,其类型是 Wanger
及其子类。
注意,“关键”来了!list2 并不允许通过 add(E e)
方法向其添加 Wanger
或者 Wangxiaoer
的对象,唯一例外的是 null
。为什么不能存呢?原因还有待探究(苦涩)。
那就奇了怪了,既然不让存放元素,那要 Arraylist<? extends Wanger>
这样的 list2 有什么用呢?
虽然不能通过 add(E e)
方法往 list2 中添加元素,但可以给它赋值。
Arraylist<Wanger> list = new Arraylist<>(4);Wanger wanger = new Wanger();
list.add(wanger);Wangxiaoer wangxiaoer = new Wangxiaoer();
list.add(wangxiaoer);Arraylist<? extends Wanger> list2 = list;Wanger w2 = list2.get(1);
System.out.println(w2);System.out.println(list2.indexOf(wanger));
System.out.println(list2.contains(new Wangxiaoer()));
Arraylist<? extends Wanger> list2 = list;
语句把 list 的值赋予了 list2,此时 list2 == list
。由于 list2 不允许往其添加其他元素,所以此时它是安全的——我们可以从容地对 list2 进行 get()
、indexOf()
和 contains()
。想一想,如果可以向 list2 添加元素的话,这 3 个方法反而变得不太安全,它们的值可能就会变。
利用 <? super Wanger>
形式的通配符,可以向 Arraylist 中存入父类是 Wanger
的元素,来看例子。
Arraylist<? super Wanger> list3 = new Arraylist<>(4);
list3.add(new Wanger());
list3.add(new Wangxiaoer());// Wanger w3 = list3.get(0);
需要注意的是,无法从 Arraylist<? super Wanger>
这样类型的 list3 中取出数据。为什么不能取呢?原因还有待探究(再次苦涩)。
虽然原因有待探究,但结论是明确的:<? extends T>
可以取数据,<? super T>
可以存数据。那么利用这一点,我们就可以实现数组的拷贝——<? extends T>
作为源(保证源不会发生变化),<? super T>
作为目标(可以保存值)。
public class Collections {public static <T> void copy(Arraylist<? super T> dest, Arraylist<? extends T> src) {for (int i = 0; i < src.size(); i++)dest.set(i, src.get(i));}
}
06、故事的未完待续
“二哥,你今天苦涩了啊!嘿嘿。竟然还有你需要探究的。”三妹开始调皮了起来。
“…”
“不要不好意思嘛,等三妹啥时候探究出来了原因,三妹给你讲,好不好?”三妹越说越来劲了。
“…”
“二哥,你还在想泛型通配符的原因啊!那三妹先去预习下个知识点了啊,你思考完了,再给我讲!”三妹看着我陷入了沉思,扔下这句话走了。
“…”
上一篇:Java:优雅地处理异常真是一门学问啊!
下一篇:教妹学Java:大有可为的集合
谢谢大家的阅读,原创不易,喜欢就随手点个赞
教妹学 Java:晦涩难懂的泛型相关推荐
- 教妹学 Java:大有可为的集合
本篇通过一种趣味化的形式来讲述 Java 的集合. 00.故事的起源 "二哥,上一篇<泛型>的反响效果怎么样啊?"三妹对她提议的<教妹学 Java>专栏很是 ...
- 教妹学 Java:动态伴侣 Groovy
点击上方"程序员小明",选择"星标" 今晚可以不加班! 00.故事的起源 "二哥,听说上一篇<多线程>被 CSDN 创始人蒋涛点赞了?&q ...
- 教妹学 Java:难以驾驭的多线程
本篇通过一种趣味化的形式来讲述 Java 的多线程. 00.故事的起源 "二哥,上一篇<集合>的反响效果怎么样啊?"三妹对她提议的<教妹学 Java>专栏很 ...
- 教妹学 Java:集合
?点 击 「沉默王二」 关 注 我 ? 00.故事的起源 "二哥,上一篇<泛型>的反响效果怎么样啊?"三妹对她提议的<教妹学 Java>专栏很是关心. &q ...
- 教妹学Java(十四):switch 语句详解
大家好,我是沉默王二,一个和黄家驹一样身高,和刘德华一样颜值的程序员.本篇文章通过我和三妹对话的形式来谈一谈"switch 语句". 教妹学 Java,没见过这么有趣的标题吧?&q ...
- 教你学java_教妹学Java:Spring 入门篇
你好呀,我是沉默王二,一个和黄家驹一样身高,刘德华一样颜值的程序员(管你信不信呢).从两位偶像的年纪上,你就可以断定我的码龄至少在 10 年以上,但实话实说,我一直坚信自己只有 18 岁,因为我有一颗 ...
- 教妹学Java:接口,抽象的另一种表现方式
接口 "哥,我看你朋友圈说<教妹学 Java>专栏收到了第一笔赞赏呀,虽然只有一块钱,但我也替你感到开心."三妹的脸上洋溢着自信的微笑,仿佛这钱是打给她的一样. &qu ...
- 教妹学Java(九):一文搞懂Java中的基本数据类型
大家好,我是沉默王二,一个和黄家驹一样身高,和刘德华一样颜值的程序员.本篇文章通过我和三妹对话的形式来谈一谈"Java 中的数据类型". 教妹学 Java,没见过这么有趣的标题吧? ...
- 教妹学Java(二十六):static 关键字解析
你好呀,我是沉默王二,(目前是)CSDN 周排名前十的博客专家.这是<教妹学 Java>专栏的第二十六篇,今天我们来谈谈 Java 的 static 关键字--什么是静态变量?什么是静态方 ...
- 教妹学Java(二十一):一文带你了解面向对象编程的所有概念
你好呀,我是沉默王二,是<Web 全栈开发进阶之路>的作者,CSDN 的博客之星.<教妹学 Java>是一套非常有趣的付费专栏,除了继续保持幽默风趣的行风风格,我还力求把每一个 ...
最新文章
- JavaScript数据类型检测总结
- 计算机四级嵌入式真题,2014年3月计算机四级嵌入式工程师真题试题及答案
- Redis系列四:redis支持的数据类型
- 2021-03-07 Nussbaum函数
- Android开发中java.lang.RuntimeException: Unable to start activity ComponentInfo{xxx}: java.lang.NullPoi
- 使用python调用matlab方法
- java执行sql列名无效_嵌套异常是java.sql.SQLException:无效的列名ORACLE
- 这几张图告诉你化学到底有多神奇!看完瞬间觉得智商都提高了!
- 图论复习——dfs树,点双,边双,强连通分量
- atxserver运行没有反应_连续生物工艺:灌流生物反应器
- centos如何安装数据库mysql
- pytorch Tensor.backward
- 讯时网关IP对接PBX
- 【招聘】阿里2022届春招实习生 - 机器学习/NLP/CV等
- Python——词频统计
- 即将毕业大学生的第一个五年计划
- python模拟内置函数reversed()_Python 3 内置函数 reversed( )
- android 怎么看架构,怎么查看手机设备架构
- Java Web课程体系
- ppt矩形里面的图片怎么放大缩小_如何使用PPT调节图片的大小
热门文章
- Spring In Action 4 学习笔记(一)Spring概览
- 【Git】Conventional Commit 约定式提交规范
- JSON.prase()和 eval()区别
- 我的世界服务器无限矿区块指令,我的世界:不用动手也能挖到钻石?来自指令玩家的方法...
- [android] 运行编译报错:Entry name ‘assets/Icoxx.png‘ collided
- 今日恐慌与贪婪指数为22 恐慌程度有所缓解
- 苹果App卡审怎么办?
- 【数学】一元函数积分学(宇哥笔记)
- Ros同一局域网下多机通信
- 【Ubuntu+ROS安装方法】