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>
就代表一个元素为String
的ArrayList
。其中类型参数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 | Collection<Integer> intergerColl = ...; |
我们知道Number
是Integer
是的超类(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 | public static <E> void swap(List<E> list, int i, int j) { |
由于使用了泛型,上面的方法可以应用于不同类型的List。但是我们也可以看到,在实现这个方法的过程中,我们没有使用基于类型参数E的任何信息。在这种情况下,我们可以用无限通配符代替类型参数E
1 | public static void swap(List<?> list, int i, int j) { |
我们可以总结出这样的经验法则,如何类型参数只在方法声明中出现,我们就可以用通配符来代替。
总结
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>
参考
- Java泛型官方文档, ORACLE
- Java泛型教程, Gilad Bracha
- Effective Java 3rd Edition,Joshua Bloch
- On Java 8,Bruce Eckel