一、初识泛型
阅读此文前需要对泛型有一定的使用和了解
泛型的使用,是由于你希望在不改变原有代码的情况下,对不同类型对象的重用,例如我们常用的:
ArrayList<String> strings= new ArrayList<>();
ArrayList<Integer> integers= new ArrayList<>();
在泛型程序设计出来之前,我们通过set一个Object类型,并get一个Object类型(之后进行强制类型转换)来实现泛型。这种方式存在的弊端显而易见,就是脱离了错误检查的机制,容易在强制类型转换的时候发生异常。
以下是我们使用泛型程序设计的一个案例:
Object原始方式:
public static void main(String[] args) {
Test test = new Test();
test.setA("张三");
String a = (String) test.getA();
}public static class Test {
private Object a;public Object getA() {
return a;
}public void setA(Object a) {
this.a = a;
}
}
泛型程序设计方式:
public static void main(String[] args) { Test<String> stringTest = new Test<>(); String a = stringTest.getA(); Integer a = stringTest.getA();//error Variable 'a' is already defined in the scope } public static class Test<T> { private T a; public T getA() { return a; } public void setA(T a) { this.a = a; } }
在上述泛型程序设计方式的案例中,很明显编译器知道 stringTest.getA() 获得的返回值是一个String类型 而不是一个Integer类型,所以给出了一个Variable 'a' is already defined in the scope警告。
二、类型参数
此时不得不提到一个概念 类型参数:
泛型程序设计方式的案例中我们通过 如下代码 声明了一个Test<String>对象,其中就使用到类型参数。
Test<String> stringTest = new Test<>();
不过这段代码 我们使用了菱形语法(也可以称之为钻石表达式)来省略了类型参数。原本的写法应该是:
Test<String> stringTest = new Test<String>();
new Test<String>()中的String 就是一个类型参数实参
三、泛型类
泛型类是指有一个或多个类型变量的类,那什么是类型变量呢?
泛型程序设计方式的案例 中我们声明了一个Test类(该类就是一个泛型类)
public static class Test<T> { .........}
其中T就是一个类型变量,当然也可声明多个类型变量,例如 下面这个泛型类:
public static class Test<T,U> { .........}
一般我们使用E表示集合类型,K、V表示键值,S、T、U等可以表示任意类型
我们使用类型参数可以理解成 把类型变量全部替换为我们提供的类型,后续会讲解 “类型擦除” 来加深我们的理解。
四、泛型方法
首先要知道的是,泛型方法在普通类和泛型类中都可以定义,下面我们来声明几个泛型方法:
public static class Test { public <T> T getA(Class<T> t) throws Exception { return t.newInstance(); } public static <T> void getB() { } public static <T> T getC(T t) { return t; } public static <T> String getD(T t) { return t.toString(); } //通过观察不难发现,泛型方法的显著特征就是方法修饰符后面的<T> }
如何调用一个泛型方法?
当调用泛型方法时可以把 <具体类型> 放在方法名前
public static void main(String[] args) throws Exception { Test test = new Test(); String t = test.<String>getA(String.class); String hello = Test.<String>getD("hello"); }不过大多数情况下我们是可以省略类型参数 <String> 因为编译器能够推断出是<String>
五、类型变量的限定
有时候需要对类型变量加以限制,比如限制类型变量T是 某个类的子类 或者 某个接口的实现类。
使用extends关键字来限制类型变量:
public static class Test { public static <T extends A> String getA(T t) { return t.toString(); } public static <T extends C> String getC(T t) { return t.toString(); } } public static class A { } public static class B extends A { } public static interface C { } public static class D implements C { }public static void main(String[] args) { Test.getA(new A()); Test.getA(new B()); Test.getC(new D()); }通过<T extends A>、 <T extends C> 限制类型变量T 为A的子类 或者C的实现类,或者说我们所传递的类型参数实参 为A的子类 或者C的实现类。
也可以使用 <T extends A & Serializable>实现对T的多重限制
后续要了解到的概念 “通配符 ?” 也可以通过extends对通配符加以限制
六、类型擦除机制
在虚拟机中不存在泛型,只有普通的类和方法。
编译器会擦除类型参数,并将类型变量替换为该类型变量的限定类型,如果类型变量没有使用extends加以限制,则将类型变量替换为Object。
对泛型程序设计方式案例中的泛型类 进行类型擦除后,得到一个原始类型 也就是 Object原始方式案例中的类 ,将T 全部替换为了Object(由于类型变量T 未使用extends加以限定),如果使用了extends加以限定那么 会使用第一个限定类型来替换类型变量,要注意的是 最好不要将标识接口(没有抽象方法的接口)作为第一个限定类型,否则编译器可能在擦除类型之后为了保证方法的正确调用会插入强制类型转换。经过类型擦除后的方法可以叫作原始方法,如果类型变量未加以限定,那么原始方法的返回值就是Object,对于调用了该方法的表达式 编译器会在表达式中插入强制类型转换。
泛型方法的类型擦除:
public static <T extends Comparable<T>> T methodA(T[] a) { return a[0]; }经过类型擦除后变为:
public static Comparable methodA(Comparable[] a) { return a[0]; }
同时经过类型擦除后的方法会带来一些问题,我给出下述案例:
public static class A<T> { private T t; public T getT() { return t; } public void setT(T t) { this.t = t; } } public static class B extends A<String>{ @Override public void setT(String s) { s=s+"B"; super.setT(s); } } public static void main(String[] args) { A<String> a = new B(); a.setT(""); String t = a.getT(); System.out.println(t);//结果为 B }当我们知道类型擦除会使得 用上泛型的方法变为原始方法,那么我们执行 a.setT(""); 实则调用的应该是经过类型擦除后的 setT(Object t) 方法,很明显setT(Object t)方法和B类中的setT(String s) 方法 不是同一个方法(方法名相同,参数列表不同),实际上应该调用的是B对象的setT(Object t)方法,但是结果却正确调用了B对象的setT(String s)方法。
这是为什么呢?
编译器为了解决多态和类型擦除产生的冲突,生成了一个 “桥方法”,而上述的B对象的setT(Object t)方法正是编译器生成的桥方法。桥方法 setT(Object t)会调用B对象的setT(String s)方法(为了保证参数正确传递,会发生强制类型转换),这样就解决了冲突问题。
针对于a.getT()方法所生成的桥方法十分奇怪,因为a.getT()方法应该是 String getT(){...},而桥方法则是 Object getT(){...} ,同时存在 两个方法名相同、参数列表相同而返回值类型不同的方法 明显是不合法的,但是对于虚拟机却可以正确处理这种情况,它会通过参数类型和返回值类型共同指定一个方法。
七、关于泛型的一些限制
1、类型参数不能使用基本类型
不能使用基本类型代替类型参数 例如:ArrayList<int> ,我们应该使用其包装类:ArrayList<Integer>。
2、类型查询只适用于原始类型
1、使用instanceof 判断对象是否是某个接口或者类的实例时,instanceof右侧只能使用类或接口的原始类型,例如:
public static class A<T> { } public void typeCheck(Object o) { if (o instanceof A) { System.out.println("true"); } }
public void typeCheck(Object o) { if (o instanceof A<String>) { //error Illegal generic type for instanceof System.out.println("true"); } }
2、getClass方法返回的是类的原始类型
A<String> a=new A<>();
A<Integer> integerA=new A<>();
System.out.println(a.getClass()==integerA.getClass());//true
3、强制转换为泛型类型会得到一个警告
Object o = new A<String>();
A<String> a = (A<String>) o;//Unchecked cast: 'java.lang.Object' to 'com.qckj.App.A<java.lang.String>'
3、不允许手动构造参数化类型的数组
观察以下两个案例
案例一:
Object[] o = new Integer[10]; o[0] = "1";由于数组的特性,我们可以实现由父类数组的引用指向子类数组的对象,在该案例中 o[0] = "1" 在运行时会抛出数组存储异常 java.lang.ArrayStoreException,数组会记住它实际元素的类型,o 只能存储Integer类型的对象。
案例二: 如果我们允许创建参数化类型的数组会发生什么?
Object[] oo=new A<String>[10];//实际这行构造参数化类型数组的代码是不通过的 oo[0]=new A<Integer>();如果允许创建参数化类型的数组,经过类型擦除 本案例会通过数组存储的检查(越过了数组存储检查机制,在类型擦除后 A<String>和A<Integer> 都可以看作是 A<Object>),但最终取出使用的时候仍会产生一个类型错误,所以不允许 new A<String>[10] 来创建参数化类型的数组。
4、通过可变参创建一个泛型数组
我们学习过可变参数,可变参数可以理解为 动态创建一个数组,如下代码:
public static class A<T> { public static void method(String... strings){ for (String string : strings) { System.out.println(string); } } } public static void main(String[] args) { A.method("a","b","c"); }
我们试着使用泛型方式,但是会得到一个警告,:
public static class A<T> { public static <T> T[] method(T... ts) {//Possible heap pollution from parameterized vararg type return ts; } } public static void main(String[] args) { String[] method = A.method("1", "2"); }
会获得一个产生 “堆污染” 的警告,如果你对自己的代码足够自信,保证程序执行的时候不会产生类型转换异常,你可以在方法或者类上加 @SuppressWarnings("unchecked") 或者@SuppressWarnings("all") 来抑制警告 或者直接忽视警告。
5、不允许实例化类型变量
以下两种创建实例的方式是不被允许的:
public static class A<T> { public T method(T t) throws Exception { return new T();//error Type parameter 'T' cannot be instantiated directly } }
public static class A<T> { public T method() throws Exception { return T.getClass().getConstructor().newInstance();//error } }
你可以通过反射的方式实例化一个对象:
public static class A { public static <T> T method(Class<T> t) throws Exception { return t.getConstructor().newInstance(); //success } } public static void main(String[] args) throws Exception { String s = A.method(String.class); Integer i = A.method(Integer.class); }为什么这种方式能通过呢?你可以从类型擦除的思维去理解它。
6、不允许手动构造泛型数组
例如:
public static class A<T> { //该方式不被允许 public T[] method() { return new T[10];//Type parameter 'T' cannot be instantiated directly } }
但是我们可以通过 四、通过可变参创建一个泛型数组 来构造一个泛型数组。
此外我们也可以通过如下案例 巧妙地运用带泛型的函数式接口来灵活地创建泛型数组(如果你对lambda表达式很了解的话,那么为什么这种方式能通过呢?你仍然可以从类型擦除的角度去理解)并指定初始容量:
public static class A<T> { public static <T> T[] method(IntFunction<T[]> intFunction, int i) { return intFunction.apply(i); } } public static void main(String[] args) { String[] method = A.method(String[]::new, 1); Integer[] method1 = A.method(Integer[]::new, 1); // String[] method2 = A.method(A<String>[]::new, 1);当然这样是行不通的,不然他就违背了 三、不允许手动构造参数化类型的数组 。 }
7、不能在静态上下文中使用类型变量
public static class A<T> {
public static T t;//error cannot be referenced from a static context
}
8、不能捕获泛型类的实例,泛型类不能拓展Throwable
1.泛型类不能拓展Throwable public static class A<T> extends Throwable{//error Generic class may not extend 'java.lang.Throwable' }
2.不能捕获泛型类的实例 public static class A<T extends Throwable> { public void method(Class<T> tClass) { try { throw tClass.newInstance(); } catch (T | InstantiationException | IllegalAccessException e) {// error Cannot catch type parameters throw new RuntimeException(e); } } }允许以下方式抛出异常:
public static class A<T extends Throwable> { public void method(Class<T> tClass) throws T, InstantiationException, IllegalAccessException { throw tClass.newInstance(); } } public static void main(String[] args) { A<RuntimeException> runtimeExceptionA = new A<>(); try { runtimeExceptionA.method(RuntimeException.class); } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } }
9、为了避免桥方法发生冲突,不允许实现同一个接口的不同参数化
例如:
public static class A implements Comparable<A>{ @Override public int compareTo(A o) { return 0; } } //这样会发生桥方法冲突,你肯定不希望有两个 public int compareTo(Object o) 同时存在 public static class B extends A implements Comparable<B>{//error }
八、 泛型类型的继承规则
1、类A与类B具有继承关系,但是C<A>与C<B>没有继承关系
public static class A {}
public static class B extends A {}
public static class C<T> {}
public static void main(String[] args) {
C<A> ac = new C<>();
C<B> bc = new C<>();
ac=bc;//error
}
2、泛型类可以拓展其他泛型类或者实现其他泛型接口
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {...}
当然 AbstractList<String> 和 ArrayList<String> 是继承关系 这点与第1条显然不同
九、 通配符 “ ?”
在了解到 八、 泛型类型的继承规则 的第1条后,我们知道了 两个类型参数如果具有继承关系,并不意味着 同一个类的这两种参数化类型具有继承关系,当然也无法实现对象传递。而通配符的出现在一定程度上解决了这个问题,同时也会存在一些限制。
1、通配符的子类型限定:使用 ?extends 对类型参数加以限定:
public static class A { } public static class B extends A { } public static class C<T> { private T t; public T getT() { return t; } public void setT(T t) { this.t = t; } } public void method(C<? extends A> o){ }?extends A 限制了我们所传递对象的类型参数应该是A 或者 A的子类
public static void main(String[] args) { App app = new App(); app.method(new C<A>()); app.method(new C<B>()); C<? extends A> ca=new C<A>(); C<? extends A> cb=new C<B>(); }C<A>、C<B>是C<? extends A>的子类型
而使用关键字 extends 后的限制就是会导致我们无法调用 C<? extends A> 对象的setT方法,但可以调用getT方法。
对于C<? extends A> 的 getT 方法 : public ?extends A getT() ,他所返回的类型 是A类或者A类的子类,我们不知道具体返回的是哪种类型,但是完全可以将返回的结果赋值给 A类的引用。
而对于C<? extends A> 的 setT 方法: public void setT(?extends A) ,看起来方法应该传递一个 A或者A的子类型参数,但是对于编译器来说它需要一个确切的类型,通配符 ?并不能匹配一个具体的类型,希望下面这个案例能够让你理解 setT 为什么无法使用:
//声明一个B2 类(此时A有两个子类 B 和 B2) public static class B2 extends A { }public static void main(String[] args) { C<? extends A> ca = new C<B>(); ca = new C<B2>(); ca = new C<A>(); }如你所见,我们可以将 C<B>、C<B2>、C<A>的对象赋值给 C<? extends A>的引用 ca,也就是说 ca 的私有属性可能是 B类型 也可能是 B2类型 或者 A类型。
假设一种情况:如果ca的引用指向 C<B>对象,那么ca引用实际指向的私有属性应该是 B类型(而C<B>的桥方法会将接收到的对象强转为B类型),之后调用 setT方法注入一个B2类型的对象就会发生一个类型错误。
你可能会认如果属性是一个A类型 那么就可以存储A、B和B2了(好像也能正确调用setT方法),很可惜你所想的情况和通配符没有任何关系,你想的应该是下面这种情况:
C<A> ca2=new C<>(); ca2.setT(new B());上面这段代码已经被编译器确定下来了,桥方法可以明确将Object类型转换为A类型 且不会发生任何变数。
2、通配符的超类型限定 :使用 ?super 对类型参数加以限定:
public static class A { } public static class B extends A{ } public static class C<T> { private T t; public T getT() { return t; } public void setT(T t) { this.t = t; } }public static void main(String[] args) { C<? super B> bc = new C<A>(); bc = new C<B>(); bc = new C<>(); bc.setT(new B()); Object t = bc.getT(); }?super B 限制了我们所传递对象的类型参数应该是B 类型 或者是 B 的父类型
对于C<? super B> 的 getT 方法 : public ?super B getT() ,返回的结果 是B或者B的父类型,只能使用Object去接收返回结果。
对于C<? super B> 的 setT 方法: public void setT(? super B),该setT方法参数只能接收 B类型及其子类型对象,从桥方法的角度去理解:桥方法中不论是将接收的参数 强转为B类型又或者是B的某个父类型,该方法只接收B类型及其子类型参数完全合法。
3.无限定通配符
可以对通配符不加以任何限定,直接使用 “?”
C<?>
C<?> 的setT方法将不能被调用,getT方法的返回值只能赋给Object引用