JAVA泛型详解——转

泛型(Generic type 或者generics)是对 Java 语言的类型系统的一种扩展,以支持建立能够按类型进行参数化的类。能够把类型参数看做是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符同样。  能够在集合框架(Collection framework)中看到泛型的动机。例如,Map类容许您向一个Map添加任意类的对象,即便最多见的状况是在给定映射(map)中保存某个特定类型(好比String)的对象。  由于Map.get()被定义为返回Object,因此通常必须将Map.get()的结果强制类型转换为指望的类型,以下面的代码所示:  Map m = new HashMap();  m.put("key", "blarg");  String s = (String) m.get("key");  要让程序经过编译,必须将get()的结果强制类型转换为String,而且但愿结果然的是一个String。可是有可能某人已经在该映射中保存了不是String的东西,这样的话,上面的代码将会抛出ClassCastException。  理想状况下,您可能会得出这样一个观点,即m是一个Map,它将String键映射到String值。这可让您消除代码中的强制类型转换,同时得到一个附加的类型检查层,该检查层能够防止有人将错误类型的键或值保存在集合中。这就是泛型所作的工做。  泛型的好处  Java 语言中引入泛型是一个较大的功能加强。不只语言、类型系统和编译器有了较大的变化,以支持泛型,并且类库也进行了大翻修,因此许多重要的类,好比集合框架,都已经成为泛型化的了。这带来了不少好处:  · 类型安全。泛型的主要目标是提升 Java 程序的类型安全。经过知道使用泛型定义的变量的类型限制,编译器能够在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者若是幸运的话,还存在于代码注释中)。  Java 程序中的一种流行技术是定义这样的集合,即它的元素或键是公共类型的,好比“String列表”或者“String到String的映射”。经过在变量声明中捕获这一附加的类型信息,泛型容许编译器实施这些附加的类型约束。类型错误如今就能够在编译时被捕获了,而不是在运行时看成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 或类文件更改。全部工做都在编译器中完成,编译器生成相似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。  泛型用法的例子  泛型的许多最佳例子都来自集合框架,由于泛型让您在保存在集合中的元素上指定类型约束。考虑这个使用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 中集合框架中的Map接口的定义的一部分:  public interface Map<K, V> {  public void put(K key, V value);  public V get(K key);  }  注意该接口的两个附加物:  * 类型参数 K 和 V 在类级别的规格说明,表示在声明一个 Map 类型的变量时指定的类型的占位符。  * 在 get()、put() 和其余方法的方法签名中使用的 K 和 V。  为了赢得使用泛型的好处,必须在定义或实例化Map类型的变量时为K和V提供具体的值。以一种相对直观的方式作这件事:  Map<String, String> m = new HashMap<String, String>();  m.put("key", "blarg");  String s = m.get("key");  当使用Map的泛型化版本时,您再也不须要将Map.get()的结果强制类型转换为String,由于编译器知道get()将返回一个String。  在使用泛型的版本中并无减小键盘录入;实际上,比使用强制类型转换的版本须要作更多键入。使用泛型只是带来了附加的类型安全。由于编译器知道关于您将放进Map中的键和值的类型的更多信息,因此类型检查从执行时挪到了编译时,这会提升可靠性并加快开发速度。  向后兼容  在 Java 语言中引入泛型的一个重要目标就是维护向后兼容。尽管 JDK 5.0 的标准类库中的许多类,好比集合框架,都已经泛型化了,可是使用集合类(好比HashMap和ArrayList)的现有代码将继续不加修改地在 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的方法如今在它们的方法签名中使用K或V,指示附加的类型约束位于Map的规格说明之下。  当声明或者实例化一个泛型的对象时,必须指定类型参数的值:  Map<String, String> map = new HashMap<String, String>();  注意,在本例中,必须指定两次类型参数。一次是在声明变量map的类型时,另外一次是在选择HashMap类的参数化以即可以实例化正确类型的一个实例时。  编译器在遇到一个Map<String, String>类型的变量时,知道K和V如今被绑定为String,所以它知道在这样的变量上调用Map.get()将会获得String类型。  除了异常类型、枚举或匿名内部类之外,任何类均可以具备类型参数。  命名类型参数  推荐的命名约定是使用大写的单个字母名称做为类型参数。这与C++ 约定有所不一样(参阅附录 A:与 C++ 模板的比较),并反映了大多数泛型类将具备少许类型参数的假定。对于常见的泛型模式,推荐的名称是:  * K —— 键,好比映射的键。  * V —— 值,好比 List 和 Set 的内容,或者 Map 中的值。  * E —— 异常类。  * T —— 泛型。  泛型不是协变的  关于泛型的混淆,一个常见的来源就是假设它们像数组同样是协变的。其实它们不是协变的。List<Object>不是List<String>的父类型。  若是 A 扩展 B,那么 A 的数组也是 B 的数组,而且彻底能够在须要B[]的地方使用A[]:  Integer[] intArray = new Integer[10];  Number[] numberArray = intArray;  上面的代码是有效的,由于一个Integer是一个Number,于是一个Integer数组是一个Number数组。可是对于泛型来讲则否则。下面的代码是无效的:  List<Integer> intList = new ArrayList<Integer>();  List<Number> numberList = intList; // invalid  最初,大多数 Java 程序员以为这缺乏协变很烦人,或者甚至是“坏的(broken)”,可是之因此这样有一个很好的缘由。若是能够将List<Integer>赋给List<Number>,下面的代码就会违背泛型应该提供的类型安全:  List<Integer> intList = new ArrayList<Integer>();  List<Number> numberList = intList; // invalid  numberList.add(new Float(3.1415));  由于intList和numberList都是有别名的,若是容许的话,上面的代码就会让您将不是Integers的东西放进intList中。可是,正以下一屏将会看到的,您有一个更加灵活的方式来定义泛型。  package com.ibm.course.generics;  import java.util.ArrayList;  import java.util.List;  public class GenericsExample {  public static void main(String[] args) {  Integer[] integer = new Integer[5];  Number[] number = integer;  System.out.println(number[0]);// null  number[0] = new Float(7.65);  System.out.println(number[0]);  System.out.println(integer[0]);  List<Integer> list = new ArrayList<Integer>();  // Type mismatch: cannot convert from List<Integer> to List<Number>  // List<Number> listObj = list;  }  }  List<Number> listObj = list;致使编译错误:Type mismatch: cannot convert from List<Integer> to List<Number>  而System.out.println(number[0]);和System.out.println(integer[0]);致使运行时异常:  Exception in thread "main" java.lang.ArrayStoreException: java.lang.Float  at com.ibm.course.generics.GenericsExample.main(GenericsExample.java:15)  类型通配符  假设您具备该方法:  void printList(List l) {  for (Object o : l)  System.out.println(o);  }  上面的代码在 JDK 5.0 上编译经过,可是若是试图用List<Integer>调用它,则会获得警告。出现警告是由于,您将泛型(List<Integer>)传递给一个只承诺将它看成List(所谓的原始类型)的方法,这将破坏使用泛型的类型安全。  若是试图编写像下面这样的方法,那么将会怎么样?  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()。  package com.ibm.course.generics;  import java.util.ArrayList;  import java.util.List;  public class GenericExample {  public static void main(String[] args) {  List<Integer> integer = new ArrayList<Integer>();  integer.add(new Integer(0));  integer.add(new Integer(1));  List<String> str = new ArrayList<String>();  str.add(new String("Hello"));  str.add(new String("World"));  List<?> li=integer;  li=str;  printList(integer);  printList(str);  }  public static void printList(List<?> l) {  for (Object o : l) {  System.out.println(o);  }  }  }  上面的例子程序没有警告也没有编译错误。  类型通配符的做用  前一屏类型通配符 中引入了类型通配符,这让您能够声明List<?>类型的变量。您能够对这样的List作什么呢?很是方便,能够从中检索元素,可是不能添加元素(能够添加null)。缘由不是编译器知道哪些方法修改列表哪些方法不修改列表,而是(大多数)变化的方法比不变化的方法须要更多的类型信息。下面的代码则工做得很好:  List<Integer> li = new ArrayList<Integer>();  li.add(new Integer(42));  List<?> lu = li;  System.out.println(lu.get(0));  为何该代码能工做呢?对于lu,编译器一点都不知道List的类型参数的值。可是编译器比较聪明,它能够作一些类型推理。在本例中,它推断未知的类型参数必须扩展Object。(这个特定的推理没有太大的跳跃,可是编译器能够做出一些很是使人佩服的类型推理,后面就会看到(在底层细节 一节中)。因此它让您调用List.get()并推断返回类型为Object。  另外一方面,下面的代码不能工做:  List<Integer> li = new ArrayList<Integer>();  li.add(new Integer(42));  List<?> lu = li;  lu.add(new Integer(43)); // error  在本例中,对于lu,编译器不能对List的类型参数做出足够严密的推理,以肯定将Integer传递给List.add()是类型安全的。因此编译器将不容许您这么作。  以避免您仍然认为编译器知道哪些方法更改列表的内容哪些不更改列表内容,请注意下面的代码将能工做,由于它不依赖于编译器必须知道关于lu的类型参数的任何信息:  List<Integer> li = new ArrayList<Integer>();  li.add(new Integer(42));  List<?> lu = li;  lu.clear();  泛型方法  (在类型参数 一节中)您已经看到,经过在类的定义中添加一个形式类型参数列表,能够将类泛型化。方法也能够被泛型化,无论它们定义在其中的类是否是泛型化的。  泛型类在多个方法签名间实施类型约束。在List<V>中,类型参数V出如今get()、add()、contains()等方法的签名中。当建立一个Map<K, V>类型的变量时,您就在方法之间宣称一个类型约束。您传递给add()的值将与get()返回的值的类型相同。  相似地,之因此声明泛型方法,通常是由于您想要在该方法的多个参数之间宣称一个类型约束。例如,下面代码中的ifThenElse()方法,根据它的第一个参数的布尔值,它将返回第二个或第三个参数:  public <T> T ifThenElse(boolean b, T first, T second) {  return b ? first : second;  }  注意,您能够调用ifThenElse(),而不用显式地告诉编译器,您想要T的什么值。编译器没必要显式地被告知 T 将具备什么值;它只知道这些值都必须相同。编译器容许您调用下面的代码,由于编译器可使用类型推理来推断出,替代T的String知足全部的类型约束:  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 的约束。经过使得泛型方法的类型参数对于方法是局部的,能够简化封闭类型的签名。  有限制类型  在前一屏泛型方法 的例子中,类型参数V是无约束的或无限制的类型。有时在尚未彻底指定类型参数时,须要对类型参数指定附加的约束。  考虑例子Matrix类,它使用类型参数V,该参数由Number类来限制:  public class Matrix<V extends Number> { ... }  编译器容许您建立Matrix<Integer>或Matrix<Float>类型的变量,可是若是您试图定义Matrix<String>类型的变量,则会出现错误。类型参数V被判断为由Number限制。在没有类型限制时,假设类型参数由Object限制。这就是为何前一屏泛型方法 中的例子,容许List.get()在List<?>上调用时返回Object,即便编译器不知道类型参数V的类型。
相关文章
相关标签/搜索