泛型在编程语言中出现的最初的目的是希望类或方法具有最广泛的表达能力。通过解耦类或者方法与所使用的类型之间的约束来实现。
通常一般的类和方法,只能应用于具体的类型,基本类型或者自定义的类,若要编写应用于多种类型的代码,这种限制会对代码的束缚很大。在Java语言处于还未出现泛型的版本时,只能通过 Object 是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。下面是一个容器代码实现部分代码。
public class Container { private Object obj; /** * @return the obj */ public Object getObj() { return obj; } /** * @param obj the obj to set */ public void setObj(Object obj) { this.obj = obj; } }虽然上述容器会达到预期效果,但就我们的目的而言,它并不是最合适的解决方案。它不是类型安全的,并且要求在检索封装对象时使用显式类型转换,因此有可能引发异常。通过泛型可以很好的解决这些问题。
Java 泛型(generics)是 JDK 1.5 中引入的一个新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数,在实例化时为所使用的容器分配一个类型,这样就可以创建一个对象来存储所分配类型的对象。 所分配的类型将用于限制容器内使用的值,这样就无需进行类型转换,还可以在编译时提供更强的类型检查。类型参数的魅力在于让程序具有更好的可读性和安全性。 这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。
官方给出的定义是:
Type inference is a Java compiler’s ability to look at each method invocation and corresponding declaration to determine the type argument (or arguments) that make the invocation applicable. The inference algorithm determines the types of the arguments and, if available, the type that the result is being assigned, or returned. Finally, the inference algorithm tries to find the most specific type that works with all of the arguments.
翻译过来便是:类型推断是 Java 编译器查看每一个方法调用和相关声明,以确定类型参数(或参数),使调用可用。推理算法确定参数类型,如果类型推断成功,那么方法返回的值就是那个类型的。最后,推理算法试图找到与所有变量的最具体类型。
观察下面的代码:
static <T> T pick(T a1, T a2) { return a2; } Serializable s = pick("d", new ArrayList<String>());编译器可以从以上代码中推导出 pick 的两个参数都是 Serializable 类型。
类型参数(又称类型变量)用作占位符,指示在运行时为类分配类型。根据需要,可能有一个或多个类型参数,并且可以用于整个类。根据惯例,类型参数是单个大写字母,该字母用于指示所定义的参数类型。下面列出推荐的标准类型参数:
E:元素 K:键 N:数字 T:类型 V:值 S、U、V 等:多参数情况中的第 2、3、4 个类型维基给出的形式化定义如下:
在一门程序设计语言的类型系统中,一个类型规则或者类型构造器是:
协变(covariant),如果它保持了这样的序关系,该序关系是:子类型 ≦ 基类型。逆变(contravariant),该序关系是:基类型 ≦ 子类型。不变(invariant),如果上述两种均不适用。先看下面的类相关定义。
class Fruit {} class Apple extends Fruit{} class Banana extends Fruit{} class RedFuji extends Apple{}协变就是符合我们正常逻辑的一种转换关系。如苹果是水果的一种,我们可以称苹果为水果。
Fruit [] f = new Apple[10];上述在Java中是完全可行的。可见数组是协变的。
// Compile Error List<Fruit> f = new ArrayList<Apple>();上述可见,泛型没有内建的协变类型。Apple 的 List 在类型上不等价于 Fruit 的 List,即使 Apple 是一种 Fruit 类型。 泛型中利用通配符实现的协变和逆变:
// 协变 List<? extends Fruit> flist = new ArrayList<Apple>(); // 逆变 List<? super Apple> alist = new ArrayList<Fruit>();上述协变和逆变在泛型中是完全可行的。后面会解释为什么可行及编译器会对这样的对象进行什么样的限制。
边界使得我们可以在泛型的参数类型上设置限制条件,这可以让我们按照边界的类型来调用方法。
通配符指在泛型表达式中的问号 ‘?’。
< ? extends T> : 可以接收 T 类型或者 T 的子类型。规定了上界。 < ? super T> :可以接收 T 类型或者 T 的父类型。规定了下界。< ?> 增加了可读性,可解读为作者想使用泛型来编写这段代码,并不是想用原生类型,虽然这个时候泛型参数可以持有任何类型,只是我们不知道这个类型是什么。
类型参数广泛应用在容器相关的类、接口和方法中。下面以几个案例介绍下类型参数的使用。
一个泛型类(generic class)就是具有一个或多个类型变量的类。在类名后,用尖括号(<>)括起来,并将类型变量写在里面,可有多个类型变量。 ArrayList 和 HashMap 的类泛型定义如下:
// ArrayList 泛型类的定义 public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { ... } // HashMap 泛型类定义 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { .... }下面使用泛型实现一个链栈,定义了一个内部类 Node
/** * Created by manuu on 17-7-25. */ public class LinkedStack<T> { private class Node<U> { U item; Node<U> next; Node() { item = null; next = null; } Node(U item, Node<U> next) { this.item = item; this.next = next; } boolean end() { return item == null && next == null; } } // 设置一个哨兵 private Node<T> top = new Node<T>(); public void push(T item) { top = new Node<T>(item, top); } // 返回 T 类型 public T pop() { if (!top.end()) { T tmp = top.item; top = top.next; return tmp; } return null; } public T peek() { if (!top.end()) { return top.item; } return null; } public static void main(String[] args) { // 设置 String 为类型 LinkedStack<String> stack = new LinkedStack<>(); stack.push("A"); stack.push("B"); stack.push("C"); String s; while ((s = stack.pop()) != null) { System.out.print(s); } } } /* Output: CBA */泛型接口的定义和泛型类相似。List 和 Map 接口的泛型定义如下:
// List 接口的泛型定义 public interface List<E> extends Collection<E> { ... } // map 接口的泛型定义 public interface Map<K,V> { ... }泛型方法的类型变量是放在修饰符后面,返回类型的前面。泛型方法可以定义在普通类中,也可以定义在泛型类中。下面是一个泛型方法的例子:
public static <T> T addAndReturn(T element, Collection<T> collection){ collection.add(element); return element; }在调用泛型方法时候,可以显式的设定类型,也可以让编译器通过类型推断来判定。
有时,类、接口或方法需要对类型变量加以约束。
考虑这样的情况,需要对类型参数声明的变量进行方法调用,如果只使用类型参数T,这意味着可以是任何一个类的对象。这个时候需要指定泛型类型,但希望控制可以指定的类型,而非不加限制。有界类型,在类型参数部分指定 extends 或 super 关键字,分别用上限或下限限制类型,从而限制泛型类型的边界。 使用的时候需要注意以下事项: 1. 不管该限定是类还是接口,统一都使用关键字 extends。 2. 可以使用 ‘&’ 符号给出多个限定。 3. 如果限定既有接口也有类,那么类必须只有一个,并且放在首位置。
泛型限定过程中利用通配符进行类型转换的时候,需要注意的事项。
先看下面代码:
public class GenericsAndCovariance { public static void main(String [] args) { List<? extends Fruit> flist = new ArrayList<Apple>(); // Compile Error : can't add any type of object // flist.add(new Apple()); // flist.add(new Fruit()); // flist.add(new Object()); flist.add(null);// 编译器允许,但无意义。 Fruit f = flist.get(0); } }从上述代码中可知,通过通配符实现了协变,虽然通配符继承自Fruit, 并不意味着这个 List 可以持有任何类型的 Fruit,在某种意义上可以看成 flist 引用没有指定具体类型。原来这个 List 持有 Apple 这样的指定的类型,但是为了向上转型为 flist,这个类型原来是什么并没那么重要了。 < ? extends Fruit> 意味着从这个列表里读出一个 Fruit 是安全的,这个列表里的所有对象至少是一个 Fruit,并且可能是从 Fruit 里导出的某种对象。
在上述代码指定了 ArrayList< ? extends Fruit>时,add() 的参数也变成了 “? extends Fruit”,这意味它可以是任何事物,这个时候编译器并不知道需要的 Fruit 是哪一个子类型,因此它不会接受任何类型的 Fruit,因为编译器无法验证 “任何事物”的类型安全性。
一旦执行了向上转型,将丢失掉向其中传递任何对象的能力,甚至 Object 也不行。
看下面的代码:
public class GenericsAndContravariant { static void writeTo(List<? super Apple> apples) { apples.add(new Apple()); apples.add(new RedFuji()); // apples.add(new Fruit()); // Error } }从上述代码可知,apples 是Apple的某种基类型的List,Apple 作为下界,所以我们向里面传递 Apple 及 Apple 的导出的任何对象是安全的。但是我们不能向内部添加 Fruit 类型,这是不安全的。
用法可以总结成:PECS ( Producer Extends,Consumer Super )。
假设你有一个 List 相关的容器,现在你想灵活的对此容器进行操作。 1. 如果你是想遍历 List,并对每一项元素操作时,此时这个容器是 Producer(生产元素),应该使用 List< ? extends Thing>。 2. 如果你是想添加元素到 List,那么此时容器是 Consumer(消费元素)List< ? super Thing>。
先看下面的代码:
public class ErasedTypeEquivalence { public void main(string [] args) { Class c1 = new ArrayList<String>().getClass(); Class c2 = new ArrayList<Integer>().getClass(); System.out.print(c1 == c2); } } /* Output true */由上述结果可知,ArrayList 和 ArrayList 在运行时事实上是相同的类型。这是由于这两个泛型在编译后都替换成了原始的类型。
实现原理:无论何时定义一个泛型类型,都自动提供了一个相应的原始的类型,原始类型就是删去类型参数后的泛型类型名。擦除类型变量,并替换成限定类型(无则为 Object)。 要明确一点的是,在泛型代码内部,无法获得任何有关泛型参数类型的信息。
在使用泛型时,任何具体的泛型信息都被擦除。这点上和 C++ 有很大的区别。C++ 为每个模板的实例化产生不同的类型。 在类型擦除的过程中,若有多个边界,此时将会擦除到它的第一个边界。
擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为”迁移兼容性“。
泛型是在 JDK1.5后才加入的,为了实现迁移兼容性,Java 的设计者们采用了类型擦除的方案。通过允许非泛型代码和泛型代码共存,擦除使这个向泛型的迁移成为了可能。然而擦除减少了泛型的泛化性。这是 Java 型实现的一种折中。
在基于擦除的实现泛型方案中,泛型类型被当作第二类类型处理,既不能在某些重要的上下文环境中使用的类型。泛型类型只有在静态类型检查期间才能出现,在此之后所有的泛型类型都会被擦除,替换成它们的非泛型上界。
大多数限制都是由采用擦除方案引起的。
这是因为擦除后数据类型变为了 Object,而 Object 并不能表示基本数据类型。然而 Java 提供了基本数据类型的包装器。
不能将泛型进行转型,instanceof 操作和 new 表达式。因为所有有关参数的类型信息都丢失了。
关于 instanceof 不能使用的解决方案可以在泛型内部设置一个类型标签。然后动态的调用isInstance()。如下
public class ClassType<T> { Class<T> kind; ... public boolean isInstance(Object arg) { return kind.isInstance(arg); } }对于下面代码编译是行不通的。
T t = new T()行不通的部分原因是因为擦除,还有部分因为编译器不能验证 T 是否具有默认的构造器。这种操作在 C++ 中很自然安全,它是在编译期检查的。 这个解决方式可以使用工厂模式,最便利的工厂对象就是Class对象,可以使用newInstance() 来创建这个类型的新对象。如下所示:
class ClassAsFactory<T> { T x; public ClassAsFactory(Class<T> kind) { try { x = kind.newInstance(); } catch (Exception e) { throw new RuntimeException(e); } } }这个方法,可能会导致运行期异常,针对这个可以编写显式的工厂来获得编译期检查。
不能实例化泛型数组。 通常的解决方案是通过使用ArrayList,这样可以获得数组的行为,以及由泛型提供的编译期的类型安全。举例如下:
public class ListOfGenerics<T> { private List<T> array = new ArrayList<T>(); public void add(T item) { array.add(item); } public T get(int index) { return array.get(index); } ... }下面的程序是不能编译的:
public class UseList<W, T> { void f(List<W> v){} void f(List<T> v){} }由于擦除的原因,重载的方法将产生相同的类型签名。 当被擦除的参数不能产生唯一的参数列表时,必须提供明显区别的方法名。
先看以下代码:
class DateTest extends pair<Date> { public void setFisrt(Date fisrt) { if (fisrt.compareTo(getFisrt()) >= 0) { super.setFisrt(fisrt); } } } class pair<T> { private T fisrt; public pair(T fisrt) { fisrt = fisrt; } public void setFisrt(T newValue) { fisrt = newValue; } public T getFisrt() { return fisrt; } }DateTest 继承自泛型类 pair,并且实现了 setFisrt 方法。按照擦除的方案,所有的参数类型都被替换成原始类型了。当我们用父类对象指向子类对象,并且调用 setFisrt 方法时,这个时候会成功调用子类的此方法,完成多态的特性。这是因为编译器会在 DateTest 中生成一个桥方法 ( bridge method )。 通过javap -c DateTest 获得以下代码:
public void setFisrt(java.util.Date); Code: 0: aload_1 1: aload_0 2: invokevirtual #2 // Method getFisrt:()Ljava/lang/Object; 5: checkcast #3 // class java/util/Date 8: invokevirtual #4 // Method java/util/Date.compareTo:(Ljava/util/Date;)I 11: iflt 19 14: aload_0 15: aload_1 16: invokespecial #5 // Method pair.setFisrt:(Ljava/lang/Object;)V 19: return public void setFisrt(java.lang.Object); Code: 0: aload_0 1: aload_1 2: checkcast #3 // class java/util/Date 5: invokevirtual #6 // Method setFisrt:(Ljava/util/Date;)V 8: return }从上述可知,在编译器生成的桥方法 setFisrt(java.lang.Object) 中调用了子类中的 setFisrt 方法。 桥方法还应用在重写方法中,当一个方法覆盖另一个方法时候,可以指定一个更严格的返回类型时。( 具有协变的返回类型 )
一个类不能实现同一个泛型接口的两种变体。由于擦除的存在,这两种变体会变成相同的接口。
不能在静态域或方法中的引用类型变量。例如下面会编译不通过:
public class Singleton<T> { private static T instance; //ERROR public static T getInstance() // ERROR { return instance; } }由于擦除,以及静态域和非静态域的工作方式,导致禁止使用带有类型变量的静态域和方法。静态域的成员是独立于对象的,而类型变量须在对象声明的时候进行绑定,所以这是不可取的。 然而这个需要和静态泛型方法有所区分。泛型方法中的泛型指的是方法中的参数变量,而不是泛型类中的参数变量。所以静态泛型方法是可取的。
既不能抛出也不能捕获泛型类的对象。实际上,甚至泛型类扩展Throwable都是不合法的。 下面的代码不能正常编译:
public class Problem<T> extends Exception { ... } //ERRORcatch 子句中不能使用类型变量。
public static <T extends Throwable> void doWork(Class<T> t){ try { ... } catch(T e){ //编译错误 ... } }由于 擦除会替换为Throwable,后面会和捕获的子类会发生冲突,Java为了避免这种冲突,直接禁止在 catch 中使用类型变量。
不过可以在异常规范中使用类型变量。以下的方法是合法的:
public static<T extends Throwable> void doWork(T t) throws T{ try{ ... }catch(Throwable realCause){ t.initCause(realCause); throw t; } }《Java 编程思想 第四版》 《深入理解 Java 虚拟机》 《Java 核心技术卷一 第九版》 https://zh.wikipedia.org/wiki/协变与逆变