JDK 5.0中的泛型类型简介

在你开始前

关于本教程

JDK 5.0(也称为Java 5.0或“ Tiger”)对Java语言进行了一些重大更改。 最重要的变化是增加了泛型类型 (泛型)-支持使用实例化时指定的抽象类型参数定义类。 泛型具有增加大型程序的类型安全性和可维护性的巨大潜力。

泛型与JDK 5.0中的其他一些新语言功能协同交互,包括增强的for循环(有时称为foreach或for / in循环),枚举和自动装箱。

本教程说明了将泛型添加到Java语言的动机,详细介绍了泛型类型的语法和语义,并提供了在类中使用泛型的介绍。

本教程适用于希望学习泛型新语言支持的中级和高级Java开发人员。 假定读者熟悉用Java语言开发接口和类以及基本的面向对象设计技术。

泛型语言功能仅在JDK 5.0和更高版本中可用。 如果您正在开发基于早期JDK版本的软件,则在迁移到JDK 5.0或更高版本之前,不能在代码中使用泛型功能。

先决条件

您必须具有一个JDK 5.0开发环境,才能使用泛型。 您可以从Sun Microsystems网站免费下载JDK 5.0

泛型简介

什么是仿制药?

泛型或泛型是对Java语言的类型系统的扩展,以支持创建可以通过类型进行参数化的类。 您可以将类型参数视为使用参数化类型时要指定的类型的占位符,就像形式化方法参数是在运行时传递的值的占位符一样。

