Java
语言基础

Java语法糖:泛型

简介:泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。

1. 泛型简介

在泛型(Generics)出现之前,下面的代码很常见:

  • public static void main(String[] args) {
  • List list = new ArrayList();
  • list.add("abc");
  • list.add("def");
  • System.out.println((String)list.get(0));
  • }

List里面的内容是Object类型的,因此可以存入或获取任何类型的对象,但是这种代码存在两个问题:

  1. 当一个对象放入集合时,集合不会记住此对象的类型,当再次从集合中取出此对象时,该对象的编译类型变成了Object;
  2. 运行时需要手动强制转换类型为具体目标类型,可能会出现java.lang.ClassCastException类型转换异常。

在泛型出现之后,可以在一定程度上避免上面的两个问题:

  • public static void main(String[] args) {
  • List<String> list = new ArrayList<String>();
  • list.add("abc");
  • list.add("def");
  • System.out.println(list.get(0));
  • }

泛型的引入极大地增强了Java语言的类型功能,带来了很多的好处:

  1. 类型安全。类型错误现在在编译期间就被捕获到了,而不是在运行时当作java.lang.ClassCastException展示出来,将类型检查从运行时挪到编译时有助于开发者更容易找到错误,并提高程序的可靠性;
  2. 消除了代码中许多的强制类型转换,增强了代码的可读性;
  3. 为较大的优化带来了可能。

泛型是JDK 1.5的一项新增特性,类似于C++语言的模板(Template),它的本质是参数化类型(Parametersized Type)的应用,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。

2. 泛型的类型擦除

在介绍泛型的使用之前,我们先看一个泛型相关的重要概念:泛型擦除,也叫做泛型的类型擦除

Java语言中的泛型只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList<Integer>ArrayList<String>就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。

我们可以观察下面的代码:

  • public static void main(String[] args) {
  • List<String> stringList = new ArrayList<String>();
  • List<Integer> integerList = new ArrayList<Integer>();
  • System.out.println(stringList.getClass() == integerList.getClass()); // true
  • }

从输出结果可知ArrayList<String>ArrayList<Integer对象的类型其实是相同的,这意味着,通过改变泛型的方式试图定义不同的重载方法也是不可以的,如下面两个重载方法:

  • public void sort(List<String> list);
  • public void sort(List<Integer> list);

这种方式的重载是不被允许的,编译器也会报错。

注1:在Sun JDK 1.6中,可以通过给这两个方法指定不同的返回值类型即可通过编译,这是由于虽然方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名之中,所以返回值不参与重载选择,但是在Class文件格式之中,只要描述符不是完全一致的两个方法就可以共存。也就是说,两个方法如果有相同的名称和特征签名,但返回值不同,那它们也是可以合法地共存于一个Class字节码文件中的。如:

  • public String sort(List<String> list);
  • public Integer sort(List<Integer> list);

注2:正是由于Java的泛型是伪泛型,运行时期还是存在着强制类型转换,因此会有一定的性能损耗的影响。

3. 泛型类

泛型类可以提供给使用者一种限定类型的方式,最常见的例子便是Java集合类如ArrayList、HashSet、HashMap等类大量运用到了泛型。下面是一个泛型类的简单定义:

  • package com.coderap.generic;
  • // 泛型类
  • class GenericClass<T> {
  • public T value;
  • public GenericClass(T value) {
  • this.value = value;
  • }
  • public T getValue() {
  • return value;
  • }
  • }
  • public class GenericTest {
  • public static void main(String[] args) {
  • GenericClass<Integer> integerGeneric = new GenericClass(123);
  • Integer integerValue = integerGeneric.getValue();
  • System.out.println("integerValue = " + integerValue); // integerValue = 123
  • GenericClass<String> stringGeneric = new GenericClass("abc");
  • String stringValue = stringGeneric.getValue();
  • System.out.println("stringValue = " + stringValue); // stringValue = abc
  • GenericClass<Float> floatGeneric = new GenericClass(3.14f);
  • Float floatValue = floatGeneric.getValue();
  • System.out.println("floatValue = " + floatValue); // floatValue = 3.14
  • }
  • }

需要注意的是,在给泛型类指定泛型创建相应的对象时,泛型类型不能是基本类型如int、long、short等。

4. 泛型接口

泛型接口也非常常见,比如java.lang包提供的Comparable接口就是一个泛型接口:

  • public interface Comparable<T> {
  • public int compareTo(T o);
  • }

定义泛型接口的方式与定义泛型类类似,但在使用时则不同。当我们自定义一个类实现了某个泛型接口时,该类存在两种情况:

  1. 具体类实现泛型接口,但不明确泛型类型;这种方式实现的具体类,泛型类型的确定会放在类被实例化的过程中,此时我们定义的类应该保留泛型的定义,如下:
  • // 实现了泛型接口的泛型类,定义时不明确泛型类型
  • class GenericClass<T> implements Comparable<T> {
  • public T value;
  • public GenericClass(T value) {
  • this.value = value;
  • }
  • public T getValue() {
  • return value;
  • }
  • @Override
  • public int compareTo(T o) {
  • return 0;
  • }
  • }

