JAVA学习笔记——泛型程序设计

目录

  • 概念
  • 定义
  • 类型参数的限定
  • 泛型代码和虚拟机
    • 桥方法 (bridge method)
  • 约束与局限性
  • 泛型类型的继承规则
  • 通配符类型
    • 概念
    • 通配符的超类型限定
    • 无限定通配符
    • 通配符捕获
  • 反射和泛型
    • 泛型 Class 类
    • 使用 `Class` 参数进行类型匹配
    • 虚拟机中的泛型类型信息

概念

所谓“泛型”,是指一段编写的代码可以被多个不同类型的对象使用,从功能上来看,类似于“方法重载”。不同的是,泛型程序只需要编写一次代码就能供多种类型的对象使用;而重载则需要重写多次,且每次重写可根据需要调整具体实现代码。

Java 中的“泛型”也可类比 C++ 中的“模板”,使用泛型机制编写的程序代码要比那些杂乱地使用 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; }
}

一个泛型类 (generic class) 就是具有一个或多个类型变量的类。此处我们定义了一个 Pair 类,在类名的后方增加了 ,表示有一个类型参数 T,于是在类的内部可以用 T 来表示某一特定类型。它的地位与其他的类型一致,只不过在类定义时作为它们的替代品,实际使用时我们可以自定义 T 具体表示什么类型,如 PairPair 等。

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

String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");

上例中,定义了一个普通类中的“泛型方法”。泛型方法既可以定义在泛型类,也可以定义在普通类。当定义在普通类时,要在方法定义的修饰符后,返回值前添加类型参数,以表示这是一个泛型方法。调用时也同样在方法名之前指定参数类型。

类型参数的限定

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;
	}
}

这个程序看似没什么问题,但在第 8 行 smallest.compareTo(a[i]) 的位置,我们无法确定实际运行时,自定义的类型 T 是否拓展了 Comparable 接口。如果没有没有拓展,则会在运行时产生错误,所以在这个程序中,我们定义的 min 方法需要对类型参数加一个限定,从而保证它一定可以调用 compareTo 方法。

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

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

<T1 extends Comparable & Serializable, T2>

如果需要限定多个接口,可以用 & 来分隔(逗号用来分隔多个类型参数)。

泛型代码和虚拟机

虚拟机没有泛型类型对象——所有对象都属于普通类。无论何时定义一个泛型类型,都自动提供了一个相应的原始类型 (raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除 (erased) 类型变 M,并替换为限定类型无限定的变量用 Object

// example 1
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; }
}

// example 2
// 擦除前
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; }
	}
}
// 擦除后
public class Interval implements Serializable
{
	private Comparable lower;
	private Comparable upper;
	...
	public Interval(Comparable first, Comparable second) { . . . }
}

// example 3
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
// 不擦除时的调用方法
// Employee buddy = buddies.getFirst();

example 1 所示,当类型擦除后,自动将 T 的位置全部替换为 Object 类。

example 2 中,我们对 T 限定了 ComparableSerializable 接口,则擦除类型后,默认将 T 替换为第一个接口类型(如果存在多个接口),其余接口仍保留拓展形式。

example 3 中,擦除 getFirst 的返回类型后将返回 Object 类型。此时编译器会自动插入一个 (Employee) 的强制类型转换,从而保证程序能正常运行。

桥方法 (bridge method)

// 原始定义
class DateInterval extends Pair<LocalDate>
{
	public void setSecond(LocalDate second)
	{
		if (second.compareTo(getFirst()) >= 0)
			super.setSecond(second)
	}
	...
}
// 擦除类型
class DateInterval extends Pair
{
	public void setSecond(LocalDate second) {...}
}

在这个例子中,当擦除类型后,我们再调用 setSecond 方法会产生一个问题:在 DateInterval 类中我们定义了一个 setSecond(LocalDate) 方法,在 Pair 中又定义了一个 setSecond(Object) 方法,此时子类的方法无法覆盖住超类的方法,会产生错误。

