第八章 泛型程序设计

为什么要使用泛型程序设计

泛型程序设计(Generic programming) 意味着编写的代码可以被很多不同类型的对象所 重用。例如, 我们并不希望为聚集 String 和 File 对象分别设计不同的类。实际上,也不需要 这样做,因为一个 ArrayList 类可以聚集任何类型的对象。这是一个泛型程序设计的实例。

类型参数的好处

在 Java 中增加范型类之前, 泛型程序设计是用继承实现的。ArrayList 类只维护一个 Object 引用的数组:

public class ArrayList // before generic classes
​
{
    ...
    private Object[] elementData;
    
    public Object get(int i) { . . , } 
    
    public void add(Object o) { . . . }
​
}

 

这种方法有两个问题。当获取一个值时必须进行强制类型转换。

ArrayList files = new ArrayList();

String filename = (String) files.get(0);

 

此外,这里没有错误检査。可以向数组列表中添加任何类的对象。

files,add(new File(". . .");

对于这个调用,编译和运行都不会出错。然而在其他地方,如果将 get 的结果强制类型 转换为 String类型, 就会产生一个错误。

泛型提供了一个更好的解决方案: 类型参数(type parameters)。ArrayList 类有一个类型 参数用来指示元素的类型:

ArrayList<String> files = new ArrayList<String>();

这使得代码具有更好的可读性。人们一看就知道这个数组列表中包含的是 String 对象。

谁想成为泛型程序员

实现一个泛型类并没有那么容易。对于类型参数,使用这段代码的程序员可能想 要内置(plugin) 所有的类。他们希望在没有过多的限制以及混乱的错误消息的状态下, 做 所有的事情。因此,一个泛型程序员的任务就是预测出所用类的未来可能有的所有用途。

定义简单泛型类

一个泛型类(generic class) 就是具有一个或多个类型变量的类。下面 是 Pair 类的代码:

public class Pair<T> { 
​
    private T first; 
​
    private T second;
    public Pair() { 
​
        first = null; 
​
        second = null; 
    } 
​
    public PairfT first, T second) { 
        this,first = first; 
        this.second = second; 
    }
    public T getFirst() { 
        return first; 
    } 
    public T getSecondO { 
        return second; 
    }
    public void setFirst(T newValue) { 
        first = newValue; 
    } public void setSecond(T newValue) { 
        second = newValue; 
    }
} 

 

Pair 类引人了一个类型变量 T,用尖括号 ( < >) 括起来,并放在类名的后面。泛型类可 以有多个类型变量。例如, 可以定义 Pair 类,其中第一个域和第二个域使用不同的类型:

public class Pair<T, U> { . . . }

类定义中的类型变量指定方法的返回类型以及域和局部变量的类型。例如,

private T first; // uses the type variable

[注]  类型变量使用大写形式,且比较短, 这是很常见的。在 Java 库中, 使用变量 E 表示集合的元素类型, K 和 V 分别表示表的关键字与值的类型。T ( 需要时还可以用临近的 字母 U 和 S) 表示“ 任意类型”。

程序清单 8-1 中的程序使用了 Pair 类。静态的 minmax方法遍历了数组并同时计算出最 小值和最大值。它用一个 Pair 对象返回了两个结果。 回想一下 compareTo 方法比较两个字符 串, 如果字符串相同则返回 0 ; 如果按照字典顺序, 第一个字符串比第二个字符串靠前, 就 返回负值, 否则, 返回正值。

//程序清单 8-1 pair1/PairTest1.java
package pair1;
​
/**
 * @version 1.01 2012-01-26
 * @author Cay Horstmann
 */
