`

Java泛型疑难解析

阅读更多

几年前当Java5还未正式发布的时候,看到过一些人写的介绍Tiger中的新特性,当时对我第一感觉冲击最大的就是泛型(generics)和注释(annotation),因为它们直接影响了我们编码的语法习惯。

在后来的使用过程中,对于泛型一直没有特别深入的使用过,没有遇到那样的需求和场景。只需要了解Java中的泛型是编译期的,运行期被“擦拭”掉了;然后还有几种通配符的表示就足够了。

 

直到一天我在查看Java5中Enum的源代码时,发现它是这么定义的:

 

public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable

 这个类似递归结构的 Enum<E extends Enum<E>> 究竟表达什么意思?

 

 

随后我又看到了在Collections工具类中的max 方法:

 

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)

 怎么TMD还会有这么复杂的泛型表达式!?

 

(幸好的是这种情况在我们实际开发过程中不多见,甚至不应该见到,大概只有像JDK这种为了保留对老版本的兼容才会设计出这么复杂的泛型表达式出来。)

 

上面的问题,你可以通过:http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf 来获取答案,中文版的在:http://blog.csdn.net/explorers/archive/2005/08/15/454837.aspx

这篇泛型指南的文章,非常详细。

 

或许你看了上面的文章,依然心存疑虑。我下面的内容则是对这篇文档的一些补充(会和这篇文档有点重合)。

 

在回到我之前抛出的问题上:

 

public static void foo(List<? extends Number> l){
	//The method add(capture#1-of ? extends Number) in the type List<capture#1-of ? extends Number>
	//is not applicable for the arguments (Integer)
        l.add(new Integer(2));  // 编译通过么? Why ?

}

 

    public static void bar(List<? super Number> l){

        l.add(new Integer(2));  // 编译通过么? Why ?

        l.add(new Float(2));    // ok?

}

 这里主要说说 <? extends T>  <? super T> 这两种通配符对于方法参数的使用原则。

 

即 PECS 原则 (producer-extends, consumer-super)  或者也叫 Get and Put 原则

 当没有使用通配符的情况下,我们定义一个方法:

    public static <E> void test(List<E> l){

        E e = l.get(0);

        l.set(0, e);

    }

 我们从List getset都没有问题,因为这个E 它的类型是某种明确的类型。

 

 

 

而当使用通配符来描述参数时,就有些不同了。

我们先定义一下两种通配符:

<? extends E> 是 Upper Bound(上限) 的通配符

<? super E> 是 Lower Bound(下限) 的通配符

 

1) 当使用 Upper Bound 通配符时:

 

 

<?> 是 <? extends Object> 的简写。(关于<?>是否和<? extends Object>完全等价,在结束的时候来描述)

 

 

在eclipse里错误提示为: The method set(int, capture#2-of ?) in the type List<capture#2-of ?> is not applicable for the arguments (int, Object)

 

注: <capture#2-of ?> 是一个占位符,表示编译器对通配符的捕获,更多见:

http://www.ibm.com/developerworks/cn/java/j-jtp04298.html

 

set报错的原因是因为此时方法中的类型是不可具体化的(reified),你可以传递一个String,Number,Book,等任何继承自Object的类作为List的参数类型给test方法,而list要求集合中的类型必须是一致的,set的时候没有办法保证set进去的数据类型是否和list中原本的类型一致,比如你传给test方法的是 List<Book>, 那么在方法中set进去一个Object显然类型就不一致了。

这也是通配符带来灵活性的同时所要付出的代价。

 

结论:使用了 <? extends E> 这样的通配符,test方法的参数list变成了只能get不能set(除了null) 或者不严谨地说它变成了只读参数了, 有些类似一个生产者,提供数据。

//2) 当使用 Lower Bound 的通配符时:

    public static void test(List<? super Number> list){

        Number n = list.get(0);             // 编译错误

        Object o = list.get(0);             // OK

        

        list.set(0, new Object());      // 编译错误

        list.set(0, new Long(0));       // OK

        list.set(0, new Integer(0));    // OK

}

 这时get只能get出最宽泛的父类型,即Object

 

这时set,必须是Number或Number的子类。

原因和上面的get类似。

 

结论: 使用了<? super E> 这种通配符,test方法的参数list的get受到了很大的制约,只能最宽泛的方式来获取list中的数据,相当于get只提供了数据最小级别的访问权限(想想,你可能原本是放进去了一个Book,却只能当作Object来访问)。

它更多适合于set的使用场景,像是一个消费者,主要用来消费数据。

 

上面便是对通配符的使用原则的说明,简单的说 PECS原则是指导我们在泛型方法中使用通配符的直接原则。参数作为生产者使用<? extends E>,作为消费者时使用<? super E> 。

 

那么说完了 PECS原则,我们再回过头来分析那两个复杂的泛型表达式是什么含义

 

1) class Enum<E extends Enum<E>>

它确实是一个 “递归类型限制”(recursive type bound)

要说明白这个问题,我们要先明白2点:

a) Enum可以理解为一组同一类型的有限的数据集合;

b) Java对Enum中的数据类型要求必须也是枚举类型(即必须是继承Enum类的)。

 

对于 a) 我们先定义一个 Enum<T> 表明定义了T这种类型作为它内部的数据类型,这么看它就像个普通的集合。

再来根据b) 定义类型E,要求E必须是继承自 Enum<T>,便成了  <E extends Enum<T>>

实际上 E和T是一回事,它们是同一类型,所以它就是 <E extends Enum<E>>

 

暂停,可能我上面的表达不太合理可能会误人子弟,递归类型限制是有些抽象,它应该有严谨的数学描述,我想不清楚怎么表达,我先用另一个简单些的例子来说明吧。

 

public static <T extends Comparable<T>> T max(List<T> list)

这个方法用来获取list 中的最大值,它定义了一个 <T extends Comparable<T>> 的类型表达式

 

这个递归类型限制的表达式容易理解一些,

 <T extends Comparable<T>> 表示的是:针对可以与自身进行比较的每个类型T。

或者说,每个实现了Comparable<T>接口的类型T,比如 String 实现了 Comparable<String> , Long实现了Comparable<Long>等等

 

而Enum因为使用enum关键字的原因,让我们忽略了它底层的实现其实也是

class EnumSample extends Enum<EnumSample> 这一事实,

比如 我们定义了 BoundKind 这样的一个枚举:

public enum BoundKind{ }

编译器会转换为:

public final class BoundKind extends java.lang.Enum<BoundKind>

 

看到了,这和  String implements Comparable<String> 类似

这样我们套回到<E extends Enum<E>> 就是 <BoundKind extends Enum<BoundKind>>

这下好理解了, <E extends Enum<E>> 就直接按字面理解:每个继承自Enum<E>的类型E,比如 BoundKind 继承自Enum<BoundKind>

 

通过与<T extends Comparable<T>>的对比,我们可以理解 <E extends Enum<E>>了。

 

那现在我们再回到Collections工具类中的max 方法:

 

  

 

 

 

我们先简化一下这个表达式,看看

<T extends Comparable<? super T>> 怎么理解?

既然 <T extends Comparable<T>> 我们都理解了,把Comparable<T> 改为

Comparable<? super T> 也没什么费解的。

 

在《Java1.5 Generics Tutorial》一文中的解释是:

精确地(exactly)和自己比较是不需要的。所需要的是 T能够和它的父类中的一个进行比较。

 

而《Effictive Java》第二版中对此是用 PECS原则来解释的:

   

下面是修改过的使用通配符类型的声明:

 

 

 

 

     为了从初始声明中得到修改后的版本,要应用PECS转换两次,最直接的是运用到参数List。它产生T实例,因此将类型从List<T>改为List<? extends T>。(ok好理解)

。。。。。。

更灵活的是运用到类型参数T。最初T 被指定用来扩展Comparable<T>,但是T的compareTo()消费T实例(并产生表示顺序关系的整值)。因此,参数化类型Comparable<T>被有限制通配符类型Comparable<? super T>取代。Comparable始终是消费者,因此使用时始终应该是Comparable<? super T> 优先于 Comparable<T>。

 

蓝色粗体的那句翻译得不好。还是看一下代码来理解吧:

    public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll) {

        Iterator<? extends T> i = coll.iterator();

        T candidate = i.next();

        while (i.hasNext()) {

            T next = i.next();

            if (next.compareTo(candidate) > 0) // here compareTo

                candidate = next;

        }

        return candidate;

    }

 5行,Bloch认为 next.compareTo(candidate) 是一句消费操作,在消费一个candidate对象时,根据PECS原则,candidate的类型应该使用 <? super T> 来提高它的灵活性。

 

我觉得Bloch将第5行当作消费操作挺别扭的,我个人偏向《Java1.5 Generics Tutorial》中的解释。

但归根到底,都是降低限制,提高比较时的灵活性。

 

最后,我们再来完整的理解:<T extends Object & Comparable<? super T>>

就只是比 <T extends Comparable<? super T>> 多了一个限制(bounds)。

 

Object & Comparable<? super T> 是一个多限定(multiple bounds)的用法,

语法为: T1 & T2 … & Tn

一个有多个界限的类型参数是所有界限中列出来的类型的子类。当多个界限被使用的时候,界限中的第一个类型被用作这个类型参数的erasure。

 

最终这个方法的返回值,按照第一个限定,擦拭为Object类型了。这是因为在以前版本中此方法就是返回的Object类型,需要兼容。

(此句话随口而说,验证发现有误,发现 类型推导 type inference 比较复杂,就不去理解了)

 

因为多限定(multiple bounds)的存在,泛型方法中又对应地增加了一个很不优雅的调用方式。下面用一段代码来说明:

public class GenericsTest {    

    static class Book {};

    static class StoryBook extends Book implements Comparable<StoryBook> {

        @Override

        public int compareTo(StoryBook o) {

            return 0; //FIXME

        }};

    static class TechBook extends Book implements Comparable<TechBook> {

        @Override

        public int compareTo(TechBook o) {

            return 0; //FIXME

        }};

 

    public static <E> Set<E> merge(Set<? extends E> s1, Set<? extends E> s2) {

        HashSet<E> newSet = new HashSet<E>(s1);

        newSet.addAll(s2);

        return newSet;

    }

    

    public static void main(String[] args) {

        HashSet<StoryBook> s1 = new HashSet<StoryBook>();

        HashSet<TechBook> s2 = new HashSet<TechBook>();

        Set<Book> sb = merge(s1, s2); // 错误

        // 需通过显式的类型参数(explicit type parameter)来告诉它要使用哪种类型

        Set<Book> bs = GenericsTest.<Book>merge(s1,s2); //OK

        // 或者

        Set<Comparable<?>> s = GenericsTest.<Comparable<?>>merge(s1,s2);

     }

  }

 

上面直接调用merge(s1,s2) 那行代码错误的提示信息:

Type mismatch: cannot convert from  Set<GenericsTest.Book&Comparable<?>> to Set<GenericsTest.Book>

这归因于泛型的类型推导(type inference),当无法推导出明确的类型时,就需要显式的描述,如上面代码中红色粗体字。

 

 

后注:

有关于 <?> 与 <? extends Object>是否是一回事

今天中午发现同事桌上有本《Java编程思想》第四版,随手翻了一下,发现泛型一章的介绍中,

有句描述:“UnboundedWildcards.java 展示了编译器处理List<?>和List<? Extends Object>时是不同的。

这让我奇怪,查看了一下它的代码,主要因为是对于Raw类型的造型为泛型时的警告信息不同。

将一个Raw的ArrayList造型给 List<?> 没有问题,而给List<? Extends Object>却会有警告。

 

在网上查了一下,发现对于<?>与<? extends Object>是否等同,是有些不同意见的。

http://mail.openjdk.java.net/pipermail/compiler-dev/2008-April/000316.html

http://bugs.sun.com/view_bug.do?bug_id=6559175

这个报告里,有两段代码反映了两者的不同:

(1)

    public static void main(String[] args)  {

        Object  customer  = null;

        foo((List<? extends String>) customer ); //[1]

        foo((List<? extends Object>) customer ); //[2] 编译有警告

        foo((List<?>) customer ); //[3]  编译没有警告

    }

    public static void foo(List<?> list) {

}

 

(2)

        Object o2 = new List<?>[3];   // 编译居然OK,估计直接当作Raw处理了

        Object o3 = new List<? extends Object>[3]; // 报错

 上面两段代码,表明了当与Raw类型造型时,<?>在编译器的处理方式的确与<? Extends Object>有所不同,根据场景它可能被编译器忽略掉泛型信息而直接当作Raw类型,而<? Extends Object>则不会。

 

 

但这种差异,有些吹毛求疵,除了跟Raw类型转换方面存在差异,在语义上两者可以认为是完全等同的,

见:http://bugs.sun.com/view_bug.do?bug_id=6480391

The introduction of the capture conversion simplified a lot of things. One of the things it did is make "?" equivalent to "? extends Object". 
Unfortunately, JLS3 doesn't say they are equivalent.

 

SUN的开发人员回复说:
? should be considered equivalent to ? extends Object. I will note this at the end of the text about bounds for wildcards in 4.5.1.
……
Hence, Foo<?> is semantically equivalent to Foo<? extends Object>
 

但查了一下发现目前 JLS3中还依然没有增加他说要加的那句注释,见:

http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.5.1

我们暂从语义上认为两者相等。

分享到:
评论

相关推荐

    Java泛型的用法及T.class的获取过程解析

    Java泛型的用法及T.class的获取过程解析 Java泛型是Java编程语言中的一种重要特性,它允许开发者在编写代码时指定类型参数,从而提高代码的灵活性和可读性。本文将详细介绍Java泛型的用法 及T.class的获取过程解析...

    JAVA泛型加减乘除

    这是一个使用JAVA实现的泛型编程,分为两部分,第一部分创建泛型类,并实例化泛型对象,得出相加结果。 第二部分用户自行输入0--4,选择要进行的加减乘除运算或退出,再输入要进行运算的两个数,并返回运算结果及...

    很好的Java泛型的总结

    Java泛型机制详解 Java泛型是Java语言中的一种机制,用于在编译期检查类型安全。Java泛型的出现解决了Java早期版本中类型安全检查的缺陷。Java泛型的好处是可以在编译期检查类型安全,避免了运行时的...

    Java泛型三篇文章,让你彻底理解泛型(super ,extend等区别)

    Java 泛型详解 Java 泛型是 Java SE 5.0 中引入的一项特征,它允许程序员在编译时检查类型安全,从而减少了 runtime 错误的可能性。泛型的主要优点是可以Reusable Code,让程序员编写更加灵活和可维护的代码。 ...

    Java泛型应用实例

    Java泛型是Java编程语言中的一个强大特性,它允许我们在定义类、接口和方法时指定类型参数,从而实现代码的重用和类型安全。在Java泛型应用实例中,我们可以看到泛型如何帮助我们提高代码的灵活性和效率,减少运行时...

    1.java泛型定义.zip

    1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1.java泛型定义.zip1....

    4.java泛型的限制.zip

    4.java泛型的限制.zip4.java泛型的限制.zip4.java泛型的限制.zip4.java泛型的限制.zip4.java泛型的限制.zip4.java泛型的限制.zip4.java泛型的限制.zip4.java泛型的限制.zip4.java泛型的限制.zip4.java泛型的限制.zip...

    java泛型技术之发展

    Java泛型是Java编程语言中的一个关键特性,它在2004年随着Java SE 5.0的发布而引入,极大地增强了代码的类型安全性和重用性。本篇文章将深入探讨Java泛型的发展历程、核心概念以及其在实际开发中的应用。 1. **发展...

    java 泛型接口示例

    Java 泛型是Java SE 5.0引入的一项重要特性,极大地增强了代码的类型安全性和重用性。泛型接口是泛型在接口中的应用,它允许我们在接口中定义带有类型参数的方法,使得实现该接口的类可以使用不同的数据类型。下面...

    Java泛型擦除深度解析:原理、影响与编程实践

    Java泛型是Java 5中引入的一项重要特性,它为编译时类型安全提供了支持。然而,Java泛型的实现机制——类型擦除,也带来了一系列的问题和限制。本文将深入探讨Java泛型擦除的工作原理、它对编程的影响,以及在实际...

    java 泛型方法使用示例

    Java 泛型是Java SE 5.0引入的一项重要特性,极大地增强了代码的类型安全性和重用性。泛型方法是泛型技术在类方法层面的应用,它允许我们定义一个可以处理多种数据类型的通用方法。下面我们将深入探讨Java泛型方法的...

    java 泛型类的类型识别示例

    在Java编程语言中,泛型(Generics)是一种强大的特性,它允许我们在编写代码时指定容器(如集合)可以存储的数据类型。这提高了代码的安全性和效率,因为编译器可以在编译时检查类型,避免了运行时...

    java泛型的内部原理及更深应用

    Java泛型是Java编程语言中的一个强大特性,它允许在定义类、接口和方法时使用类型参数,从而实现参数化类型。这使得代码更加安全、可读性更强,并且能够减少类型转换的必要。在“java泛型的内部原理及更深应用”这个...

    java泛型学习ppt

    "Java 泛型学习" Java 泛型是 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的...

    Java泛型深入解析:类型安全的灵活编程

    Java泛型是一个强大的特性,它提供了类型安全的集合操作和代码重用性。通过理解泛型的工作原理和高级特性,开发者可以编写出更安全、更灵活的代码。尽管泛型有一些限制,但它仍然是现代Java编程中不可或缺的一部分。

    SUN公司Java泛型编程文档

    Java泛型是Java编程语言中的一个关键特性,它在2004年随着JDK 5.0的发布被引入。这个特性极大地提高了代码的类型安全性和可读性,减少了在运行时出现ClassCastException的可能性。SUN公司的Java泛型编程文档,包括...

Global site tag (gtag.js) - Google Analytics