可以看出,在这种方式下,具体类类名后面依旧需要添加与接口泛型标示符一致的<T>用于表明此时具体类依旧是一个泛型类。

  1. 在实现泛型接口的具体类的定义时明确泛型类型;这种方式下,具体类的泛型类型在声明时就确定了,实例化过程中无法更改。如下:
  • // 实现了泛型接口的泛型类,定义时明确泛型类型
  • class GenericClass implements Comparable<String> {
  • public String value;
  • public GenericClass(String value) {
  • this.value = value;
  • }
  • public String getValue() {
  • return value;
  • }
  • @Override
  • public int compareTo(String o) {
  • return 0;
  • }
  • }

在这种方式下,具体类中实现的泛型接口中的方法已经明确了对应的泛型类型。

5. 泛型通配符

在泛型的使用中,List<Object>不是List<String>的父类型,List<Number>不是List<Integer>的父类型,试图用以下方式赋值是不允许的:

  • public static void main(String[] args) {
  • List<Number> numberList = new ArrayList<Number>();
  • List<Integer> integerList = new ArrayList<Integer>();
  • numberList = integerList; // #Error: Type mismatch: cannot convert from List<Integer> to List<Number>
  • }

第4行将报错Type mismatch: cannot convert from List<Integer> to List<Number>。针对这个问题,Java给开发者提供了通配符?

  • public static void main(String[] args) {
  • List<String> stringList = new ArrayList<String>();
  • List<Integer> integerList = new ArrayList<Integer>();
  • printList(stringList);
  • printList(integerList);
  • }
  • private static void foreach(List<?> list) {
  • for (Object o : list)
  • System.out.println(o);
  • }

<?>是泛型类型通配符,表示是任何泛型的父类型,这样List<Object>List<String>这些都可以传递进入foreach(List<?> list)方法中,注意这里的参数不能写成List<E>,这样就报错了,E未定义。当然<?>也可以不加,不过如果传递一个List<E>给List,相当于传递一个只承诺将它当作List(原始类型)的方法,这将会破坏使用泛型的类型安全,编译器会发出警告。

注意,此处?是类型实参,而不是类型形参 。它是一种真实的类型,可以把?看成所有类型的父类。另外,使用类型通配符,只能从中检索元素,不能添加元素。

6. 泛型方法

泛型方法和泛型类中具有泛型特性的方法并不一样。泛型类是在实例化类的时候指明泛型的具体类型,而泛型方法是在调用方法的时候指明泛型的具体类型。

我们先观察两个方法:

  • class GenericClass<T> {
  • public T value;
  • public GenericClass(T value) {
  • this.value = value;
  • }
  • public T getValue() {
  • return value;
  • }
  • public <T> T genericMethod(Class<T> clazz) throws InstantiationException, IllegalAccessException {
  • T instance = clazz.newInstance();
  • return instance;
  • }
  • }

在上面的泛型方法的定义中,getValue()虽然返回值定义为了泛型类型T,但它并不是泛型方法;而genericMethod(Class<T> clazz)才是泛型方法,其中的<T>表示声明此方法为泛型方法,而T表示方法的返回值类型。只有在定义中声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。与泛型类的定义一样,此处T可以随便写为任意标识。

需要注意的是,就算类没有被泛型化,它其中的方法也可以是泛型方法,下面的代码是正确的:

  • class NormalClass {
  • public <T> T genericMethod(Class<T> clazz) throws InstantiationException, IllegalAccessException {
  • T instance = clazz.newInstance();
  • return instance;
  • }
  • }

下面的代码对常见的出现泛型定义的方法做了详细的对比:

  • // 泛型类
  • class GenericClass<T> {
  • public T value;
  • public GenericClass(T value) {
  • this.value = value;
  • }
  • /**
  • * 普通方法,虽然在方法中使用了泛型,但是这并不是一个泛型方法
  • * 这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型
  • */
  • public T getValue() {
  • return value;
  • }
  • /**
  • * 这个是泛型方法,其中的E可以换做任何其他的描述符
  • * 甚至是类的泛型类型T,此时这两个T描述符代表的并非是同一种泛型类型
  • */
  • public <E> E genericMethod(Class<E> clazz) throws InstantiationException, IllegalAccessException {
  • E instance = clazz.newInstance();
  • return instance;
  • }
  • // 普通方法,只是使用了Generic<Number>这个泛型类型做形参
  • public void foreach(List<Number> list) {
  • for (Number number : list) {
  • System.out.println(number);
  • }
  • }
  • // 普通方法,只不过使用了Generic<?>这个泛型类型做形参
  • public void iterate(List<?> list) {
  • for (Object o : list) {
  • System.out.println(o);
  • }
  • }
  • }