public class PairTest1
{
   public static void main(String[] args)
   {
      String[] words = { "Mary", "had", "a", "little", "lamb" };
      Pair<String> mm = ArrayAlg.minmax(words);
      System.out.println("min = " + mm.getFirst());
      System.out.println("max = " + mm.getSecond());
   }
}
​
class ArrayAlg
{
   /**
    * Gets the minimum and maximum of an array of strings.
    * @param a an array of strings
    * @return a pair with the min and max value, or null if a is null or empty
    */
   public static Pair<String> minmax(String[] a)
   {
      if (a == null || a.length == 0) return null;
      String min = a[0];
      String max = a[0];
      for (int i = 1; i < a.length; i++)
      {
         if (min.compareTo(a[i]) > 0) min = a[i];
         if (max.compareTo(a[i]) < 0) max = a[i];
      }
      return new Pair<>(min, max);
   }
}

 

泛型方法

前面已经介绍了如何定义一个泛型类。实际上,还可以定义一个带有类型参数的简单方法。

class ArrayAlg {
    public static <T> T getMiddle(T... a) {
        return a[a.length / 2]; 
    } 
} 

 

这个方法是在普通类中定义的,而不是在泛型类中定义的。然而,这是一个泛型方法, 可以从尖括号和类型变量看出这一点。注意,类型变量放在修饰符(这里是 public static) 的 后面,返回类型的前面。

泛型方法可以定义在普通类中,也可以定义在泛型类中。

当调用一个泛型方法时 在方法名前的尖括号中放人具体的类型:

String middle = ArrayAlg.<String>getMiddle("]ohnM, "Q.n, "Public");

在这种情况(实际也是大多数情况)下,方法调用中可以省略 <String> 类型参数。编译 器有足够的信息能够推断出所调用的方法。

 

类型变量的限定

有时,类或方法需要对类型变量加以约束。下面是一个典型的例子。我们要计算数组中 的最小元素:

 class ArrayAIg 
 { 
     public static <T> T min(T[] a) // almost correct 
 { 
     if (a null || a.length = 0) 
         return null; 
     T smallest = a[0]; 
     for (int i = 1 ; i < a.length; i++) 
         if (smallest.compareTo(a[i]) > 0) 
             smallest = a[i]; 
     return smallest; 
 } 
} 

 

泛型的 min方法只能被实现了 Comparable 接口的类(如 String、 LocalDate 等)的数 组调用。由于 Rectangle类没有实现 Comparable 接口, 所以调用 min将会产生一个编译错误。

在程序清单 8-2 的程序中,重新编写了一个泛型方法 minmax。这个方法计算泛型数组的 最大值和最小值, 并返回 Pair<T>

//程序清单 8-2 pair2/PairTest2.java 
package pair2;
​
import java.util.*;
​
/**
 * @version 1.01 2012-01-26
 * @author Cay Horstmann
 */
public class PairTest2
{
   public static void main(String[] args)
   {
      GregorianCalendar[] birthdays = 
         { 
            new GregorianCalendar(1906, Calendar.DECEMBER, 9), // G. Hopper
            new GregorianCalendar(1815, Calendar.DECEMBER, 10), // A. Lovelace
            new GregorianCalendar(1903, Calendar.DECEMBER, 3), // J. von Neumann
            new GregorianCalendar(1910, Calendar.JUNE, 22), // K. Zuse
         };
      Pair<GregorianCalendar> mm = ArrayAlg.minmax(birthdays);
      System.out.println("min = " + mm.getFirst().getTime());
      System.out.println("max = " + mm.getSecond().getTime());
   }
}
​
class ArrayAlg
{
   /**
      Gets the minimum and maximum of an array of objects of type T.
      @param a an array of objects of type T
      @return a pair with the min and max value, or null if a is 
      null or empty
   */
   public static <T extends Comparable> Pair<T> minmax(T[] a) 
   {
      if (a == null || a.length == 0) return null;
      T min = a[0];
      T max = a[0];
      for (int i = 1; i < a.length; i++)
      {
         if (min.compareTo(a[i]) > 0) min = a[i];
         if (max.compareTo(a[i]) < 0) max = a[i];
      }
      return new Pair<>(min, max);
   }
}

泛型代码和虚拟机

类型擦除

无论何时定义一个泛型类型, 都自动提供了一个相应的原始类型 ( raw type)。原始类型 的名字就是删去类型参数后的泛型类型名。擦除(erased) 类型变M, 并替换为限定类型(无 限定的变量用 Object)。

