Java 泛型

目录

Java 泛型

我们为什么需要泛型

自从Java SE 1.5引入泛型(Generics) 之前,Java程序员想要写出通用的代码有点难度。比如想要得到Java某个集合(Collection)的最大值,在没有泛型的情况下,我们需要针对每个特定类型去写特定的求最大值方法。

比如针对Number集合我们需要实现

1
public static Number max(NumberCollection coll, NumberComparator comp)

针对Integer集合我们需要实现

1
public static Integer max(IntegerCollection coll, IntegerComparator comp)

显而易见,这样实现起来是非常没有效率的。我们需要为每个不同的类型实现重复的逻辑,重复在编程中是非常罪恶的。当然,为了减少重复,我们也可以有这样的实现

1
public static Object max(ObjectCollection coll, ObjectComparator comp)

因为Java所有的类都是Object的子类。当然这样实现的坏处就是需要做对象转型(Casting)

1
Integer maxInterger = (Integer) max(coll, comp)

然而对象转型也是非常罪恶的,因为一旦错误地使用了对象转型,代码只有到运行阶段(runtime) 才会报错。所以我们要尽可能的避免对象转型。

而有了泛型以后,我们只需要实现

1
public static <T> T max(Collection<T> coll, Comparator<T> comp)

其中T叫做类型参数(Type paramter),如果一个类(Class),一个接口(Interface) 或者一个方法(Method)定义时(declaration)有一个或者多个类型参数,那么我们就叫他们泛型类(Generic class)泛型接口(Generic interface)泛型方法(Generic method)。而泛型类,泛型接口和泛型方法就被统称为泛型(Generic types, Generics)

定义时,泛型是由类,接口和方法名跟着一个由尖括号包围的参数化类型(Parameterized types) 组成的。例如

  • ArrayList<E> ArrayList类有一个类型参数,E,它表示元素类型。读作元素E的ArrayList。
  • Map<K, V> Map接口有两个类型参数,K,V,分别表示键和值的类型,读作K到V的映射。
  • T max(Collection<T> coll) max方法有一个类型参数,T,表示对象类型,这个不太好读。

使用时,我们用实际类型参数(Actual type paramter) 替换类型参数,比如ArrayList<String> 就代表一个元素为StringArrayList。其中类型参数E被实际类型参数String替代了。

在英语里Generic有通用的含义,这也揭示了Java泛型的本质:让类,接口和方法变得更加通用

有限通配符

有限通配符(Bounded Wildcards) 是Java泛型(Java Generics) 里的概念,这里的有限不是和无限对应的,而是有上限和下限的意思。所以有限通配符又分为下限通配符上限通配符。在一些翻译中,Bounded Wildcards也被翻译为有界通配符,相应的,有界通配符又分为下界通配符上界通配符

下限通配符

上面说到有了泛型以后,我们只需要实现

1
public static <T> T max(Collection<T> coll, Comparator<T> comp)

便可以对任意类型的数据求最大值,但是上面的方法签名(method signature) 也有一些限制。

1
2
3
Collection<Integer> intergerColl = ...;
Comparator<Number> numberComp = ...;
Collections.max(intergerColl, numberComp); // 编译出错

我们知道NumberInteger是的超类(Super type),每一个Intger类也是Number类,所以Number类的比较器应该可以用于比较Integer类。

然而上面的代码会在编译阶段(Compile time)出错,这是因为类型参数T限制了我们只能使用Integer类的比较器。在这里,限制比较器的类型和集合类型完全一样是没有必要的。其实我们可以放宽限制,只要比较器类型是集合类型T的超类型就可以了。这样,我们可以让求最大值方法变得更通用。修改后的函数签名如下

1
public static <T> T max(Collection<T> coll, Comparator<? super T> comp)

这里?就是通配符(Wildcard),? super T就是下限通配符(Lower Bounded Wildcards)。它表示某个类?T的超类。

这样我们在这里使用Number类的比较器来比较Integer集合了。

下限通配符? super T中,T是表示下限类型,它既可以是一个类型参数,也可以是一个实际类型参数。

  • Comparator<? super T> 类型参数
  • Comparator<? super Integer> 实际类型参数

至于为什么叫做下限,我们可以这么类比。族谱里爸爸在上面,儿子在下面。下限通配符以某个类型的子类型为下限,它匹配包括这个子类型的所有超类型。

上限通配符

通过上限通配符,我们把求最大值方法变得通用了。试想限制我们想要实现一个Number集合累加,可以有如下的函数签名

1
public static Number sum(Collection<Number> inputs)

但是它只能用于累加Number,如果我们也想累加Number的子类Double,Integer呢,可以使用具有实际类型的上限通配符

1
public static Number sum(Collection<? extends Number> inputs)

这样我们就可以累加Double,Intger类型的集合了。

这里? extends T就是上限通配符(Upper Bounded Wildcards)。它表示某个类?T的子类。

无限通配符

无限通配符(Unbounded Wildcards)里的无限不是和有限对应的无限,而没有上下限的意思。有时,无限通配符(Unbounded Wildcards)也会被翻译为无界通配符。他们指代的都是同一个概念。

无限通配符记作?, 表示未知类型(unknown type)。 比如List<?>, 表示类型未知的List。

试想我们实现了如下在List中交换元素的方法

1
2
3
4
public static <E> void swap(List<E> list, int i, int j) {
final List l = list;
l.set(i, l.set(j, l.get(i)));
}

由于使用了泛型,上面的方法可以应用于不同类型的List。但是我们也可以看到,在实现这个方法的过程中,我们没有使用基于类型参数E的任何信息。在这种情况下,我们可以用无限通配符代替类型参数E

1
2
3
4
public static void swap(List<?> list, int i, int j) {
final List l = list;
l.set(i, l.set(j, l.get(i)));
}

我们可以总结出这样的经验法则,如何类型参数只在方法声明中出现,我们就可以用通配符来代替。

总结

Java通过让类,接口和方法在定义时有一个类型参数的形式让代码变得更加通用(泛化),这个就叫做泛型。由于Java泛型也有一些限制,这里没有阐明技术原因(技术原因请参考Java的协变,逆变与不变),仅从几个把代码变得更通用得需求出发,引入通配符?的概念。

通配符在Java里三种形式,分别是

  • 下限通配符,<? super LowerBoundedClass>,通配LowerBoundedClass和它的超类。也可以用类型参数代替实际类型,<? super T>
  • 上限通配符, <? extends UpperBoundedClass> 统配UpperBoundedClass和它的子类。也可以用类型参数代替实际类型,<? extends T>
  • 无限通配符, ? 通配任意类型

关于通配符上下限的说法,来自于ORACLE关于泛型的官方文档,以及Gilad Bracha关于泛型的教程。如果觉得难以理解,可以参考在《On Java 8》里,Bruce Eckel的超类通配符子类通配符的说法。

  • 超类通配符,<? super LowerBoundedClass>,通配LowerBoundedClass和它的超类。也可以用类型参数代替实际类型,<? super T>
  • 子类通配符 <? extends UpperBoundedClass> 统配UpperBoundedClass和它的子类。也可以用类型参数代替实际类型,<? extends T>

参考

  1. Java泛型官方文档, ORACLE
  2. Java泛型教程, Gilad Bracha
  3. Effective Java 3rd Edition,Joshua Bloch
  4. On Java 8,Bruce Eckel