Java从零开始系列06:泛型程序设计

学习目标

  • 定义简单泛型类
  • 泛型方法
  • 类型变量的限定
  • 泛型代码和虚拟机
  • 限制与局限性
  • 泛型类型的继承规则
  • 通配符类型
  • 反射和泛型

一、定义简单泛型类

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

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

常见的做法是类型变量使用大写字母,而且很简短。Java库使用变量E表示集合的元素类型,K和V分别表示表的键和值的类型,T(必要时还可以使用U和S)表示任意类型。

可以用具体的类型转换变量来实例化(instantiate)泛型类型。如:Pair

换句话说,泛型类相当于普通类的工厂。

二、泛型方法

还可以定义一个带有类型参数的方法:

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

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

当调用一个泛型方法时,可以把具体类型包围在尖括号中,放在方法名前面:
String middle = ArrayAlg.getMiddle("John", "Q.", "Public");

这种情况下,方法调用可以省略类型参数。即
String middle = ArrayAlg.getMiddle("John", "Q.", "Public");

三、类型变量的限定

有时,类或方法需要对类变量加以约束。如,计算数组中的最小元素:

class ArrayAlg
{
	public static <T> T min(T[] a)
	{
		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;
	}
}

此方法只能限制T只能是实现了Comparable的接口(包含一个方法compareTo的准接口)的类。可以通过对类型变量T设置一个限定(bound)来实现这一点:
public static < T extends Comparable> T min(T[] a)...

实际上Comparable接口本身就是一个泛型类型。

一个类型变量或通配符可以有多个限定,如:
T extends Comparable & Serializable

限定类型(bounding type)的子类型T(subtype)和限定类型和可以是类,也可以是接口。在Java继承中,可以根据需要拥有多个接口超类型,但最多有一个限定可以是类。

四、泛型代码和虚拟机

虚拟机没有泛型类型对象–所有对象都属于普通类。

(一)类型擦除

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

(二)转换泛型表达式

编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。

当访问一个泛型字段时也要插入强制类型转换。

(三)转换泛型方法

类型擦除也会出现在泛型方法中。通常认为类似
public static < T extends Comparable> T min(T[] a)
的泛型方法是整个一组方法,而擦除类型之后,只剩下一个方法:
public static Comparable min(Comparable[] a)
方法的擦除带来两个问题。如:

class DateInterval extends Pair<LocalDate>
{
	public void setSecond(LocalDate second)
	{
		...
	}
}

日期区间是一对LocalDate对象,这个类擦除后变成:

class DateInterval extends Pair
{
	public void setSecond(LocalDate second)
	{
		...
	}
}

还有另一个从Pair继承的setSecond方法,即:
public void setSecond(Object second)

我们希望setSecond调用具有多态性,类型擦除与多态发生了冲突。为解决这一问题,编译器在DateInterval类生成了一个桥方法(bridge method):
public void setSecond (Object second) { setSecond((LocalDate) second); }

对于Java泛型的转换,有以下几个事实:

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

五、限制与局限性

使用Java泛型时有以下限制

  1. 不能用基本类型实例化类型参数
  2. 运行时类型查询只适用于原始类型
  3. 不能创造参数化类型的数组
  4. Varargs警告
  5. 不能实例化类型变量
  6. 不能实例化类型变量
  7. 不能构造泛型数组
  8. 泛型类型的静态上下文中类型变量无效
  9. 不能抛出或捕获泛型类的实例
  10. 可以取消对检查型异常的检查
  11. 注意擦除后的冲突

六、泛型类型的继承规则

通常,PairPair没有任何关系。

泛型类可以扩展或实现其他的泛型类。

七、通配符类型

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

也可以通过通配符类型将Pair 传递给方法:
public static void printBuddies(Pair p)

通配符限定与类型变量限定十分类似,但通配符可以指定一个超类型限定(supertype bound)。如? super Manager,这个通配符限制为Manager的所有超类型。

直观的将,带有超类型限定的通配符允许你写入一个泛型对象,而带有子类型限定的通配符允许你读取一个泛型对象。

还可以使用无限定的通配符,如Pair。与Pair的本质不同在于:可以用任意Object对象调用原始Pair类的方法。

通配符捕获只有在非常限定的情况下才是合法的。编译器必须能够保证通配符表示单个确定的类型。

八、反射和泛型

Java泛型的突出特性之一是在虚拟机中擦除泛型类型。擦除的类仍然保留原先泛型的微弱记忆。如以下方法:
public static Comparable min(Comparable[] a)
擦除泛型方法后:
public static > T min(T[] a)
可以使用反射API来确定:

  • 这个泛型方法有一个名为T的类型参数
  • 这个类型参数有一个子类型限定,其本身又是一个泛型类型
  • 这个限定类型有一个通配符参数
  • 这个通配符参数有一个超类型限定
  • 这个泛型方法有一个泛型数组参数

参考资料:

狂神说Java
Java核心技术 卷I(第11版)


上一章:Java从零开始系列05:异常、断言和日志
下一章:Java从零开始系列07:集合

你可能感兴趣的:(java,c#,开发语言)