首页 > 代码库 > 泛型的古怪与优雅
泛型的古怪与优雅
最近,我在准备Oracle Java SE 7的开发人员专业认证,偶然看到一些关于Java泛型很奇怪的用法。当然,我也看到了一些优雅灵巧的代码。我觉得这些例子很值得分享,不仅因为它们可以让 你的设计选择变得简单,还会使代码具有更好的健壮性和可重用性。如果对泛型不熟悉,其中的一些例子会不容易理解。我决定把这篇文章分成四部分,这与学习和 工作中对泛型所积累的经验可以很好地吻合。
你了解泛型么?
注意一下我们能看到的(代码),就会发现在很多Java框架中,泛型是非常常见的。从Web框架到Java自身的集合类型,都能看到它的身影。由于 在我之前已经有很多人讲解过这个主题,这里我首先只列出我觉得有价值的资源,之后重点讨论一些很少被提及或讲解不清的内容(主要是指网上的笔记或文章)。 因此,如果你缺少对泛型核心概念的理解,可以参考以下资料:
- SCJP Sun Certified Programmer for Java 6 Exam Katherine Sierra和Bert Bates著
- 对我来说,这本书的主要作用就是备战Oracle的OCP考试。但我逐渐意识到,这本书中的泛型笔记对任何想研究泛型、使用泛型的人也是有益的。这本书确实值得一读,但它是针对Java 6的,所以讲解的不够全面,必须得自己查找遗失的内容,比如“
<>
”操作符。
- 对我来说,这本书的主要作用就是备战Oracle的OCP考试。但我逐渐意识到,这本书中的泛型笔记对任何想研究泛型、使用泛型的人也是有益的。这本书确实值得一读,但它是针对Java 6的,所以讲解的不够全面,必须得自己查找遗失的内容,比如“
- Lesson: Generics (Updated) Oracle文档
- Oracle提供的资源。你可以在这份Java教程中学习到很多简单的例子。它让你大致地了解泛型,为以后学习更复杂的内容打下基础,比如下面这本书。
- Java Generics and Collections Maurice Naftalin和Philip Wadler著
- O’Reilly出版的另一本经典Java著作。 这本书很有条理,内容全面详实。遗憾的是这本书也过时了,和第一本书一样,在一定条件下使用。
使用泛型时,什么不允许?
假设你了解泛型,也想学到更多,那让我们先知道什么不能做。出人意料的是,很多东西不能在泛型中使用。我选择了下面6个在使用泛型时需要避免的例子。
<T>
类型的静态成员
很多没有经验的开发者常犯的错误就是尝试声明静态成员。如下所示,这样做会导致编译错误:Cannot make a static reference to the non-static type T
。
1 2 3 4 | public class StaticMember<T> { // causes compiler error static T member; } |
实例化类型<T>
另一个错误就是使用new操作符实例化泛型类型。这样做会导致编译错误:Cannot instantiate the type T
。
1 2 3 4 5 6 7 | public class GenericInstance<T> { public GenericInstance() { // causes compiler error new T(); } } |
与基本类型不兼容
使用泛型的一个最大的限制似乎就是与基本类型不兼容。虽然你不能在声明中直接使用基本类型,但是你可以使用合适包装器(Wrapper
)类型作为替代,并且运行良好。下面的这个例子说明了这种情形:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class Primitives<T> { public final List<T> list = new ArrayList<>(); public static void main(String[] args) { final int i = 1 ; // causes compiler error // final Primitives<int> prim = new Primitives<>(); final Primitives<Integer> prim = new Primitives<>(); prim.list.add(i); } } |
第一次实例化Primitives
类会产生编译错误,提示信息类似于:Syntax error on token "int", Dimensions expected after this token.
。这个问题可以通过包装器和自动打包技术来绕过。
<T>
类型的数组
使用泛型的另一个显著的限制就是不能实例化参数类型的数组。考虑到数组类型的特点,原因是很明显的——数组类型在运行中维护着自己的类型信息。如果在运行时,它的类型完整性被破坏,就会抛出运行时异常ArrayStoreException
。
1 2 3 4 5 6 7 | public class GenericArray<T> { // this one is fine public T[] notYetInstantiatedArray; // causes compiler error public T[] array = new T[ 5 ]; } |
但如果你想直接实例化一个泛型数组,就会产生编译错误:Cannot create a generic array of T
。
泛型异常类
有时,开发者想要在抛出异常时,传入一个泛型实例。在Java中是行不通的。下面的例子就是这么做的。
1 2 | // causes compiler error public class GenericException<T> extends Exception {} |
当你尝试创建一个这样的异常时,你会得到这样的错误信息:The generic class GenericException<T> may not subclass java.lang.Throwable.
super
和extends
的另一种含义
最后值得一提的,尤其对初学者来说,就是super和extends在泛型中的含义。为了写出设计良好的泛型代码,知道他们的意义很重要。
<? extends T>
- 含义:继承自类型T以及类型T自身的通配符。
<? super T>
- 含义:类型T的父类型以及类型T自身的通配符。
优雅之处
我非常喜欢Java的一个特性就是它的强类型。众所周知,泛型是在Java 5时引入的,它可以让我们更容易地使用集合类(除了集合类,泛型在其他地方应用也十分广泛,但这是设计泛型的主要原因)。尽管泛型只提供编译时保护,不会 进入字节码,但它的安全类型保护方式非常高效。下面是关于泛型的几个不错特性或者用例。
接口和类一样也可以使用泛型
这可能并不意外,接口和泛型可以很好地兼容。尽管接口和泛型一起使用很常见,但我还是觉得这是一个很不错的特性。这允许开发者创建具有类型安全且考虑到重用的高效代码。比如下面的例子,这是java.lang
包的Comparable
接口:
1 2 3 | public interface Comparable<T> { public int compareTo(T o); } |
泛型的简单引入,忽略了compareTo
方法的实例检查,使得代码更具连贯性和可读性。通常,泛型以及参数类型顺序的引入,使代码更容易阅读与理解。
泛型允许优雅地使用边界
谈到边界通配符,Collections
类包中有一个很好的例子。这个类中声明了copy
方法,使用边界通配符来保证列表复制操作的类型安全。以下是它的定义。
public static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }
让我们仔细分析下。copy
方法是一个静态泛型方法,返回类型为void
。它接收两个参数——目的列表和源列表(都有边界)。目的列表被限制为只能存储T类型父类或T类型的实例。相反,源列表存储T类型子类或T类型的实例。这两个限制保证了两个集合以及他们之间复制操作的类型安全。数组中的类型安全被破坏时,会抛出前面提到的ArrayStoreException
异常,所以我们不必关注数组的类型安全问题。
泛型支持多边界
不难想象,有人想在一个简单的边界条件中使用多个边界。实际上,这很容易做到。考虑下面一个例子:我需要创建一个方法,它接收的参数是一个可以比较(实现Comparable
接口)的数字List
。在没有泛型的时代,为了完成上述需求,开发者不得不创建一个不必要的接口ComparableList
。
1 2 3 4 5 6 7 8 9 10 11 | public class BoundsTest { interface ComparableList extends List, Comparable {} class MyList implements ComparableList { ... } public static void doStuff( final ComparableList comparableList) {} public static void main( final String[] args) { BoundsTest.doStuff( new BoundsTest(). new MyList()); } } |
在下面的尝试中,我们忽视了(不使用泛型的)限制。使用泛型允许我们创建具体的类来满足需求,还可以让doStuff
方法足够开放。我发现的唯一缺点就是语法冗长。但代码还是容易阅读与理解,可以忽略这个缺点。
1 2 3 4 5 6 7 8 9 10 | public class BoundsTest { class MyList<T> implements List<T>, Comparable<T> { ... } public static <T, U extends List<T> & Comparable<T>> void doStuff( final U comparableList) {} public static void main( final String[] args) { BoundsTest.doStuff( new BoundsTest(). new MyList<String>()); } } |
古怪之处
我决定在本文的最后一章描述我遇到的两个(泛型)最奇怪的概念或用法。你可能永远都不会看到这样的代码,但我觉得提下它应该很有意思。那么没有其他烦扰的事,让我们看看这奇怪的东西。
别扭的代码
正如其他语言的语法,你可能会见到一些看起来相当古怪的代码。我想知道最最奇怪的代码是什么样的,它是否甚至能通过编译。我所能想到的就只有下面的了。你猜猜这段代码能否通过编译?
1 2 3 4 5 | public class AwkwardCode<T> { public static <T> T T(T T) { return T; } } |
即使例子中的代码很糟糕,但它能通过编译并且运行起来也没有任何问题。第一行声明泛型类AwkwardCode
,第二行声明泛型方法T
。方法T
是一个返回类型T
的泛型方法。它接收的参数是T类型的,很不幸参数的名称也是T。这个参数从方法体中返回。
泛型方法的调用
最后一个例子展示引用类型和泛型如何一起工作。我偶然发现了这个问题,有一段代码在调用(泛型)方法的时并没有包含类型参数签名,但通过了编译。一个人只要有一点泛型的编程经验,第一眼看到这样的代码可能会比较吃惊。你能解释一下下面代码的行为吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public class GenericMethodInvocation { public static void main( final String[] args) { // 1. returns true System.out.println(Compare.<String> genericCompare( "1" , "1" )); // 2. compilation error System.out.println(Compare.<String> genericCompare( "1" , new Long( 1 ))); // 3. returns false System.out.println(Compare.genericCompare( "1" , new Long( 1 ))); } } class Compare { public static <T> boolean genericCompare( final T object1, final T object2) { System.out.println( "Inside generic" ); return object1.equals(object2); } } |
好了,让我们分析下。第一次调用genericCompare
很直接。我标注了方法的参数类型并提供了两个同样类型的对象——这里没有什么神秘的。第二次调用genericCompare
,在编译时会失败,是因为Long
类型不是String
。最后,第三次调用genericCompare
会返回false
。这次很奇怪,因为这个方法被声明为接收两个相同类型的参数,但这里传入一个String
字面量和一个Long
对象却完全没有问题。这是由编译时的类型擦除过程导致的。由于这次的方法调用没有使用泛型的类型参数<String>
,编译器没办法告诉你传入了两个不同类型的参数。永远记住这一点,(两个参数)最近的共享父类被用来搜索匹配的方法。也就是说,当genericCompare
接受了参数object1
和object2
时,他们被转化为Object类型,但是运行时的多态机制会将他们按照String
和Long
实例来比较——所以这个方法返回false
。现在让我们稍微改一下这段代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public class GenericMethodInvocation { public static void main( final String[] args) { // 1. returns true System.out.println(Compare.<String> genericCompare( "1" , "1" )); // 2. compilation error System.out.println(Compare.<String> genericCompare( "1" , new Long( 1 ))); // 3. returns false System.out.println(Compare.genericCompare( "1" , new Long( 1 ))); // compilation error Compare.<? extends Number> randomMethod(); // runs fine Compare.<Number> randomMethod(); } } class Compare { public static <T> boolean genericCompare( final T object1, final T object2) { System.out.println( "Inside generic" ); return object1.equals(object2); } public static boolean genericCompare( final String object1, final Long object2) { System.out.println( "Inside non-generic" ); return object1.equals(object2); } public static void randomMethod() {} } |
新代码在Compare
类中增加了一个非泛型版本的genericCompare
方法,定义了一个新的randomMethod
方法。randomMethod
方法什么都不做,被GenericMethodInvocation
类的主函数调用了两次。这次的代码可以在第二次调用genericCompare
方法,这是因为我提供了新的方法可以匹配调用。但这又是一种奇怪的用法,针对它,可以提出了一个问题——第二次调用是泛型吗?事实证明——不是。但它仍然可以使用泛型的语法——<String>
。为了更清楚地证明这种用法,我使用泛型语法来调用randomMethod
方法。确实可行,这再一次归功于类型擦除过程——擦除了泛型标记。
但是,在这里使用边界通配符时,情况发生了变化。编译器发出一条明确的编译错误信息:Wildcard is not allowed at this location
,使代码编译失败。要想让代码编译并执行,你必须注释掉第12行。代码更改后,输出结果如下:
1 2 3 4 5 6 | Inside generic true Inside non-generic false Inside non-generic false |
泛型的古怪与优雅