欢迎来到HugNew-拥抱变化,扫一扫右边二维码关注微信订阅号:Martin说 或 加QQ群:427697041互相交流,Stay hungry, Stay foolish.

泛型的美与丑

J2SE Martin 2473℃ 0评论

你理解泛型了吗

随便看一下你就会发现Java的许多框架中都用到了泛型。从WEB应用框架到Java集合框架本身。这个话题已经有不少人讲过了,这里我只列出一些我认为比较有价值的资源,以及一些别人没有提及过的东西,或者是没有讲得那么细的。因此如果你不太了解泛型的核心概念的话,你可以参考一下下面这些资料:

  • SCJP Sun Certified Programmer for Java 6 Exam[1] 对我而言,这本书主要是为了准备Oracle的OCP认证考试的。不过后来我发现书中介绍泛型的部分,对于想了解泛型和学习如何使用的人非常有帮助。非常值得一读,不过该书是基于Java 6的,要能看懂下面的代码你可能还得补补钻石操作符相关的东西。
  • Oracle的泛型教程[2] 这是Oracle自己提供的资源。在这个Java教程中可以看到许多简单的例子。你能了解到关于泛型的一些基本概念,为下面这本书讲到的一些更深入的话题打好基础。
  • Java泛型与集合[3] 这是O’Reilly Media的另一本不错的Java书籍。该书条理清晰,内容翔实。不幸的是,它也有点过时了,跟上面提供的第一本书一样。

什么是泛型不能做的?

假设你已经知道泛型了,现在想更深入地学习一下,那么我们来看下有什么东西是它做不了的。让人惊讶的是,泛型有很多事都干不了。下面我挑出了6个示例,这些都是使用泛型的时候应该避免的错误用法。

一些经验不太丰富的程序员的一个常见错误就是,他们想要声明一个泛型的静态成员。正如下例中所看到的,这么做的最终结果就是编译器报错:Cannot make a static reference to the non-static type T。

public class StaticMember<T> {
    // causes compiler error
    static T member;
}

实例化类型

另一个错误就是想通过泛型来new出一个实例。这么做的话,编译器也会报错:Cannot instantiate the type T。

public class GenericInstance<T> {
 
    public GenericInstance() {
        // causes compiler error
        new T();
    }
}

不兼容基础类型

泛型的一个最大的限制就是它们不兼容基础类型。很明显你不能直接在声明中使用基础类型,不过你可以用对应的包装类来解决这一问题。下面这个例子就是对应的解决方案:

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。这个问题通过包装类和自动装箱机制可以很容易解决掉。

泛型的另一个局限性就是它无法初始化泛型数组。原因非常明显,这是由于数组对象的特性决定的——它们会在运行时保留对应的类型信息。如果你破坏了它的运行时类型的一致性,就会抛出ArrayStoreException的运行时异常。

public class GenericArray<T> {
    //这么写是OK的
    public T[] notYetInstantiatedArray;
 
    // causes compiler error
    public T[] array = new T[5];
}

如果你想直接初始化一个泛型数组,最后会以编译器抛出Cannot create a generic array of T的错误而告终。

泛型的异常类

有的时候,开发人员需要将一个泛型对象和异常一块抛出去。在Java里是不能这么做的。下面这个例子:

// causes compiler error
public class GenericException<T> extends Exception {}

如果你想创建一个这样的异常,你最终只会得到一个The generic class GenericException may not subclass java.lang.Throwable的错误。

弄串了super和extends的意思

最后一个值得一提的问题就是,尤其对于新手而言,就是super和extends关键字的含义。要想设计出好的代码,了解这点至关重要。

  • <? extends T> 这指的是T的任意子类,包括T类型自身
  • <? super T> 这指的是T的任意父类,包括T类型自己。

