第十五章 泛型(干货)
泛型这一章不知道是因为翻译的原因还是什么,感觉《编程思想》讲的混乱无比。花了很多的时间研究泛型,现在力求用最简单的语言,最简明的示例把这一章说清楚。
前言
还是要来区分一下细节的概念,对理解泛型影响还是蛮大的。
1、<T>与不在<>中的T的区别
class A <T> { <T> T getT(T t) { return t; } } public class Demo1 { public static void main(String[] args) { // TODO Auto-generated method stub A<String> a = new A<String>(); String s = a.getT("test"); System.out.println(s); } }
输出结果:
test
结果分析:<T>这里的T叫类型参数,是在定义泛型方法、泛型类时使用的,它本质是参数。
getT(T t):这里面的T称为参数化类型,它是将原本的类型参数化了。
从中还能够看出,泛型方法与泛型类是相互独立的。泛型类中的T,在泛型方法中不可见。
2、[T、E、K、V]
其实类型参数用什么字母表示都无所谓,高兴的话可以A<WWW>,如果同学们看JDK文档的话,了解这些字母的含义还是有益处的。
T:代表一般的任意类
E:代表元素(Element),或者异常(Exception)
K:代表键(Key)
V:代表值(Value)
3、介绍一些学习泛型会用到的,但是jdk文档说的很让人迷惑的接口
(1)ParameterizedType 参数化类型,表示例如、Collection<String>。
(2)TypeVariable 就是类型参数,也就是Collection<E>和Map<K,V>中的E,K和V。
(3)GenericDeclaration 泛型声明,只有实现了这个接口的类才能够声明泛型,事实上,只有泛型方法(包括构造方法),泛型类。
也就只有Class,Constructor,Method三个类实现了此接口。
如果前言很迷或也没有关系,有个印象就好。笔者也是从小白开始学习的,后文力求把令人困惑的地方说清楚。
一、泛型类与泛型接口
接口就是极度抽象的类,所以泛型类与泛型接口在本质上没有差别。
1、示例一:通过一个简单的实例了解泛型类与接口的用法
interface Interface<T> { void printClass(T t); } class Base1<T> implements Interface<T> { public void printClass(T t) { System.out.println(t.getClass().getName()); } } class Base2 implements Interface<Integer> { public void printClass(Integer t) { System.out.println(t.getClass().getName()); } } public class Demo2 { public static void main(String[] args) { Base1<String> b1 = new Base1<String>(); Base2 b2 = new Base2(); b1.printClass("test"); b2.printClass(1); } }
输出结果:
java.lang.String
java.lang.Integer
结果分析:
从这里可以看到继承或是实现一个泛型类的方式,基类带有类型参数<T>则子类比带,基类不带则子类不带。
二、泛型方法
1、示例一:静态方法要使用泛型能力必须成为泛型方法。
class C { static <T> String getNmae(T t) { return t.getClass().getName(); } } public class Demo3 { public static void main(String[] arg) { C c = new C(); System.out.println(C.getNmae(c)); } }
输出结果:
chapter15.C
结果分析:静态方法是不可以访问所属泛型类的类型参数的,要拥有泛型能力必须成为泛型方法。如前言中所述,泛型方法与泛型类是相互独立的。如果泛型方法与泛型类类型参数同名,则泛型类的类型参数在泛型方法中不可见,判断是以泛型方法为主。泛型方通常不需要指明类型参数的实际类型,编译器会进行类型参数推断为我们找出实际类型。在特殊的时候也可以显示声明,比如,Arrays.<Integer>asList(1,2,3);。
三、类型擦除
1、类型擦除的定义
类型擦除:泛型的信息只存在于编译阶段,在进入虚拟机之前,与泛型相关的信息会全部被擦除。
通俗的讲,在虚拟机内部,泛型类、泛型方法与普通类、普通方法没有什么区别。泛型的引入也是为了兼容以前老旧的代码和类库。
2、类型擦除的解析
(1)一个经典问题
List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); System.out.println(l1.getClass() == l2.getClass());
这个程序输出的结果是true,原因是类型擦除。
让我们来通过RTTI看一下l1与l2在虚拟机中的类型是什么
import java.util.*; public class Demo5 { public static void main(String[] args) { List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); System.out.println("l1.getClass().getName() : " + l1.getClass().getName()); System.out.println("l2.getClass().getName() : " + l2.getClass().getName()); } }
输出结果:
l1.getClass().getName() : java.util.ArrayList
l2.getClass().getName() : java.util.ArrayList
结果分析:可以看到,l1与l2在虚拟机中没有任何的泛型信息,与普通的类引用是一样的。
(2)泛型类内的T在虚拟机中究竟代表什么讯息
import java.lang.reflect.*; class Erasure<T> { T t; } public class Demo6 { public static void main(String[] args) { Erasure<String> e = new Erasure<String>(); Field[] f = e.getClass().getDeclaredFields(); System.out.print("FiledName : " + f[0].getName()); System.out.println(" FieldType : " + f[0].getType()); } }
输出结果:
FiledName : t FieldType : class java.lang.Object
结果分析:可以看到T在虚拟机中的类型是class java.lang.Object而不是String。
总结:现在我们可不可这样说,在虚拟机中,Erasure<String>被擦除成原生类型Erasure,Erasure泛型类内的T被擦除成Object。这么说没有问题,但是少了一种情况,边界。
四、泛型引发的动作
class A <T> { T t; void set(T t1) { t = t1; } T get() { return t; } } public class Demo1 { public static void main(String[] args) { // TODO Auto-generated method stub A<String> a = new A<String>(); a.set("test"); String s = a.get(); System.out.println(s); } }
输出结果:
test
结果说明:上文不是说过类型擦除会将类内部的T擦成Object吗,但是这里get()返回的是确切的类型String啊。原因是编译器在编译这部分代码时,会在get()方法调用之后,插入类型转换(String)Object,将Object转换成String。泛型引发的动作,即为对传进来的值进行额外的编译期检查,对传出去的值进行转型。
五、边界
同学们鸭,千万千万不要把边界与通配符的上下限弄混淆了呀。区别放到后面说哦。
1、边界基本用法
边界:用于限定传递给类型参数T的类型,<T extends X>X以及X的子类可以传递给类型参数T,用于定义泛型方法、泛型类。
将上面代码稍改一下:
class Erasure<T extends Number> { T t; } public class Demo6 { public static void main(String[] args) { Erasure<Integer> e = new Erasure<Integer>(); Field[] f = e.getClass().getDeclaredFields(); System.out.print("FiledName : " + f[0].getName()); System.out.println(" FieldType : " + f[0].getType()); } }
输出结果:
FiledName : t FieldType : class java.lang.Number
结果分析:惊了,这里居然有类型信息了。事实上,泛型擦除是擦除到上边界的,这里擦除到了Number。Object是所有类的基类,所以Erasure<T>类内部的T擦除到了上边界Object。注意,泛型中不准Erasure<T super Integer>这样定义泛型类的,没有下边界这一说。
这下咱们可以下结论了
总结:在虚拟机中,Erasure<T>被擦除成Erasure(原生类型),Erasure<T>类内部的T被擦除到上边界。
六、通配符"?"
1、无限定通配符
1.1无限定通配符的引入
这样写会报编译错误吗?
class Base {} class Derived extends Base{} List<Derived> l1 = new ArrayList<Base>(); List<Base> l2 = l1;
解释说明:肯定报编译错误。因为List<Base>根本就不是List<Derived>的基类,在编译器看来,他们根本就是不同的类型。类型擦除以后,在虚拟机中他们就是一样的类型。但是在实际编程中确实有这样的需求,想要一种引用可以指向多种类型的对象。所以就引入了通配符。
List<?> l = l1;
l = l2;
这样写就都可行。
1.2无限定通配符的限制
示例:
import java.util.*; public class Demo4 { public static void main(String[] args) { List<?> l = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5)); //l.add(6);//报参数不合适的错误 l.add(null); //Integer i = l.get(0);//报类型不匹配 Object o = l.get(0); System.out.println(o); System.out.println(l); System.out.println("l.size() = " + l.size()); } }
输出结果:
1
[1, 2, 3, 4, 5, null]
l.size() = 6
结果分析:由于"?"代表未知的类型,所以List<?>容器中存放的就是指向未知类型的引用,故而向容器中添加任何类型的数据都是禁止的。但,任何的引用都可以赋值为null,所以添加null值是准许的。同样道理,从List<?>容器中读出来的也是未知类型,所以使用Object对象才能够接收。这里的未知类型是针对编译器而言的,虚拟机是知道实际类型的,报错指的也是通不过编译。所以可以借助反射,绕过编译器去做很多编译器不准的事。
1.3"T"与"?"的区别
T:是一个参数
?:通配符,但终究代表一个具体类型
class Hold<T> {} Hold<?> h;
分析:T相当于一个参数,用于定义泛型类、泛型方法。可以将?传递给T,上限通配符,下限通配符是一样的道理。
2、有上限的通配符
2.1有上限通配符<? extends X>
示例:
import java.util.*; class Father {} class Son extends Father {} public class Demo7 { public static void main(String[] args) { List<? extends Father> l = new ArrayList<Father>(Arrays.asList(new Son())); Father f = new Father(); //l.add(f);//编译错误,参数类型不合适 l.add(null); f = l.get(0); System.out.println(f); } }
输出结果:
chapter15.Son@22f71333
结果分析:还是想要在这里强调,通配符虽然可以代表很多类型,但是在引用赋值以后,其实就是一种具体类型了。(就像支付宝集五福,万能福变成敬业福)。谈论的泛型的类型都是针对编译而言的,虚拟机根本就不知道泛型。上限通配符也是不能够写入的,道理同上述。但是它可以读出,List<? extends Father>这样写,就是在告诉编译器,List容器中的引用类型肯定是Father的子类,用一个Father类型的引用肯定能够接受List方法的返回值。
3、有下限的通配符
3.1有下限的通配符<? super X>
class Father {} class Son extends Father {} public class Demo7 { public static void main(String[] args) { List<? super Son> l = new ArrayList<Father>(Arrays.asList()); Son s = new Son(); //l.add(new Father());//编译错误类型不合适 l.add(s); l.add(null); Object o = l.get(0); System.out.println(l); System.out.println(o); } }
输出结果:
[chapter15.Son@22f71333, null]
chapter15.Son@22f71333
结果分析:可以看到下限通配符有了写的能力,List<? super Son>这样写是在告诉编译器,List中的引用一定是Son及其祖先类型的,放心把Son及其子类写入Llist。编译器并不知道从此List中读出数据的类型,只能用Object接。
4、边界与通配符的区别
边界<T extends X>:在定义泛型类,泛型方法时使用,用于限制传入类型参数T的数据类型必须是X及其子孙型。
通配符:在声明域时使用,是一个具体的类型,<? extends X>用于限定传递给引用的对象其泛型类型必须是X及其子孙型,其余同理。
import java.util.*; class Father {} class Son extends Father {} class Hold<T extends Father> { List<? extends T> l; Hold(List<? extends T> l1) { l = l1; } List<? extends T> get() { return l; } } public class Demo7 { public static void main(String[] args) { List<Son> l = new ArrayList<Son>(Arrays.asList()); Hold<Son> h = new Hold<Son>(l); Son s = new Son(); l.add(s); l.add(null); List<?> l2 = h.get(); System.out.println(l2); } }
输出结果:
[chapter15.Son@3498ed, null]
结果分析:这个示例有点绕,但还是能够体现出他们的区别。同样这里也体现了extends在不同环境下的不同含义。
七、擦除的补偿
泛型的擦除会产生很多的限制,所以要采取一些措施来弥补这些缺陷。
1、缺陷:任何在运行时需要知道确切类型信息的操作都无法完成
class Erasure<T> { public static void f(Object o) { if(o instanceof T) { //Error T var = new T(); //Error T[] array = new T[10]; //Error T[] array1 = (T[])new Object[10]; //Unchecked warning } } }
2、对o instanceof T进行补偿(换一种方式实现此功能)
public class Demo8 { public static void main(String[] args) { Integer i = 1; System.out.println(Integer.class.isInstance(i)); } }
结果分析:可以使用Class对象自带的动态的isInstance()方法。
3、创建类型实例
class ClassAsFactory<T> { T x; T create(Class<T> kind) { try { x = kind.newInstance(); System.out.println("Create succeeded"); return x; }catch(Exception e) { e.printStackTrace(); } return null; } } public class Demo8 { public static void main(String[] args) { ClassAsFactory<Demo8> c = new ClassAsFactory<Demo8>(); c.create(Demo8.class); } }
结果分析:还是通过Class对象的newInstance()方法创建。
4、创建泛型数组
原则上是不能够创建泛型数组的,数组会跟踪自己的实际类型,类型擦除以后数组没办法跟踪自己的实际类型。
成功创建泛型数组的唯一方式:创建一个被类型擦除的数组,然后对其转型。
T[] array = (T[]) new Object[10];
事实上,这样会得到一个Unchecked警告。
八、泛型中要注意的问题
1、基本类型是不能够传递给类型参数的
List<int> \\错误
2、一个类不可实现同一个泛型接口的两种变体
interface Payable<T> {} class Employee implements Payable<Employee> {} class Hourly extends Employee implements Payable<Hourly> {}
编译通不过
3、转型警告
(T)object
4、重载
void f(List<A> l) {} void f(List<B> l) {}
由于类型擦除的原因,List<A>与List<B>是同样类型,虚拟机没办法根据参数区别重载方法。
5、自限定类型
class SelfBounded<T extends SelfBounded<T>> {} class A extends SelfBounded<A> {}
6、List相关泛型的区别
List<T>:T是类型参数,前面说过,这种形式用在泛型类定义的地方。
List<T extends Object>:同上,不过这里限定了传给类型参数T的参数类型是Object的子孙类。
List:原生类,前面讲过,List<T>会擦除成List。用法与List<?>一样,没有List<?>好。只有老式代码才会出现List,使用List就会给警告,一般不用。
List<?>:?通配符,此类型引用接受任何类型对象。
List<Object>:与List<?>的区别是,它可以添加(add)任何类型的对象,引用只能接受如ArrayList<Object>这般对象。
List<? extends Object>:和List<?>基本一样,略有区别,忽略也没多大影响。。