为了解决这个问题,编译器会在 DateInterval 类中自动生成一个桥方法 (bridge method):

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

除此以外,如果 DateIntervel 类也定义了 getSecond() 方法,擦除方法后同样会有两个 getSecond() 方法,只不过一个返回值是 Object,一个返回值是 LocalDate。编译器同样会调用桥方法来解决这个问题。

注意,我们自己编写程序时,不允许两个方法的签名完全相同,如Object getSecond()Employee getSecond(),但编译器自动生成的桥方法是允许通过的,因为 JVM 根据方法名、参数和返回值类型来确定调用的方法,但仅仅是编译器生成的方法被允许,不包括我们自己定义的方法。

总结

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

约束与局限性

  1. 不能用基本类型实例化类型参数
    不能用类型参数代替基本类型。因此,没有 Pair,只有 Pair。因为当类型擦除后,Pair 类含有 Object 类型的域,而 Object 不能存储 double 值。

  2. 运行时类型查询只适用于原始类型
    对一个 Pair 实例化的对象,如 a = Pairb = Pair,调用 a.getClass()b.getClass(),返回的都是 Pair.class,是 Pair 的原始类型,而不会因为参数泛化参数不同导致类型不同。

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

Pair<String>[] table = new Pair<String>[10];	// Error
Pair<String>[] table;	// Accept
table[0] = new Pair<String>;	// Accept
// 可以声明参数化类型数组变量,但不能创建数组实例
  1. Varargs 警告
    我们已知 Java 不支持泛型类型的数组,当一个方法的参数个数是可变的,该参数又包含类型化参数。此时,传入方法时又会生成一个泛化类型的数组,编译器会产生一个警告。
    可以采用两种方法来抑制这个警告。一种方法是为包含 addAll 调用的方法增加注解 @SuppressWamings("unchecked")。或者在 Java SE 7中,还可以用 @SafeVarargs 直接标注 addAll 方法。现在就可以提供泛型类型来调用这个方法了。对于只需要读取参数数组元素的所有方法,都可以使用这个注解,这仅限于最常见的用例。

  2. 不能实例化类型变量
    不能使用像 new T(...)newT[...]T.class 这样的表达式中的类型变量。

  3. 不能构造泛型数组
    就像不能实例化一个泛型实例一样,也不能实例化数组。不过原因有所不同,毕竟数组会填充 null 值,构造时看上去是安全的。不过,数组本身也有类型,用来监控存储在虚拟机中的数组。这个类型会被擦除。

  4. 泛型类的静态上下文中类型变量无效
    不能在静态域或方法中引用类型变量。

    public class Singleton<T>
     {
     	private static T singleInstance; // Error
     	
     	public static T getSingleInstance() // Error
     	{
     		if (singleinstance == null) // construct new instance of T
     		return singleInstance;
     	}
     }
    
  5. 不能抛出或捕获泛型类的实例
    既不能抛出也不能捕获泛型类对象。实际上,甚至泛型类扩展 Throwable 都是不合法的。

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

  7. 注意擦除后的冲突
    当泛型类型被擦除时,无法创建引发冲突的条件。泛型规范说明还提到另外一个原则:“要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。

泛型类型的继承规则

在使用泛型类时,需要了解一些有关继承和子类型的准则。

  1. 考虑一个类和一个子类,ManagerEmployee 的一个子类,但 Pair 不是 Pair 的一个子类。

JAVA学习笔记——泛型程序设计_第1张图片

图1 pair 类之间没有继承关系
  1. 永远可以将参数化类型转换为一个原始类型,在与遗留代码衔接时,这个转换非常必要。
  2. 泛型类可以扩展或实现其他的泛型类,就这一点而言,与普通的类没有什么区别。例如,ArrayList 类可以实现 List 接口,这意味着,一个 ArrayList 可以被转换为一个 List。但是,一个 ArrayList 不是一个ArrayList List

JAVA学习笔记——泛型程序设计_第2张图片

