1.泛型方法的使用
泛型方法并非必须在泛型中进行定义使用,也可以在非泛型类中进行定义使用,即可以在普通类中进行定义使用。
1.1泛型方法的基本使用
1.1.1在泛型类中的使用
就是类名后面跟着<T>,T可以在创建类对象的时候传入,类型参数T被用于泛型类中的使用。
public class GenericClass<T> {
}
public T genericFunc(T data) {
return data;
}
泛型类中的成员方法,即可以使泛型类创建对象时指定的类型参数,也可以在定义函数的时候,指定新的泛型参数,并在调用函数的时候进行使用:
在函数定义时指定泛型参数,语法是:public <U> 返回值 函数签名 => 即定义给函数的泛型参数,需要将泛型参数指定在函数返回值定义之前。
// 携带泛型参数的函数
public <U> T genericFunc2(T data1, U data2) {
System.out.println(data2);
return data1;
}
但是如果是static静态方法,那么只能使用函数自身定义的泛型参数,不能使用泛型类的泛型参数(因为对象还没有创建,根本不知道传递给泛型类的泛型参数是什么)
// 如果是定义的静态函数, 必须要给函数传递参数了
public static <T> T genericStaticFunc(T data) {
return data;
}
GenericClass<String> genericClass = new GenericClass<>();
String res = genericClass.genericFunc("你好");
System.out.println(res);
String res2 = genericClass.genericFunc2("你好", 200);
System.out.println(res2);
Integer res3 = GenericClass.genericStaticFunc(200);
System.out.println(res3);
1.1.2在普通类中的使用
其实在普通类中和泛型类中使用的区别不大,只是因为泛型类可以接收类型参数罢了,只能在定义方法的时候,给方法指定泛型参数,让方法使用自己定义的泛型参数。
// 定义静态的泛型函数
public static <T> T genericStaticFunc(T data) {
return data;
}
// 定义成员泛型函数
public <T> T genericFunc(T data) {
return data;
}
String res = CommonClass.genericStaticFunc("牛逼");
System.out.println(res);
CommonClass commonClass = new CommonClass();
int res2 = commonClass.genericFunc(520);
System.out.println(res2);
1.2泛型方法的类型推断
1.从定义的这个方法来看泛型方法是如何根据传入的参数进行类型推断的:
// 类型推断
public static <T> T genericStaticsFunc2(T data) {
return data;
}
进行调用的时候可以在函数前面使用<Integer>进行指定函数的类型参数。
int res = CommonClass.<Integer>genericStaticsFunc2(888);
System.out.println(res);
当然也可以进行省略,此时编译器可以根据传入的参数类型进行推断泛型参数的类型:
因为参数的类型是函数定义的泛型参数类型,所以可以通过参数类型来推断泛型类型。
int res2 = CommonClass.genericStaticsFunc2(888);
System.out.println(res2);
String res3 = CommonClass.genericStaticFunc3(new ArrayList<String>() {
{
add("牛马");
}
});
System.out.println(res3);
要点:泛型函数参数的推断主要依赖的就是参数类型进行推断,只有函数的参数才能提供足够多的信息帮助函数去推断信息。
4.当然也可以不通过函数参数去推断泛型参数信息,但是总感觉有一种脱了裤子放屁的感觉:
这样其实就是限定了T就是String类型的,那么使用函数泛型参数的意义何在呢?
public static <T> T genericStaticFunc4() {
return (T) "牛逼";
}
要点:函数使用泛型参数的时候,尽量要配合泛型类型的函数参数进行使用。
// 成员方法的类型推断
public <T> T genericFunc2(T data) {
return data;
}
无论是成员方法还是静态方法,函数调用时的泛型参数传递都是在函数调用签名前的<>中进行指定函数的类型参数。
CommonClass commonClass = new CommonClass();
String res = commonClass.<String>genericFunc2("你好");
System.out.println(res);
int res2 = commonClass.genericFunc2(888);
System.out.println(res2);
1.3类型推断的本质
1.3.1类型推断的例子
定义了一个静态泛型函数,定义了类型变量T,接受一个T类型的参数列表,并且返回一个T类型的返回值,这个函数是参数数组中的其中一个进行返回了出去。
// 类型推断的本质
public static <T> T typeInference(T ...data) {
return data[0];
}
// 测试类型推断
public static void test1() {
Number number = GenericClass.typeInference(5.2, 10, 8);
System.out.printf(number.toString());
}
调用这个泛型函数时候,进行传入的参数有double类型的,也有int类型,但是最终我们通过接收的对象是一个Number类型的参数,即最终T被推断为了Number类型的。
这是为什么呢?为什么编译器最终将T类型参数推断为了Number类型的呢?其实很简单,在调用typeInference方法的时候,传进去的参数有是int和double类型的,但是在方法中都被定义为T类型,int类型和double类型会进行装箱为Integer和Double类型的(因为泛型参数只能是Object的子类,究其原因是泛型在JVM中会发生类型擦除,所有的类型参数都会被类型擦除为Object类型),编译器在利用这些信息推断T的类型的时候,会先判断这些不同具体类型的参数有有没有共同的父类/实现接口,如果没有就会推断失败,如果有就会推断为相应的直接父类/实现接口类型。
1.3.2从编译器和源码的角度分析
可以发现Integer和Double这两个类均继承Number类并且实现了omparable类。
public final class Integer extends Number implements Comparable<Integer> {
}
public final class Double extends Number implements Comparable<Double> {
}
所以按我们刚刚的分析(编译器通过函数参数去推断分析的时候,如果参数的类型不同,会将泛型类型推断为所有参数类型共同的直接父类/实现接口),typeInference函数除了可以将T类型推断为Number类型以外,应该也可以推断为实现接口Comparable:
确实是可以推断为Comparable<?>,由于Integer/Double实现Comparable接口的泛型参数指定的类型是不一样的,所以类型最终被推断为Comparable<?>的类型。
Comparable<?> comparable = GenericClass.typeInference(1, 2, 3.0);
System.out.println(comparable.toString());
根据著名大师Peter Von Der ahe所指出的,如果想要知道Java编译器到底将泛型推断为什么类型,故意将推断的出类型赋予给一个错误的变量,然后查看编译器的报错信息,是一个很好的选择。
可以发现我们typeInference执行后返回的结果T类型的对象赋予给一个错误的类型Double,编译器就会出现报错,抛出异常,编译器抛出的异常表示了inferenceType提供的类型是Number & Comparable<? extendds Number & Comparable<?>>类型的(Java的泛型是支持联合类型的)。
2.类型变量的限定
2.1什么是类型变量的限定?
Java泛型机制提供了使用extends来限定类型参数的上限类型。
当我们想要进行限定Java的泛型类型为某一个类型的子类型的时候,就可以使用extends进行限定。
2.2为什么要限定泛型参数的类型呢?
假设我们需要接收一个List<T>类型的参数,使用compareTo方法进行将List中存储的数据进行比较出来最大值返回出去。
T类型其实追溯到底就是一个Object类型(虚拟机进擦除类型之后最终会变为Object类型),但是正因为是Object类型,所以编译器不知道T类型的对象是否有compareTo这个方法,就会抛出错误。
private static <T> T compareDataHasError(List<T> list) {
T max = list.get(0);
for (int index = 1; index < list.size(); index++) {
max = list.get(index).compareTo(max) > 0 ? list.get(index) : max;
}
return max;
}
所以必须进行设定T extends Comparable<T>,使用extends进行限定T的类型,必须实现了Comparable接口,即一定实现了compareTo方法,T类型的对象一定是支持调用compareTo方法的。这样编译器就能够编译通过了。
private static <T extends Comparable<T>> T compareData(List<T> list) {
T max = list.get(0);
for (int index = 1; index < list.size(); index++) {
max = list.get(index).compareTo(max) > 0 ? list.get(index) : max;
}
return max;
}
2.3extends限定T类型的标准用法
2.3.1基本使用
<T extends BoudingType>,限定T类型必须是BoudingType类型的子类型。
// 1. 基本使用
private static <T extends Comparable<T>> void test(T data) {
data.compareTo(data);
}
2.3.2联合使用
<T extends BoudingType1 & BoudingType2 ...>,限定T类型可以属于多个BoudingType的子类型。
但是使用&进行联合限定的时候,由于Java是单继承体系,所以一个类只能属于一个类的类型,故使用extends进行限定类型参数T的类型的时候,类型列表中只能有一个类型是类,其它只能是接口,并且是类的类型必须要放在类型列表的第一个位置。
这个函数定义的泛型限定了T类型的函数必须是继承了Integer类,而且实现了Comparable<Integer>接口。
// 2. 联合限定使用
private static <T extends Integer & Comparable<Integer>> void test2(T data) {
System.out.println(data.compareTo(0));
}
3.泛型代码和虚拟机
3.1类型擦除
3.1.1什么是类型擦除
类型擦除机制就是,Java中定义一个泛型类型的时候,无论什么时候,都会有一个与其对应的原始类型,这个原始类型就是去掉类型参数后的类型名称。
即假设现在有一个类型是Comparable<Integer>的对象被定义了,这个类型会有一个为Comparable的原始类型对应。
在编译后,类型变量都会被Java编译器进行类型变量擦除为类型变量的限定类型,即extends进行限定的类型,如果没有使用extends进行限定,就会擦除为Object类型。
3.1.2Java类擦除机制
3.1.2.1类型参数无extends限定
public class TestClass01<T> {
private T field;
public T getField() {
return field;
}
public void setField(T field) {
this.field = field;
}
}
public class CommonTestClass01 {
private Object field;
public Object getField() {
return field;
}
public void setField(Object field) {
this.field = field;
}
}
3.1.2.2类型参数有extends类型限制
public class TestClass01<T extends Number> {
private T field;
public T getField() {
return field;
}
public void setField(T field) {
this.field = field;
}
}
public class CommonTestClass01 {
private Number field;
public Number getField() {
return field;
}
public void setField(Number field) {
this.field = field;
}
}
3.1.2.3类型参数有extends联合类型限制
TestClass01<T extends Integer & Comparable<Integer>> => 如果将类中定义的类型,使用extends进行联合限定,那么就会将类型擦除为第一个限定对象:
import java.io.Serializable;
public class TestClass01<T extends Comparable & Serializable> {
private T field;
public T getField() {
return field;
}
public void setField(T field) {
this.field = field;
}
}
public class CommonTestClass01 {
private Comparable field;
public Comparable getField() {
return field;
}
public void setField(Comparable field) {
this.field = field;
}
}
3.1.2.4联合类型限定时需要注意的问题
如果我们使用extends进行联合类型限定的时候,类型最终会擦除为extends限定的第一个类型,如果有类型中有类的时候,那肯定不用说,必须将类放在限定类型列表的第一个位置,但是如果全都是接口,虽然接口类型没有顺序限制,但是还是建议将定义的方法多且经常使用的接口类型放在接口列表的组前面。
假设我们将前面<T extends Comparable & Serializablle>替换为<T extend Serializable & Comparable>,那么编译器进行编译后,就会转换为:
import java.io.Serializable;
public class CommonTestClass01 {
private Serializable field;
public Serializable getField() {
return field;
}
public void setField(Serializable field) {
this.field = field;
}
}
但是编译器如果在进行调用这个类中关于Comparable的方法的时候,可能要进行强制类型转换,就会消耗系统的资源。
所以说建议将定义方法多,且方法常用的接口声明在限定类型列表的第一个位置,方便类型擦除为该接口类型,减少类型转换的次数,节省系统资源。
3.1.3Java的类型擦除和CPP的不同
CPP的模板会为每个具有泛型类型的类创建不同的类型,但是Java不会,Java所有的泛型类型,最终都是对应的擦除后的类型。
无论TestClass派生出去多少不同的泛型类,最终其实在编译器进行编译后,都会擦除为TestClass<Object>类型,最终这些泛型类对应的所有对象都会被编译器将类型擦除为TestClass<Object>这个类型,所以说在整个运行体系中,仅有一个类型,不会出现类型爆炸的情况。
但是CPP的模板不是这样的,CPP的模板会将所有定义的泛型类,初始化为一个类型,最终会导致出现模板代码爆炸的情况。
要点:Java的泛型不会导致类爆炸,但是CPP的模板会导致模板代码爆炸。
3.2转换泛型表达式
3.2.1定义Pair<T>类
这个类定义了一个类型参数,这个类型参数规定了字段first的类型。
public class Pair<T> {
private T first;
public T getFirst() {
return first;
}
public void setFirst(T item) {
first = item;
}
}
这个类在编译期会进行类型擦除,将类型参数擦除为Object。
public class CommonPair {
private Object first;
public Object getFirst() {
return first;
}
public void setFirst(Object item) {
first = item;
}
}
3.2.3定义Employee类
public class Employee {
}
3.2.4测试和总结
因为我们知道在系统中,Pair使用getFirst的时候,返回的是Object类型的变量,之所以我们不用强转,是因为虚拟机在内部已经帮助我们完成了一步强制类型转换。
即虚拟机在执行pair.getFirst()代码的时候,进行执行了两步操作:一是从Pair实例化对象中获取到了first中对应的数据(Object类型的),二是将Object类型的数据进行了强制类型转换。
public static void main(String[] args) {
Employee employee = new Employee();
Pair<Employee> pair = new Pair<>();
pair.setFirst(employee);
Employee first = pair.getFirst();
}
3.3转换泛型方法
泛型方法也会发生类型擦除,接下来看一下泛型擦除带来的问题,主要是和多态造成了冲突。
3.3.1泛型方法中的泛型擦除
public static class Person {
}
// 泛型函数
public static <T extends Comparable<T>> T min(T[] arr) {
return arr[0];
}
// 定义的泛型函数
public static Comparable minClearGeneric(Comparable[] arr) {
return arr[0];
}
3.3.2方法泛型擦除带来的问题 => 和多态发生冲突
3.3.2.1改造后的Pair类:
又进行定义了一个T类型的字段,以及相应的getter和setter方法。
public class Pair<T> {
private T first;
public T getFirst() {
return first;
}
public void setFirst(T item) {
first = item;
}
private T second;
public T getSecond() {
return second;
}
public void setSecond(T localDate) {
second = localDate;
}
}
3.3.2.2定义的DateInterval类
这个类继承了Pair类,并为Pair类指定了一个泛型参数:LocalDate。
DateInterval中定义了一个seSecond方法,接收一个LocalDate对象,然后调用父类(即Pair)中的getFirst方法,和之前的first字段中存储的LocalDate数据进行比较,如果比first中的时间字段大(即晚于first中定义的时间),就可以进行调用父类(即Pair)中的setSecond方法,为second字段进行赋值。
import java.time.LocalDate;
public class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
if (second.compareTo(getFirst()) >= 0) {
super.setSecond(second);
}
}
public LocalDate getFirst() {
return (LocalDate) super.getFirst();
}
}
其中Pai关键的方法就是getFirst方法和setSecond方法:
public LocalDate getFirst() {
return first;
}
public void setSecond(LocalDate localDate) {
second = localDate;
}
3.3.2.3泛型擦除后的DateInterval类
DateInterval进行泛型擦除后,继承的Pair其实就从Pair<LocalDate>变为了Pair<Object>。
public class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
if (second.compareTo(getFirst()) >= 0) {
super.setSecond(second);
}
}
public LocalDate getFirst() {
return (LocalDate) super.getFirst();
}
}
这也意味着Pair里面的方法,经过泛型擦除后,所有的T参数类型都被转换为了Object。
也就是说Pair已经发生了泛型擦除,已经不是编码时期待的样子。
3.3.2.4泛型擦除后的Pair类
这里只展示其中最重要的两个方法,Pair在泛型擦除后,最重要的主要是这两个方法:
public void setFirst(Object item) {
first = item;
}
public void setSecond(Object localDate) {
second = localDate;
}
3.3.2.5问题追溯
从两个角度上去思考:从我们调用的是派生类还是基类的引用对象,以及getFirst和setSecond这两个方法的角度去思考这个问题。
假设我们调用是派生类LocalDate,那么其实setSecond是没有问题的,因为Java是支持自动向上转型的,setSecond在类型擦除后,接收的参数是Object,完全可以兼容调用的。
但是getFirst就不一样了,getFirst返回的参数变为了Object,这样的话LocalDate的compareTo就没办法接收了,因为Java是不支持默认向下转型的,需要强制转型,但是这里编码并没有进行强制的向下转型。
再从引用类型是Pair父类的角度去分析,Pair父类会触发多态机制,也就是说会根据函数签名以及协变机制去寻找子类中的多态方法。
由于子类DateInterval重写了父类中的setSecond方法,但是呢,父类的setSecond方法由于类型擦除的缘故,导致父类的setSecond的函数签名变为了setSecond(Object localDate),但是派生类中的函数签名是setSecond(LocalDate localDate),出现了非常大的问题就是,多态失效了啊,发生了多态的冲突吗,通过类型擦除的角度去理解会发现,此时整个系统的逻辑是这样的:
当我们创建出DateInterval对象的时候,使用基类Pair类型进行引用的时候,此时调用setSecond方法的时候,接收的参数对象是LocalDate类型的,但是,由于类型擦除的原因,Pair中的setSecond泛型被擦除为setSecond(Objec localDate)了,这样问题就出现了,Client客户端去调用Pair引用的DateLocal对象时,首先会发现Pair类中没有setSecond(localDate: LocalDate)这个函数签名,但是又发现基类Pair中有setSecon(localDate: Object)这个函数签名,并且LocalDate是可以向上转型为Object,然后就会将LocalDate对象强转为Object,并调用基类Pair中的setSecond(localDate: Object)了。
这样就出现了问题,我们本来想要通过多态进行调用DateInterval中的setSecond(localDate: LocalDate)方法,但是因为泛型擦除的原因,很有可能导致,方法不会按照我们想象中的样子被调用。
要点:类型擦除可能会导致多态失效,继而导致方法不会按照按照我们想要的方式执行。
3.3.3Java编译器解决多态失效
Java编译器解决了多态失效的问题,使用的方法就是桥方法,构建出了一个完美的桥接器。
3.3.3.1桥方法
桥方法的设计思想其实是一种设计模式,桥接器模式,将两个方法通过桥梁进行构建在一起。
在多态这里,Java编译器就是采用了桥方法,桥方法将派生类被调用的方法与基类的方法进行连接,维护了多态的可用性。
编译器在DateInterval类中新增了getFirst(): Object和setSecond(LocalDate: Object)两个方法,这两个方法可以连接起来对应基类Pair的多态,维护了多态性。
接下来分析生成的具体的桥方法,以及这些桥方法具体是如何起作用的,维护多态的正常运行调用函数时返回参数的兼容性。
3.3.3.2具体的桥方法
1.桥方法:getSecond(localDate: Object)
这个方法进行调用了自身的setSecond(LocalDate localDate)方法。
// 桥接出的getSecond方法
public void setSecond(Object second) {
setSecond((LocalDate) second);
}
看一下setSecond是如何在这个过程中是如何进行桥接的:
新建了一个派生类对象DateInterval,这个对象的引用复制给了派生类类型,并且调用了setSecond()方法,传入了一个LocalDate类型的对象。
Pair<LocalDate> dateInterval = new DateInterval();
dateInterval.setSecond(LocalDate.now());
整个流程是这样的,Client先调用Pair引用对象的setSecon方法,然后编译器发现Pair引用的是一个派生类对象,所以要去检测是否有多态,发现DateInterval中存在一个setSecond(Object LocalDate)方法,然后JDK会去调用DateInterval.setSecond(Object localDate) => 桥方法,实现桥接器的功能,然后调用桥方法可以去调用DateInterval.setSecond(LocalDate localDate),最终在这个方法中去调用Pair.setSecond(Object localDate)方法。
要点:使用桥方法实现了桥接器模式,实现了将多态进行联系起来的作用。
由于我们在DateInteval类中进行调用了getFirst方法,但是getFirst方法由于类型擦除的原因,返回的是一个Object对象,这样compareTo方法是无法进行正常接收调用的,因为它接收的应该是一个LocalDate对象,不应该是一个Object,所以需要生成一个getFirs(): LocalDate方法进行桥接,实现调用父类方法并进行强转返回值。
public LocalDate getFirst() {
return (LocalDate) super.getFirst();
}
3.3.4函数签名冲突时,桥方法如何解决
3.3.4.1问题分析
在DateInterval中进行增加一个getSecond方法,负责调用父类的getSecoind(),然后将强转后的返回值进行返回出去。
public LocalDate getSecond() {
return super.getSecond();
}
这个方法也会引起多态的冲突,假如客户端以下面的方式进行调用:
Pair<LocalDate> dateInterval = new DateInterval();
dateInterval.setFirst(LocalDate.now());
dateInterval.setSecond(LocalDate.now());
dateInterval.getSecond();
由于是以Pair父类引用的方式进行调用的getSecond方法,但是由于类型擦除的缘故,Pair中的getSecond方法会被擦除为:
public Object getSecond() {
return second;
}
又因为Pair父类中的getSecond和子类中的getSecond函数签名是一致的,那么肯定就会出现冲突的问题,因为子类中定义的getSecond的返回值是LocalDate,但是父类中定义getSecond的返回值是Object,这种情况在Java中是完全不允许存在的,是一种冲突的情况。
要点:类型擦除不仅导致了多态出现了问题,也导致了Java函数签名的冲突问题。
解决方案:Java编译器使用桥方法和字节码差异性生成的方案,解决了该问题。
3.3.4.2Java编译器桥方法的解决方案
Java编译器使用桥方法解决了多态的问题,又调用了Java编译器字节码差异化机制解决了函数签名冲突问题。
public Object getSecond() {
return super.getSecond();
}
public LocalDate getSecond() {
return (LocalDate) getSecond();
}
2.为了消除函数签名的冲突,Java编译器使用了差异化字节码
因为在JVM中,函数签名相同的方法被认为是相同的方法,因为JVM中相同函数签名方法对应的字节码是相同的。
但是Java编译器在这里使用了差异性字节码,为两个方法签名相同的方法生成了对应的字节码文件。
即Java编译器为两个方法签名相同的方法生成了两套不同的字节码文件,这样就可以让JVM可以通过两套对应的字节码文件找到相关方法,不会引起冲突。
要点:Java编译器使用差异性字节码的方式,为两个函数签名相同的方法生成了不同的字节码文件。
3.3.5桥方法解决可协变类型
桥方法不仅仅被Java用于解决泛型擦除问题,也被用于可协变类型的使用。
现在定义了一个类实现了Cloneable接口,并且重写clone方法,方便进行调用。
Employee实现了Cloneable方法,重写了Object中定义的clone方法,并且clone方法被定义为可协变类型(即clone方法的返回值是Object的子类型)
public class Employee implements Cloneable {
@Override
public Employee clone() throws CloneNotSupportedException {
return (Employee) super.clone();
}
}
Object中定义的clone方法是一个native原生方法,返回值被定义为一个Object类型。
protected native Object clone() throws CloneNotSupportedException;
可协变性其实也是通过桥方法实现的,其实在Java虚拟机内部也要通过返回值来判定是否进行重写了父类的方法。
1.Employee中编写的方法。2.Java编译器合成的桥方法。
// 在Employee中编写的方法
@Override
public Employee clone() throws CloneNotSupportedException {
return (Employee) this.clone();
}
// 合成的桥方法
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
3.3.6总结:Java编译器合成桥方法可以解决什么问题?
3.3.7泛型转换的事实
1.虚拟机中没有泛型参数(即类型参数),只有普通的函数和方法。
T super Comparable则会将类型参数T替换为Comparable参数。
因为一个类型可能是实现了多个接口,但是在类型擦除的时候,会将类型擦除为指定限定类型,但是在Java中进行调用该对象实现的其他接口定义的方法的时候,为了保证类型安全,需要进行强制类型转换。
3.4调用遗留的代码
由于重复书写构造函数和getter/setter方法太费劲了,所以这次直接引入外部库Lombok:
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombokhttps://mvnrepository.com/artifact/org.projectlombok/lombokhttps://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
Lombok提供了大量的注解,仅仅需要将这些注解使用在对应的类上,就可以轻易的生成相应的构造方法,getter,setter方法。
3.4.1传统的方法
调用遗留的代码是一个大佬之前提出的一个术语,遗留的代码的意思是这样的,以前的时候Java还没有引入泛型的时候,会定义一些没有泛型参数的类,当Java开始引入泛型的时候,对这些类进行改造引入泛型,或者是一开始设计类的时候,当时的需求并不需要引入泛型,但是随着项目的发展,需求的迭代,需要对这个类进行改造,引入泛型来解决相关问题。此时以前在应用这个类的时候,使用这个类的原始类型的时候(比如声明一个原始类型的变量,接收一个原始类型的参数),这些代码都被称为遗留的代码。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class NoGenericEmployee {
private String name;
private String data;
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class NoGenericCompany {
private NoGenericEmployee employee;
}
此时还是能够正常进行运行的,没有抛出任何警告,说明逻辑没问题
NoGenericCompany noGenericCompany = new NoGenericCompany();
NoGenericEmployee noGenericEmployee = new NoGenericEmployee("员工", "数据");
noGenericCompany.setEmployee(noGenericEmployee);
System.out.println(noGenericEmployee.getData());
3.4.2传统类引入泛型
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Employee<T> {
private String name;
private T data;
}
Company类没有进行任何更改,表明Company类目前引用的Emloyee原始类型,均被称之为遗留的代码:
public class Company {
private Employee employee;
public Employee getEmployee() {
return employee;
}
public void setEmployee(Employee employee) {
employee.setData(19);
this.employee = employee;
}
}
使用遗留的代码的时候,会发生警告,下面是调用Company遗留的代码和Employee新型泛型代码结合的使用:
这样进行使用的时候,会抛出黄色警告,因为Company遗留的代码存储的employee是原始类型Employee,没有记录泛型参数类型(遗留的代码在编译中被认为是Employee<Object>),所以赋值给有类型参数的Employee变量的时候,编译器无法保证这次赋值是没问题的(因为遗留的代码是没有类型参数记录的),所以会抛出unchecked警告,未检查类型异常。
Company company = new Company();
Employee<Integer> employee2 = new Employee<>("牛马", 10086);
company.setEmployee(employee2);
Employee<Integer> employee = company.getEmployee();
System.out.println(employee.getData());
抛出了黄色警告,unchecked,意思是说,获取到的employee对象赋值给Employee<Integer>有类型参数的类型是不安全的,泛型类型指定的可能是错误的。
3.4.3解决方案
使用@SuppressWarning注解,这个注解可以消除代码警告,SuppressWarning注解可以消除代码抛出的warning警告,@SuppressWarning可以被用在字段赋值语句上(例如将原始类型的对象赋值给带有泛型参数类型的参数上),可以将字段赋值的unchecked警告消除,如果直接注解在方法上,会直接将方法中所有的语句出现的unchecked语句清除(掩盖)。
可以发现当我们在字段上使用uncheckd的时候,警告就消失了。
当然我们也可以在方法上使用@SuppressWarning注解,可以直接将方法内所有的unchecked异常消除:
这种非赋值语句,是无法在语句上使用@SuppressWarning的,可以在测试方法上进行使用消去警告。
4.限制和局限性
4.1不能使用基本类型实例化类型参数
由于八种基本数据类型(byte/short/int/long/float/double/char/boolean)不是Object类型的,也就是说这八种基本类型是不属于Java对象管理的范畴的,由于Java实现泛型要基于类型擦除机制,而这八种数据类型无法擦除为Object类型或者Object类型的派生类型,所以八种基本数据类型并不支持泛型。
4.2运行时类型查询仅适用于原始类型
由于类型擦除的原因,在运行时Java虚拟机是不会记录类型参数的实际类型,所以在运行时我们根本不可能会查到在编码时规范的泛型类型。
4.2.1Instanceof不可判断带有泛型参数的类型
比如instanceof,instanceof时无法进行判断带有泛型参数的类型的,只能进行判断是否为原始类型。
当我们使用instanceof判断一个对象是否为泛型类型时,Java编译器会抛出泛型类型非法的警告:
Employee<Integer, Integer> employee = new Employee<>();
System.out.println(employee instanceof Employee<Integer, Integer>);
System.out.println(employee instanceof Employee<T, U>);
System.out.println(employee instanceof Employee);
4.2.2强转为泛型类型时会抛出警告
Employee<String, String> newEmployee = (Employee<String, String>) employee;
为什么会抛出这个警告呢,因为在Java虚拟机中,由于泛型擦除的原因,导致虚拟机中根本就不存在泛型,所以进行强转的时候,会警告,因为编译器根本无法判断出来,这次强转到底正确吗,如果是一次不正确的强转,就会抛出运行时错误,所以会抛出这个未检查的警告。
4.2.3使用getClass获取类型时均为原始类型
由于泛型擦除的原因,使用getClass只能获取到原始类型,不能获取到带有泛型参数的类型。
现在获取Employee<Integer, Integer>和Employee<String, String>对象类型(调用getClass方法进行获取)。
Employee<Integer, Integer> user = new Employee<>();
Employee<String, String> user2 = new Employee<>();
Class<? extends Employee> aClass = user.getClass();
Class<? extends Employee> bClass = user2.getClass();
System.out.println(aClass);
System.out.println(bClass);
System.out.println(aClass == bClass);
可以发现打印出来的类型都是原始类型,不带泛型参数,并且系统认为两个类型的对象的Class对象是同一个Class对象。
要点:Java中所有的泛型类最终都在虚拟机中对应同一个原型Class对象,即不可能会发生类爆炸的行为。
4.3不能创建参数化类型的数组
4.3.1现象分析
当我们尝试直接创建带有泛型参数数组的时候,会抛出错误 => generic array create not allowed,即创建泛型类型的数组是不被允许的。
// 1. 直接创建带有泛型参数的数组会抛出异常
new Pair<String>[];
4.3.2分析原因
假设Java允许创建泛型数组,但是类型擦除后就变为了以下的样子:
// 2. 发生泛型擦除之后的数组就会变成以下的样子
Pair[] pairs = new Pair[10];
又因为数组是支持向上转型的,所以我们可以将Pair数组赋值给一个Object数组变量:
Object[] pairs2 = pairs;
Object[] pairs2 = pairs;
pairs2[0] = new Pair<Integer>();
但是存储其它的非Pair的数据,仍然会导致ArrayStoreException:
pairs2[1] = "牛马";
4.3.3类型擦除引发的机制错乱
数组存储的数据的类型必须要在编译期确定,数组会记住初始化时存储的元素类型,并且存储元素的类型要求强一致性的,即使发生了向上转型也会要求强一致性。
毫无疑问的是,泛型擦除机制打破这种这种机制,会导致数组存储数据的强一致性消失(类型数据被擦除了,在虚拟机中只有原始的数据类型,数组根本找不到初始化的带有泛型参数的数据类型)
要点:数组为了保证初始化时类型的强一致性记忆,拒绝使用带有泛型参数的类型初始化数据。
4.3.4允许声明参数化类型的数组
虽然不允许初始化参数化类型的数组,但是允许声明带有参数化类型的数组类型:
4.3.5允许初始化原始类型的数组
可以看下面的几行代码,我们声明了一个原始类型的Pair数组,Pair是一个泛型类,由于数组被声明为存储原始类型的数据,所以可以向数组中存储任意泛型参数的数据,取出数据中的数据也可以赋值给任意参数化类型。
当我们向Pair[]中存储类型参数为String的Pair对象数据时,取出数据时可以将数据赋值给类型为Integer的Pair数据,不会抛出编译时异常。
但是如果我们取出了其中泛型类型的数据(此时泛型类型的类型参数为String类型),但是由于我们赋值给了Pair<Integer>时,取出的泛型类型的数据在调用时会编译器当作Integer类型的进行调用,但是String是无法强制转换为Integer的,会抛出ClassCastException的强制类型转换失败的异常。
Pair[] pairs = new Pair[10];
pairs[0] = new Pair<String>("牛马");
System.out.println(pairs[0].getName());
Pair<Integer> pair = pairs[0];
System.out.println(pair.getName().doubleValue());
要点:不建议初始化原始类型的数组进行使用,只会抛出运行时异常,编译期无法捕捉到异常。
4.3.6允许初始化通配符类型的数组
可以看下面几行代码,可以将数组的类型声明为Pair<?>类型的,即泛型参数指定为通配符类型?,这是被允许的。
现在将一个泛型类型为String的Pair对象存储到类型为Pair<?>的数组中。
但是当我们尝试取出数据赋值给带有具体类型参数的数据的时候,就会抛出错误,提示我们数组中存储的数据是Pair<?>类型的,不能赋值给Pair<Interger>。
Pair<?>[] pairs = new Pair<?>[10];
pairs[0] = new Pair<String>("牛马");
System.out.println(pairs[0].getName());
Pair<Integer> errPair = pairs[0];
Pair<String> errPair2 = pairs[0];
但是进行强制类型转换的时候,我们可以转换为任意的类型参数类型的,编译期不会抛出任何错误,即编译期不会进行检查类型的问题。
Pair<Integer> pair = (Pair<Integer>) pairs[0];
System.out.println(pair.getName().doubleValue());
在强制类型转换的时候,会抛出unchecked cast type的warning警告:
最终因为我们在String类型的对象上调用了Integer对象的方法,最终会抛出错误:
4.3.7数组和泛型的总结归纳
数组结合泛型进行使用的时候,大部分类型转换异常的情况下,都不会抛出编译期错误,只会在运行期抛出错误,所以不建议使用数组存储带有泛型的类的对象。
解决方案:建议使用集合进行替代数组,集合的类型具有编译期检测的强一致性。
4.4可变形参的警告
4.4.1函数中的可变参数
函数中可以定义相应的可变参数,可变参数在底层原理其实就是就是借助的数组。
public static void testVarArgs(String ...strs) {
System.out.println(Arrays.toString(strs));
}
VarArgs.testVarArgs("你好啊", "蛋蛋", "蛋蛋要每天开心呀");
4.4.2泛型可变参数的问题
public class Pair<T> {
private T name;
}
public static <T> void addAll(Collection<T> coll, T ...ts) {
for (T t : ts) {
System.out.println(t);
}
System.out.println(coll);
}
传入一个可变类型参数即T类型的参数是Pair<String>类型的,即函数中推断出来的可变参数的泛型类型是Pair<String>类型。
Collection<Pair<String>> coll = new ArrayList<>();
Pair<String> aPair = new Pair<>();
Pair<String> bPair = new Pair<>();
VarArgs.addAll(coll, aPair, bPair);
但是被推断为Pair<String>类型的数组就出现问题了,我们都清楚,泛型数组是非法的,Java中是不允许创建泛型数组的。
1.在定义该函数的位置抛出了Possible heap pollution from parameterized vararg type,varargs可能会造成堆污染的错误。
2.在尝试调用这个函数的时候,抛出了varargs参数的未选中泛型数组创建。
如果想要彻底搞清楚为什么会出现堆污染的现象,是一个极其困难的过程,里面涉及到的东西实在是太多,很难一概而论。
4.4.3解决泛型可变参数的问题
想要抑制警告,不让编译器抛出警告,可以使用@SuppressWarnings注解或者是@SafeVarargs注解。
1.使用@SuppressWarning进行注解调用addAll的地方和定义addAll的地方,来消除堆污染警告未选中泛型数创建的警告。
@SuppressWarnings("unchecked")
public static void test02() {
Collection<Pair<String>> coll = new ArrayList<>();
Pair<String> aPair = new Pair<>();
Pair<String> bPair = new Pair<>();
VarArgs.addAll(coll, aPair, bPair);
}
@SuppressWarnings("unchecked")
public static <T> void addAll(Collection<T> coll, T ...ts) {
for (T t : ts) {
System.out.println(t);
}
System.out.println(coll);
}
可以发现此时调用addAll方法的时候,就不会抛出警告了,定义addAll的地方也不会抛出警告了。
要点:使用@SuppressWarnings("unchecked")进行解决问题的时候,需要在调用函数和定义函数的时候进行注解(即调用处和定义处进行注解)
2.使用@SafeVarargs直接注解addAll方法,可以消除定义addAll时抛出未选中泛型数组创建的异常。
直接在定义addAll方法的地方注解@SafeVarargs,就可以在直接将调用处和定义处的警告都消除。
仅在定义addAll函数时,使用@SafeVarargs注解。
@SafeVarargs
public static <T> void addAll(Collection<T> coll, T ...ts) {
for (T t : ts) {
System.out.println(t);
}
System.out.println(coll);
}
总结一下,消去可变参数与泛型混用抛出的警告时,可以使用以下的解决方案:
4.5不能实例化类型变量
4.5.1现象分析
下面使用类型变量进行创建对象的时候是非法的,会抛出:Tyype parameter 'T' cannot be instantiated directly。即类型参数'T'不能被直接实例化。
private T first;
private T second;
public Pair() {
first = new T();
second = new T();
}
4.5.2分析原因
原因就是因为T类型的类型变量被擦除了类型,变为了Object类型,JVM虚拟机中不知道T类型到底是什么类型(没有进行记录类型信息),所以无法在运行时根据类型参数T进行实例化对象,故这种操作就是非法的。
4.5.3使用函数式接口实现运行时动态类型实例化
从Java8开始函数式接口出现之后,使用函数式接口弥补泛型不可以进动态类型实例化的问题也是一个不错的主意。
最重要的设计就是前面这两行代码,主要是私有化了一个有参构造器(接收T类型的变量first和second为指定字段进行赋值),使用static静态方法进行调用Pair构造器进行实例化Pair对象。
static静态方法makePair是一个泛型函数,声明了一个类型变量T,接收的是Supplier<T>类型的参数(这个参数可以返回一个T类型的实例化变量)。
private Pair(T first, T second) {
this.first = first;
this.second = second;
}
public static <T> Pair<T> makePair(Supplier<T> supplier) {
// 这里进行实例化对象的时候,调用了两次supplier的get方法
return new Pair<>(supplier.get(), supplier.get());
}
调用makePair方法的时候需要传入一个匿名类对象,这里使用的是Lambda表达式来实例化一个匿名类对象,调用的是String的构造器进行实例化一个String对象。
在makePair静态工厂方法中,进行调用了两次supplier.get(),即进行调用了两次new String()进行实例化对象。
Pair<String> pair = Pair.makePair(String::new);
String first = pair.getFirst();
String second = pair.getSecond();
System.out.println(first == second);
从测试结果可以看到使用这种方式可以动态的实例化对象,并且创建的对象是不同的(主要是因为supplier返回的是new String(),supplier被调用了两次,new 出来两个String对象)
缺点:使用这种简洁的方式进行实例化的时候,必须在JAVA8才能进行正常使用哦。
4.5.4使用反射调用Constructor.newInstance实现运行时动态类型实例化
实现反射调用Constructor.newInstance是比较复杂的,主要是因为有如下的遗憾:
first = T.class.getConstructor().newInstance();
编译器抛出了Cannot access class object of a type parameter:无法访问类型参数的类对象。
原因不言而喻,T类型在编译期会被擦除为Object对象类型,T类型在虚拟机中根本找不到这个类型信息,所以没有办法进行实例化对象。
如何解决这个问题呢?需要我们设计一个合理的API进行获取Class类型的数据,此时我们就可以进行利用Class对象的泛型机制进行解决。
在虚拟机中,Class对象是泛型化的,并且每个类型的Class对象都对应一个唯一的实例。
其实就是在运行时传进来指定对象的Class类型对象,并调用反射进行构造 => getConstructor().newInstance()
// 使用泛型化静态方法进行构造Pair
public static <T> Pair<T> makePair(Class<T> cl) {
try {
return new Pair<>(cl.getConstructor().newInstance(), cl.getConstructor().newInstance());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
Pair<String> pair = Pair.makePair(String.class);
System.out.println(pair.getFirst() == pair.getSecond());
这里不说明怎么继续处理异常了,就是增加了一个Object类型的可变参数,并获取到其class反射对象列表,根据反射列表进行找到对应的Constructor构造器,调用newInstance方法接收传递进来的参数。
// 使用泛型化+参数静态方法进行构造Pair
public static <T> Pair<T> makePair(Class<T> cl, Object ...args) {
System.out.println(Arrays.toString(args));
// 获取所有参数的Class对象
Class<?>[] clsArr = Arrays.stream(args).map(Object::getClass).toArray(Class<?>[]::new);
System.out.println(Arrays.toString(clsArr));
try {
return new Pair<>(cl.getConstructor(clsArr).newInstance(args), cl.getConstructor(clsArr).newInstance(args));
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
Pair<String> pair2 = Pair.makePair(String.class, "test");
System.out.println(pair2.getFirst());
System.out.println(pair2.getSecond());
System.out.println(pair2.getFirst() == pair2.getFirst());
4.5.5运行时动态根据传入类型创建对应对象的总结
在Java中想要在运行时动态根据传入的类型(而不是写死在代码中)创建对应类型对象不能使用泛型来做,主要是由于Java的泛型底层借助了类型擦除机制。
但是智慧的前人仍然总结出了在Java中运行时动态创建对象的方案:
4.6不能构造泛型数组
4.6.1为什么不能构造泛型数组
就像不能实例化类型变量一样,数组也是不能使用类型参数进行实例化的.
主要是因为类型擦除带来的影响,类型擦除会带来较大的影响,因为数组本身是带有类型,用来监控虚拟机中的数组存储。但是这个类型会被擦除,就会带来相应的问题。
因为数组要求在编译期记住自己的类型,并进行监控防止存入其他不兼容的数据,但是由于泛型是依赖类型擦除实现的,类型被擦除后,就会导致数组无法完成自己监控原始初始化类型的机制了。
private static <T extends Comparable> T[] minmax(T ...a) {
T[] mm = new T[2];
return mm;
}
设想一下,如果这段代码是可以被实例化的,T类型会被类型擦除为Comparable,那么这个函数进行创建出的数组基本上都是Comparable[]类型的。
4.6.2ArrayList的做法 => 限制类型替代法
ArrayList中进行设定数组进行存储数据的时候,使用的是Object[],并没有采用泛型数组。
其实这是一个比较好的设计的思考,如果数组被设计在泛型类中,想要让这个数组存储的对象灵活多变(随着泛型类型的变化而变化),完全可以将数组设定为泛型类型擦去后的限制类型。
transient Object[] elementData; // non-private to simplify nested class access
要点:如果泛型类中的泛型数组经常被用于泛型类内部进行调用,可以将泛型数组设定为Object数组,这样就可以达到泛型数组的效果了。
4.6.3返回泛型类型数组的做法 => 匿名函数指示法
如果在minmax这个返回一个T泛型类型的函数上使用限制类型替代法替换泛型类型,是一个不太好的选择,看下面的代码:
接收泛型T类型的参数a,创建一个Comparable类型的数组(使用限制类型替代法,将泛型T类型进行替代为类型擦去后的限制类型Comparable类型),进行使用填充T类型的数据到Comparable[]中(因为T类型的限制上限为Comparable,所以可以进行自动隐式类型转换),然后将Comparble[]进行强转为T[]类型的数组进行返回出去。
但是在继承体系上,T类型可以是Comparable类型,Comparable类型不可以是T类型。
private static <T extends Comparable> T[] minmax(T ...a) {
Comparable[] comparables = new Comparable[2];
if (a.length >= 2) {
for (int i = 0; i < 2; i++) {
comparables[i] = a[i];
}
} else {
return (T[]) comparables;
}
return (T[]) comparables;
}
所以这段代码最终会抛出ClassCastException异常。
匿名函数指示法是真的逆天,使用IntFunction<R>作为参数,指定T[]作为类型参数的实际类型,使用这个函数的时候传入一个构造数组对象的方法引用作为参数,在函数内部使用IntFunction的apply方法,传入一个数字,就可以根据这个数字创建出来一个指定长度的T类型的数组了。
private static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T ...a) {
T[] apply = constr.apply(2);
apply[0] = a[0];
return apply;
}
进行使用minmax的时候,需要将数组构造器强转为IntFunction<String[]>出入,并将参数传进去。
String[] arr = minmax((IntFunction<String[]>) String[]::new, "牛逼");
System.out.println(Arrays.toString(arr));
在内部使用的时候,使用IntFunction的apply方法进行创建了一个长度为2的String数组,然后将index索引为0位置进行赋值,索引为1位置是null,最终返回打印。
IntFunction中定义的apply抽象方法,接收一个int类型的变量(正好作为声明数组长度的变量),然后返回一个泛型类型R类型的对象
@FunctionalInterface
public interface IntFunction<R> {
/**
* Applies this function to the given argument.
*
* @param value the function argument
* @return the function result
*/
R apply(int value);
}
上面的方法引用可能比较难以理解,但是扩写为Lambda表达式确实比较好理解了:
String[] minmax = minmax((IntFunction<String[]>) (len) -> new String[len]);
4.6.4ArrayList的解决方案 => 限制类型替代法+传入指定类型数组接收法
ArrayList中有两个toArray方法,这两个方法彼此互相为重载方法。
第一个方法使用的是限制类型替代法,第二个方法使用的传入指定类型数组接收法。
由于ArrayList是一个泛型类(泛型参数是E),但是里面存储元素的数组不是E类型的,而是Object类型的,由此可见内部使用了限制类替代法,并且内部调用的toArray方法,将集合以数组的形式返回时,也是返回的Object[]类型的数组。
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
传入一个指定类型的数组进行接收,这样ArrayList就知道要将自身的数据填入到什么类型的数组里面了,填入到这个执行类型的数组中之后,将这个数组返回出去,这样返回出去的数组也是携带类型的。
要点:这里是声明的泛型函数进行推断的T的类型,可以保留类型。
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
3.为什么不直接将存储的Object数组使用Arrays.copyof拷贝后,转换为ArrayList定义的泛型类型之后,强转为ArrayList定义的E类型再返回出去呢?
首先类中进行定义的E类型本身就是一种假象,因为E类型最终会被类型擦除为Object,在运行时就是往Object类型进行转换,elementData本身就是Object[]类型的,使用E[[强转后还是Object[]类型的,根本不会转换成功的。
其次,因为类型E会被擦除为Object的原因,强制转换后的类型依然是Object[]类型的,但是编译器接收的时候,会以E的编译期类型为基准,比如说E的类型是Integer的,那么使用E[]进行强转后,使用E的编译器类型进行接收是(即使用Integer[]类型进行接收),编译器是不会抛出错误的,但是在运行期,这是一个将Object[]类型转换为Integer[]类型的向下转型的操作,一定会抛出ClassCastException异常的。
先进行定义一个泛型测试类,其中使用Object[]类型的elements字段进行存储元素,并初始化一个Object[]空数组值。
定义了两个获取elements的方法,一个是直接将原始的elements数组进行强转为T[]类型返回出去,另一个是使用Arrays.copy将elements复制为一个全新的数组后返回出去。
import java.util.Arrays;
public class TestClass<T> {
public Object[] elements = {};
public T[] getElements() {
return (T[]) elements;
}
public T[] getCopyElements() {
return (T[]) Arrays.copyOf(elements, elements.length);
}
}
我们直接使用getElements的时候,是不会抛出错误的:
其实如果严格按照类型转换进行分析,将一个Object[]尝试转换为Integer[]类型的数组,肯定是要抛出ClassCastException异常的,但是这里并没有有出现错误,可见我们的推测是完全正确,泛型T在编译器被擦除为Object之后,使用T[]进行强制类型转换的时候,会强制转换为Object[],和没进行转换的效果是一样的。
TestClass<Integer> integerTestClass = new TestClass<>();
integerTestClass.getElements();
但是如果我们使用Integer[]尝试去接收这个返回出来的数组的时候,就会抛出错误了:
TestClass<Integer> integerTestClass = new TestClass<>();
Integer[] elements = integerTestClass.getElements();
4.6.5构造泛型数组的解决方案
现在通用的解决方案都是:限制类型替代法 + 匿名函数指示法/传入指定类型数组接收法,这样就可以解决构造泛型数组百分之八十以上的问题。
4.7泛型类型的静态上下文中类型变量无效
4.8不能抛出或者捕获泛型类的实例
即不能适用throws将泛型类型的错误抛出,也不能使用try...catch进行捕获泛型类的实例。
实际上我们定义泛型类的时候,继承Throwable也会抛出编译期错误的,这些都是不被允许的。
public class TestClass<T> extends Throwable {
}
会抛出Generic class may not extend "java.lang.Throwable" 泛型类不能继承Throwable对象。
public class TestClass<T extends Throwable> {
private T exception;
public void throwException() {
throw exception;
}
}
会抛出Unhandled exception: T => 不被处理的exception:T。
public void throwException2() throws T {
System.out.println(exception);
}
private void catchException() {
try {
System.out.println(exception);
} catch (T e) {
e.printStackTrace();
}
}
会抛出Cannot catch type parameters 不能捕获类型参数的异常。
4.9可以取消对检查型异常的检查
Java异常处理机制要求保证为检查型异常提供一个Exception Handler(异常处理器),但是泛型却可以巧妙的取消这个机制。
4.10注意擦除后的冲突
假如定义一个泛型类Pair<T>,定义一个equals方法,接收一个泛型类型T类型的参数:
public class Pair<T> {
private T first;
private T second;
public boolean equals(T value) {
return first.equals(value) && second.equals(value);
}
}
'equals(T)' in 'Pair' clashes with 'equals(Object)' in 'java.lang.Object';both methods have same ensure, yet neither overrides the other。
‘Pair’ 中的 ‘equals(T)’ 与 ‘equals(T)’ 与 ‘java.lang.Object’ 中的 'equals(Object)'冲突;两种方法具有相同的函数签名,但都不会覆盖另一个。
也就是说:Pair<T>中定义的equals(T value)方法和Pair<T>的原始父类Object中的equals(Object obj)出现了函数签名冲突,抛出了编译期错误。
假设我们设定创建一个Pair<String>对象,如果我们不了解泛型擦除机制,那么我们会以为在Pair对象中存在以下两个方法:
public boolean equals(String value) {
return first.equals(value) && second.equals(value);
}
public boolean equals(Object obj) {
return (this == obj);
}
但是其实经过类型擦除后,最终变成equals(Object value),这就会和Object中的equals发生冲突。
要点:其实在Java继承机制中是允许子重写父类中方法的,但是如果是类型擦除后的重写是不支持的。
4.11泛型与接口参数化规范要求
4.11.1规范要求
泛型规范中有一个我们必须遵循的原则:"为了支持擦除类型,我们需要施加一个限制:倘若两个接口类型是同一接口的不同参数,一个类或类型变量就不能同时作为这两个接口类型的子类。"
定义Employee类型,implements实现Comparable<Employee>,并实现了其中的方法compareTo。
public class Employee implements Comparable<Employee> {
@Override
public int compareTo(Employee o) {
return 0;
}
}
定义Manager类型,继承了Employee,实现Comparable<Manager>,也就是等于Manager类实现了Comparable<Employee>接口和Comparble<Manager>,但是这样编译器是不允许的。
public class Manager extends Employee implements Comparable<Manager> {
}
'java.lang.Comparable' cannot be inherited with different type arguments: 'Employee' and 'Manager'。
意思就是说Comparable不能使用不同类型参数继承:使用了‘Employee’和'Manager'。
要点:类去实现一个接口的时候,不能实现了两个原始类型相同,但是参数类型不同的接口类。
4.11.2冲突原因
主要还是类型擦除的原因,Comparable<Employee>和Comparable<Manager>,类型擦除完之后都是Comparable,在JVM虚拟机中都是一个类型。
一个类只能同时进行实现一个Comparable,不能多次实现同一个接口。
主要还是JVM虚拟机的桥方法生成冲突的原因,实现了Comparable<X>类会获得一个桥方法:
public int compareTo(Object o) {
return compareTo((X) o);
}
那么实现了Comparable<Employee>和Comparable<Manager>,生成的桥方法X类型就是Employee和Manager,但是这样两个桥方法就是冲突的:
生成的这两个桥方法是签名冲突的,所以Java不允许同一个类实现了一个原始类型相同但是类型参数不同的接口。
public int compareTo(Object o) {
return compareTo((Employee) o);
}
public int compareTo(Object o) {
return compareTo((Manager) o);
}