例如, Pair<T> 的原始类型如下所示:

public class Pair { 
    private Object first; 
    private Object second; 
    public Pair(Object first, Object second) { 
        this.first = first; 
        this.second = second;
    }
    public Object getFirst() { 
        return first; 
    } 
    public Object getSecond() { 
        return second; 
    } 
    public void setFirst(Object newValue) { 
        first = newValue; 
    } 
    public void setSecond(Object newValue) { 
        second = newValue; 
    }
} 

 

因为 T 是一个无限定的变量, 所以直接用 Object 替换。

原始类型用第一个限定的类型变量来替换, 如果没有给定限定就用 Object 替换。例如, 类 Pair<T> 中的类型变量没有显式的限定, 因此, 原始类型用 Object 替换 T。假定声明了一 个不同的类型。

public class Interval<T extends Comparable & Serializable〉implements Serializable { 
    private T lower; private T upper;
    public Interval(T first, T second) { 
        if (first.compareTo(second) <= 0) {
            lower = first; upper = second; 
        } else { 
            lower = second; 
            upper = first; 
        } 
    }
}

 

原始类型 Interval 如下所示:

 public class Interval implements Serializable { 
     private Comparable lower; 
     private Coiparable upper; 
     public Interval(Coiparable first, Coiparable second) {
         . . . 
     } 
 }

 

[注]  切换限定: class Interval<T extends Serializable & Comparable> 会发生什么。如果这样做, 原始类型用 Serializable 替换 T, 而编译器在必要时要向 Comparable 插入强制类型转换。为了提高效率,应该将标签(tagging) 接口(即没有方 法的接口)放在边界列表的末尾。

 

翻译泛型表达式

当程序调用泛型方法时,如果擦除返回类型, 编译器插入强制类型转换。例如,下面这 个语句序列

Pair<Employee> buddies = . . .;

Employee buddy = buddies.getFirst();

擦除 getFirst 的返回类型后将返回 Object 类型。编译器自动插人 Employee 的强制类型转换。

也就是说,编译器把这个方法调用翻译为两条虚拟机指令:

•对原始方法 Pair.getFirst 的调用。

•将返回的 Object 类型强制转换为 Employee类型。

当存取一个泛型域时也要插人强制类型转换。假设 Pair 类的 first 域和 second 域都是公 有的(也许这不是一种好的编程风格,但在 Java中是合法的)。表达式:

Employee buddy = buddies.first;

也会在结果字节码中插人强制类型转换。

翻译泛型方法

类型擦除也会出现在泛型方法中。程序员通常认为下述的泛型方法

public static <T extends Comparable〉 T nrin(T[] a)

是一个完整的方法族,而擦除类型之后,只剩下一个方法:

public static Comparable min(Comparable=[] a)

注意,类型参数 T 已经被擦除了, 只留下了限定类型 Comparable。

变量 pair 已经声明为类型 Pair<LocalDate>, 并且这个类型只有一个简单的方法叫setSecond, 即 setSecond(Object)。 虚拟机用 pair 引用的对象调用这个方法。这个对象是 Datelnterval 类型的, 因而将会调用 Datelnterval.setSecond(Object)方法。这个方法是合成的桥方法。

总之,需要记住有关 Java 泛型转换的事实:

•虚拟机中没有泛型,只有普通的类和方法。

•所有的类型参数都用它们的限定类型替换。

•桥方法被合成来保持多态。

•为保持类型安全性,必要时插人强制类型转换。

 

约束与局限性

不能用基本类型实例化类型参数

没有 Pair<double>, 只 有 Pair<Double>。 当然, 其原因是类型擦除。擦除之后, Pair 类含有 Object 类型的域, 而 Object 不能存储 double值。

这的确令人烦恼。但是,这样做与 Java语言中基本类型的独立状态相一致。这并不是一 个致命的缺陷— —只有 8 种基本类型, 当包装器类型(wrapper type) 不能接受替换时, 可以使用独立的类和方法处理它们。

