首页 > 代码库 > 泛型的古怪与优雅

泛型的古怪与优雅

最近,我在准备Oracle Java SE 7的开发人员专业认证,偶然看到一些关于Java泛型很奇怪的用法。当然,我也看到了一些优雅灵巧的代码。我觉得这些例子很值得分享,不仅因为它们可以让 你的设计选择变得简单,还会使代码具有更好的健壮性和可重用性。如果对泛型不熟悉,其中的一些例子会不容易理解。我决定把这篇文章分成四部分,这与学习和 工作中对泛型所积累的经验可以很好地吻合。

你了解泛型么?

注意一下我们能看到的(代码),就会发现在很多Java框架中,泛型是非常常见的。从Web框架到Java自身的集合类型,都能看到它的身影。由于 在我之前已经有很多人讲解过这个主题,这里我首先只列出我觉得有价值的资源,之后重点讨论一些很少被提及或讲解不清的内容(主要是指网上的笔记或文章)。 因此,如果你缺少对泛型核心概念的理解,可以参考以下资料:

  • SCJP Sun Certified Programmer for Java 6 Exam Katherine Sierra和Bert Bates著
    • 对我来说,这本书的主要作用就是备战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.

superextends的另一种含义

最后值得一提的,尤其对初学者来说,就是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接受了参数object1object2时,他们被转化为Object类型,但是运行时的多态机制会将他们按照StringLong实例来比较——所以这个方法返回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

采集
<style></style>

泛型的古怪与优雅