提交 ede651f0 编写于 作者: 沉默王二's avatar 沉默王二 💬

泛型

上级 77e96f07
......@@ -129,12 +129,16 @@
### **泛型**
- [撸个注解有什么难的](docs/fanxing/annotation.md)
- [晦涩难懂的泛型](docs/generic/generic.md)
### **注解**
- [撸个注解有什么难的](docs/annotation/annotation.md)
### **枚举**
- [单例的最佳实现方式——枚举](docs/enum/enum.md)
......
## 为什么重写 equals 时必须重写 hashCode 方法
“二哥,我在读《Effective Java》 的时候,第 11 条规约说重写 equals 的时候必须要重写 hashCode 方法,这是为什么呀?”三妹单刀直入地问。
“三妹啊,这个问题问得非常好,因为它也是面试中经常考的一个知识点。今天哥就带你来梳理一下。”我说。
Java 是一门面向对象的编程语言,所有的类都会默认继承自 Object 类,而 Object 的中文意思就是“对象”。
Object 类中有这么两个方法:
```java
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
```
1)hashCode 方法
这是一个本地方法,用来返回对象的哈希值(一个整数)。在 Java 程序执行期间,对同一个对象多次调用该方法必须返回相同的哈希值。
2)equals 方法
对于任何非空引用 x 和 y,当且仅当 x 和 y 引用的是同一个对象时,equals 方法才返回 true。
“二哥,看起来两个方法之间没有任何关联啊?”三妹质疑道。
“单从这两段解释上来看,的确是这样的。”我解释道,“但两个方法的 doc 文档中还有这样两条信息。”
第一,如果两个对象调用 equals 方法返回的结果为 true,那么两个对象调用 hashCode 方法返回的结果也必然相同——来自 hashCode 方法的 doc 文档。
第二,每当重写 equals 方法时,hashCode 方法也需要重写,以便维护上一条规约。
“哦,这样讲的话,两个方法确实关联上了,但究竟是为什么呢?”三妹抛出了终极一问。
“hashCode 方法的作用是用来获取哈希值,而该哈希值的作用是用来确定对象在哈希表中的索引位置。”我说。
哈希表的典型代表就是 HashMap,它存储的是键值对,能根据键快速地检索出对应的值。
```java
public V get(Object key) {
HashMap.Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
```
这是 HashMap 的 get 方法,通过键来获取值的方法。它会调用 getNode 方法:
```java
final HashMap.Node<K,V> getNode(int hash, Object key) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof HashMap.TreeNode)
return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
```
通常情况(没有发生哈希冲突)下,`first = tab[(n - 1) & hash]` 就是键对应的值。**按照时间复杂度来说的话,可表示为 O(1)**
如果发生哈希冲突,也就是 `if ((e = first.next) != null) {}` 子句中,可以看到如果节点不是红黑树的时候,会通过 do-while 循环语句判断键是否 equals 返回对应值的。**按照时间复杂度来说的话,可表示为 O(n)**
HashMap 是通过拉链法来解决哈希冲突的,也就是如果发生哈希冲突,同一个键的坑位会放好多个值,超过 8 个值后改为红黑树,为了提高查询的效率。
显然,从时间复杂度上来看的话 O(n) 比 O(1) 的性能要差,这也正是哈希表的价值所在。
“O(n) 和 O(1) 是什么呀?”三妹有些不解。
“这是时间复杂度的一种表示方法,随后二哥专门给你讲一下。简单说一下 n 和 1 的意思,很显然,n 和 1 都代表的是代码执行的次数,假如数据规模为 n,n 就代表需要执行 n 次,1 就代表只需要执行一次。”我解释道。
“三妹,你想一下,如果没有哈希表,但又需要这样一个数据结构,它里面存放的数据是不允许重复的,该怎么办呢?”我问。
“要不使用 equals 方法进行逐个比较?”三妹有些不太确定。
“这种方法当然是可行的,就像 `if ((e = first.next) != null) {}` 子句中那样,但如果数据量特别特别大,性能就会很差,最好的解决方案还是 HashMap。”
HashMap 本质上是通过数组实现的,当我们要从 HashMap 中获取某个值时,实际上是要获取数组中某个位置的元素,而位置是通过键来确定的。
```java
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
```
这是 HashMap 的 put 方法,会将键值对放入到数组当中。它会调用 putVal 方法:
```java
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 拉链
}
return null;
}
```
通常情况下,`p = tab[i = (n - 1) & hash])` 就是键对应的值。而数组的索引 `(n - 1) & hash` 正是基于 hashCode 方法计算得到的。
```java
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
```
“那二哥,你好像还是没有说为什么重写 equals 方法的时候要重写 hashCode 方法呀?”三妹忍不住了。
“来看下面这段代码。”我说。
```java
public class Test {
public static void main(String[] args) {
Student s1 = new Student(18, "张三");
Map<Student, Integer> scores = new HashMap<>();
scores.put(s1, 98);
Student s2 = new Student(18, "张三");
System.out.println(scores.get(s2));
}
}
class Student {
private int age;
private String name;
public Student(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public boolean equals(Object o) {
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
}
```
我们重写了 Student 类的 equals 方法,如果两个学生的年纪和姓名相同,我们就认为是同一个学生,虽然很离谱,但我们就是这么草率。
在 main 方法中,18 岁的张三考试得了 98 分,很不错的成绩,我们把张三和他的成绩放到 HashMap 中,然后准备取出:
```
null
```
“二哥,怎么输出了 null,而不是预期当中的 98 呢?”三妹感到很不可思议。
“原因就在于重写 equals 方法的时候没有重写 hashCode 方法。”我回答道,“equals 方法虽然认定名字和年纪相同就是同一个学生,但它们本质上是两个对象,hashCode 并不相同。”
![](https://cdn.jsdelivr.net/gh/itwanger/jmx-java/images/core-points/equals-hashcode-01.png)
“那怎么重写 hashCode 方法呢?”三妹问。
“可以直接调用 Objects 类的 hash 方法。”我回答。
```java
@Override
public int hashCode() {
return Objects.hash(age, name);
}
```
Objects 类的 hash 方法可以针对不同数量的参数生成新的哈希值,hash 方法调用的是 Arrays 类的 hashCode 方法,该方法源码如下:
```java
public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
```
第一次循环:
```
result = 31*1 + Integer(18).hashCode();
```
第二次循环:
```
result = (31*1 + Integer(18).hashCode()) * 31 + String("张三").hashCode();
```
针对姓名年纪不同的对象,这样计算后的哈希值很难很难很难重复的;针对姓名年纪相同的对象,哈希值保持一致。
再次执行 main 方法,结果如下所示:
```
98
```
因为此时 s1 和 s2 对象的哈希值都为 776408。
![](https://cdn.jsdelivr.net/gh/itwanger/jmx-java/images/core-points/equals-hashcode-02.png)
“每当重写 equals 方法时,hashCode 方法也需要重写,原因就是为了保证:如果两个对象调用 equals 方法返回的结果为 true,那么两个对象调用 hashCode 方法返回的结果也必然相同。”我点题了。
“OK,get 了。”三妹开心地点了点头,看得出来,今天学到了不少。
------
PS:点击「阅读原文」可直达《教妹学Java》专栏的在线阅读地址,可以收藏夹伺候一波了!
大家把赞和在看安排一波,好吗?让二哥的动力连载的动力再大一点。
## 枚举
“今天我们来学习枚举吧,三妹!”我说,“同学让你去她家玩了两天,感觉怎么样呀?”
“心情放松了不少。”三妹说,“可以开始学 Java 了,二哥。”
“OK。”
“枚举(enum),是 Java 1.5 时引入的关键字,它表示一种特殊类型的类,继承自 java.lang.Enum。”
“我们来新建一个枚举 PlayerType。”
```java
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}
```
“二哥,我没看到有继承关系呀!”
“别着急,看一下反编译后的字节码,你就明白了。”
```java
public final class PlayerType extends Enum
{
public static PlayerType[] values()
{
return (PlayerType[])$VALUES.clone();
}
public static PlayerType valueOf(String name)
{
return (PlayerType)Enum.valueOf(com/cmower/baeldung/enum1/PlayerType, name);
}
private PlayerType(String s, int i)
{
super(s, i);
}
public static final PlayerType TENNIS;
public static final PlayerType FOOTBALL;
public static final PlayerType BASKETBALL;
private static final PlayerType $VALUES[];
static
{
TENNIS = new PlayerType("TENNIS", 0);
FOOTBALL = new PlayerType("FOOTBALL", 1);
BASKETBALL = new PlayerType("BASKETBALL", 2);
$VALUES = (new PlayerType[] {
TENNIS, FOOTBALL, BASKETBALL
});
}
}
```
“看到没?Java 编译器帮我们做了很多隐式的工作,不然手写一个枚举就没那么省心省事了。”
- 要继承 Enum 类;
- 要写构造方法;
- 要声明静态变量和数组;
- 要用 static 块来初始化静态变量和数组;
- 要提供静态方法,比如说 `values()``valueOf(String name)`
“确实,作为开发者,我们的代码量减少了,枚举看起来简洁明了。”三妹说。
“既然枚举是一种特殊的类,那它其实是可以定义在一个类的内部的,这样它的作用域就可以限定于这个外部类中使用。”我说。
```java
public class Player {
private PlayerType type;
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}
public boolean isBasketballPlayer() {
return getType() == PlayerType.BASKETBALL;
}
public PlayerType getType() {
return type;
}
public void setType(PlayerType type) {
this.type = type;
}
}
```
PlayerType 就相当于 Player 的内部类。
由于枚举是 final 的,所以可以确保在 Java 虚拟机中仅有一个常量对象,基于这个原因,我们可以使用“==”运算符来比较两个枚举是否相等,参照 `isBasketballPlayer()` 方法。
“那为什么不使用 `equals()` 方法判断呢?”三妹问。
```java
if(player.getType().equals(Player.PlayerType.BASKETBALL)){};
```
“我来给你解释下。”
“==”运算符比较的时候,如果两个对象都为 null,并不会发生 `NullPointerException`,而 `equals()` 方法则会。
另外, “==”运算符会在编译时进行检查,如果两侧的类型不匹配,会提示错误,而 `equals()` 方法则不会。
![](https://cdn.jsdelivr.net/gh/itwanger/jmx-java/images/enum/enum-01.png)
“枚举还可用于 switch 语句,和基本数据类型的用法一致。”我说。
```java
switch (playerType) {
case TENNIS:
return "网球运动员费德勒";
case FOOTBALL:
return "足球运动员C罗";
case BASKETBALL:
return "篮球运动员詹姆斯";
case UNKNOWN:
throw new IllegalArgumentException("未知");
default:
throw new IllegalArgumentException(
"运动员类型: " + playerType);
}
```
“如果枚举中需要包含更多信息的话,可以为其添加一些字段,比如下面示例中的 name,此时需要为枚举添加一个带参的构造方法,这样就可以在定义枚举时添加对应的名称了。”我继续说。
```java
public enum PlayerType {
TENNIS("网球"),
FOOTBALL("足球"),
BASKETBALL("篮球");
private String name;
PlayerType(String name) {
this.name = name;
}
}
```
“get 了吧,三妹?”
“嗯,比较好理解。”
“那接下来,我就来说点不一样的。”
“来吧,我准备好了。”
“EnumSet 是一个专门针对枚举类型的 Set 接口(后面会讲)的实现类,它是处理枚举类型数据的一把利器,非常高效。”我说,“从名字上就可以看得出,EnumSet 不仅和 Set 有关系,和枚举也有关系。”
“因为 EnumSet 是一个抽象类,所以创建 EnumSet 时不能使用 new 关键字。不过,EnumSet 提供了很多有用的静态工厂方法。”
![](https://cdn.jsdelivr.net/gh/itwanger/jmx-java/images/enum/enum-02.png)
“来看下面这个例子,我们使用 `noneOf()` 静态工厂方法创建了一个空的 PlayerType 类型的 EnumSet;使用 `allOf()` 静态工厂方法创建了一个包含所有 PlayerType 类型的 EnumSet。”
```java
public class EnumSetTest {
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}
public static void main(String[] args) {
EnumSet<PlayerType> enumSetNone = EnumSet.noneOf(PlayerType.class);
System.out.println(enumSetNone);
EnumSet<PlayerType> enumSetAll = EnumSet.allOf(PlayerType.class);
System.out.println(enumSetAll);
}
}
```
“来看一下输出结果。”
```java
[]
[TENNIS, FOOTBALL, BASKETBALL]
```
有了 EnumSet 后,就可以使用 Set 的一些方法了,见下图。
![](https://cdn.jsdelivr.net/gh/itwanger/jmx-java/images/enum/enum-03.png)
“除了 EnumSet,还有 EnumMap,是一个专门针对枚举类型的 Map 接口的实现类,它可以将枚举常量作为键来使用。EnumMap 的效率比 HashMap 还要高,可以直接通过数组下标(枚举的 ordinal 值)访问到元素。”
“和 EnumSet 不同,EnumMap 不是一个抽象类,所以创建 EnumMap 时可以使用 new 关键字。”
```java
EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);
```
有了 EnumMap 对象后就可以使用 Map 的一些方法了,见下图。
![](https://cdn.jsdelivr.net/gh/itwanger/jmx-java/images/enum/enum-04.png)
和 HashMap(后面会讲)的使用方法大致相同,来看下面的例子。
```java
EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);
enumMap.put(PlayerType.BASKETBALL,"篮球运动员");
enumMap.put(PlayerType.FOOTBALL,"足球运动员");
enumMap.put(PlayerType.TENNIS,"网球运动员");
System.out.println(enumMap);
System.out.println(enumMap.get(PlayerType.BASKETBALL));
System.out.println(enumMap.containsKey(PlayerType.BASKETBALL));
System.out.println(enumMap.remove(PlayerType.BASKETBALL));
```
“来看一下输出结果。”
```
{TENNIS=网球运动员, FOOTBALL=足球运动员, BASKETBALL=篮球运动员}
篮球运动员
true
篮球运动员
```
“除了以上这些,《Effective Java》这本书里还提到了一点,如果要实现单例的话,最好使用枚举的方式。”我说。
“等等二哥,单例是什么?”三妹没等我往下说,就连忙问道。
“单例(Singleton)用来保证一个类仅有一个对象,并提供一个访问它的全局访问点,在一个进程中。因为这个类只有一个对象,所以就不能再使用 `new` 关键字来创建新的对象了。”
“Java 标准库有一些类就是单例,比如说 Runtime 这个类。”
```java
Runtime runtime = Runtime.getRuntime();
```
“Runtime 类可以用来获取 Java 程序运行时的环境。”
“关于单例,懂了些吧?”我问三妹。
“噢噢噢噢。”三妹点了点头。
“通常情况下,实现单例并非易事,来看下面这种写法。”
```java
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
```
“要用到 volatile、synchronized 关键字等等,但枚举的出现,让代码量减少到极致。”
```java
public enum EasySingleton{
INSTANCE;
}
```
“就这?”三妹睁大了眼睛。
“对啊,枚举默认实现了 Serializable 接口,因此 Java 虚拟机可以保证该类为单例,这与传统的实现方式不大相同。传统方式中,我们必须确保单例在反序列化期间不能创建任何新实例。”我说。
“好了,关于枚举就讲这么多吧,三妹,你把这些代码都手敲一遍吧!”
“好勒,这就安排。二哥,你去休息吧。”
“嗯嗯。”讲了这么多,必须跑去抽烟机那里安排一根华子了。
\ No newline at end of file
## 异常最佳实践
“三妹啊,今天我来给你传授几个异常处理的最佳实践经验,以免你以后在开发中采坑。”我面带着微笑对三妹说。
“好啊,二哥,我洗耳恭听。”三妹也微微一笑,欣然接受。
“好,那哥就不废话了。开整。”
--------
**1)尽量不要捕获 RuntimeException**
阿里出品的嵩山版 Java 开发手册上这样规定:
>尽量不要 catch RuntimeException,比如 NullPointerException、IndexOutOfBoundsException 等等,应该用预检查的方式来规避。
正例:
```java
if (obj != null) {
//...
}
```
反例:
```java
try {
obj.method();
} catch (NullPointerException e) {
//...
}
```
“哦,那如果有些异常预检查不出来呢?”三妹问。
“的确会存在这样的情况,比如说 NumberFormatException,虽然也属于 RuntimeException,但没办法预检查,所以还是应该用 catch 捕获处理。”我说。
**2)尽量使用 try-with-resource 来关闭资源**
当需要关闭资源时,尽量不要使用 try-catch-finally,禁止在 try 块中直接关闭资源。
反例:
```java
public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
inputStream.close();
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
```
“为什么呢?”三妹问。
“原因也很简单,因为一旦 `close()` 之前发生了异常,那么资源就无法关闭。直接使用 [try-with-resource](https://mp.weixin.qq.com/s/7yhHOG0SVCfoHdhtZHfeVg) 来处理是最佳方式。”我说。
```java
public void automaticallyCloseResource() {
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
```
“除非资源没有实现 AutoCloseable 接口。”我补充道。
“那这种情况下怎么办呢?”三妹问。
“就在 finally 块关闭流。”我说。
```java
public void closeResourceInFinally() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
} catch (FileNotFoundException e) {
log.error(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error(e);
}
}
}
}
```
**3)不要捕获 Throwable**
Throwable 是 exception 和 error 的父类,如果在 catch 子句中捕获了 Throwable,很可能把超出程序处理能力之外的错误也捕获了。
```java
public void doNotCatchThrowable() {
try {
} catch (Throwable t) {
// 不要这样做
}
}
```
“到底为什么啊?”三妹问。
“因为有些 error 是不需要程序来处理,程序可能也处理不了,比如说 OutOfMemoryError 或者 StackOverflowError,前者是因为 Java 虚拟机无法申请到足够的内存空间时出现的非正常的错误,后者是因为线程申请的栈深度超过了允许的最大深度出现的非正常错误,如果捕获了,就掩盖了程序应该被发现的严重错误。”我说。
“打个比方,一匹马只能拉一车厢的货物,拉两车厢可能就挂了,但一 catch,就发现不了问题了。”我补充道。
**4)不要省略异常信息的记录**
很多时候,由于疏忽大意,开发者很容易捕获了异常却没有记录异常信息,导致程序上线后真的出现了问题却没有记录可查。
```java
public void doNotIgnoreExceptions() {
try {
} catch (NumberFormatException e) {
// 没有记录异常
}
}
```
应该把错误信息记录下来。
```java
public void logAnException() {
try {
} catch (NumberFormatException e) {
log.error("哦,错误竟然发生了: " + e);
}
}
```
**5)不要记录了异常又抛出了异常**
这纯属画蛇添足,并且容易造成错误信息的混乱。
反例:
```java
try {
} catch (NumberFormatException e) {
log.error(e);
throw e;
}
```
要抛出就抛出,不要记录,记录了又抛出,等于多此一举。
反例:
```java
public void wrapException(String input) throws MyBusinessException {
try {
} catch (NumberFormatException e) {
throw new MyBusinessException("错误信息描述:", e);
}
}
```
这种也是一样的道理,既然已经捕获了,就不要在方法签名上抛出了。
**6)不要在 finally 块中使用 return**
阿里出品的嵩山版 Java 开发手册上这样规定:
>try 块中的 return 语句执行成功后,并不会马上返回,而是继续执行 finally 块中的语句,如果 finally 块中也存在 return 语句,那么 try 块中的 return 就将被覆盖。
反例:
```java
private int x = 0;
public int checkReturn() {
try {
return ++x;
} finally {
return ++x;
}
}
```
“哦,确实啊,try 块中 x 返回的值为 1,到了 finally 块中就返回 2 了。”三妹说。
“是这样的。”我点点头。
----------
“好了,三妹,关于异常处理实践就先讲这 6 条吧,实际开发中你还会碰到其他的一些坑,自己踩一踩可能印象更深刻一些。”我说。
“那万一到时候我工作后被领导骂了怎么办?”三妹委屈地说。
“新人嘛,总要写几个 bug 才能对得起新人这个称号嘛。”我轻描淡写地说。
“好吧。”三妹无奈地叹了口气。
PS:点击「阅读原文」可直达《教妹学Java》专栏的在线阅读地址,可以收藏夹伺候一波了!
## 泛型
“二哥,为什么要设计泛型啊?”三妹开门见山地问。
“三妹啊,听哥慢慢给你讲啊。”我说。
Java 在 1.5 时增加了泛型机制,据说专家们为此花费了 5 年左右的时间(听起来很不容易)。有了泛型之后,尤其是对集合类的使用,就变得更规范了。
看下面这段简单的代码。
```java
ArrayList<String> list = new ArrayList<String>();
list.add("沉默王二");
String str = list.get(0);
```
“三妹,你能想象到在没有泛型之前该怎么办吗?”
“嗯,想不到,还是二哥你说吧。”
嗯,我们可以使用 Object 数组来设计 `Arraylist` 类。
```java
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` 中存取数据。
```java
Arraylist list = new Arraylist();
list.add("沉默王二");
list.add(new Date());
String str = (String)list.get(0);
```
“三妹,你有没有发现这两个问题?”
- Arraylist 可以存放任何类型的数据(既可以存字符串,也可以混入日期),因为所有类都继承自 Object 类。
- 从 Arraylist 取出数据的时候需要强制类型转换,因为编译器并不能确定你取的是字符串还是日期。
“嗯嗯,是的呢。”三妹说。
对比一下,你就能明显地感受到泛型的优秀之处:使用**类型参数**解决了元素的不确定性——参数类型为 String 的集合中是不允许存放其他类型元素的,取出数据的时候也不需要强制类型转换了。
“二哥,那怎么才能设计一个泛型呢?”
“三妹啊,你一个小白只要会用泛型就行了,还想设计泛型啊?!不过,既然你想了解,那么哥义不容辞。”
首先,我们来按照泛型的标准重新设计一下 `Arraylist` 类。
```java
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,元素的首字母),使用尖括号 `<>` 括起来,放在类名的后面。
然后,我们可以用具体的类型(比如字符串)替换类型变量来实例化泛型类。
```java
Arraylist<String> list = new Arraylist<String>();
list.add("沉默王三");
String str = list.get(0);
```
Date 类型也可以的。
```java
Arraylist<Date> list = new Arraylist<Date>();
list.add(new Date());
Date date = list.get(0);
```
其次,我们还可以在一个非泛型的类(或者泛型类)中定义泛型方法。
```java
class Arraylist<E> {
public <T> T[] toArray(T[] a) {
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
}
}
```
不过,说实话,泛型方法的定义看起来略显晦涩。来一副图吧(注意:方法返回类型和方法参数类型至少需要一个)。
![](https://cdn.jsdelivr.net/gh/itwanger/jmx-java/images/generic/generic-01.png)
现在,我们来调用一下泛型方法。
```java
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`
在解释这个限定符之前,我们假设有三个类,它们之间的定义是这样的。
```java
class Wanglaoer {
public String toString() {
return "王老二";
}
}
class Wanger extends Wanglaoer{
public String toString() {
return "王二";
}
}
class Wangxiaoer extends Wanger{
public String toString() {
return "王小二";
}
}
```
我们使用限定符 `extends` 来重新设计一下 `Arraylist` 类。
```java
class Arraylist<E extends Wanger> {
}
```
当我们向 `Arraylist` 中添加 `Wanglaoer` 元素的时候,编译器会提示错误:`Arraylist` 只允许添加 `Wanger` 及其子类 `Wangxiaoer` 对象,不允许添加其父类 `Wanglaoer`
```java
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` 可以缩小泛型的类型范围。
“哦,明白了。”三妹若有所思的点点头,“二哥,听说虚拟机没有泛型?”
“三妹,你功课做得可以啊。哥可以肯定地回答你,虚拟机是没有泛型的。”
“怎么确定虚拟机有没有泛型呢?”三妹问。
“只要我们把泛型类的字节码进行反编译就看到了!”用反编译工具将 class 文件反编译后,我说,“三妹,你看。”
```java
// 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.java
package 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`,结果会怎么样呢?”三妹这个问题问的很巧妙。
来看这段代码。
```java
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];
}
}
```
反编译后的结果如下。
```java
// 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.java
package com.cmower.java_demo.fanxing;
// Referenced classes of package com.cmower.java_demo.fanxing:
// Wanger
class 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`)”
“二哥,类型擦除会有什么问题吗?”三妹又问了一个很有水平的问题。
“三妹啊,你还别说,类型擦除真的会有一些问题。”我说,“来看一下这段代码。”
```java
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<String>) is the same as another method in type
Cmower
>
>Erasure of method method(Arraylist<Date>) is the same as another method in type
Cmower
大致的意思就是,这两个方法的参数类型在擦除后是相同的。
也就是说,`method(Arraylist<String> list)``method(Arraylist<Date> list)` 是同一种参数类型的方法,不能同时存在。类型变量 `String``Date` 在擦除后会自动消失,method 方法的实际参数是 `Arraylist list`
有句俗话叫做:“百闻不如一见”,但即使见到了也未必为真——泛型的擦除问题就可以很好地佐证这个观点。
“哦,明白了。二哥,听说泛型还有通配符?”
“三妹啊,哥突然觉得你很适合作一枚可爱的程序媛啊!你这预习的功课做得可真到家啊,连通配符都知道!”
通配符使用英文的问号(?)来表示。在我们创建一个泛型对象时,可以使用关键字 `extends` 限定子类,也可以使用关键字 `super` 限定父类。
我们来看下面这段代码。
```java
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>` 形式的通配符,可以实现泛型的向上转型,来看例子。
```java
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 中添加元素,但可以给它赋值。
```java
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` 的元素,来看例子。
```java
Arraylist<? super Wanger> list3 = new Arraylist<>(4);
list3.add(new Wanger());
list3.add(new Wangxiaoer());
// Wanger w3 = list3.get(0);
```
需要注意的是,无法从 `Arraylist<? super Wanger>` 这样类型的 list3 中取出数据。
“三妹,关于泛型,这里还有一篇很不错的文章,你等会去看一下。”我说。
>https://www.pdai.tech/md/java/basic/java-basic-x-generic.html
“对泛型机制讲的也很透彻,你结合二哥给你讲的这些,再深入的学习一下。”
“好的,二哥。”
## Java 不能实现真正泛型的原因是什么?
简单来回顾一下类型擦除,看下面这段代码。
```java
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 是不同的类。
但由于类型擦除的原因,以上代码是不会编译通过的——编译器会提示一个错误:
>'method(ArrayList<String>)' clashes with 'method(ArrayList<Date>)'; both methods have same erasure
也就是说,两个 `method()` 方法经过类型擦除后的方法签名是完全相同的,Java 是不允许这样做的。
也就是说,按照我们的假设:如果 Java 能够实现真正意义上的泛型,两个 `method()` 方法是可以同时存在的,就好像方法重载一样。
```java
public class Cmower {
public static void method(String list) {
}
public static void method(Date list) {
}
}
```
为什么 Java 不能实现真正意义上的泛型呢?背后的原因是什么?
第一,兼容性
Java 在 2004 年已经积累了较为丰富的生态,如果把现有的类修改为泛型类,需要让所有的用户重新修改源代码并且编译,这就会导致 Java 1.4 之前打下的江山可能会完全覆灭。
想象一下,你的代码原来运行的好好的,就因为 JDK 的升级,导致所有的源代码都无法编译通过并且无法运行,是不是会非常痛苦?
类型擦除就完美实现了兼容性,Java 1.5 之后的类可以使用泛型,而 Java 1.4 之前没有使用泛型的类也可以保留,并且不用做任何修改就能在新版本的 Java 虚拟机上运行。
老用户不受影响,新用户可以自由地选择使用泛型,可谓一举两得。
第二,不是“实现不了”
*这部分内容参考自 R大@RednaxelaFX*
Pizza,1996 年的实验语言,在 Java 的基础上扩展了泛型。
>Pizza 教程地址:http://pizzacompiler.sourceforge.net/doc/tutorial.html
这里插一下 Java 的版本历史,大家好有一个时间线上的观念。
- 1995年5月23日,Java语言诞生
- 1996年1月,JDK1.0 诞生
- 1997年2月18日,JDK1.1发布
- 1998年2月,JDK1.1被下载超过2,000,000次
- 2000年5月8日,JDK1.3发布
- 2000年5月29日,JDK1.4发布
- 2004年9月30日18:00 PM,J2SE1.5 发布
也就是说,Pizza 在 JDK 1.0 的版本上就实现了“真正意义上的”泛型,我引过来两段例子,大家一看就明白了。
首先是 StoreSomething,一个泛型类,标识符是大写字母 A 而不是我们熟悉的大写字母 T。
```java
class StoreSomething<A> {
A something;
StoreSomething(A something) {
this.something = something;
}
void set(A something) {
this.something = something;
}
A get() {
return something;
}
}
```
这个 A 呢,可以是任何合法的 Java 类型:
```java
StoreSomething<String> a = new StoreSomething("I'm a string!");
StoreSomething<int> b = new StoreSomething(17+4);
b.set(9);
int i = b.get();
String s = a.get();
```
对吧?这就是我们想要的“真正意义上的泛型”,A 不仅仅可以是引用类型 String,还可以是基本数据类型。要知道,Java 的泛型不允许是基本数据类型,只能是包装器类型。
![](https://cdn.jsdelivr.net/gh/itwanger/jmx-java/images/generic/true-generic-01.png)
除此之外,Pizza 的泛型还可以直接使用 `new` 关键字进行声明,并且 Pizza 编译器会从构造方法的参数上推断出具体的对象类型,究竟是 String 还是 int。要知道,Java 的泛型因为类型擦除的原因,程序员是无法知道一个 ArrayList 究竟是 `ArrayList<String>` 还是 `ArrayList<Integer>` 的。
```java
ArrayList<Integer> ints = new ArrayList<Integer>();
ArrayList<String> strs = new ArrayList<String>();
System.out.println(ints.getClass());
System.out.println(strs.getClass());
```
输出结果:
```
class java.util.ArrayList
class java.util.ArrayList
```
都是 ArrayList 而已。
那 Pizza 这种“真正意义上的泛型”为什么没有被 Java 采纳呢?这是大家都很关心的问题。
事实上,Java 的核心开发组对 Pizza 的泛型设计非常感兴趣,并且与 Pizza 的设计者 Martin 和 Phil 取得了联系,新合作了一个项目 Generic Java,争取在 Java 中添加泛型支持,但不引入 Pizza 的其他功能,比如说函数式编程。
*这里再补充一点维基百科上的资料,Martin Odersky 是一名德国计算机科学家,他和其他人一起设计了 Scala 编程语言,以及 Generic Java(还有之前的 Pizza),他实现的 Generic Java 编译器成为了 Java 编译器 javac 的基础。*
站在马后炮的思维来看,Pizza 的泛型设计和函数式编程非常具有历史前瞻性。然而 Java 的核心开发组在当时似乎并不想把函数式编程引入到 Java 中。
以至于 Java 在 1.4 之前仍然是不支持泛型的,为什么 Java 1.5 的时候又突然支持泛型了呢?
当然是到了不支持不行的时候了。
没有泛型之前,我们可以这样写代码:
```java
ArrayList list = new ArrayList();
list.add("沉默王二");
list.add(new Date());
```
不管是 String 类型,还是 Date 类型,都可以一股脑塞进 ArrayList 当中,这看起来似乎很方便,但取的时候就悲剧了。
```java
String s = list.get(1);
```
这样取行吗?
不行。
还得加上强制转换。
```java
String s = (String) list.get(1);
```
但我们知道,这行代码在运行的时候必然会出错:
```
Exception in thread "main" java.lang.ClassCastException: java.util.Date cannot be cast to java.lang.String
```
这就又回到“兼容性”的问题了。
Java 语言和其他编程语言不一样,有着沉重的历史包袱,1.5 之前已经有大量的程序部署在生产环境下了,这时候如果一刀切,原来没有使用泛型的代码直接扼杀了,后果不堪想象。
Java 一直以来都强调兼容性,我认为这也是 Java 之所以能被广泛使用的主要原因之一,开发者不必担心 Java 版本升级的问题,一个在 JDK 1.4 上可以跑的代码,放在 JDK 1.5 上仍然可以跑。
*这里必须得说明一点,J2SE1.5 的发布,是 Java 语言发展史上的重要里程碑,为了表示该版本的重要性,J2SE1.5 也正式更名为 Java SE 5.0,往后去就是 Java SE 6.0,Java SE 7.0。。。。*
但 Java 并不支持高版本 JDK 编译生成的字节码文件在低版本的 JRE(Java 运行时环境)上跑。
![](https://cdn.jsdelivr.net/gh/itwanger/jmx-java/images/generic/true-generic-02.png)
针对泛型,兼容性具体表现在什么地方呢?
```java
ArrayList<Integer> ints = new ArrayList<Integer>();
ArrayList<String> strs = new ArrayList<String>();
ArrayList list;
list = ints;
list = strs;
```
表现在上面这段代码必须得能够编译运行。怎么办呢?
就只能搞类型擦除了!
真所谓“表面上一套,背后玩另外一套”呀!
编译前进行泛型检测,`ArrayList<Integer>` 只能放 Integer,`ArrayList<String>` 只能放 String,取的时候就不用担心类型强转出错了。
但编译后的字节码文件里,是没有泛型的,放的都是 Object。
Java 神奇就神奇在这,表面上万物皆对象,但为了性能上的考量,又存在 int、double 这种原始类型,但原始类型又没办法和 Object 兼容,于是我们就只能写 `ArrayList<Integer>` 这样很占用内存空间的代码。
这恐怕也是 Java 泛型被吐槽的原因之一了。
一个好消息是 Valhalla 项目正在努力解决这些因为泛型擦除带来的历史遗留问题。
![](https://cdn.jsdelivr.net/gh/itwanger/jmx-java/images/generic/true-generic-03.png)
Project Valhalla:正在进行当中的 OpenJDK 项目,计划给未来的 Java 添加改进的泛型支持。
>源码地址:http://openjdk.java.net/projects/valhalla/
让我们拭目以待吧!
![](https://cdn.jsdelivr.net/gh/itwanger/jmx-java/images/generic/true-generic-04.png)
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册