图2 泛型列表类型中子类型间的联系

通配符类型

概念

通配符类型中,允许类型参数变化。例:Pair,表示任何泛型 Pair 类型,它的类型参数是 Employee 的子类。

public static void printBuddies(Pair<Employee> p)
{
	Employee first = p.getFirst();
	Employee second = p.getSecond();
	System.out.println(first.getName() + " and " + second.getName() + " are buddies.");
}

在上面这个例子中,根据继承规则,不能将 Pair 传入该方法,而使用通配符就解决了这一“不方便”的限定:

public static void printBuddies(Pair<? extends Employee> p)

这样,该方法就能传入 Pair 作为参数,因为 ManagerEmployee 的子类。

注意,虽然通配符类型解决了参数子类的问题,但就上例而言,使用了通配符,则该方法只能传入 Employee 子类作为类型参数的参数,而不能传入其他的特定类型。

因为在编译器中,Pair 的方法是这样的:

? extends Employee getFirst();
void setFirst(? extends Employee)

编译器只知道需要传入 Employee 子类,而会拒绝其他类型,例如 String,因为其他的类型无法用 ? 来进行匹配。

JAVA学习笔记——泛型程序设计_第3张图片

图3 使用通配符的子类型关系

通配符的超类型限定

Pair<? super Manager>

这个通配符限制为 Manager 的所有超类型。只能传递 Manager 类型的对象,或者某个子类型(如 Executive) 对象。另外,如果调用 getFirst,不能保证返回对象的类型。只能把它赋给一个 Object

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

无限定通配符

? getFirst();
void setFirst(?);

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

例子

public static boolean hasNulls(Pair<?> p)
{
	return p.getFirst() = null || p.getSecond() =null;
}
// 等价于
public static <T> boolean hasNulls(Pair<T> p)

通配符捕获

编写一个交换成对元素的方法:public static void swap(Pair p)。通配符不是类型变量,因此, 不能在编写代码中使用 ? 作为一种类型,以下方式是非法的:

? t = p.getFirst(); // Error
p.setFirst(p.getSecond());
p.setSecond(t);

交换时,必须要保存二者中的一个元素,因此我们可以用一个辅助方法 swapHelper 来解决这个问题:

public static <T> void swapHelper(Pair<T> p)
{
	T t = p.getFirst();
	p.setFirst(p.getSecond());
	p.setSecond(t);
}

public static void swap(Pair<?> p) { swapHelper(p); }

再使用 swap 方法调用 swapHelper,在这种情况下,swapHelper 方法的参数 T 捕获通配符。它不知道是哪种类型的通配符,但是,这是一个明确的类型,并且 swapHelper 的定义只有在 T 指出类型时才有明确的含义。

通配符捕获只有在有许多限制的情况下才是合法的。编译器必须能够确信通配符表达的是单个、确定的类型。例如,ArrayList> 中的 T 永远不能捕获 ArrayList> 中的通配符。数组列表可以保存两个 Pair,分别针对 ? 的不同类型。

反射和泛型

泛型 Class 类

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

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

使用 Class 参数进行类型匹配

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 的一个对象。makePair 方法的类型参数 TEmployee 匹配,并且编译器可以推断出这个方法将返回一个 Pair

虚拟机中的泛型类型信息

Java 泛型的卓越特性之一是在虚拟机中泛型类型的擦除。擦除的类仍然保留一些泛型祖先的微弱记忆,例如原始的 Pair 类知道源于泛型类 Pair,即使一个 Pair 类型的对象无法区分是由 PaiKString> 构造的还是由 Pair 构造的。

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

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

图 4 给出了继承层次。注意,最后 4 个子类型是接口,虚拟机将实例化实现这些接口的适当的类。

JAVA学习笔记——泛型程序设计_第4张图片

图4 Type 类和它的后代

相关方法

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

参考资料

  1. 《Java核心技术 卷1 基础知识》
  2. 泛型常见 Q&A

你可能感兴趣的:(JAVA学习笔记,java,泛型)