核心技术:泛型

来自Wikioe
跳到导航 跳到搜索


为什么要使用泛型

泛型程序设计(Generic programming) 意味着编写的代码可以被很多不同类型的对象所重用。


类型参数( type parameters )。ArrayList 类有一个类型参数用来指示元素的类型:

ArrayList<String> files = new ArrayList<String>();
  • 在Java SE 7 及以后的版本中, 构造函数中可以省略泛型类型:
    ArrayList<String> files = new ArrayList<>();
    

定义简单泛型类

一个泛型类( generic class ) 就是具有一个或多个类型变量的类。

  • 换句话说,泛型类可看作普通类的工厂。

泛型方法

  • 类型变量放在修饰符(这里是public static ) 的后面,返回类型的前面。
class ArrayAlg
{
   public static <T> T getMiddle(T... a)
   {
      return a[a.length / 2];
   }
}

泛型变量的限定

是将T 限制为实现了Comparable 接口(只含一个方法compareTo 的标准接口)的类:

public static <T extends Coiparab1e> T min(T[] a) . . .
  • 使用关键字extends 而不是implements
  • T 和绑定类型可以是类, 也可以是接口
  • 一个类型变量或通配符可以有多个限定:
    T extends Comparable & Serializable
    
    限定类型用“&”分隔,而逗号用来分隔类型变量。

泛型代码和虚拟机

类型擦除

无论何时定义一个泛型类型, 都自动提供了一个相应的原始类型( raw type )。

  • 原始类型的名字就是删去类型参数后的泛型类型名。
  • 擦除(erased) 类型变量, 并替换为限定类型(无限定的变量用Object)。


如:

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

擦除为:

public class Pair
{
   private Object first;
   private Object second;
   public Pair() { first = null ; second = null ; }
   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; }
}


  • 原始类型用第一个限定的类型变量来替换, 如果没有给定限定就用Object 替换。

例:

public class Interval <T extends Comparable & Serializableimplements 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; }
   }
}

擦除为:

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

翻译泛型表达式

对于如下表达式:

Pair<Employee> buddies = . .;
Employee buddy = buddies.getFirst()

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

  1. 对原始方法Pair.getFirst 的调用。
  2. 将返回的Object 类型强制转换为Employee 类型。

翻译泛型方法

类型擦除也会出现在泛型方法中:

public static <T extends Comparable> T min(T[] a)

擦除为:

public static Comparable min(Comparable[] a)


方法的擦除带来了两个复杂问题。如:

class DateInterval extends Pair<LocalDate> 
{
   public void setSecond (Local Date second) 
   {
      if (second.compareTo(getFi rst()) >= 0) 
         super. setSecond (second); 
   }
   ...
}

这个类擦除后变成:

class DateInterval extends Pair// after erasure 
{
   public void setSecond (Local Date second) { . . . } 
   ...
}

而Datelnterval同时存在一个从Pair继承的setSecond方法,即:

pub1ic void setSecond (Object second)

考虑下面的语句序列:

DateInterval interval = new Datelnterval (...) ; 
Pair<LocalDate> pair= interval; // OK--assignment to superclass 
pair.setSecond (aDate);

在这里,类型擦除与多态发生了冲突。要解决这个问题,就需要编译器在Datelnterval类中生成一个桥方法(bridgemethod):

public void setSecond(Object second) 
{ 
   setSecond((Date) second); 
}


有关 Java 泛型转换的事实:

  • 虚拟机中没有泛型,只有普通的类和方法
  • 所有的类型参数都用它们的限定类型替换
  • 桥方法被合成来保持多态
  • 为保持类型安全性,必要时插入强制类型转换

调用遗留代码

约束与局限性

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

没有Pair<double>, 只有Pair<Double>。

其原因是类型擦除:擦除之后, Pair 类含有Object 类型的域, 而Object 不能存储double 值;

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

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


例如: 类型判断:

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

实际上仅仅测试a 是否是任意类型的一个Pair。

强制类型转换:

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

同样的道理, getClass 方法总是返回原始类型。例如:

Pair<String> stringPair = . .
Pair< EmployeeemployeePair = . .
if (stringPair.getClassO == employeePair .getClassO) // they are equal

其比较的结果是true, 这是因为两次调用getClass 都将返回Pair.class。

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

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

只是不允许创建这些数组, 而声明类型为“Pair<String>[]”的变量仍是合法的。不过不能用“new Pair<String>[10]”初始化这个变量。


如:

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