运行时类型查询只适用于原始类型

虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。

例如:

if (a instanceof Pair<String>) // Error

实际上仅仅测试 a 是否是任意类型的一个 Pair。下面的测试同样如此:

if (a instanceof Pair<T>) // Error

或强制类型转换:

Pair<St「ing> p = (Pair<String>) a; // Warning-can only test that a is a Pair

为提醒这一风险, 试图查询一个对象是否属于某个泛型类型时,倘若使用 instanceof 会 得到一个编译器错误, 如果使用强制类型转换会得到一个警告。

 

不能创建参数化类型的数组

不能实例化参数化类型的数组, 例如:

Pair<String>[] table = new Pair<String>[10]; // Error

数组会记住它的元素类型, 如果试图存储其他类型的元素, 就会抛出一个 ArrayStoreException 异常:

objarray[0] = "Hello"; // Error component type is Pair

不过对于泛型类型, 擦除会使这种机制无效。以下赋值:

objarray[0] = new Pair<Employee>0;

能够通过数组存储检査, 不过仍会导致一个类型错误。出于这个原因, 不允许创建参数 化类型的数组。 需要说明的是, 只是不允许创建这些数组, 而声明类型为 Pair<String>[] 的变量仍是合法 的。不过不能用

new Pair<String>[10]

初始化这个变量。

Varargs 警告

可以采用两种方法来抑制这个警告。一种方法是为包含 addAll 调用的方法增加注解@ SuppressWamings("unchecked")。 或者在 Java SE 7中, 还 可 以 用@SafeVarargs 直 接 标 注 addAll 方法:

@SafeVarargs

public static <T> void addAll(Collection<T> coll, T... ts)

现在就可以提供泛型类型来调用这个方法了。对于只需要读取参数数组元素的所有方 法,都可以使用这个注解,这仅限于最常见的用例。

[注]  可以使用 @SafeVarargs 标注来消除创建泛型数组的有关限制, 方法如下: @SafeVarargs static <E> E()array(E... array) { return array; } 现在可以调用: Pair<String>[] table = array(pairl,pai「2); 这看起来彳艮方便,不过隐藏着危险。以下代码: Object□ objarray = table; objarray[0] = new Pair<Employee>(); 能顺利运行而不会出现 ArrayStoreException 异常(因为数组存储只会检查擦除的类 型),但在处理 table[0] 时你会在别处得到一个异常。

不能实例化类型变置

不能使用像 new T(...),newT[...] 或 T.class 这样的表达式中的类型变量。例如, 下面的 Pair<T> 构造器就是非法的:

public Pair() { first = new T(); second = new T(); } // Error

类型擦除将 T 改变成 Object, 而且, 本意肯定不希望调用 new Object()。在 Java SE 8 之后, 最好的解决办法是让调用者提供一个构造器表达式。例如:

Pair<String> p = Pair.makePairCString::new);

makePair方法接收一个 Supplier<T>,这是一个函数式接口,表示一个无参数而且返回 类型为 T 的函数:

public static <T> Pair<T> makePair(Supplier<T> constr) { 
​
return new Pair<>(constr.get0.constr.get0);
​
} 

 

比较传统的解决方法是通过反射调用 Clasmewlnstance 方法来构造泛型对象。

不能构造泛型数组

类型擦除会让这个方法永远构造 Comparable[2] 数组。 如果数组仅仅作为一个类的私有实例域, 就可以将这个数组声明为 Object[],并且在获 取元素时进行类型转换。例如’ ArrayList 类可以这样实现:

public class ArrayList<E> { 
    private Object口 elements;
    ...
    @SuppressWarnings("unchecked") public E get(int n) { return (E) elements[n]; } 
    public void set(int n, E e) { elements[n] = e; } // no cast needed
} 

 

实际的实现没有这么清晰:

 public class ArrayList<E> { 
     private E[] elements;
     ...
     public ArrayList() { elements = (ED) new Object[10]; 
                        }
 }

 

