# 泛型机制
重视基础,基础不牢,地动山摇。
泛型出现前,想要在一个类中存储的数据可以是不同的类,就只有写成 Object,比如集合 List,就只能是 Object[]
,而不是 T[]
。这样写会出现的问题是,使用 List 的人需要在取出数据时强制转换。
String s = (String)list.get(0); |
但是 Object 在编译阶段并不具备良好的类型判断能力,也就是说:
List list = new ArrayList(); // 假设现在还没有泛型 | |
list.add(123); | |
String s = (String)list.get(0); // 编译期不会报错,运行才会出错 |
并不止这么一个问题,还有就是我想只要一个存储整数的 list,但是 list.add("123")
也不会出错,但是明显为之后对 list
的其他操作埋下隐患。
所以 JDK5 新增了泛型,能够在编译阶段检查类型安全,大大提升效率。
你需要明白,没有泛型,只是用 Object 也是可以的,如果你写代码能够保证绝对正确的话,绝对不会出现异常强转。
# 泛型基本使用
泛型将数据类型的确定控制在了编译阶段,所以在编写代码的时候就能明确泛型的类型,如果类型不符合,就无法通过编译,之后会讲到泛型是如何实现明确类型的。
- 泛型类:
class Point<T,K> { | |
//... | |
static void test() { | |
T t; // 会报错 | |
} | |
} |
在使用 Point
类时才会具体指定 T
是什么,所以泛型不能用于静态方法,以及不能直接实例化。
- 泛型接口:
interface MyInfo<T>() { | |
T getvar(); | |
} | |
// 也可以在实现(继承)的时候明确接口(父类)的类型:implements MyInfo<Integer>{} | |
class Solution<T,L> implements MyInfo<L> { | |
@Override | |
public L getVar() { | |
return null; | |
} | |
} |
- 泛型方法:
public <T> T get(Class<T> c) throws InstantiationException, IllegalAccessException { | |
return c.newInstance(); | |
} |
返回值前必须加上 <T>
声明这是一个泛型方法,泛型方法比泛型类更灵活,在调用的时候指明类型。之前说静态变量和静态方法不能使用泛型,是因为静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。但是静态泛型方法可以存在的:
public static <T> T show(T one){ // 这是正确的 | |
return null; | |
} |
重要的其实就是:我在调用方法的时候能不能明确泛型的具体类型。
# 泛型界限
比如我们 Socre 只存储整数或小数,就要对泛型的上界做出限定:
public class Score<T extends Number> { | |
private final T value; | |
} |
同样的,下界限定就是:
public class Palte<T super Fruit> { | |
// 只能 Fruit 及其父类可以写入该类 | |
} |
其实泛型上下界机制更多是为了解决泛型中隐含的转换问题:
B extends A
,则A tmp = new B()
是不会报错的,这就是多态,父类引用可以指向子类对象。- 但是
List<B> list = new ArrayList<>()
,List<A> tmp = list
就会出错,这就是泛型隐含的强转问题。
现在加了上下界之后姐可以转换了:
// 上界 | |
List<Son> list1 = new ArrayList<>(); | |
List<? extends Parent> tmp1 = list1; | |
// 下界 | |
List<Parent> list2 = new ArrayList<>(); | |
List<? super Son> tmp2 = list2; |
# 泛型擦除
为了兼容之前的版本,Java 泛型的实现采取了 “伪泛型” 的策略,即 Java 在语法上支持泛型,但是在编译阶段会进行所谓的 “类型擦除”(Type Erasure)。
将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
对应原则是:
- 如果类型参数是无限制通配符或没有上下界限定则替换为 Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
// 编译前 | |
public abstract class A <T extends Number>{ // 设定上界为 Number | |
abstract T test(T t); | |
} | |
// 编译后 | |
public abstract class A { | |
abstract Number test(Number t); // 上界 Number,因为现在只可能出现 Number 的子类 | |
} |
- 自动产生 “桥接方法” 以保证擦除类型后的代码仍然具有泛型的 “多态性”。首先要理解的是,类型擦除会造成多态的冲突,解决办法就是桥接方法。
// 例子 | |
class Pair<T> { | |
private T value; | |
public T getValue() { return value; } | |
public void setValue(T value) { this.value = value; } | |
} | |
// 子类继承 | |
class DateInter extends Pair<Date> { | |
@Override | |
public void setValue(Date value) {super.setValue(value);} | |
@Override | |
public Date getValue() {return super.getValue();} | |
} |
我们原意是:将父类的泛型类型限定为 Date,那么父类里面的两个方法的参数都为 Date 类型。
但是父类类型擦除后,就变成了 Object
:
class Pair<Object> { | |
private Object value; | |
public Object getValue() { return value; } | |
public void setValue(Object value) { this.value = value; } | |
} |
而子类的重写方法的参数列表还是 Date
,这就不是重写了,而是重载!** 我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突。**JVM 是如何解决这种情况的?
通过 javap -c DataInter
反编译可以看到子类有四个方法,最后的两个方法,就是编译器自己生成的桥方法。我们展示一对方法 - 桥接方法看看:
public java.util.Date getValue(); // 我们重写的 getValue 方法 | |
Code: | |
0: aload_0 | |
1: invokespecial #23 // Method Pair.getValue:()Ljava/lang/Object; | |
4: checkcast #26 // class java/util/Date | |
7: areturn | |
public java.lang.Object getValue(); // 编译时由编译器生成的桥方法 | |
Code: | |
0: aload_0 | |
1: invokevirtual #28 // Method getValue:() Ljava/util/Date 去调用我们重写的 getValue 方法; | |
4: areturn |
子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的 setvalue
和 getValue
方法上面的 @Oveerride
只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。
为了实现泛型的多态,JVM 允许一个不合法的事情就是:,桥接方法 Object getValue()
和我们的方法 Date getValue()
同时存在,但是这两个方法的参数列表和方法名都相同,按理来说会报错,但是 JVM 就是允许捏。
# 编译器检查
既然泛型在编译期被擦除为原始类型,那么是如何进行编译器检查的?Java 编译器是通过先检查代码中泛型的类型,然后先进行类型擦除,再进行编译。
List<String> list = new ArrayList<>(); | |
List list2 = new ArrayList<String>();// 这样不会报错,但是也不会有泛型检查 | |
list2.add(10);// 没有泛型检查,不会报错 |
我们可以有上述写法, new ArrayList<>()
只是在内存中开辟一片空间,真正设计类型检查的是他的引用,因为我们是通过 list
这个引用来调用它的方法的。
类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
# 一个设计理念问题
先看代码:
ArrayList<String> list1 = new ArrayList<Object>(); // 编译错误 | |
ArrayList<Object> list2 = new ArrayList<String>(); // 编译错误 |
为什么这两种情况不被允许呢?
- 第一种情况,我们可以拓展一下第一种情况:
ArrayList<Object> list1 = new ArrayList<Object>(); | |
list1.add(new Object()); | |
list1.add(new Object()); | |
ArrayList<String> list2 = list1; // 编译错误 |
第四行代码, list1
放的是 Object
,而 list2
放的是 String
,一个是类型转化时容易出下 ClassCastException
,再者就是 list1
引用检查出两个不符合。
- 第二种情况,同样先拓展一下:
ArrayList<String> list1 = new ArrayList<String>(); | |
list1.add(new String()); | |
list1.add(new String()); | |
ArrayList<Object> list2 = list1; // 编译错误 |
String
转 Object
总没错吧(就是子类转父类),为什么还不对呢?诚然这种转换不会出现问题,但是这样的意义是什么?我们使用了泛型,到头来,还是要自己强转,违背了泛型设计的初衷。所以 java 不允许这么干。再说,你如果又用 list2 往里面 add () 新的对象,那么到时候取得时候,我怎么知道我取出来的到底是 String 类型的,还是 Object 类型的呢?
# 异常 & 反射 & 泛型
具体内容详见异常,反射篇章(还没写)
# 参考
https://www.yuque.com/qingkongxiaguang/javase/rk6if6#8045759e
https://pdai.tech/md/java/basic/java-basic-x-generic.html