我最喜欢Java的一点就是它的强类型。我们都知道 ,Java 5中引入了泛型,这样使得集合的操作变得更简单。尽管提供的只是编译期的泛型,字节码中并没有相应的信息,但是它们对于确保类型安全还是提供了很大的帮助。下面这些例子展示了泛型的一些不错的特性,以及使用场景。

同时支持类和接口

泛型是支持接口的,这当然一点也不奇怪。尽管接口中使用泛型已经非常常见了,但我仍然觉得这是一个非常酷的特性。这使得程序员可以更高效地写出类型安全和支持重用的代码。比如说,看下java.lang包下的Comparable类的一个例子:

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 接口才能实现这个功能。

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方法更加通用和开放。唯一的缺点就是这么写的语法太冗长了。不过它的可读性还是不错的,也很容易理解,我觉得这并不算是个问题。

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

古怪的地方

本文的最后一节我准备讲下迄今为止我遇到的关于泛型的两个最奇特的结构或者行为。很有可能你永远也碰不到这样的代码,但我觉得提一下它还是挺有意思的。废话不多说了,直接看下代码吧。

丑陋的代码

和别的语言的结构一样,你可能会碰到一些写得非常恶心的代码。以前我在想,最变态的代码会是什么样的,它能通过编译器的编译吗。最后果然让我碰到了这样的代码。你能猜下这段代码能不能通过编译吗?

public class AwkwardCode<T> {
    public static <T> T T(T T) {
        return T;
    }
}

尽管这的确是一个糟糕的编码实践,但它的确能成功编译,同时程序运行得也没有什么问题。第一行声明的是一个泛型的AwkwardCode类,第二行,声明了一个类型为T的泛型方法。这个方法T返回的是一个T的的实例。它接受类型为T的参数,并且不幸的是,参数名也叫T。这个参数会在方法体里面返回。

泛型的方法调用

最后要看的是类型推导遇上泛型又会擦出什么样的火花。我是在看见一段代码没有泛型签名但也能通过编译的时候发现这个问题的。如果你对泛型了解得不是那么深入的话,你第一眼看到这样的代码会吓一跳。你能解释一下下面这段代码吗?

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。最后,第三次方法调用返回的是false 。这个看起来很奇怪,因为这个方法声明了要接受两个相同类型的参数的,而传一个String和Long对象居然也可以通过。这是因为编译期进行了类型擦除。因为方法调用 没有使用语法,编译器无法知道 你传的是两个不同的类型。你要牢记,当匹配方法声明的时候,使用的是最接近的共同继承的类型。这意味着,当genericCompare方法接受object1和object2时,它们被强制转化成了Object,当比较Strign和Long类型时方法会返回false。我们来修改下这段代码。

public class GenericMethodInvocation {
 
    public static void main(final String[] args) {
        // 1. returns true
        System.out.println(Compare.<String> genericCompare("1", "1"));
        // 2. return false
        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 方法,并在mian方法中调用了两次。这段代码中第二次的genericCompare调用是没问题的,这是因为我提供了一个新的方法,正好能匹配上这次调用 。不过这引发了另一个奇怪的行为——第二个方法调用到底是不是泛型的?当然不是。但是,你还是能使用的泛型语法。为了更好的说明这个问题,我增加了一个新的方法randomMethod,还是多亏了类型擦除的功能,这么调用也是可以的,泛型语法被擦除掉了。

不过,当有边界通配符的话情况就变了。编译器会明确地告诉我们编译错误:Wildcard is not allowed at this location。这段代码是无法编译的。要想代码能够通过编译你得把第12行给注释掉。修改完之后的代码运行结果如下:

Inside generic
true
Inside non-generic
false
Inside non-generic
false

注:很多人会说Java的泛型算不上真正的泛型。这种话题咱就不说了,吵得太多了。。

原创文章转载请注明出处:泛型的美与丑[4]

英文原文链接[5]

转载请注明:HugNew » 泛型的美与丑

喜欢 (0)or分享 (0)
发表我的评论
取消评论

表情