擦除之后, table 的类型是Pair[]。 可以把它转换为Object[] :

Object[] objarray = table;

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


  • 可以声明通配类型的数组, 然后进行类型转换:
    Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];
    

= Varargs 警告(可变参数警告)

如:

public static <T> void addAll(Collections coll, T... ts)
{
   for (t : ts) coll.add⑴
}
  • 实际上参数ts 是一个数组。

调用

Col1ection<Pair<String» table = . . .;
Pair<String> pairl = . . .;
Pair<String> pair2 = . .
addAll (table, pairl, pair2);

为了调用这个方法,Java 虚拟机必须建立一个Pair<String> 数组, 这就违反了前面的规则。不过,对于这种情况, 规则有所放松, 你只会得到一个警告,而不是错误。


可以采用两种方法来抑制这个警告。

  1. 调用addAll的方法增加注解@SuppressWamings("unchecked")
  2. 直接标注addAll方法@SafeVarargs

不能示例化类型变量

不能使用像“new T(...)”,“newT[...]” 或“T.class” 这样的表达式中的类型变量。


  • Class 类本身是泛型。例如, String.class 是一个 Class<String> 的实例(事实上,String.class 是一个 Class<String>唯一的实例)。

不能构造泛型数组

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

不能在静态域或方法中引用类型变量。


如:

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

可以声明一个Singleton<Random> 共享随机数生成器, 声明一个Singlet0n<JFileChooser> 共享文件选择器对话框。但是, 这个程序无法工作。
类型擦除之后, 只剩下Singleton 类, 它只包含一个singlelnstance 域。

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

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

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

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


【??????】


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

注意擦除后的冲突

要想支持擦除的转换, 就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类, 而这两个接口是同一接口的不同参数化。


如:

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

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

泛型类的继承规则

无论S 与T 有什么联系,通常, Pai<S> 与Pair<T>没有什么联系。
Pair 类之间没有继承关系.jpg


泛型类可以扩展或实现其他的泛型类。就这一点而言,与普通的类没有什么区别。
例如, ArrayList<T> 类实现List<T> 接口:
泛型列表类型中子类型间的联系.jpg

通配符类型

通配符概念

通配符类型中,允许类型参数变化。


例如, 通配符类型

Pair<? extends Employee

表示任何泛型Pair 类型。类型Pair<Manager> 是Pair<? extends Employee> 的子类型:

使用通配符的子类型关系.jpg

通配符的超类型限定

通配符限定与类型变量限定十分类似,但是,还有一个附加的能力, 即可以指定一个超类型限定(supertypebound):

? super Manager

这个通配符限制为Manager 的所有超类型。

带有超类型限定的通配符.jpg


  • 直观地讲,带有超类型限定的通配符可以向泛型对象写人,带有子类型限定的通配符可以从泛型对象读取。

无限定通配符

例如

Pair<?>

有以下方法:

? getFi rst()
void setFirst(?)

getFirst 的返回值只能赋给一个Object。setFirst 方法不能被调用, 甚至不能用Object 调用。
Pair<?> 和Pair 本质的不同在于: 可以用任意Object 对象调用原始Pair 类的setObject方法。


【???】

通配符捕获

【???】

反射和泛型

泛型 Class 类

Class类是泛型的。例如,String.class实际上是一个Class<String>类的对象(事实上,是唯一的对象)。


下面Class<T> 中的方法就使用了类型参数:

T newInstance()
T cast(Object obj)
T[] getEnumConstants()
Class<? super T> getSuperclassO
Constructors getConstructor(C1ass... parameterTypes)
Constructors getDeclaredConstructor(Class... parameterTypes)
  1. newlnstance 方法返回一个实例,这个实例所属的类由默认的构造器获得。它的返回类型目前被声明为T, 其类型与Class<T> 描述的类相同,这样就免除了类型转换。
  2. 如果给定的类型确实是T 的一个子类型,cast 方法就会返回一个现在声明为类型T 的对象, 否则,抛出一个BadCastException 异常。
  3. 如果这个类不是enum 类或类型T 的枚举值的数组, getEnumConstants 方法将返回null。
  4. 最后, getConstructor 与getdeclaredConstructor 方法返回一个Constructor<T> 对象。
    Constructor 类也已经变成泛型, 以便newlnstance 方法有一个正确的返回类型。

相关方法

