`

【转】java 泛型 通配符

阅读更多

通配符

在本文的前面的部分里已经说过了泛型类型的子类型的不相关性。但有些时候,我们希望能够像使用普通类型那样使用泛型类型:

◆ 向上造型一个泛型对象的引用

◆ 向下造型一个泛型对象的引用

向上造型一个泛型对象的引用

例如,假设我们有很多箱子,每个箱子里都装有不同的水果,我们需要找到一种方法能够通用的处理任何一箱水果。更通俗的说法,A是B的子类型,我们需要找到一种方法能够将C<A>类型的实例赋给一个C<B>类型的声明。

为了完成这种操作,我们需要使用带有通配符的扩展声明,就像下面的例子里那样:

  1. List<Apple> apples =  new  ArrayList<Apple>();  
  2. List<?  extends  Fruit> fruits = apples; 

“? extends”是泛型类型的子类型相关性成为现实:Apple是Fruit的子类型,List<Apple> 是 List<? extends Fruit> 的子类型。

向下造型一个泛型对象的引用

现在我来介绍另外一种通配符:? super。如果类型B是类型A的超类型(父类型),那么C<B> 是 C<? super A> 的子类型:

  1. List<Fruit> fruits =  new  ArrayList<Fruit>();  
  2. List<?  super  Apple> = fruits; 

为什么使用通配符标记能行得通?

原理现在已经很明白:我们如何利用这种新的语法结构?

? extends

让我们重新看看这第二部分使用的一个例子,其中谈到了Java数组的子类型相关性:

  1. Apple[] apples =  new  Apple[ 1 ];  
  2. Fruit[] fruits = apples;  
  3. fruits[ 0 ] =  new  Strawberry(); 

就像我们看到的,当你往一个声明为Fruit数组的Apple对象数组里加入Strawberry对象后,代码可以编译,但在运行时抛出异常。

现在我们可以使用通配符把相关的代码转换成泛型:因为Apple是Fruit的一个子类,我们使用? extends 通配符,这样就能将一个List<Apple>对象的定义赋到一个List<? extends Fruit>的声明上:

  1. List<Apple> apples =  new  ArrayList<Apple>();  
  2. List<?  extends  Fruit> fruits = apples;  
  3. fruits.add( new  Strawberry()); 

这次,代码就编译不过去了!Java编译器会阻止你往一个Fruit list里加入strawberry。在编译时我们就能检测到错误,在运行时就不需要进行检查来确保往列表里加入不兼容的类型了。即使你往list里加入Fruit对象也不行:

  1. fruits.add( new  Fruit()); 

你没有办法做到这些。事实上你不能够往一个使用了? extends的数据结构里写入任何的值。

原因非常的简单,你可以这样想:这个? extends T 通配符告诉编译器我们在处理一个类型T的子类型,但我们不知道这个子类型究竟是什么。因为没法确定,为了保证类型安全,我们就不允许往里面加入任何这种类 型的数据。另一方面,因为我们知道,不论它是什么类型,它总是类型T的子类型,当我们在读取数据时,能确保得到的数据是一个T类型的实例:

  1. Fruit get = fruits.get( 0 ); 

? super

使用 ? super 通配符一般是什么情况?让我们先看看这个:

  1. List<Fruit> fruits =  new  ArrayList<Fruit>();  
  2. List<?  super  Apple> = fruits; 

我们看到fruits指向的是一个装有Apple的某种超类(supertype)的List。同样的,我们不知道究竟是什么超类,但我们知道 Apple和任何Apple的子类都跟它的类型兼容。既然这个未知的类型即是Apple,也是GreenApple的超类,我们就可以写入:

  1. fruits.add( new  Apple());  
  2. fruits.add( new  GreenApple()); 

如果我们想往里面加入Apple的超类,编译器就会警告你:

  1. fruits.add( new  Fruit());  
  2. fruits.add( new  Object()); 

因为我们不知道它是怎样的超类,所有这样的实例就不允许加入。

从这种形式的类型里获取数据又是怎么样的呢?结果表明,你只能取出Object实例:因为我们不知道超类究竟是什么,编译器唯一能保证的只是它是个Object,因为Object是任何Java类型的超类。

存取原则和PECS法则

总结 ? extends 和 the ? super 通配符的特征,我们可以得出以下结论:

◆ 如果你想从一个数据类型里获取数据,使用 ? extends 通配符

◆ 如果你想把对象写入一个数据结构里,使用 ? super 通配符

◆ 如果你既想存,又想取,那就别用通配符。

这就是Maurice Naftalin在他的《Java Generics and Collections》这本书中所说的存取原则,以及Joshua Bloch在他的《Effective Java》这本书中所说的PECS法则。

Bloch提醒说,这PECS是指”Producer Extends, Consumer Super”,这个更容易记忆和运用。

 

------------

    上面的接最底下的:

 

The Java Tutorial

Java Generics and Collections, by Maurice Naftalin and Philip Wadler

Effective Java中文版(第2版), by Joshua Bloch.

尽管有这么多丰富的资料,有时我感觉,有很多的程序员仍然不太明白Java泛型的功用和意义。这就是为什么我想使用一种最简单的形式来总结一下程序员需要知道的关于Java泛型的最基本的知识。

Java泛型由来的动机

理解Java泛型最简单的方法是把它看成一种便捷语法,能节省你某些Java类型转换(casting)上的操作:

  1. List<Apple> box = ...;  
  2. Apple apple = box.get( 0 ); 

上面的代码自身已表达的很清楚:box是一个装有Apple对象的List。get方法返回一个Apple对象实例,这个过程不需要进行类型转换。没有泛型,上面的代码需要写成这样:

  1. List box = ...;  
  2. Apple apple = (Apple) box.get( 0 ); 

很明显,泛型的主要好处就是让编译器保留参数的类型信息,执行类型检查,执行类型转换操作:编译器保证了这些类型转换的绝对无误。

相对于依赖程序员来记住对象类型、执行类型转换——这会导致程序运行时的失败,很难调试和解决,而编译器能够帮助程序员在编译时强制进行大量的类型检查,发现其中的错误。

泛型的构成

由泛型的构成引出了一个类型变量的概念。根据Java语言规范,类型变量是一种没有限制的标志符,产生于以下几种情况:

◆ 泛型类声明

◆ 泛型接口声明

◆ 泛型方法声明

◆ 泛型构造器(constructor)声明

泛型类和接口

如果一个类或接口上有一个或多个类型变量,那它就是泛型。类型变量由尖括号界定,放在类或接口名的后面:

  1. public   interface  List<T>  extends  Collection<T> {  
  2.     ...  
  3.     } 

简单的说,类型变量扮演的角色就如同一个参数,它提供给编译器用来类型检查的信息。

Java类库里的很多类,例如整个Collection框架都做了泛型化的修改。例如,我们在上面的第一段代码里用到的List接口就是一个泛型类。在那段代码里,box是一个List对象,它是一个带有一个Apple类型变量的List接口的类实现的实例。编译器使用这个类型变量参数在get方法被调用、返回一个Apple对象时自动对其进行类型转换。

实际上,这新出现的泛型标记,或者说这个List接口里的get方法是这样的:

  1. T get( int  index); 

get方法实际返回的是一个类型为T的对象,T是在List<T>声明中的类型变量。

泛型方法和构造器(Constructor)

非常的相似,如果方法和构造器上声明了一个或多个类型变量,它们也可以泛型化。

  1. public   static  <t> T getFirst(List<T> list) 

这个方法将会接受一个List<T>类型的参数,返回一个T类型的对象。

例子

你既可以使用Java类库里提供的泛型类,也可以使用自己的泛型类。

类型安全的写入数据…

下面的这段代码是个例子,我们创建了一个List<String>实例,然后装入一些数据:

  1. List<String> str =  new  ArrayList<String>();  
  2. str.add( "Hello " );  
  3. str.add( "World." ); 

如果我们试图在List<String>装入另外一种对象,编译器就会提示错误:

  1. str.add( 1 );  // 不能编译  

类型安全的读取数据…

当我们在使用List<String>对象时,它总能保证我们得到的是一个String对象:

  1. String myString = str.get( 0 ); 

遍历

类库中的很多类,诸如Iterator<T>,功能都有所增强,被泛型化。List<T>接口里的iterator()方 法现在返回的是Iterator<T>,由它的T next()方法返回的对象不需要再进行类型转换,你直接得到正确的类型。

  1. for  (Iterator<String> iter = str.iterator(); iter.hasNext();) {  
  2. String s = iter.next();  
  3. System.out.print(s);  

使用foreach

“for each”语法同样受益于泛型。前面的代码可以写出这样:

  1. for  (String s: str) {  
  2. System.out.print(s);  

这样既容易阅读也容易维护。

自动封装(Autoboxing)和自动拆封(Autounboxing)

在使用Java泛型时,autoboxing/autounboxing这两个特征会被自动的用到,就像下面的这段代码:

  1. List<Integer> ints =  new  ArrayList<Integer>();  
  2. ints.add( 0 );  
  3. ints.add( 1 );  
  4. int  sum =  0 ;  
  5. for  ( int  i : ints) {  
  6. sum += i;  

然而,你要明白的一点是,封装和解封会带来性能上的损失,所有,通用要谨慎的使用。

子类型

在Java中,跟其它具有面向对象类型的语言一样,类型的层级可以被设计成这样:

Java泛型简明教程  

在Java中,类型T的子类型既可以是类型T的一个扩展,也可以是类型T的一个直接或非直接实现(如果T是一个接口的话)。因为“成为某类型的子类型”是一个具有传递性质的关系,如果类型A是B的一个子类型,B是C的子类型,那么A也是C的子类型。在上面的图中:

◆ FujiApple(富士苹果)是Apple的子类型

◆ Apple是Fruit(水果)的子类型

◆ FujiApple(富士苹果)是Fruit(水果)的子类型

所有Java类型都是Object类型的子类型。

B类型的任何一个子类型A都可以被赋给一个类型B的声明:

  1. Apple a = ...;  
  2. Fruit f = a; 

泛型类型的子类型

如果一个Apple对象的实例可以被赋给一个Fruit对象的声明,就像上面看到的,那么,List<Apple> 和 a List<Fruit>之间又是个什么关系呢?更通用些,如果类型A是类型B的子类型,那C<A> 和 C<B>之间是什么关系?

答案会出乎你的意料:没有任何关系。用更通俗的话,泛型类型跟其是否子类型没有任何关系。

这意味着下面的这段代码是无效的:

  1. List<Apple> apples = ...;  
  2. List<Fruit> fruits = apples; 

下面的同样也不允许:

  1. List < Apple >  apples;  
  2. List < Fruit >   fruits  = ...;  
  3. apples  =  fruits

为什么?一个苹果是一个水果,为什么一箱苹果不能是一箱水果?

在某些事情上,这种说法可以成立,但在类型(类)封装的状态和操作上不成立。如果把一箱苹果当成一箱水果会发生什么情况?

  1. List<Apple> apples = ...;  
  2. List<Fruit> fruits = apples;  
  3. fruits.add( new  Strawberry()); 

如果可以这样的话,我们就可以在list里装入各种不同的水果子类型,这是绝对不允许的。

另外一种方式会让你有更直观的理解:一箱水果不是一箱苹果,因为它有可能是一箱另外一种水果,比如草莓(子类型)。

 

这是一个需要注意的问题吗?

应该不是个大问题。而程序员对此感到意外的最大原因是数组和泛型类型上用法的不一致。对于泛型类型,它们和类型的子类型之间是没什么关系的。而对于数组,它们和子类型是相关的:如果类型A是类型B的子类型,那么A[]是B[]的子类型:

  1. Apple[] apples = ...;  
  2. Fruit[] fruits = apples; 

可是稍等一下!如果我们把前面的那个议论中暴露出的问题放在这里,我们仍然能够在一个apple类型的数组中加入strawberrie(草莓)对象:

  1. Apple[] apples =  new  Apple[ 1 ];  
  2. Fruit[] fruits = apples;  
  3. fruits[ 0 ] =  new  Strawberry(); 

这样写真的可以编译,但是在运行时抛出ArrayStoreException异常。因为数组的这特点,在存储数据的操作上,Java运行时需要检查类型的兼容性。这种检查,很显然,会带来一定的性能问题,你需要明白这一点。

重申一下,泛型使用起来更安全,能“纠正”Java数组中这种类型上的缺陷。

现在估计你会感到很奇怪,为什么在数组上会有这种类型和子类型的关系,我来给你一个《Java Generics and Collections》这本书上给出的答案:如果它们不相关,你就没有办法把一个未知类型的对象数组传入一个方法里(不经过每次都封装成 Object[]),就像下面的:

  1. void  sort(Object[] o); 

泛型出现后,数组的这个个性已经不再有使用上的必要了(下面一部分我们会谈到这个),实际上是应该避免使用。

 

转自:http://blog.csdn.net/flfna/article/details/6576394

分享到:
评论

相关推荐

    Java 泛型通配符的一个实例

    * 一个参数通配符的实例 * 说明:对一个包含了数值元素的集合进行汇总运算。在这种情况下,用户并不关心 * 集合中的每一个对象是什么类型,只要它是数值型即可,而且,用户也希望集合中可以 * 存放不同类型的数值...

    Java泛型通配符

    NULL 博文链接:https://mydownload.iteye.com/blog/1330570

    java基础-泛型通配符

    java基础-泛型通配符

    java泛型例子

    java泛型例子 内涵泛型类,泛型接口,泛型方法,泛型通配符使用,泛型上界下界,泛型数组,嵌套泛型等,很详细。放入myeclipse可用,已测试。

    Java中泛型通配符的使用方法示例

    主要介绍了Java中泛型通配符的使用方法,结合实例形式分析了java中泛型通配符的功能、语法及在泛型类创建泛型对象中的使用方法,需要的朋友可以参考下

    一看就懂 详解JAVA泛型通配符T,E,K,V区别

    泛型从字面上理解,是指一个类、接口或方法支持多种类型,使之广泛化、一般化和更加通用。通配符只有在修饰一个变量时会用到,使用它可方便地引用包含了多种类型的泛型;下面我们来深入了解一下吧

    详谈Java泛型中T和问号(通配符)的区别

    下面小编就为大家带来一篇详谈Java泛型中T和问号(通配符)的区别。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧

    java泛型常用通配符实例解析

    主要介绍了java泛型常用通配符实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

    浅谈Java泛型通配符解决了泛型的许多诟病(如不能重载)

    下面小编就为大家带来一篇浅谈Java泛型通配符解决了泛型的许多诟病(如不能重载)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧

    JAVA泛型总结

    java 泛型详解 实例 class Point class Notepad,V&gt;{ // 此处指定了...通配符、受限泛型、泛型无法向上转型、泛型接口、泛型方法、通过泛型方法返回泛型类型实例、使用泛型统一传入的参数类型、泛型数组、泛型的嵌套设置

    java中的泛型通配符的使用

    这是小编自己学习的心得,想通过这个平台对大家共享,希望大家前来评价一下,我及时改正,通配符这个是在泛型中使用的一个可以帮助大家更加方便简洁的去利用代码,它是其他泛型的一个总父类!

    Java 泛型最全指南(定义和使用+继承泛型类/实现泛型接口+泛型的边界+通配符+类型擦除)

    Java 泛型最全指南(定义和使用+继承泛型类/实现泛型接口+泛型的边界+通配符+类型擦除)

    Java泛型:概念、用法与优势

    Java泛型是一种强大的特性,它使得我们可以编写更加通用、类型安全的代码。通过使用泛型类、泛型方法以及通配符,我们可以处理多种数据类型而无需重复编写代码,并且在编译时进行类型检查,避免了运行时的类型错误。...

    泛型实例详解

    通过7个实例详细介绍泛型的使用,包括普通泛型、通配符、泛型方法、泛型接口、受限泛型、泛型的转型、泛型数组。相信看完这几个泛型的例子就会使用泛型了。注意,这里并不介绍泛型的具体语法,只介绍泛型的使用。

    Java中的泛型

    讲述泛型及泛型的子类型、通配符

    Java 基础泛型.pdf

    Java泛型概念 Java泛型是一种在编译时进行类型检查和类型推断的机制,它可以让我们编写更加通用、可重用的代码,提高了代码的可读性和可维护性,同时保证了类型安全。 Java泛型的核心思想是类型参数化,即在类、接口...

    Java 泛型总结(三):通配符的使用

    在泛型的使用中,还有个重要的东西叫通配符,本文介绍通配符的使用。具有很好的参考价值。下面跟着小编一起来看下吧

    Java语言 泛型讲解案例代码 (泛型类、泛型接口、泛型方法、无界及上下限通配符、泛型对协变和逆变的支持、类型擦除 ...)

    学习和理解Java泛型的基本概念和语法; 实际项目中需要使用泛型来增加类型安全性和重用性的开发任务。 目标: 本代码资源的目标是帮助读者理解泛型的用法和优势,并通过实际的示例代码加深对泛型的掌握。读者可以...

    java深度历险

    JAVA泛型 28 类型擦除 28 实例分析 29 通配符与上下界 30 类型系统 31 开发自己的泛型类 32 最佳实践 32 参考资料 33 目录 3 JAVA注解 34 使用注解 34 开发注解 35 处理注解 35 实例分析 38 参考资料 39 JAVA反射与...

    java泛型总结.docx

    通配符:可以使用通配符来限制类型参数的范围。例如,List表示一个可能是Number或其子类类型的列表。 类型推断:在Java 7及以上版本中,可以使用varargs和钻石操作符来推断类型参数。例如,List[] lists = Arrays....

Global site tag (gtag.js) - Google Analytics