一、泛型的由来
Java是一种强类型语言,日常开发中,无论是类的属性还是方法的参数和返回值都需要明确指定具体数据类型。这实际上是把类和方法与数据类型绑定了,这种理所当然的思想影响了编程的抽象性和灵活性。事实上,代码与它们能够操作的数据类型不是必须绑定的,同一套代码可以用于多种数据类型。这样,不仅可以复用代码,降低耦合,而且可以提高代码的可读性和安全性。
先来回顾一下,在Java开发中我们是如何一步一步突破数据类型限制的。我们以Fan类为例:
第一种情况,这个类只处理一种数据类型Person,那直接声明参数为此类型即可:
public class Fan { Person first; public Fan(Person first) { this.first = first; } public Person getFirst() { return first; } }
第二种情况,如果这个类可以处理多个数据类型,比如,除了Person还有Book和Article,我们一般会让这三种数据类型继承同一父类或实现统一接口(Namable),然后以父类或接口作为参数类型:
public class Fan { Namable first; public Fan(Namable first) { this.first = first; } public Namable getFirst() { return first; } }
第三种情况,如果继续扩展,这个类可以处理所有数据类型,按照上面的思路,我们去找所有数据类型的父类,Object!于是我们可以这样写:
public class Fan { Object first; public Fan(Object first) { this.first = first; } public Object getFirst() { return first; } }
第三种情况虽然已经是泛型的雏形,而且泛型的底层原理也确实是这样的,但我认为这样写依然没有跳出数据类型的思维方式,而且缺乏安全性和可读性。
正真的泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入。如下,T就是类型参数:
public class Fan <T> { T first; public Fan(T first) { this.first = first; } public T getFirst() { return first; } }
如果Object也可以解决同样的问题,为什么还要引入泛型?泛型有两个好处:
- 更好的安全性
- 更好的可读性
要解释安全性需要了解Java编译:Java有编译器和Java虚拟机,编译器将Java源代码转换成.class文件,虚拟机加载并运行.class文件。对于泛型类,Java编译器会将泛型代码转换为普通代码,这个过程 会将类型参数T擦除,替换为Object,插入必要的强制类型转换。也就是说:
泛型是通过类型擦除来实现的,Java虚拟机运行时对泛型基本一无所知!
泛型的安全性体现在代码调用上,如下泛型类调用和Object代码调用:
/*Object用法*/ Fan obj = new Fan("lilei"); Integer age = (Integer)obj.getFirst(); //不会有编译错误,但运行时出错 /*泛型用法*/ Fan<String> f = new Fan<>("lilei"); Integer age2 = f.getFirst(); //开发环境或编译器都会直接报错
使用Object方法无法在编译时发现错误,只有运行时才会报错;而使用泛型,由于输入了类型参数,在预编译或编译时就能及时发现错误。
泛型在可读性上也是优于Object的,比如Fan构造函数有多个参数时,全部写成Object造成阅读困难,而泛型可以按如下方式写:
public class Fan { /*都是Object类型,造成阅读困难*/ Object first; Object second; public Fan(Object first, Object second) { this.first = first; this.second = second; } } public class Fan <T,U> { /*泛型可以用不同通配符表示,更好阅读*/ T first; U second; public Fan(T first, U second) { this.first = first; this.second = second; } }
总是,泛型是计算机程序中一种重要的思维方式,它将数据结构和算法与数据类型想分离,使得同一套数据结构和算法能够应用于各种数据类型,而且可以保证类型安全,提高可读性。
二、泛型的一般写法
泛型可以在三个地方使用:
(1)泛型类
public class Fan <T,U> { T first; U second; public Fan(T first, U second) { this.first = first; this.second = second; } }
(2)泛型方法
除了泛型类,方法也可以是泛型的,而且,一个方法是不是泛型的,与它所在类是不是泛型没有什么关系。
public static <T> int getIndex(T[] arr, T elm) { for (int i = 0; i < arr.length; i++) { if (arr[i].equals(elm)) { return i; } } return -1; }
(3)泛型接口
实现泛型接口时,应该指定具体的类型。
public interface Comparable<T> { int compareTo(T to); }
三、泛型通配符
除了像上面那样声明类型参数外,泛型还可以用通配符 ? 来表示,通配符有三种写法:
- <?> ——无限定通配符,可以是任意类型
- <? extends E> ——限定通配符,可以是E或E的任意子类
- <? super E> ——超类型通配符,可以是E或E的任意父类
下面解释之前,我们先设定一个场景:有三个类A,B,C,其中,B和C都继承于A
public class A{} public class B extends A{} public class C extends A{}
如果,我们定义两个List:
List<A> listA = new ArrayList<A>(); List<B> listB = new ArrayList<B>();
这两个List是不能互相赋值的,即下面语句不合法:
listA = listB; //非法,因为listA指向listB后,可以通过listA向其中添加C元素,造成listB含有C元素,不合法 listB = listA; //非法,因为listA中可能含有C元素,会造成listB中包含C元素,不合法
那现在我们可以得出结论:如果一个方法接收的参数是List<A>类型,我们无法传入List<B>或List<C>类型实参;同样如果接收的参数是List<B>,更不能传入List<A>或List<C>。这样就造成一种不便:即使方法算法是一样的,因为参数不同,我们需要写多个方法。有没有办法只写一个方法,就可以指定接收特定类型范围的集合呢?泛型通配符就可以简化这种写法:
通配符机制的目的是:让一个持有特定类型(比如A类型)的集合能够强制转换为持有A的子类或父类型的集合。
泛型通配符主要针对以下两种需求:
- 从一个泛型集合里面读取元素
- 往一个泛型集合里面插入元素
除了这两种情况,一般情况下,我们依然使用类型参数来表达泛型。
下面分别介绍三种通配符的用法:
(1)无限定通配符 <?>
List<?> 的意思是这个集合是一个可以持有任意类型的集合,它可以是List<A>,也可以是List<B>,或者List<C>等等。因为不知道集合是哪种类型,所以只能够对集合进行读操作。并且你只能把读取到的元素当成 Object 实例来对待:
public void processElements(List<?> elements){ for(Object o : elements){ Sysout.out.println(o); } }
(2)上限定通配符 <? extends A>
List<? extends A> 代表的是一个可以持有 A及其子类(如B和C)的实例的List集合。依然不能写,但是读取后不用转Object了,可以转换成A:
public void processElements(List<? extends A> elements){ for(A a : elements){ System.out.println(a.getValue()); } }
(3)下限定通配符 <? super A>
List<? super A> 的意思是List集合 list,它可以持有 A 及其父类的实例,当你知道集合里所持有的元素类型都是A及其父类的时候,此时往list集合里面插入A及其子类(B或C)是安全的,但读取时依然只能转换成Object:
public static void insertElements(List<? super A> list){ list.add(new A()); list.add(new B()); list.add(new C()); }
四、泛型的局限性
我们已经知道泛型会在编译阶段擦除类型参数,替换为Object,这会对泛型的使用造成一些限制:
(1)类型参数不能使用基本数据类型:
Fan<int> f = new Fan<int>(10); // type params cannot be of primitive type
(2)不能像下面这样重载方法,因为类型擦除后,两个方法是完全一样的:
public static void test(Fan<Integer> arr){} public static void test(Fan<String> arr){}
(3)不能直接类型参数实例化对象,因为会造成误解,用户以为是实例化的T类型,实际上类型擦除后是Object类型,所以干脆被Java禁止
T elm = new T(); //不合法,Java禁止
如果一定要实例化,只能使用反射方法
public static <T> T create(Class<T> type) { try { return type.newInstance(); } catch (Exception e) { return null; } }
(4)对于泛型类声明的类型参数,实例变量和方法可以使用,但静态变量和方法不可以使用,因为静态变量和方法只有一份,无法满足所有实例化类型:
public class Singleton<T> { private static T instance; //不合法,T不能在静态变量中使用 public synchronized static T getInstance() { //不合法,T不能在静态方法中使用 } }
但是,静态方法可以单独声明自己的类型参数,即泛型方法
public class Singleton { public synchronized static T getInstance() { //合法,该方法为泛型方法 } }
(5)Java不允许创建泛型数组,因为数组是Java直接支持的类型,本身就会因赋值不当造成运行时错误,泛型数组同样会引起延时类运行错误,比较复杂,不再详解。