java.lang.Class<T>
  • T newInstanceO
    返回无参数构造器构造的一个新实例。
  • T cast(Object obj)
    如果obj 为null 或有可能转换成类型T, 则返回obj ; 否则拋出BadCastException异常。
  • T[ ] getEnumConstants( ) 5.0
    如果T 是枚举类型, 则返回所有值组成的数组,否则返回null。
  • Class<? super T> getSuperclass( )
    返回这个类的超类。如果T 不是一个类或Object 类, 则返回null。
  • Constructor<T> getConstructor(Class. * * parameterTypes) 1.1
  • Constructor<T> getDeclaredConstructor(Class. * . parameterTypes) 1.1
    获得公有的构造器, 或带有给定参数类型的构造器。
java.lang.reflect.Constmctor<T>
  • T newlnstance(0bject . . . parameters )
    返回用指定参数构造的新实例。

使用 Class<T> 参数进行类型匹配

示例:

public static <T> Pair<T> makePair(Class<T> c) throws InstantiationException,
IllegalAccessException
{
   return new Pair<>(c.newInstance(), c.newInstance()) ;
}

如果调用

makePair(Employee.class)

Employee.class 是类型Class<Employee> 的一个对象。makePair 方法的类型参数T 同Employee匹配, 并且编译器可以推断出这个方法将返回一个Pair<Employee>。

虚拟机中的泛型类型信息

在虚拟机中泛型类型会被擦除,擦除的类仍然保留一些泛型祖先的微弱记忆。


例如,对于

public static Comparable min(Coniparable[] a)

是一个泛型方法的擦除:

public static <T extends Comparable? super T>> T min(T[] a)

可以使用反射API来确定:

  • 这个泛型方法有一个叫做T 的类型参数。
  • 这个类型参数有一个子类型限定, 其自身又是一个泛型类型。
  • 这个限定类型有一个通配符参数。
  • 这个通配符参数有一个超类型限定。
  • 这个泛型方法有一个泛型数组参数。


为了表达泛型类型声明, 使用java.lang.reflect 包中提供的接口Type。这个接口包含下列子类型:

  • Class 类,描述具体类型。
  • TypeVariable 接口, 描述类型变量(如T extends Comparable<? super T>) 0
  • WildcardType 接口, 描述通配符(如?super T)。
  • ParameterizedType 接口, 描述泛型类或接口类型(如Comparable<? super T>)。
  • GenericArrayType 接口, 描述泛型数组(如T[ ])。

Type 类和它的后代.jpg

相关方法

java.lang.Class<T>
  • TypeVariable[ ] getTypeParameters( ) 5.0
    如果这个类型被声明为泛型类型, 则获得泛型类型变量,否则获得一个长度为0 的数组。
  • Type getGenericSuperclass( ) 5.0
    获得被声明为这一类型的超类的泛型类型; 如果这个类型是Object 或不是一个类类型( class type), 则返回null。
  • Type[ ] getGenericInterfaces( ) 5.0
    获得被声明为这个类型的接口的泛型类型(以声明的次序),否则, 如果这个类型没有实现接口,返回长度为0 的数组。
java.lang.reflect.Method
  • TypeVariableC ] getTypeParameters( ) 5.0
    如果这个方法被声明为泛型方法, 则获得泛型类型变量,否则返回长度为0 的数组。
  • Type getGenericReturnType( ) 5.0
    获得这个方法被声明的泛型返回类型。
  • Type[ ] getGenericParameterTypes( ) 5.0
    获得这个方法被声明的泛型参数类型。如果这个方法没有参数, 返回长度为0 的数组。
java.lang.reflect.TypeVariable
  • String getName( )
    获得类型变量的名字。
  • Type[ ] getBounds( )
    获得类型变量的子类限定,否则, 如果该变量无限定, 则返回长度为0 的数组。
java.Iang.reflect.WildcardType
  • Type[ ] getUpperBounds( )
    获得这个类型变量的子类( extends) 限定, 否则, 如果没有子类限定,则返回长度为0 的数组。
  • Type[ ] getLowerBounds( )
    获得这个类型变量的超类(super) 限定,否则, 如果没有超类限定, 则返回长度为0的数组。
java.Iang.reflect.ParameterizedType
  • Type getRawType( )
    获得这个参数化类型的原始类型。
  • Type[ ] getActualTypeArguments( )
    获得这个参数化类型声明时所使用的类型参数。
  • Type getOwnerType( )
    如果是内部类型, 则返回其外部类型, 如果是一个顶级类型, 则返回null。
java.Iang.reflect.GenericAnrayType
  • Type getGenericComponentType( )
    获得声明该数组类型的泛型组件类型。