可以在集合框架中看到仿制药的动机。 例如, Map类允许您将任何类的条目添加到Map ,即使在给定的map中仅存储某种类型的对象(例如String是很常见的用例。

因为Map.get()被定义为返回Object ,所以通常必须将Map.get()的结果Map.get()转换为期望的类型,如以下代码所示:

Map m = new HashMap();
m.put("key", "blarg");
String s = (String) m.get("key");

要使程序编译,必须将get()的结果强制转换为String ,并希望结果确实是String 。 但是可能有人在此映射中存储了String以外的内容,在这种情况下,上面的代码将抛出ClassCastException

理想情况下,您想捕捉一下m是一个将String键映射到String值的Map的概念。 这样一来,您就可以消除代码中的强制类型转换,同时获得一层额外的类型检查,这可以防止某人在集合中存储错误类型的键或值。 这就是泛型为您所做的。

仿制药的好处

向Java语言添加泛型是一项重大改进。 不仅对语言,类型系统和编译器进行了重大更改以支持通用类型,而且还对类库进行了全面改进,以便使许多重要的类(例如Collections框架)都成为通用类。 这带来了许多好处:

  • 输入安全性。 泛型的主要目标是提高Java程序的类型安全性。 通过了解使用泛型类型定义的变量的类型范围,编译器可以在更大程度上验证类型假设。 没有泛型,这些假设仅存在于程序员的头脑中(或者,如果幸运的话,在代码注释中)。

    Java程序中一种流行的技术是定义其元素或键是通用类型的集合,例如“ String列表”或“从StringString映射”。 通过捕获变量声明中的其他类型信息,泛型使编译器可以强制执行这些其他类型约束。 现在可以在编译时捕获类型错误,而不是在运行时显示为ClassCastException 。 将类型检查从运行时移到编译时,可以帮助您更轻松地发现错误并提高程序的可靠性。

  • 消除演员阵容。 泛型的一个附带好处是,您可以从源代码中消除许多类型转换。 这使代码更具可读性,并减少了出错的机会。

    尽管减少的转换需求减少了使用泛型类的代码的冗长性,但声明泛型类型的变量会相应地增加冗长性。 比较以下两个代码示例。

    此代码不使用泛型:

    List li = new ArrayList();
    li.put(new Integer(3));
    Integer i =  (Integer) li.get(0);

    此代码使用泛型:

    List<Integer> li = new ArrayList<Integer>();
    li.put(new Integer(3));
    Integer i =  li.get(0);

    在简单程序中只使用一次泛型类型的变量不会导致冗长的净节省。 但是对于许多使用通用类型变量的大型程序,节省下来的费用开始增加了。

  • 潜在的性能提升。 泛型为进一步优化创造了可能性。 在泛型的初始实现中,编译器将相同的强制类型转换插入生成的字节码中,程序员在没有泛型的情况下会指定这些类型。 但是,更多类型信息可供编译器使用这一事实允许在将来的JVM版本中进行优化。

由于实现了泛型的方式,(几乎)不需要更改JVM或类文件即可支持泛型类型。 所有工作都在编译器中完成,该编译器生成的代码类似于您在没有泛型的情况下编写的代码(带有强制转换),并且对其类型安全性更有信心。

通用用法示例

泛型类型的许多最佳示例来自Collections框架,因为泛型使您可以对存储在集合中的元素指定类型约束。 考虑一下使用Map类的示例,该类涉及某种程度的乐观,即Map.get()返回的结果实际上将是String

Map m = new HashMap();
m.put("key", "blarg");
String s = (String) m.get("key");

如果有人在地图中放置了String以外的内容,则上述代码将引发ClassCastException 。 泛型允许您表达类型约束,即m是将String键映射到String值的Map 。 这样一来,您就可以消除代码中的强制类型转换,同时获得另一层类型检查,以防止某人在集合中存储错误类型的键或值。

以下代码示例显示了JDK 5.0中Collections框架中Map接口定义的一部分:

public interface Map<K, V> {
  public void put(K key, V value);
  public V get(K key);
}

请注意该接口的两个附加功能:

  • 在类级别上对类型参数 KV的规范,表示在声明Map类型的变量时将指定的类型的占位符
  • get()put()和其他方法的方法签名中使用KV

为了获得使用泛型的好处,在定义或实例化Map类型的变量时,必须提供KV具体值。 您可以通过相对简单的方式执行此操作:

Map<String, String> m = new HashMap<String, String>();
m.put("key", "blarg");
String s = m.get("key");

使用通用版本的Map ,您不再需要将Map.get()的结果Map.get()String ,因为编译器知道get()将返回String

您不会在使用泛型的版本中保存任何击键; 实际上,它比使用强制转换的版本需要更多的输入。 通过使用泛型类型可以节省更多的类型安全性。 因为编译器对将放入Map的键和值的类型有更多的了解,所以类型检查从执行时间转移到编译时间,从而提高了可靠性并加快了开发速度。

向后兼容

向Java语言中添加泛型的一个重要目标是保持向后兼容性。 尽管已泛化了JDK 5.0中标准类库中的许多类,例如Collections框架,但是使用Collections类(例如HashMapArrayList现有代码将在JDK 5.0中未经修改地继续工作。 当然,不利用泛型的现有代码不会获得泛型的其他类型安全优势。

泛型类型的基础

类型参数

定义通用类或声明通用类的变量时,可以使用尖括号指定形式类型参数 。 形式和实际类型参数之间的关系类似于形式和实际方法参数之间的关系,不同之处在于类型参数表示类型,而不是值。

泛型类中的类型参数几乎可以在任何可以使用类名的地方使用。 例如,以下是java.util.Map接口的定义的摘录:

public interface Map<K, V> {
  public void put(K key, V value);
  public V get(K key);
}

Map接口有两种类型的参数化-键类型K和值类型V 现在,将(没有泛型)接受或返回Object在其签名中使用KV ,从而指示了Map规范基础上的其他类型约束。

在声明或实例化通用类型的对象时,必须指定类型参数的值:

Map<String, String> map = new HashMap<String, String>();

请注意,在此示例中,您必须指定两次类型参数-一次声明变量map的类型,第二次选择HashMap类的参数化,以便实例化正确类型的实例。

当编译器遇到Map<String, String>类型的变量时,它知道KV现在绑定到String ,因此它知道在该变量上Map.get()的结果将具有String类型。

除异常类型,枚举或匿名内部类以外的任何类都可以具有类型参数。

命名类型参数

建议的命名约定是将大写单字母名称用作类型参数。 这与C ++约定(请参阅附录A:与C ++模板的比较 )不同,并且反映了大多数泛型类将具有少量类型参数的假设。 对于常见的通用模式,建议的名称为:

  • K-键,例如地图的键
  • V-一个值,例如ListSet的内容或Map的值
  • E-异常类
  • T-通用类型

泛型类型不是协变的

与泛型类型混淆的一个常见原因是假设它们像数组一样是协变的。 他们不是。 这是说List<Object> 不是 List<String>的超类型的一种奇特的说法。

如果A扩展了B,则A的数组也将是B的数组,并且您可以在需要B[]地方自由提供A[]

Integer[] intArray = new Integer[10];  
Number[] numberArray = intArray;

上面的代码有效,因为IntegerNumber ,并且Integer数组是 Number数组。 但是,对于泛型则不是这样。 以下代码无效:

List<Integer> intList = new ArrayList<Integer>();
List<Number> numberList = intList; // invalid

最初,大多数Java程序员都发现这种缺乏协方差的烦人甚至是“破损”的东西,但是有充分的理由。 如果您可以将List<Integer>分配给List<Number> ,则以下代码将违反泛型应该提供的类型安全性:

List<Integer> intList = new ArrayList<Integer>();
List<Number> numberList = intList; // invalid
numberList.add(new Float(3.1415));

因为intListnumberList是别名,所以上面的代码(如果允许)将允许您将Integers放入intList 。 但是,有一种方法可以编写可以接受通用类型系列的灵活方法,如您在下一个面板中所看到的。

输入通配符

假设您有以下方法:

void printList(List l) { 
  for (Object o : l) 
    System.out.println(o); 
}

上面的代码在JDK 5.0上编译,但是如果尝试使用List<Integer>调用它,则会收到警告。 发生警告的原因是,您将泛型类型( List<Integer> )传递给仅承诺将其视为List (所谓的raw type )的方法,这可能会损害使用泛型的类型安全性。

如果您尝试编写如下方法,该怎么办:

void printList(List<Object> l) { 
  for (Object o : l) 
    System.out.println(o); 
}

它仍然不会编译,因为List<Integer> 不是 List<Object> (如上一节所述, 泛型类型不是协变的 )。 确实很烦人-现在您的通用版本比原始的非通用版本有用!

解决方法是使用通配符类型 :

void printList(List<?> l) { 
  for (Object o : l) 
    System.out.println(o); 
}

上面代码中的问号是类型通配符。 它的发音为“未知”(如“未知列表”)。 List<?>是任何常规List的超类型,因此您可以将List<Object>List<Integer>List<List<List<Flutzpah>>>自由地传递给printList()

输入通配符

上一节“ 类型通配符 ”介绍了类型通配符,它​​使您可以声明类型为List<?>变量。 这样的List可以做什么? 非常方便的是,您可以从中检索元素,但不能向其中添加元素。 这样做的原因不是编译器知道哪些方法修改了列表,哪些没有。 正是(大多数)变异方法比非变异方法需要更多的类型信息。 以下代码可以正常工作:

List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
System.out.println(lu.get(0));

为什么这样做? 编译器不知道luList的type参数的值。 但是,编译器足够聪明,可以执行某种类型推断。 在这种情况下,它推断未知类型参数必须扩展Object 。 (这个特殊的推论并不是很大的飞跃,但是编译器可以做出一些令人印象深刻的类型推论,正如您稍后将看到的那样(在List.get() details中 )。因此,它可以让您调用List.get()并将返回类型推论为Object

另一方面,以下代码不起作用:

List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
lu.add(new Integer(43));  // error

在这种情况下,编译器无法对luList的类型参数进行足够强的推断,以确保将Integer传递给List.add()是类型安全的。 因此,编译器将不允许您执行此操作。

以免使您仍然认为编译器对哪些方法更改列表的内容以及哪些方法不更改有一些概念,请注意,以下代码将起作用,因为它不依赖于编译器是否必须了解有关清单类型参数的任何信息。 lu

List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
lu.clear();

通用方法

您已经看到(在Type参数中 ),可以通过在其定义中添加形式类型参数列表来使类通用。 无论方法定义的类是否是通用的,方法都可以通用。

通用类跨多个方法签名强制执行类型约束。 在List<V> ,类型参数V出现在get()add()contains()等的签名中。当您创建Map<K, V>类型的变量时,您将声明跨类型约束方法。 您传递给put()的值将与get()返回的类型相同。

同样,在声明泛型方法时,通常这样做是因为要在该方法的多个参数之间声明类型约束。 例如,根据以下代码中ifThenElse()方法的第一个参数的布尔值,它将返回第二个或第三个参数:

public <T> T ifThenElse(boolean b, T first, T second) {
  return b ? first : second;
}

注意,您可以调用ifThenElse()而不用明确告诉编译器您想要T值。 不需要明确告诉编译器T的值是什么。 它只知道它们必须全部相同。 编译器允许您调用以下代码,因为编译器可以使用类型推断来推断用String代替T满足所有类型约束:

String s = ifThenElse(b, "a", "b");

同样,您可以致电:

Integer i = ifThenElse(b, new Integer(1), new Integer(2));

但是,编译器不允许以下代码,因为没有任何类型可以满足所需的类型约束:

String s = ifThenElse(b, "pi", new Float(3.14));

您为什么选择使用通用方法,而不是将T类型添加到类定义中? 在至少两种情况下,这是有道理的:

  • 当泛型方法是静态的时,在这种情况下不能使用类类型参数。
  • 当对T的类型约束确实是方法的局部约束时,这意味着不存在将相同类型T用于同一类的另一个方法签名的约束。 通过将通用方法的类型参数设置为方法的局部方法,可以简化封闭类的签名。

有界类型

在上一节的示例Generic Methods中 ,类型参数V是无约束或无界的类型。 有时您需要在类型参数上指定其他约束,而仍不能完全指定它。

考虑示例Matrix类,该示例使用由Number类限制的类型参数V

public class Matrix<V extends Number> { ... }

编译器将允许您创建Matrix<Integer>Matrix<Float>类型的变量,但是如果您尝试定义Matrix<String>类型的变量,则会发出错误。 类型参数V据说受 Number 。 在没有类型限制的情况下,假定类型参数受Object 。 这就是为什么上一节中的示例Generic方法允许List.get()List<?>上调用时返回Object ,即使编译器不知道类型参数V的类型。

一个简单的通用类

编写基本的容器类

至此,您已经准备好编写一个简单的泛型类。 到目前为止,泛型类的最常见用例是容器类(例如Collections框架)或值持有者类(例如WeakReferenceThreadLocal 。 让我们写一个类似于List的类,它充当容器,使用泛型来表达Lhist所有元素都具有相同类型的约束。 为了简化实现, Lhist使用固定大小的数组来存储值,并且不接受空值。

Lhist类将具有一个类型参数V ,这是Lhist中值的Lhist ,并将具有以下方法:

public class Lhist<V> { 
  public Lhist(int capacity) { ... }
  public int size() { ... }
  public void add(V value) { ... }
  public void remove(V value) { ... }
  public V get(int index) { ... }
}

要实例化Lhist ,您只需在声明一个时指定type参数,并指定所需的容量:

Lhist<String> stringList = new Lhist<String>(10);

实现构造函数

在实现Lhist类时,您将遇到的第一个绊脚石是构造函数。 您想这样实现:

public class Lhist<V> { 
  private V[] array;
  public Lhist(int capacity) {
    array = new V[capacity]; // illegal
  }
}

这似乎是分配后备数组的自然方法,但是很遗憾,您不能这样做。 原因很复杂; 稍后您将在“血腥细节”中涉及擦除的主题时,将对它们有所了解 。 做你想要的事情的方式是丑陋的和违反直觉的。 构造函数的一种可能实现是此方法(它使用Collections类采用的方法):

public class Lhist<V> { 
  private V[] array;
  public Lhist(int capacity) {
    array = (V[]) new Object[capacity];
  }
}

或者,您可以使用反射实例化数组。 但是,这样做需要将附加参数传递给构造函数- 类文字 ,例如Foo.class 。 类字面量也将在后面的Class <T>部分中讨论。

实施方法

实施Lhist的其余方法要容易Lhist 。 这是Lhist类的完整实现:

public class Lhist<V> {
    private V[] array;
    private int size;

    public Lhist(int capacity) {
        array = (V[]) new Object[capacity];
    }

    public void add(V value) {
        if (size == array.length)
            throw new IndexOutOfBoundsException(Integer.toString(size));
        else if (value == null)
            throw new NullPointerException();
        array[size++] = value;
    }

    public void remove(V value) {
        int removalCount = 0;
        for (int i=0; i<size; i++) {
            if (array[i].equals(value))
                ++removalCount;
            else if (removalCount > 0) {
                array[i-removalCount] = array[i];
                array[i] = null;
            }
        }
        size -= removalCount;
    }

    public int size() { return size; }

    public V get(int i) {
        if (i >= size)
            throw new IndexOutOfBoundsException(Integer.toString(i));
        return array[i];
    }
}

请注意,您使用的形式类型参数V中,将接受或返回方法V ,但是你没有方法或字段什么任何想法V了,因为它是不知道的通用代码。

使用Lhist

使用Lhist类很容易。 要定义整数Lhist ,只需在声明和构造函数中为type参数提供实际值:

Lhist<Integer> li = new Lhist<Integer>(30);

编译器知道li.get()返回的任何值都将是Integer类型,并且将强制传递给li.add()li.remove()都将是Integer 。 除了构造函数的怪异实现方式之外,您无需做任何特别的事情即可使Lhist成为通用类。

Java类库中的泛型

集合类

到目前为止,Java类库中最大的泛型支持使用者是Collections框架。 正如容器类是C ++中模板的主要动机(请参阅附录A:与C ++模板的比较 )(尽管它们随后已被使用得多)一样,提高Collection类的类型安全性是C语言中泛型的主要动机。 Java语言。 Collections类还充当泛型使用方式的模型,因为它们演示了泛型类型的几乎所有标准技巧和习惯用法。

所有标准收集接口均已生成-Collection Collection<V>List<V>Set<V>Map<K,V> 。 同样,集合接口的实现使用相同的类型参数生成,因此HashMap<K,V>实现Map<K,V>等。

Collections类还使用泛型的许多“技巧”和习惯用法,例如上限和下限通配符。 例如,在接口Collection<V>addAll方法的定义如下:

interface Collection<V> {
  boolean addAll(Collection<? extends V>);
}

此定义将通配符类型参数与有界类型参数结合在一起,使您可以将Collection<Integer>的内容添加到Collection<Number>

如果类库将addAll()定义为采用Collection<V> ,则将无法将Collection<Integer>的内容添加到Collection<Number> 。 与其将addAll()的参数限制为包含与要添加的集合完全相同的类型的集合,还可以使传递给addAll()的集合中的元素更合适,使其更合理。添加到您的收藏中。 有界类型使您可以这样做,而有界通配符的使用使您摆脱了组成另一个在其他任何地方都不会使用的占位符名称的要求。

作为生成类可以如何更改其语义的微妙示例(如果您不小心的话),请注意Collection.removeAll()的参数类型为Collection<?> ,而不是Collection<? extends V> Collection<? extends V> 。 这是因为可以将混合类型的集合传递给removeAll() ,并且更严格地定义removeAll将会改变方法的语义和实用性。 这说明生成一个现有类比定义一个新的泛型类要困难得多,因为您必须小心不要更改该类的语义或破坏现有的非泛型代码。

其他容器类

除了Collections类之外,Java类库中的其他几个类还充当值的容器。 这些类包括WeakReferenceSoftReferenceThreadLocal 。 它们都针对容器的值类型进行了泛化,因此WeakReference<T>是对T类型的对象的弱引用,而ThreadLocal<T>是对ThreadLocal<T>类型的线程局部变量的句柄。

泛型不仅用于容器

泛型类型最常见,最直接的用法是容器类,例如Collections类或引用类(例如WeakReference<T> 。) Collection<V>中的type参数的含义在直观上很明显- “所有都是V类型的值的集合。” 同样, ThreadLocal<T>有一个明显的解释-“类型为T的线程局部变量”。 但是,泛型规范中的任何内容都与遏制无关。

在诸如Comparable<T>Class<T>类的类型中,类型参数的含义更为细微。 有时,例如在Class<T> ,类型变量主要用于帮助编译器进行类型推断。 有时,就像在神秘的Enum<E extends Enum<E>> ,可以在类层次结构的结构上施加约束。

可比的<T>

Comparable接口已被泛化,以便实现Comparable的对象声明可以与之进行比较的类型。 (通常,这是对象本身的类型,但有时可能是超类。)

public interface Comparable<T> { 
  public boolean compareTo(T other);
}

因此, Comparable接口包含类型参数T ,这是实现Comparable的类可以与之比较的对象的类型。 这意味着,如果要定义实现Comparable的类(例如String ,则不仅必须声明该类支持比较,而且还必须声明可比较的对象,通常它本身就是:

public class String implements Comparable<String> { ... }

现在考虑二进制max()方法的实现。 您想要采用两个相同类型的参数,两个参数必须是Comparable ,并且必须彼此可Comparable 。 幸运的是,如果您使用泛型方法和有界类型参数,那将相对简单:

public static <T extends Comparable<T>> T max(T t1, T t2) {
  if (t1.compareTo(t2) > 0)
    return t1;
  else 
    return t2;
}

在这种情况下,您将定义一个泛型方法,该方法在类型T泛化,并且您必须对其进行扩展(实现) Comparable<T> 。 这两个参数都必须是T类型,这意味着它们是同一类型,支持比较并且可以相互比较。 简单!

更好的是,编译器将在调用max()时使用类型推断来确定T含义。 因此,以下调用有效,而无需完全指定T

String s = max("moo", "bark");

编译器将确定T的预期值为String ,并将进行相应的编译和类型检查。 但是,如果您尝试使用未实现Comparable<X>的类X参数调用max() ,则编译器将不允许这样做。

类别<T>

Class已经泛化了,但是首先让许多人感到困惑。 Class<T>类型参数T的含义是什么? 事实证明,这是被引用的类实例。 怎么可能? 那不是通函吗? 即使没有,为什么还要这样定义呢?

在以前的JDK中, Class.newInstance()方法的定义返回Object ,然后您可能会将其转换为另一种类型:

class Class { 
  Object newInstance();
}

但是,使用泛型,您可以使用更特定的返回类型定义Class.newInstance()方法:

class Class<T> { 
  T newInstance();
}

如何创建Class<T>类型的实例? 与非泛型代码一样,您有两种方法:调用方法Class.forName()或使用类文字X.classClass.forName()定义为返回Class<?> 。 另一方面,类文字X.class被定义为具有Class<X>类型,因此String.class具有Class<String>类型。

Foo.class设置为Class<Foo>类型有什么好处? 最大的好处是,它可以通过类型推断的魔力提高使用反射的代码的类型安全性。 另外,您无需将Foo.class.newInstance()Foo

考虑一种从数据库中检索一组对象并返回JavaBeans对象集合的方法。 您可以通过反射实例化和初始化创建的对象,但这并不意味着类型安全性必须完全超出范围。 考虑以下方法:

public static<T> List<T> getRecords(Class<T> c, Selector s) {
  // Use Selector to select rows
  List<T> list = new ArrayList<T>();
  for (/* iterate over results */) {
    T row = c.newInstance();
    // use reflection to set fields from result
    list.add(row);  
  }
  return list;
}

您可以像这样简单地调用此方法:

List<FooRecord> l = getRecords(FooRecord.class, fooSelector);

编译器将从FooRecord.class类型为Class<FooRecord>的事实推断出getRecords()的返回类型。 您可以使用类文字来构造新实例,并向编译器提供类型信息以供其用于类型检查。

用Class <T>替换T []

Collection接口包括一种用于将集合的内容复制到调用方指定类型的数组中的方法:

public Object[] toArray(Object[] prototypeArray) { ... }

toArray(Object[])的语义是,如果传递的数组足够大,则应使用它存储结果; 否则,将使用反射分配相同类型的新数组。 通常,仅将数组作为参数传递以提供所需的返回类型是一种廉价的技巧,但是在添加泛型之前,这是将类型信息传递给方法的最方便的方法。

使用泛型,您可以采用一种更直接的方法。 而不是像上面那样定义toArray() ,通用的toArray()可能看起来像这样:

public<T> T[] toArray(Class<T> returnType)

调用这样的toArray()方法很简单:

FooBar[] fba = something.toArray(FooBar.class);

尚未更改Collection接口以使用此技术,因为这会破坏许多现有的collection实现。 但是,如果从头开始使用泛型重新构建Collection ,则几乎可以肯定会使用此惯用法来指定其希望返回值是哪种类型。

Enum<E>

枚举是JDK 5.0中Java语言的其他新增功能之一。 当使用enum关键字声明枚举时,编译器会在内部为您生成一个扩展Enum的类,并为每个枚举值声明静态实例。 因此,如果您说:

public enum Suit {HEART, DIAMOND, CLUB, SPADE};

编译器将在内部生成一个名为Suit的类,该类扩展了java.lang.Enum<Suit>并具有名为HEARTDIAMONDCLUBSPADE常量( public static final )成员,每个成员均为Suit类。

Class一样, Enum是一个泛型类。 但是与Class不同,它的签名稍微复杂一些:

class Enum<E extends Enum<E>> { . . . }

这到底是什么意思? 这不是导致无限递归吗?

让我们逐步进行。 类型参数EEnum各种方法中使用,例如compareTo()getDeclaringClass() 。 为了使这些类型安全,必须在Enum类上泛化Enum类。

那么, extends Enum<E>部分呢? 那也有两个部分。 第一部分说,作为Enum类型参数的类本身必须是Enum子类型,因此您不能声明X类来扩展Enum<Integer> 。 第二部分说,任何扩展Enum类都必须将自身作为类型参数传递。 即使Y扩展了Enum ,也不能声明X扩展Enum<Y>

总而言之, Enum是一个参数化类型,只能为其子类型实例化,然后这些子类型将继承依赖于该子类型的方法。 ew! 幸运的是,对于Enum ,编译器将为您完成工作,并且正确的事情发生了。

与非通用代码互操作

数百万行现有代码使用Java类库中已泛化的类,例如Collections框架, ClassThreadLocal 。 重要的是,JDK 5.0中的改进不会破坏所有代码,因此编译器允许您使用泛型类而无需指定其类型参数。

当然,“旧方法”比新方法安全性低,因为您绕过了编译器准备为您提供的类型安全性。 如果您尝试将List<String>传递给接受List的方法,它将起作用,但是编译器将发出警告,提示类型安全性可能会丢失(所谓的“未检查的转换”警告)。

没有类型参数的泛型类型,例如声明为List类型而不是List<Something>的变量,称为原始类型 。 原始类型是与参数化类型的任何实例兼容的分配,但是这样的分配将生成未检查的转换警告。

为了消除某些未检查的转换警告,假设您尚未准备好生成所有代码,则可以改用通配符类型参数。 使用List<?>而不是ListList是原始类型; List<?>是具有未知类型参数的泛型类型。 编译器将对它们进行不同的处理,并可能发出较少的警告。

在任何情况下,编译器在生成字节码时都会生成强制类型转换,因此在任何情况下生成的字节码都不会比没有泛型时的安全性低。 如果您通过使用原始类型或使用类文件玩游戏来破坏类型安全性,则将获得与没有泛型时相同的ClassCastExceptionArrayStoreException

托收的收藏

为了帮助从原始集合类型迁移到通用集合类型,Collections框架添加了一些新的集合包装器,以为某些类型安全错误提供预警。 就像Collections.unmodifiableSet()工厂方法用不允许进行任何修改的Set包装现有Set一样, Collections.checkedSet() (还包括checkedList()checkedMap() )工厂方法会创建包装器或视图类这样可以防止您将错误类型的变量放入集合中。

所有checkedXxx()方法均以类文字作为参数,因此它们可以(在运行时)检查是否允许修改。 典型的实现如下所示:

public class Collections {  
  public static <E> Collection<E> 
    checkedCollection(Collection<E> c, Class<E> type ) { 
    return new CheckedCollection<E>(c, type); 
  } 

  private static class CheckedCollection<E> implements Collection<E> { 
    private final Collection<E> c; 
    private final Class<E> type; 

    CheckedCollection(Collection<E> c, Class<E> type) { 
      this.c = c; 
      this.type = type; 
    } 

    public boolean add(E o) { 
      if (!type.isInstance(o)) 
        throw new ClassCastException(); 
      else
        return c.add(o); 
    } 
  } 
}

血腥细节

清除

Perhaps the most challenging aspect of generic types is erasure , which is the technique underlying the implementation of generics in the Java language. Erasure means that the compiler basically throws away much of the type information of a parameterized class when generating the class file. The compiler generates code with casts in it, just as programmers did by hand before generics. The difference is that the compiler has first validated a number of type-safety constraints that it could not have validated without generic types.

The implications of implementing generics through erasure are considerable and, at first, confusing. Although you cannot assign a List<Integer> to a List<Number> because they are different types, variables of type List<Integer> and List<Number> are of the same class! To see this, try evaluating this expression:

new List<Number>().getClass() == new List<Integer>().getClass()

The compiler generates only one class for List . By the time the bytecode for List is generated, little trace of its type parameter remains.

When generating bytecode for a generic class, the compiler replaces type parameters with their erasure . For an unbounded type parameter ( <V> ), its erasure is Object . For an upper-bounded type parameter ( <K extends Comparable<K>> ), its erasure is the erasure of its upper bound (in this case, Comparable ). For type parameters with multiple bounds, the erasure of its leftmost bound is used.

If you inspected the generated bytecode, you would not be able to tell the difference between code that came from List<Integer> and List<String> . The type bound T is replaced in the bytecode with T 's upper bound, which is usually Object .

擦除的含义

Erasure has a number of implications that might seem odd at first. For example, because a class can implement an interface only once, you cannot define a class like this:

// invalid definition
class DecimalString implements Comparable<String>, Comparable<Integer> { ... }

In light of erasure, the above declaration simply does not make sense. The two instantiations of Comparable are the same interface, and they specify the same compareTo() method. You cannot implement a method or an interface twice.

Another, much more annoying implication of erasure is that you cannot instantiate an object or an array using a type parameter. This means you can't use new T() or new T[10] in a generic class with a type parameter T . The compiler simply does not know what bytecode to generate.

There are some workarounds for this issue, generally involving reflection and the use of class literals ( Foo.class ), but they are annoying. The constructor in the Lhist example class displayed one such technique for working around the problem (see Implementing the constructor ), and the discussion of toArray() (in Replacing T[] with Class<T> ) offered another.

Another implication of erasure is that it makes no sense to use instanceof to test if a reference is an instance of a parameterized type. The runtime simply cannot tell a List<String> from a List<Number> , so testing for (x instanceof List<String>) doesn't make any sense.

Similarly, the following method won't increase the type safety of your programs:

public <T> T naiveCast(T t, Object o) { return (T) o; }

The compiler will simply emit an unchecked warning, because it has no idea whether the cast is safe or not.

Types versus classes

The addition of generic types has made the type system in the Java language more complicated. Previously, the language had two kinds of types -- reference types and primitive types. For reference types, the concepts of type and class were basically interchangeable, as were the terms subtype and subclass .

With the addition of generics, the relationship between type and class has become more complex. List<Integer> and List<Object> are distinct types, but they are of the same class. Even though Integer extends Object , a List<Integer> is not a List<Object> , and it cannot be assigned or even cast to List<Object> .

On the other hand, now there is a new weird type called List<?> , which is a supertype of both List<Integer> and List<Object> . And there is the even weirder List<? extends Number> . The structure and shape of the type hierarchy got a lot more complicated. Types and classes are no longer mostly the same thing.

Covariance

As you learned earlier (see Generic types are not covariant ), generic types, unlike arrays, are not covariant. An Integer is a Number , and an array of Integer is an array of Number . Therefore, you can freely assign an Integer[] reference to a variable of type Number[] . But a List<Integer> is not a List<Number> , and for good reason -- the ability to assign a List<Integer> to a List<Number> could subvert the type checking that generics are supposed to provide.

This means that if you have a method argument that is a generic type, such as Collection<V> , you cannot pass a collection of a subclass of V to that method. If you want to give yourself the freedom to do so, you must use bounded type parameters, such as Collection<T extends V> (or Collection<? extends V> .)

数组

You can use generic types in most situations where you could use a nongeneric type, but there are some restrictions. For example, you cannot declare an array of a generic type (except if the type arguments are unbounded wildcards). The following code is illegal:

List<String>[] listArray = new List<String>[10]; // illegal

Permitting such a construction could create problems, because arrays in Java language are covariant, but parameterized types are not. Because any array type is type-compatible with Object[] (a Foo[] is an Object[] ), the following code would compile without warning, but it would fail at runtime, which would undermine the goal of having any program that compiles without unchecked warnings be type-safe:

List<String>[] listArray = new List<String>[10]; // illegal
Object[] oa = listArray;
oa[0] = new List<Integer>();
String s = lsa[0].get(0); // ClassCastException

If, on the other hand, listArray were of type List<?> , an explicit cast would be required in the last line. Although it would still generate a runtime error, it would not undermine the type-safety guarantees offered by generics (because the error would be in the explicit cast). So arrays of List<?> are permitted.

New meanings for extends

Before the introduction of generics in the Java language, the extends keyword always meant that a new class or interface was being created that inherited from another class or interface.

With the introduction of generics, the extends keyword has another meaning. You use extends in the definition of a type parameter ( Collection<T extends Number> ) or a wildcard type parameter ( Collection<? extends Number> ).

When you use extends to denote a type parameter bound, you are not requiring a subclass-superclass relationship, but merely a subtype-supertype relationship. It is also important to remember that the bounded type does not need to be a strict subtype of the bound; it could be the bound as well. In other words, for a Collection<? extends Number> , you could assign a Collection<Number> (although Number is not a strict subtype of Number ) as well as a Collection<Integer> , Collection<Long> , Collection<Float> , and so on.

In any of these meanings, the type on the right-hand side of extends can be a parameterized type ( Set<V> extends Collection<V> ).

Bounded types

So far, you've seen one kind of type bound -- the upper bound . Specifying an upper bound constrains a type parameter to be a supertype of (or equal to) a given type bound, as in Collection<? extends Number> . It is also possible, though less common, to specify a lower bound , which you write as Collection<? super Foo> . Only wildcards can have lower bounds.

In addition to specifying a type constraint on the type parameter, specifying a bound has another significant effect. If a type T is known to extend Number , then the methods and fields of Number can be accessed through a variable of type T . It might not be known at compile time what the value of T is, but it is known at least to be a Number .

There are some restrictions on which classes can act as type bounds. Primitive types and array types cannot be used as type bounds (but array types can be used as wildcard bounds). Any reference type (including parameterized types) can be used as a type bound.

class C <T extends int> // illegal
class C <T extends Foo[]> // illegal
class C <T extends Foo> //legal
class C <T extends Foo<? extends Moo<T>>> //legal
class C <T, V extends T> // legal

One place where you might use a lower bound is in a method that selects elements from one collection and puts them in another. 例如:

class Bunch<V> {
  public void add(V value) { ... }
  public void copyTo(Collection<? super V>) { ... }
  ...
}

The copyTo() method copies all the values from the Bunch into a specified collection. Rather than specify that it must be a Collection<V> , you can specify that it be a Collection<? super V> , which means copyTo() can copy the contents of a Bunch<String> to a Collection<Object> or a Collection<String> , rather than just a Collection<String> .

The other common case for lower bounds is with the Comparable interface. Rather than specifying:

public static <T extends Comparable<T>> T max(Collection<T> c) { ... }

You can be more flexible in what types you accept:

public static <T extends Comparable<? 
  super T>> T max(Collection<T> c) { ... }

This way, you can pass a type that is comparable to its supertype, in addition to a type that is comparable to itself, for some additional flexibility. This becomes valuable for classes that extend classes that are already Comparable :

public class Base implements Comparable<Base> { ... }
public class Child extends Base { }

Because Child already implements Comparable<Base> (which it inherits from the superclass Base ), you can pass it to the second example of max() above, but not the first.

Multiple bounds

A type parameter can have more than one bound. This is useful when you want to constrain a type parameter to be, say, both Comparable and Serializable . The syntax for multiple bounds is to separate the bounds with an ampersand:

class C<T extends Comparable<? super T> & Serializable>

A wildcard type can have a single bound -- either an upper or a lower bound. A named type parameter can have one or more upper bounds. A type parameter with multiple bounds can be used to access the methods and fields of each of its bounds.

Type parameters and type arguments

In the definition of a parameterized class, the placeholder names (such as V in Collection<V> ) are referred to as type parameters . They have a similar role to that of formal arguments in a method definition. In a declaration of a variable of a parameterized class, the type values specified in the declaration are referred to as type arguments . These have a role similar to actual arguments in a method call. So given the definition:

interface Collection<V> { ... }

and the declaration:

Collection<String> cs = new HashSet<String>();

the name V (which can be used throughout the body of the Collection interface) is called a type parameter. In the declaration of cs , both usages of String are type arguments (one for Collection<V> and the other for HashSet<V> .)

There are some restrictions on when you can use type parameters. Most of the time, you can use them anyplace you can use an actual type definition. 但是也有例外。 You cannot use them to create objects or arrays, and you cannot use them in a static context or in the context of handling an exception. You also cannot use them as supertypes ( class Foo<T> extends T ), in instanceof expressions, or as class literals.

Similarly, there are some restrictions on which types you can use as type arguments. They must be reference types (not primitive types), wildcards, type parameters, or instantiations of other parameterized types. So you can define a List<String> (reference type), a List<?> (wildcard), or a List<List<?>> (instantiation of other parameterized types). Inside the definition of a parameterized type with type parameter T, you could also declare a List<T> (type parameter.)

Wrapping up

摘要

The addition of generic types is a major change to both the Java language and the Java class libraries. Generic types (generics) can improve the type safety, maintainability, and reliability of Java applications, but at the cost of some additional complexity.

Great care was taken to ensure that existing classes will continue to work with the generified class libraries in JDK 5.0, so you can get started with generics as quickly or as slowly as you like.

附录

Appendix A: Comparison to C++ templates

The syntax for generic classes bears a superficial similarity to the template facility in C++. However, there are substantial differences between the two. For example, a generic type in Java language cannot take a primitive type as a type parameter -- only a reference type. This means that you can define a List<Integer> , but not a List<int> . (However, autoboxing can help make a List<Integer> behave like a List of int.)

C++ templates are effectively macros; when you use a C++ template, the compiler expands the template using the provided type parameters. The C++ code generated for List<A> differs from the code generated for List<B> , because A and B might have different operator overloading or inlined methods. And in C++, List<A> and List<B> are actually two different classes.

Generic Java classes are implemented quite differently. Objects of type ArrayList<Integer> and ArrayList<String> share the same class, and only one ArrayList class exists. The compiler enforces type constraints, and the runtime has no information about the type parameters of a generic type. This is implemented through erasure , explained in The gory details .


翻译自: https://www.ibm.com/developerworks/java/tutorials/j-generics/j-generics.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值