需要注意的是,泛型类型也可以用作可变参数:

  • public <T> void foreach(T... args){
  • for(T t : args){
  • System.out.println(t);
  • }
  • }

7. 泛型约束

在使用泛型的时候,我们还可以约束传入的泛型类型的上下边界。通过extends约束上界,super关键字约束下界。上界约束可以规定传入的泛型必须是某个类型的子类型,而下界约束用于规定传入的泛型必须是否个类型的父类型。

假设目前有A、B、C三个类,B继承于A,C继承于B:

  • class A {
  • }
  • class B extends A {
  • }
  • class C extends B {
  • }
  1. 定义class的时候不能用通配符?或下界约束super,只可以使用extends关键字进行上界约束,代码如下:
  • // 泛型类
  • class GenericClass<T extends B> {
  • T value;
  • public GenericClass(T value) {
  • this.value = value;
  • }
  • public T getValue() {
  • return value;
  • }
  • }

此时在使用GenericClass,只能传入B类型或B的子类C类型为泛型,传入A类型作为泛型将会报错:

  • public class GenericTest {
  • public static void main(String[] args) {
  • // GenericClass<A> aGenericClass = new GenericClass(new A());
  • // System.out.println("aGenericClass = " + aGenericClass.getValue().getClass());
  • GenericClass<B> bGenericClass = new GenericClass(new B());
  • System.out.println("bGenericClass = " + bGenericClass.getValue().getClass()); // bGenericClass = class com.coderap.generic.B
  • GenericClass<C> cGenericClass = new GenericClass(new C());
  • System.out.println("cGenericClass = " + cGenericClass.getValue().getClass()); // cGenericClass = class com.coderap.generic.C
  • }
  • }
  1. 作为方法的参数,泛型可以使用? extends B或者? super C,前者表示实际类型只可以是B类对象或B的子类对象,后者表示实际类型只可以是C类对象或C的父类对象,以下两种写法都是正确的:
  • public static void main(String[] args) {
  • List<? super B> blist = new ArrayList<B>();
  • blist.add(new C());
  • blist.add(new B());
  • iterate(blist);
  • // List<? extends B> clist = new ArrayList<B>(); // 这种写法添加下面两个元素时会报错
  • // clist.add(new B()); // 报错
  • // clist.add(new C()); // 报错
  • // 下面的写法可行
  • List<B> clist = new ArrayList<B>();
  • clist.add(new B());
  • clist.add(new C());
  • foreach(clist);
  • }
  • public static void foreach(List<? extends B> list) {
  • for (B b : list) {
  • System.out.println(b);
  • }
  • }
  • public static void iterate(List<? super C> list) {
  • for (Object o : list) {
  • System.out.println(o);
  • }
  • }
  1. 作为局部变量的参数,泛型可以使用? extends B或者? super C,不过前者没有什么意义,后者表示只可以传以C为父类的对象,所以以下的写法是正确的:
  • List<? super B> blist = new ArrayList<B>();
  • blist.add(new C());
  • blist.add(new B());
  • // List<? extends B> clist = new ArrayList<B>(); // 这种写法添加下面两个元素时会报错
  • // clist.add(new B()); // 报错
  • // clist.add(new C()); // 报错
  • // 下面的写法可行
  • List<B> clist = new ArrayList<B>();
  • clist.add(new B());
  • clist.add(new C());
  • foreach(clist);

8. 泛型数组

看到了很多文章中都会提起泛型数组,经过查看Sun的说明文档,在Java中是不能创建一个确切的泛型类型的数组的。也就是说下面的这个例子是不可以的:

  • List<String>[] ls = new ArrayList<String>[10];

而使用通配符创建泛型数组是可以的,如下面这个例子:

  • List<?>[] ls = new ArrayList<?>[10];

这样也是可以的:

  • List<String>[] ls = new ArrayList[10];

下面使用Sun的一篇文档的一个例子来说明这个问题:

  • List<String>[] lsa = new List<String>[10]; // Not really allowed.
  • Object o = lsa;
  • Object[] oa = (Object[]) o;
  • List<Integer> li = new ArrayList<Integer>();
  • li.add(new Integer(3));
  • oa[1] = li; // Unsound, but passes run time store check
  • String s = lsa[1].get(0); // Run-time error: ClassCastException.

这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。

而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。

下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。

  • List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
  • Object o = lsa;
  • Object[] oa = (Object[]) o;
  • List<Integer> li = new ArrayList<Integer>();
  • li.add(new Integer(3));
  • oa[1] = li; // Correct.
  • Integer i = (Integer) lsa[1].get(0); // OK