这里, 强制类型转换 E[ ] 是一个假象, 而类型擦除使其无法察觉。

泛型类的静态上下文中类型变量无效

不能在静态域或方法中引用类型变量。例如, 下列高招将无法施展:

public class Singleton<T> 
{ 
    private static T singlelnstance; // Error 
    public static T getSinglelnstanceO // Error
    { 
        if (singleinstance == null) construct new instanceof T 
            return singlelnstance; 
    } 
}

 

如果这个程序能够运行, 就可以声明一个 Singleton<Random> 共享随机数生成器, 声明 一个 Singlet0n<JFileCh00Ser> 共享文件选择器对话框。但是, 这个程序无法工作。类型擦除 之后, 只剩下 Singleton类,它只包含一个 singlelnstance 域。 因此, 禁止使用带有类型变量 的静态域和方法。

不能抛出或捕获泛型类的实例

既不能抛出也不能捕获泛型类对象。实际上, 甚至泛型类扩展 Throwable 都是不合法的。

既不能抛出也不能捕获泛型类对象。实际上, 甚至泛型类扩展 Throwable 都是不合法的。 例如, 以下定义就不能正常编译:

public class Problem<T> extends Exception { /* ...*/ } // Error can't extend Throwable

catch 子句中不能使用类型变量。

可以消除对受查异常的检查

Java 异常处理的一个基本原则是, 必须为所有受查异常提供一个处理器。不过可以利用 泛型消除这个限制。关键在于以下方法:

@SuppressWamings("unchecked") 
​
public static <T extends Throwable〉void throwAs(Throwable e) throws T { 
​
throw (T) e; 
​
} 

 

假设这个方法包含在类 Block 中, 如果调用

Block.<RuntimeException>throwAs(t);

编译器就会认为 t 是一个非受查异常。

通过使用泛型类、擦除和@SuppressWamings 注解, 就能消除 Java 类型系统的部分基本 限制。

注意擦除后的冲突

当泛型类型被擦除时, 无法创建引发冲突的条件。

下面是一个示例。假定像下面这样将 equals方法添加到 Pair 类中:

public class Pair<T> 
{ public boolean equals(T value) { return first,equals(value) && second,equals(value); }
 ...
}

 

考虑一个 Pair<String>。从概念上讲, 它有两个 equals 方法:

boolean equals(String) // defined in Pair<T>

boolean equals(Object) // inherited from Object

但是,直觉把我们引入歧途。方法擦除

boolean equals(T) 就是

boolean equals(Object)

与 Object.equals方法发生冲突。

当然,补救的办法是重新命名引发错误的方法。

泛型规范说明还提到另外一个原则: “ 要想支持擦除的转换, 就需要强行限制一个类或类 型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。 ” 例如, 下述代码是非法的:

class Employee implements Coinparab1e<Emp1oyee> { . . . } 
class Manager extends Employee implements Comparable<Hanager> 
{... } // Error Manager 

 

会实现Comparable<Employee> 和 Comparable<Manager>, 这是同一接口的不同 参数化。

泛型类型的继承规则

永远可以将参数化类型转换为一个原始类型。例如,PaiKEmployee> 是原始类型 Pair 的 一个子类型。在与遗留代码衔接时,这个转换非常必要。

必须注意泛型与 Java 数组之间的重要区别。可以将一个 Manager□ 数组賦给一个 类型为 Employee[] 的变量:

Manager口 managerBuddies = { ceo, cfo };

Employee[] employeeBuddies = managerBuddies; // OK

然而,数组带有特别的保护。如果试图将一个低级别的雇员存储到 employeeBuddies[0], 虚拟机将会抛出 ArrayStoreException 异常。

永远可以将参数化类型转换为一个原始类型。例如,PaiKEmployee> 是原始类型 Pair 的 一个子类型。在与遗留代码衔接时,这个转换非常必要。

posted on 2020-08-13 21:23  ♌南墙  阅读(156)  评论(0)    收藏  举报