ammayjxf

 

2009年12月14日

java 初始化

解析 Java 类和对象的初始化过程

由一个单态模式引出的问题谈起

developerWorks
文档选项
将打印机的版面设置成横向打印模式

打印本页

将此页作为电子邮件发送

将此页作为电子邮件发送


级别: 初级

张 国建 (guojian.zhang@gmail.com), 软件工程师, 北京高伟达西南分软

2006 年 8 月 31 日

类的初始化和对象初始化是 JVM 管理的类型生命周期中非常重要的两个环节,Google 了一遍网络,有关类装载机制的文章倒是不少,然而类初始化和对象初始化的文章并不多,特别是从字节码和 JVM 层次来分析的文章更是鲜有所见。

本文主要对类和对象初始化全过程进行分析,通过一个实际问题引入,将源代码转换成 JVM 字节码后,对 JVM 执行过程的关键点进行全面解析,并在文中穿插入了相关 JVM 规范和 JVM 的部分内部理论知识,以理论与实际结合的方式介绍对象初始化和类初始化之间的协作以及可能存在的冲突问题。

问题引入

近日我在调试一个枚举类型的解析器程序,该解析器是将数据库内一万多条枚举代码装载到缓存中,为了实现快速定位枚举代码和具体枚举类别的所有枚举元素,该类在装载枚举代码的同时对其采取两种策略建立内存索引。由于该类是一个公共服务类,在程序各个层面都会使用到它,因此我将它实现为一个单例类。这个类在我调整类实例化语句位置之前运行正常,但当我把该类实例化语句调整到静态初始化语句之前时,我的程序不再为我工作了。

下面是经过我简化后的示例代码:


[清单一]
package com.ccb.framework.enums;
                        import java.util.Collections;
                        import java.util.HashMap;
                        import java.util.Map;
                        public class CachingEnumResolver {
                        //单态实例 一切问题皆由此行引起
                        private static final CachingEnumResolver SINGLE_ENUM_RESOLVER = new
                        CachingEnumResolver();
                        /*MSGCODE->Category内存索引*/
                        private static Map CODE_MAP_CACHE;
                        static {
                        CODE_MAP_CACHE = new HashMap();
                        //为了说明问题,我在这里初始化一条数据
                        CODE_MAP_CACHE.put("0","北京市");
                        }
                        //private, for single instance
                        private CachingEnumResolver() {
                        //初始化加载数据  引起问题,该方法也要负点责任
                        initEnums();
                        }
                        /**
                        * 初始化所有的枚举类型
                        */
                        public static void initEnums() {
                        // ~~~~~~~~~问题从这里开始暴露 ~~~~~~~~~~~//
                        if (null == CODE_MAP_CACHE) {
                        System.out.println("CODE_MAP_CACHE为空,问题在这里开始暴露.");
                        CODE_MAP_CACHE = new HashMap();
                        }
                        CODE_MAP_CACHE.put("1", "北京市");
                        CODE_MAP_CACHE.put("2", "云南省");
                        //..... other code...
                        }
                        public Map getCache() {
                        return Collections.unmodifiableMap(CODE_MAP_CACHE);
                        }
                        /**
                        * 获取单态实例
                        *
                        * @return
                        */
                        public static CachingEnumResolver getInstance() {
                        return SINGLE_ENUM_RESOLVER;
                        }
                        public static void main(String[] args) {
                        System.out.println(CachingEnumResolver.getInstance().getCache());
                        }
                        }
                        

想必大家看了上面的代码后会感觉有些茫然,这个类看起来没有问题啊,这的确属于典型的饿汉式单态模式啊,怎么会有问题呢?

是的,他看起来的确没有问题,可是如果将他 run 起来时,其结果是他不会为你正确 work。运行该类,它的执行结果是:


[清单二]
CODE_MAP_CACHE为空,问题在这里开始暴露.
                        {0=北京市}
                        

我的程序怎么会这样?为什么在 initEnum() 方法里 CODE_MAP_CACHE 为空?为什么我输出的 CODE_MAP_CACHE 内容只有一个元素,其它两个元素呢????!!

看到这里,如果是你在调试该程序,你此刻一定觉得很奇怪,难道是我的 Jvm 有问题吗?非也!如果不是,那我的程序是怎么了?这绝对不是我想要的结果。可事实上无论怎么修改 initEnum() 方法都无济于事,起码我最初是一定不会怀疑到问题可能出在创建 CachingEnumResolver 实例这一环节上。正是因为我太相信我创建 CachingEnumResolver 实例的方法,加之对 Java 类初始化与对象实例化底层原理理解有所偏差,使我为此付出了三、四个小时--约半个工作日的大好青春。

那么问题究竟出在哪里呢?为什么会出现这样的怪事呢?在解决这个问题之前,先让我们来了解一下JVM的类和对象初始化的底层机制。





回页首


类的生命周期



上图展示的是类生命周期流向;在本文里,我只打算谈谈类的"初始化"以及"对象实例化"两个阶段。





回页首


类初始化

类"初始化"阶段,它是一个类或接口被首次使用的前阶段中的最后一项工作,本阶段负责为类变量赋予正确的初始值。

Java 编译器把所有的类变量初始化语句和类型的静态初始化器通通收集到 <clinit> 方法内,该方法只能被 Jvm 调用,专门承担初始化工作。

除接口以外,初始化一个类之前必须保证其直接超类已被初始化,并且该初始化过程是由 Jvm 保证线程安全的。另外,并非所有的类都会拥有一个 <clinit>() 方法,在以下条件中该类不会拥有 <clinit>() 方法:

  • 该类既没有声明任何类变量,也没有静态初始化语句;
  • 该类声明了类变量,但没有明确使用类变量初始化语句或静态初始化语句初始化;
  • 该类仅包含静态 final 变量的类变量初始化语句,并且类变量初始化语句是编译时常量表达式。




回页首


对象初始化

在类被装载、连接和初始化,这个类就随时都可能使用了。对象实例化和初始化是就是对象生命的起始阶段的活动,在这里我们主要讨论对象的初始化工作的相关特点。

Java 编译器在编译每个类时都会为该类至少生成一个实例初始化方法--即 "<init>()" 方法。此方法与源代码中的每个构造方法相对应,如果类没有明确地声明任何构造方法,编译器则为该类生成一个默认的无参构造方法,这个默认的构造器仅仅调用父类的无参构造器,与此同时也会生成一个与默认构造方法对应的 "<init>()" 方法.

通常来说,<init>() 方法内包括的代码内容大概为:调用另一个 <init>() 方法;对实例变量初始化;与其对应的构造方法内的代码。

如果构造方法是明确地从调用同一个类中的另一个构造方法开始,那它对应的 <init>() 方法体内包括的内容为:一个对本类的 <init>() 方法的调用;对应用构造方法内的所有字节码。

如果构造方法不是通过调用自身类的其它构造方法开始,并且该对象不是 Object 对象,那 <init>() 法内则包括的内容为:一个对父类 <init>() 方法的调用;对实例变量初始化方法的字节码;最后是对应构造子的方法体字节码。

如果这个类是 Object,那么它的 <init>() 方法则不包括对父类 <init>() 方法的调用。





回页首


类的初始化时机

本文到目前为止,我们已经大概有了解到了类生命周期中都经历了哪些阶段,但这个类的生命周期的开始阶段--类装载又是在什么时候被触发呢?类又是何时被初始化的呢?让我们带着这三个疑问继续去寻找答案。

Java 虚拟机规范为类的初始化时机做了严格定义:"initialize on first active use"--" 在首次主动使用时初始化"。这个规则直接影响着类装载、连接和初始化类的机制--因为在类型被初始化之前它必须已经被连接,然而在连接之前又必须保证它已经被装载了。

在与初始化时机相关的类装载时机问题上,Java 虚拟机规范并没有对其做严格的定义,这就使得 JVM 在实现上可以根据自己的特点提供采用不同的装载策略。我们可以思考一下 Jboss AOP 框架的实现原理,它就是在对你的 class 文件装载环节做了手脚--插入了 AOP 的相关拦截字节码,这使得它可以对程序员做到完全透明化,哪怕你用 new 操作符创建出的对象实例也一样能被 AOP 框架拦截--与之相对应的 Spring AOP,你必须通过他的 BeanFactory 获得被 AOP 代理过的受管对象,当然 Jboss AOP 的缺点也很明显--他是和 JBOSS 服务器绑定很紧密的,你不能很轻松的移植到其它服务器上。嗯~……,说到这里有些跑题了,要知道 AOP 实现策略足可以写一本厚厚的书了,嘿嘿,就此打住。

说了这么多,类的初始化时机就是在"在首次主动使用时",那么,哪些情形下才符合首次主动使用的要求呢?

首次主动使用的情形:

  • 创建某个类的新实例时--new、反射、克隆或反序列化;
  • 调用某个类的静态方法时;
  • 使用某个类或接口的静态字段或对该字段赋值时(final字段除外);
  • 调用Java的某些反射方法时
  • 初始化某个类的子类时
  • 在虚拟机启动时某个含有main()方法的那个启动类。

除了以上几种情形以外,所有其它使用JAVA类型的方式都是被动使用的,他们不会导致类的初始化。





回页首


我的问题究竟出在哪里

好了,了解了JVM的类初始化与对象初始化机制后,我们就有了理论基础,也就可以理性的去分析问题了。

下面让我们来看看前面[清单一]的JAVA源代码反组译出的字节码:


[清单三]
public class com.ccb.framework.enums.CachingEnumResolver extends
                        java.lang.Object{
                        static {};
                        Code:
                        0:	new	#2; //class CachingEnumResolver
                        3:	dup
                        4:	invokespecial	#14; //Method "<init>":()V  ①
                        7:	putstatic	#16; //Field
                        SINGLE_ENUM_RESOLVER:Lcom/ccb/framework/enums/CachingEnumResolver;
                        10:	new	#18; //class HashMap              ②
                        13:	dup
                        14:	invokespecial	#19; //Method java/util/HashMap."<init>":()V
                        17:	putstatic	#21; //Field CODE_MAP_CACHE:Ljava/util/Map;
                        20:	getstatic	#21; //Field CODE_MAP_CACHE:Ljava/util/Map;
                        23:	ldc	#23; //String 0
                        25:	ldc	#25; //String 北京市
                        27:	invokeinterface	#31,  3; //InterfaceMethod
                        java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;   ③
                        32:	pop
                        33:	return
                        private com.ccb.framework.enums.CachingEnumResolver();
                        Code:
                        0:	aload_0
                        1:	invokespecial	#34; //Method java/lang/Object."<init>":()V
                        4:	invokestatic	#37; //Method initEnums:()V                  ④
                        7:	return
                        public static void initEnums();
                        Code:
                        0:	getstatic	#21; //Field CODE_MAP_CACHE:Ljava/util/Map;    ⑤
                        3:	ifnonnull	24
                        6:	getstatic	#44; //Field java/lang/System.out:Ljava/io/PrintStream;
                        9:	ldc	#46; //String CODE_MAP_CACHE为空,问题在这里开始暴露.
                        11:	invokevirtual	#52; //Method
                        java/io/PrintStream.println:(Ljava/lang/String;)V
                        14:	new	#18; //class HashMap
                        17:	dup
                        18:	invokespecial	#19; //Method java/util/HashMap."<init>":()V      ⑥
                        21:	putstatic	#21; //Field CODE_MAP_CACHE:Ljava/util/Map;
                        24:	getstatic	#21; //Field CODE_MAP_CACHE:Ljava/util/Map;
                        27:	ldc	#54; //String 1
                        29:	ldc	#25; //String 北京市
                        31:	invokeinterface	#31,  3; //InterfaceMethod
                        java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;   ⑦
                        36:	pop
                        37:	getstatic	#21; //Field CODE_MAP_CACHE:Ljava/util/Map;
                        40:	ldc	#56; //String 2
                        42:	ldc	#58; //String 云南省
                        44:	invokeinterface	#31,  3; //InterfaceMethod
                        java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;    ⑧
                        49:	pop
                        50:	return
                        public java.util.Map getCache();
                        Code:
                        0:	getstatic	#21; //Field CODE_MAP_CACHE:Ljava/util/Map;
                        3:	invokestatic	#66; //Method
                        java/util/Collections.unmodifiableMap:(Ljava/util/Map;)Ljava/util/Map;
                        6:	areturn
                        public static com.ccb.framework.enums.CachingEnumResolver getInstance();
                        Code:
                        0:	getstatic	#16;
                        //Field SINGLE_ENUM_RESOLVER:Lcom/ccb/framework/enums/CachingEnumResolver;   ⑨
                        3:	areturn
                        }
                        

如果上面[清单一]显示,清单内容是在 JDK1.4 环境下的字节码内容,可能这份清单对于很大部分兄弟来说确实没有多少吸引力,因为这些 JVM 指令确实不像源代码那样漂亮易懂。但它的的确确是查找和定位问题最直接的办法,我们想要的答案就在这份 JVM 指令清单里。

现在,让我们对该类从类初始化到对象实例初始化全过程分析[清单一]中的代码执行轨迹。

如前面所述,类初始化是在类真正可用时的最后一项前阶工作,该阶段负责对所有类正确的初始化值,此项工作是线程安全的,JVM会保证多线程同步。

第1步:调用类初始化方法 CachingEnumResolver.<clinit>(),该方法对外界是不可见的,换句话说是 JVM 内部专用方法,<clinit>() 内包括了 CachingEnumResolver 内所有的具有指定初始值的类变量的初始化语句。要注意的是并非每个类都具有该方法,具体的内容在前面已有叙述。

第2步:进入 <clinit>() 方法内,让我们看字节码中的 "①" 行,该行与其上面两行组合起来代表 new 一个 CachingEnumResolver 对象实例,而该代码行本身是指调用 CachingEnumResolver 类的 <init>()方法。每一个 Java 类都具有一个 <init>() 方法,该方法是 Java 编译器在编译时生成的,对外界不可见,<init>() 方法内包括了所有具有指定初始化值的实例变量初始化语句和java类的构造方法内的所有语句。对象在实例化时,均通过该方法进行初始化。然而到此步,一个潜在的问题已经在此埋伏好,就等着你来犯了。

第3步:让我们顺着执行顺序向下看,"④" 行,该行所在方法就是该类的构造器,该方法先调用父类的构造器 <init>() 对父对象进行初始化,然后调用 CachingEnumResolver.initEnum() 方法加载数据。

第4步:"⑤" 行,该行获取 "CODE_MAP_CACHE" 字段值,其运行时该字段值为 null。注意,问题已经开始显现了。(作为程序员的你一定是希望该字段已经被初始化过了,而事实上它还没有被初始化)。通过判断,由于该字段为 NULL,因此程序将继续执行到 "⑥" 行,将该字段实例化为 HashMap()。

第5步:在 "⑦"、"⑧" 行,其功能就是为 "CODE_MAP_CACHE" 字段填入两条数据。

第6步:退出对象初始化方法 <init>(),将生成的对象实例初始化给类字段 "SINGLE_ENUM_RESOLVER"。(注意,此刻该对象实例内的类变量还未初始化完全,刚才由 <init>() 调用 initEnum() 方法赋值的类变量 "CODE_MAP_CACHE" 是 <clinit>() 方法还未初始化字段,它还将在后面的类初始化过程再次被覆盖)。

第7步:继续执行 <clinit>()方法内的后继代码,"②" 行,该行对 "CODE_MAP_CACHE" 字段实例化为 HashMap 实例(注意:在对象实例化时已经对该字段赋值过了,现在又重新赋值为另一个实例,此刻,"CODE_MAP_CACHE"变量所引用的实例的类变量值被覆盖,到此我们的疑问已经有了答案)。

第8步:类初始化完毕,同时该单态类的实例化工作也完成。

通过对上面的字节码执行过程分析,或许你已经清楚了解到导致错误的深层原因了,也或许你可能早已被上面的分析过程给弄得晕头转向了,不过也没折,虽然我也可以从源代码的角度来阐述问题,但这样不够深度,同时也会有仅为个人观点、不足可信之嫌。





回页首


如何解决

要解决上面代码所存在的问题很简单,那就是将 "SINGLE_ENUM_RESOLVER" 变量的初始化赋值语句转移到 getInstance() 方法中去即可。换句话说就是要避免在类还未初始化完成时从内部实例化该类或在初始化过程中引用还未初始化的字段。





回页首


写在最后

静下浮燥之心,仔细思量自己是否真的掌握了本文主题所引出的知识,如果您觉得您已经完全或基本掌握了,那么很好,在最后,我将前面的代码稍做下修改,请思考下面两组程序是否同样会存在问题呢?


程序一
	public class CachingEnumResolver {
                        public  static Map CODE_MAP_CACHE;
                        static {
                        CODE_MAP_CACHE = new HashMap();
                        //为了说明问题,我在这里初始化一条数据
                        CODE_MAP_CACHE.put("0","北京市");
                        initEnums();
                        }
                        


程序二
	public class CachingEnumResolver {
                        private static final CachingEnumResolver SINGLE_ENUM_RESOLVER;
                        public  static Map CODE_MAP_CACHE;
                        static {
                        CODE_MAP_CACHE = new HashMap();
                        //为了说明问题,我在这里初始化一条数据
                        CODE_MAP_CACHE.put("0","北京市");
                        SINGLE_ENUM_RESOLVER = new CachingEnumResolver();
                        initEnums();
                        }
                        

最后,一点关于 JAVA 群体的感言:时下正是各种开源框架盛行时期,Spring 更是大行其道,吸引着一大批 JEE 开发者的眼球(我也是 fans 中的一员)。然而,让我们仔细观察一下--以 Spring 群体为例,在那么多的 Spring fans 当中,有多少人去研究过 Spring 源代码?又有多少人对 Spring 设计思想有真正深入了解呢?当然,我是没有资格以这样的口吻来说事的,我只是想表明一个观点--学东西一定要"正本清源"。

献上此文,谨以共勉。



关于作者

 

北京高伟达西南分软 Java EE 软件工程师,三年 Java EE 项目经验,行业方向为银行 OCRM 系统。对 JAVA 有着浓厚的兴趣,业余研究 AOP/ESB 方向。


posted @ 2009-12-16 22:48 ammay 阅读(687) | 评论 (0)编辑 收藏

泛型

编者注:在从Java in a Nutshell,5th edith中摘录的两部部分中的第一部分,David Flanagan描述了如何使用泛型。这部分,David Flanagan将具体告诉你如何创建自己的泛型和泛型方法,并且以Java核心API很多重要的泛型作为结束总结。

创建泛型和泛型方法
创建一个简单的泛型是非常容易的。首先,在一对尖括号(< >)中声明类型变量,以逗号间隔变量名列表。在类的实例变量和方法中,可以在任何类型的地方使用那些类型变量。切记,类型变量仅在编译时存在,所以不能使用instanceof和new这类运行时操作符来操作类型变量。

让我们以一个简单的例子来开始这部分的学习,而后将精简这个例子。这段代码定义了一个树形数据结构,使用类型变量V代表存储在各个树结点中的值。

import java.util.*;
/**
* A tree is a data structure that holds values of type V.
* Each tree has a single value of type V and can have any number of
* branches, each of which is itself a Tree.
*/
public class Tree {
// The value of the tree is of type V.
V value;

// A Tree can have branches, each of which is also a Tree

List<tree branches = new ArrayList<tree();

// Here's the constructor. Note the use of the type variable V.
public Tree(V value) { this.value = value; }

// These are instance methods for manipulating the node value and branches.
// Note the use of the type variable V in the arguments or return types.
V getValue() { return value; }
void setValue(V value) { this.value = value; }
int getNumBranches() { return branches.size(); }
Tree getBranch(int n) { return branches.get(n); }
void addBranch(Tree branch) { branches.add(branch); }
}


正如你所看到的,命名一个类型变量习惯于一个大写字母。使用一个字母可以同现实中那些具有描述性的,长的实际变量名有所区别。使用大写字母要同变量命名规则一致,并且要区别于局部变量,方法参数,成员变量,而这些变量常常使用一个小写字母。集合类中,比如java.util中常常使用类型变量E代表“Element type”。T和S常常用来表示范型变量名(好像使用i和j作为循环变量一样)。

注意到,当一个变量被声明为泛型时,只能被实例变量和方法调用(还有内嵌类型)而不能被静态变量和方法调用。原因很简单,参数化的泛型是一些实例。静态成员是被类的实例和参数化的类所共享的,所以静态成员不应该有类型参数和他们关联。方法,包括静态方法,可以声明和使用他们自己的类型参数,但是,调用这样一个方法,可以被不同地参数化。这些内容将在本章后面谈到。

类型变量绑定
上面例子中的Tree中的类型变量V是不受约束的:Tree可以被参数化为任何类型。以前我们常常会设置一些约束条件在需要使用的类型上:也许我们需要强制一个类型参数实现一个或多个接口,或是一个特定类的子类。这可以通过指明类型绑定来完成。我们已经看到了统配符的上界,而且使用简单的语法可以指定一般类型变量的上界。后面的代码,还是使用Tree这个例子,并且通过实现Serializable和Comparable来重写。为了做到这点,例子中使用类型变量绑定来确保值类型的Serializable和Comparable。

import java.io.Serializable;
import java.util.*;

public class Tree>
implements Serializable, Comparable<tree

{
V value;
List<tree branches = new ArrayList<tree();

public Tree(V value) { this.value = value; }

// Instance methods
V getValue() { return value; }
void setValue(V value) { this.value = value; }
int getNumBranches() { return branches.size(); }
Tree getBranch(int n) { return branches.get(n); }
void addBranch(Tree branch) { branches.add(branch); }

// This method is a nonrecursive implementation of Comparable<tree

// It only compares the value of this node and ignores branches.
public int compareTo(Tree that) {
if (this.value == null && that.value == null) return 0;
if (this.value == null) return -1;
if (that.value == null) return 1;
return this.value.compareTo(that.value);
}

// javac -Xlint warns us if we omit this field in a Serializable class
private static final long serialVersionUID = 833546143621133467L;
}


一个类型变量的绑定是通过extends后的名字和一个类型列表(这可以是参数化的,就像Comparable一样)表达的。注意当有不止一个绑定时,就像上面例子中的,绑定的类型要用&作为分隔符,而不是使用逗号。都后用来分隔类型变量,如果用来分隔类型变量绑定,就会模棱两可。一个类型变量可以有任何数量的绑定,包括任何数量的借口和至多一个类。

范型中的通配符
上一章的例子中我们看到了通配符和控制参数化类型的通配符绑定。这些在范型中同样非常有用。当前设计的Tree要求每个节点有相同类型的值,V。也许这样太严格了,也许我们应该让Tree的branches能够存放V的子类而不全是V。这个版本的Tree(删除了Comparable和Serializable接口的实现)这样做会更灵活。

public class Tree {
// These fields hold the value and the branches
V value;
List<tree<? extends V>> branches = new ArrayList<tree<? extends V>>();

// Here's a constructor
public Tree(V value) { this.value = value; }

// These are instance methods for manipulating value and branches
V getValue() { return value; }
void setValue(V value) { this.value = value; }
int getNumBranches() { return branches.size(); }
Tree<? extends V> getBranch(int n) { return branches.get(n); }
void addBranch(Tree<? extends V> branch) { branches.add(branch); }
}


通配符绑定允许我们在枝节点上增加一个Tree,比如,一个树枝Tree
Tree t = new Tree(0); // Note autoboxing
t.addBranch(new Tree(1)); // int 1 autoboxed to Integer


通过getBranch()查询树枝,而树枝的返回类型不知道,所以必须使用统配符来表达。接下来的两个是合法的,但第三个不是:
Tree<? extends Number> b = t.getBranch(0);
Tree<?> b2 = t.getBranch(0);
Tree b3 = t.getBranch(0); // compilation error


当我们这样来查询一个树枝时,不能精确确定它的返回类型,但是存在类型的上限,所以,我们可以这样做:
Tree<? extends Number> b = t.getBranch(0);
Number value = b.getValue();


那我们不能做什么呢?设定树枝的值,或者在原有的树枝上添加新的树枝。早前章节解释的,上界的存在不会改变返回值的类型不可知,编译器没有足够的信息让我们安全的给setValue()或者一个树枝(包括值类型)的addBranch()传递一个值。下面的两行代码都是非法的:
b.setValue(3.0); // Illegal, value type is unknown
b.addBranch(new Tree(Math.PI));


这个例子在设计时找到了一个平衡点:使用绑定通配符使得数据结构更加灵活,但是减少了安全使用其中方法的可能。这个设计是好是坏就要根据上下文联系了。通常,好的范型设计是非常困难的。幸运的是,大多我们要使用的已经在java.util包中设计好了,而不用我们自己再去设计。

范型方法
  正如前面说的,范型只能被实例成员调用,而不是静态成员。同实例方法一样,静态方法也可以使用通配符。尽管静态方法不能使用包含他们的类中的类型变量,但是他们可以声明自己的类型变量。当一个方法声明了自己的类型变量,就叫做范型方法。

这里有一个要添加到Tree中的静态方法。他不是一个范型方法,但是使用了绑定的通配符,就好像先前我们看到的sumList()一样:
/** Recursively compute the sum of the values of all nodes on the tree */
public static double sum(Tree<? extends Number> t) {
double total = t.value.doubleValue();
for(Tree<? extends Number> b : t.branches) total += sum(b);
return total;
}


  通过通配符的上界绑定,声明自己的类型变量来重写这个方法:
public static  double sum(Tree t) {
N value = t.value;
double total = value.doubleValue();
for(Tree<? extends N> b : t.branches) total += sum(b);
return total;
}


  范型的sum()不比通配符版本的简单,而且声明变量并没有让我们获得什么。这种情况下,通配符方案要比范型方法更有效,当一个类型变量用来表达两个参数之间或者参数和返回值之间的关系时,范型方法才是需要的。请看下面的例子:
// This method returns the largest of two trees, where tree size
// is computed by the sum() method. The type variable ensures that
// both trees have the same value type and that both can be passed to sum().
public static Tree max(Tree t, Tree u) {
double ts = sum(t);
double us = sum(u);
if (ts > us) return t;
else return u;
}
  

这个方法使用类型变量N来约束参数和返回值有相同类型,并且参数是Number或者他的子类。

  使得参数具有相同类型也许是有争议的,应该让我们能调用max()不论是Tree或者Tree。一种方法是使用两个不相干的类型变量来表示两个不相干的值类型。注意,我们不能在方法的返回时使用变量而必须使用通配符:
public static 
Tree<? extends Number> max(Tree t, Tree u) {...}


  既然两个类型变量N和M没有任何联系,而且每个仅在签名的时候使用,他们没有提供比通配符更多的好处,这种方法最好这样写:
public static Tree<? extends Number> max(Tree<? extends Number> t,
Tree<? extends Number> u) {...}


所有在这里的范型方法都是静态的,这并不是必须的,实例方法也可以声明自己的类型变量。

调用范型方法
当你使用范型时,必须指定实际类型参数来代替相应的类型变量。但这些对范型方法有些不同:编译器总是能计算出基于你所传递的参数的相应范型方法参数。考虑一下上面定义的max(),作为例子:
public static  Tree max(Tree t, Tree u) {...}


当你调用这个方法时,不需要指明N,因为N是隐含地由t和u指明。在后面的代码中,编译器决定N为Integer:

Tree x = new Tree(1);
Tree y = new Tree(2);
Tree z = Tree.max(x, y);


编译器判断范型方法的参数类型称为类型推断。类型推断是相对于知觉推断的。而实际编译器的实现方法是一种非常复杂的过程,超过了这本书的讨论范围。更多的细节在The Java Language Specification, Third Edition的第十五章。
让我们看一个更加复杂的类型推断,考虑一下这个方法:

public class Util {
/** Set all elements of a to the value v; return a. */
public static T[] fill(T[] a, T v) {
for(int i = 0; i < a.length; i++) a[i] = v;
return a;
}
}


这里有两个该方法的调用:

Boolean[] booleans = Util.fill(new Boolean[100], Boolean.TRUE);
Object o = Util.fill(new Number[5], new Integer(42));


在第一个例子中,编译器可以轻松的推断出T是Boolean类型,第二个例子中,编译器判断T是Number。
在非常罕见的情况下,你可能会显示的指明范型方法的参数类型。有时候这是必要的,比如,当范型方法不需要参数时。考虑一下java.util.Collections.emptySet():返回一个空集合,但是不同于Collections.singleton()(可以在参考部分察看),他不带任何参数,但需要指明返回类型。通过在方法名前的<>中,可以显示的指明参数类型:

Set empty = Collections.emptySet();


  类型参数不能同没有限制的方法名结合使用:他们必须跟随在一个.后或者在关键字new后,或者在关键字this前,或者构造函数的super前。
可以证明,如果如果你将Collections.emptySet()的返回值赋给一个变量,就像我们上边通过类型推断机制推断基于变量类型的参数类型。尽管显示的类型说明可以更加清楚,但这不是必要的,可以像下面一样重写:

Set empty = Collections.emptySet();


  在方法调用表达式中,显示的说明emptySet()的返回值类型是必要的。比如,假设你要调用一个名为printWords()的方法,该方法仅需一个Set的参数,如果你想传递一个空的集合给该方法,就要像下面一样写:
printWords(Collections.emptySet());


这种情况下,显示的类型说明是必要的。

范型方法和数组
早先我们看到,编译器不允许创建一个类型参数化的数组。但是对于范型的使用会是不同的。考虑一下前面定义的Util.fill(),它得以第一个参数和返回值类型都是T[]。而方法体内不必创建任何参数为T的数组,所以这个方法是合法的。

如果你创建一个方法使用varargs(参见第二章的2.6.4)和类型变量,记住调用varargs隐含创建一个数组,请看下面的例子:

/** Return the largest of the specified values or null if there are none */
public static > T max(T... values) { ... }


你可以使用一个Integer类型来调用这个方法,因为编译器会在调用的时候插入必要的数组创建代码。但是你不能将参数转换为Comparable来调用这个方法,因为创建一个Comparable[]是不合法的。

参数化异常
异常是在运行时抛出和捕获的。没有办法让编译器完成类型检查,来保证在catch块中抛出的未知的类型匹配异常。由于这个原因,catch块很可能不包含类型变量和通配符。既然不可能保证在运行时捕获一个编译器时类型参数完整性异常,所以不允许创建任何Throwable类型的子类。参数化异常是不允许的。
但是你可以使用类型变量在throw块里的方法签名中。看看下面的例子:

public interface Command {
public void doit(String arg) throws X;
}


这个接口描述了一个“command”:一块代码只有一个String类型的参数,没有返回值。代码可能抛出一个类型为X的异常。这里有一个例子使用这个接口:

Command save = new Command() {
public void doit(String filename) throws IOException {
PrintWriter out = new PrintWriter(new FileWriter(filename));
out.println("hello world");
out.close();
}
};

try { save.doit("/tmp/foo"); }
catch(IOException e) { System.out.println(e); }



范型个案研究:比较和枚举
Java1.5引入的范型新特性,在1.5的API中有使用,特别多的是在java.util包中,但是在java.lang,java.lang.reflect和java.util.concurrent中也有。这些API都是经过仔细的斟酌创建的,通过学习这些API我们可以学到很多好的设计方法。

java.util中的范形是比较简单的:因为大多都是集合类,类型变量也是代表集合中的元素。java.lang中的几个重要范型是比较难以理解的,他们不是集合,而且第一眼很不容易理解为什么设计成范型。学习这些范型可以让我们更深层次的理解范形的工作机制,并且介绍一些我们没有提到的概念。特别的,我们要检查Comparable接口和Enum类(枚举类型的超类,后面一张讲解)并且学习一些重要但是很少使用的范型特性,比如通配符下界。

在java1.5中,Comparable接口被修改为范型的。大多数的类都实现了这个接口,考虑一下Integer:

public final class Integer extends Number implements Comparable


原先的Comparable接口在类型安全方面是有问题的。两个继承了Comparable接口的对象可能不能相互比较。JDK5.0前,非范形的Comparable接口是非常有用但是不安全的,而现在的接口,捕获了我们需要的信息:他告诉我们一个对象是可比较的,并且可以同什么比较。

现在,考虑一下comparable类的子类。Integer是final的,所以不能有子类,那么让我们看看java.math.BigInteger:

public class BigInteger extends Number implements Comparable


如果我们实现一个BiggerInteger类是BigInteger的子类,他从父类那里继承了Comparable接口,但是注意继承的是Comparable而不是Comparable。这意味着BigInteger和BiggerInteger是可以相互比较的,这是非常好的。BiggerInteger可以重载compareTo(),但是不允许实现一个不同的参数化的Comparable。这就是说BiggerInteger不能同时继承BigInteger和实现Comparable

当你使用可比较的对象时(当写排序算法的时候)记住两点。首先,使用原生类型是不够充分的:考虑到类型安全,必须指明同什么比较。接下来,类型是不允许同自己比较的:有时候他会同他的祖先比较。为了具体说明,考虑java.util.Collections.max():

这是一个冗长而且复杂的方法标签,我们来一步步考虑:
方法中包含一个类型变量T,并且有复杂的绑定,稍后我们返回来讨论。
方法的返回值类型是T。
方法名是max()。

方法的参数是一个集合。元素的类型指定为绑定的通配符。我们并不知道元素的确切类型,但直到有一个上限T。所以我们知道元素的类型要么为T,要么是T的子类。集合的任何元素都可以作为返回值使用。

这些是比较简单的,本章我们已经看到了通配符上界,我们再来看看max()中的类型变量声明:

>


要说明的第一点,T必须实现了Comparable接口。(范型的语法使用关键字extends来代表类型绑定,不论是类或接口)这是期望的,因为这个方法是找到集合中最大的元素。但是观察这个参数化的Comparable接口,这是一个通配符,但是这个通过关键字super来绑定,而不是extends。这是下界绑定。? extends T是我们熟悉的上界绑定:这意味着T或者其子类。? super T比较少用:这意味着T或者他的超类。

总结一下,类型变量声明表明:“T是一个实现了Comparable接口或者他的父类实现了该接口的类型。”Collections.min()和Collections.binarySearch()有着相同的声明。
对其他的下界通配符(对于Comparable接口没有作用)的例子,Collections中的addAll(),copy(),和fill()。观察addAll()的声明:

public static  boolean addAll(Collection<? super T> c, T... a)


这是一个varargs方法,接受任意数量的参数,并且传递给他们一个T[],命名为a。他将a中的所有元素都赋给集合c。集合的元素类型虽然不知道,但是有一个下界:元素均为T或者T的超类。不论类型是什么,我们可以确定数组的元素都是类型的实例,所以将数组的元素添加到集合中是合法的。

返回到我们先前讨论的上界通配符,如果有一个集合的元素是上界通配符,那么都是只读的。考虑List<? extends Serializable>。我们知道,所有的元素都是Serializable,所以像get()这样的方法返回一个Serializable类型的返回值。编译器不允许我们调用add()这样的方法,因为实际的元素类型是不可知的。不能够添加绝对的Serializable对象到list中,因为实现他们的类可能不是正确的类型。

既然上界统配符的结果是只读的,所以你可能会期望下界通配符来实现只写的集合。实际并不是这样,假设这里有一个List<? extends Integer>。元素的实际类型是不知道的,但是可能性是Integer或者他的祖先类Number和Object。无论实际类型是什么,将Integer类型(而不是Number和Object对象)的元素添加到list中是安全的。无论实际类型是什么,list中所有元素都是Object对象的实例,所以list中像get()一样的方法返回Object。

最后,让我们把注意力放到java.lang.Enum类。Enum是所有枚举类型的父类,它实现了Comparable接口,但是有一个让人迷惑的范型声明方法:

public class Enum> implements Comparable, Serializable


第一眼,类型变量E的声明在一个循环中。再仔细的看一看:声明真正说明了,Enum必须是一个本身就是Enum类型的类型。这种表面上的循环是很显然的,如果我们看到了implements子句。正如我们看到的,Comparable类通常被定义为可以同自己比较的。而且他们的子类也可以同他们的父类比较。从另一个方面将,Enum实现了Comparable接口不是为了他本身,而是为了他的子类E。


资源:
·Onjava.com:Onjava.com
·Matrix-Java开发者社区:http://www.matrix.org.cn/


Java, java, J2SE, j2se, J2EE, j2ee, J2ME, j2me, ejb, ejb3, JBOSS, jboss, spring, hibernate, jdo, struts, webwork, ajax, AJAX, mysql, MySQL, Oracle, Weblogic, Websphere, scjp, scjd
Java中的泛型 第二部分

posted @ 2009-12-14 22:55 ammay 阅读(235) | 评论 (0)编辑 收藏

泛型类型

泛型类型,第一部分

作者: David Flanagan

翻译:cat


版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明
作者:
David Flanagan;cat
原文地址:
http://www.onjava.com/pub/a/onjava/excerpt/javaian5_chap04/index.html
中文地址:
http://www.matrix.org.cn/resource/article/43/43864_Generic_Types.html
关键词: Generic Types


编辑按:《Java in a Nutshell, 5th Edition》覆盖了jdk5.0中很多变化和新特征,其中最重要的就是泛型。在本文的第一部分,作者David Flanagan介绍了如何使用泛型;而在第二部分,作者描述了如何写你自己的泛型和泛型方法。

Java5.0的新特性之一是引入了泛型类型和泛型方法。一个泛型类型通过使用一个或多个类型变量来定义,并拥有一个或多个使用一个类型变量作为一个参数或者返回值的占位符。例如,类型java.util.List<E>是一个泛型类型:一个list,其元素的类型被占位符E描述。这个类型有一个名为add()的方法,被声明为有一个类型为E的参数,同时,有一个get()方法,返回值被声明为E类型。

为了使用泛型类型,你应该为类型变量详细指明实际的类型,形成一个就像List<String>类似的参数化类型。[1]指明这些额外的类型信息的原因是编译器据此能够在编译期为您提供很强的类型检查,增强您的程序的类型安全性。举个例子来说,您有一个只能保持String对象的List,那么这种类型检查就能够阻止您往里面加入String[]对象。同样的,增加的类型信息使编译器能够为您做一些类型转换的事情。比如,编译器知道了一个List<String>有个get()方法,其返回值是一个String对象,因此您不再需要去将返回值由一个Object强制转换为String。

Java.util包中的集合类在java5.0中已经被做成了泛型,也许您将会在您的程序中频繁的使用到他们。类型安全的集合类就是一个泛型类型的典型案例。即便您从没有定义过您自己的泛型类型甚至从未用过除了java.util中的集合类以外的泛型类型,类型安全的集合类的好处也是极有意义的一个标志——他们证明了这个主要的新语言特性的复杂性。

我们从探索类型安全的集合类中的基本的泛型用法开始,进而研究更多使用泛型类型的复杂细节。然后我们讨论类型参数通配符和有界通配符。描绘了如何使用泛型以后,我们阐明如何编写自己的泛型类型和泛型方法。我们对于泛型的讨论将结束于一趟对于JavaAPI的核心中重要的泛型类型的旅行。这趟旅程将探索这些类型以及他们的用法,旅程的目的是为了让您对泛型如何工作这个问题有个深入的理解。

类型安全集合类

Java.util类包包含了Java集合框架(Java Collections Framework),这是一批包含对象的set、对象的list以及基于key-value的map。第五章将谈到集合类。这里,我们讨论的是在java5.0中集合类使用类型参数来界定集合中的对象的类型。这个讨论并不适合java1.4或更早期版本。如果没有泛型,对于集合类的使用需要程序员记住每个集合中元素的类型。当您在java1.4种创建了一个集合,您知道您放入到集合中的对象的类型,但是编译器不知道。您必须小心地往其中加入一个合适类型的元素,当需要从集合中获取元素时,您必须显式的写强制类型转换以将他们从Object转换为他们真是的类型。考察下边的java1.4的代码。

public static void main(String[] args) {
    // This list is intended to hold only strings.
    // The compiler doesn't know that so we have to remember ourselves.
    List wordlist = new ArrayList();  

    // Oops! We added a String[] instead of a String.
    // The compiler doesn't know that this is an error.
    wordlist.add(args);

    // Since List can hold arbitrary objects, the get() method returns
    // Object.  Since the list is intended to hold strings, we cast the
    // return value to String but get a ClassCastException because of
    // the error above.
    String word = (String)wordlist.get(0);
}


泛型类型解决了这段代码中的显示的类型安全问题。Java.util中的List或是其他集合类已经使用泛型重写过了。就像前面提到的, List被重新定义为一个list,它中间的元素类型被一个类型可变的名称为E的占位符描述。Add()方法被重新定义为期望一个类型为E的参数,用于替换以前的Object,get()方法被重新定义为返回一个E,替换了以前的Object。

在java5.0中,当我们申明一个List或者创建一个ArrayList的实例的时候,我们需要在泛型类型的名字后面紧跟一对“<>”,尖括号中写入我们需要的实际的类型。比如,一个保持String的List应该写成“List<String>”。需要注意的是,这非常象给一个方法传一个参数,区别是我们使用类型而不是值,同时使用尖括号而不是圆括号

Java.util的集合类中的元素必须是对象化的,他们不能是基本类型。泛型的引入并没有改变这点。泛型不能使用基本类型:我们不能这样来申明——Set<char>或者List<int>。记住,无论如何,java5.0中的自动打包和自动解包特性使得使用Set<Character>或者List<Integer>和直接使用char和int值一样方便。(查看第二章以了解更多关于自动打包和自动解包的细节)。

在Java5.0中,上面的例子将被重写为如下方式:

public static void main(String[] args) {
    // This list can only hold String objects
    List<String> wordlist = new ArrayList<String>();

    // args is a String[], not String, so the compiler won't let us do this
    wordlist.add(args);  // Compilation error!

    // We can do this, though.  
    // Notice the use of the new for/in looping statement
    for(String arg : args) wordlist.add(arg);

    // No cast is required.  List<String>.get() returns a String.
    String word = wordlist.get(0);
}


值得注意的是代码量其实并没有比原来那个没有泛型的例子少多少。使用“(String)”这样的类型转换被替换成了类型参数“<String>”。 不同的是类型参数需要且仅需要声明一次,而list能够被使用任何多次,不需要类型转换。在更长点的例子代码中,这一点将更加明显。即使在那些看上去泛型语法比非泛型语法要冗长的例子里,使用泛型依然是非常有价值的——额外的类型信息允许编译器在您的代码里执行更强的错误检查。以前只能在运行起才能发现的错误现在能够在编译时就被发现。此外,以前为了处理类型转换的异常,我们需要添加额外的代码行。如果没有泛型,那么当发生类型转换异常的时候,一个ClassCastException异常就会被从实际代码中抛出。

就像一个方法可以使用任意数量的参数一样,类允许使用多个类型变量。接口Java.util.Map就是一个例子。一个Map体现了从一个key的对象到一个value的对象的映射关系。接口Map申明了一个类型变量来描述key的类型而另一个类型变量来描述value的类型。举个例子来说,假设您希望做一个String对象到Integer对象的映射关系:

public static void main(String[] args) {
    // A map from strings to their position in the args[] array
    Map<String,Integer> map = new HashMap<String,Integer>();

    // Note that we use autoboxing to wrap i in an Integer object.
    for(int i=0; i < args.length; i++) map.put(args[i], i);  

    // Find the array index of a word.  Note no cast is required!
    Integer position = map.get("hello");

    // We can also rely on autounboxing to convert directly to an int,
    // but this throws a NullPointerException if the key does not exist
    // in the map
    int pos = map.get("world");
}


象List<String>这个一个参数类型其本身也是也一个类型,也能够被用于当作其他类型的一个类型变量值。您可能会看到这样的代码:

// Look at all those nested angle brackets!
Map<String, List<List<int[]>>> map = getWeirdMap();

// The compiler knows all the types and we can write expressions
// like this without casting.  We might still get NullPointerException
// or ArrayIndexOutOfBounds at runtime, of course.
int value = map.get(key).get(0).get(0)[0];

// Here's how we break that expression down step by step.
List<List<int[]>> listOfLists = map.get(key);
List<int[]> listOfIntArrays = listOfLists.get(0);
int[] array = listOfIntArrays.get(0);
int element = array[0];


在上面的代码里,java.util.List<E>和java.util.Map<K,V>的get()方法返回一个类型为E的list元素或者一个类型为V的map元素。注意,无论如何,泛型类型能够更精密的使用他们的变量。在本书中的参考章节查看List<E>,您将会看到它的iterator( )方法被声明为返回一个Iterator<E>。这意味着,这个方法返回一个跟list的实际的参数类型一样的一个参数类型的实例。为了具体的说明这点,下面的例子提供了不使用get(0)方法来获取一个List<String>的第一个元素的方法。

List<String> words = // ...initialized elsewhere...
Iterator<String> iterator = words.iterator();
String firstword = iterator.next();


理解泛型类型

本段将对泛型类型的使用细节做进一步的探讨,以尝试说明下列问题:
不带类型参数的使用泛型的后果
参数化类型的体系
一个关于编译期泛型类型的类型安全的漏洞和一个用于确保运行期类型安全的补丁
为什么参数化类型的数组不是类型安全的

未经处理的类型和不被检查的警告
即使被重写的Java集合类带来了泛型的好处,在使用他们的时候您也不被要求说明类型变量。一个不带类型变量的泛型类型被认为是一个未经处理的类型(raw type)。这样,5.0版本以前的java代码仍然能够运行:您显式的编写所有类型转换就像您已经这样写的一样,您可能会被一些来自编译器的麻烦所困扰。查看下列存储不同类型的对象到一个未经处理的List:

List l = new ArrayList();
l.add("hello");  
l.add(new Integer(123));
Object o = l.get(0);


这段代码在java1.4下运行得很好。如果您用java5.0来编译它,javac编译了,但是会打印出这样的“抱怨”:

Note: Test.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.


如果我们加入-Xlint参数后重新编译,我们会看到这些警告:

Test.java:6: warning: [unchecked]
    unchecked call to add(E) as a member of the raw type java.util.List
        l.add("hello");  
         ^
Test.java:7: warning: [unchecked]
    unchecked call to add(E) as a member of the raw type java.util.List
        l.add(new Integer(123));

         ^
编译在add()方法的调用上给出了警告,因为它不能够确信加入到list中的值具有正确的类型。它告诉我们说我们使用了一个未经处理的类型,它不能验证我们的代码是类型安全的。注意,get()方法的调用是没有问题的,因为能够被获得的元素已经安全的存在于list中了。

如果您不想使用任何的java5.0的新特性,您可以简单的通过带-source1.4标记来编译他们,这样编译器就不会再“抱怨”了。如果您不能这样做,您可以忽略这些警告,通过使用一个“@SuppressWarnings("unchecked")”注解(查看本章的4.3节)隐瞒这些警告信息或者升级您的代码,加入类型变量描述。[2]下列示例代码,编译的时候不再会有警告但仍然允许您往list中放入不同的类型的对象。

List<Object> l = new ArrayList<Object>();
l.add("hello");  
l.add(123);              // autoboxing
Object o = l.get(0);


参数化类型的体系

参数化类型有类型体系,就像一般的类型一样。这个体系基于对象的类型,而不是变量的类型。这里有些例子您可以尝试:

ArrayList<Integer> l = new ArrayList<Integer>();
List<Integer> m = l;                            // okay
Collection<Integer> n = l;                      // okay
ArrayList<Number> o = l;                        // error
Collection<Object> p = (Collection<Object>)l;   // error, even with cast


一个List<Integer>是一个Collection<Integer>,但不是一个List<Object>。这句话不容易理解,如果您想理解为什么泛型这样做,这段值得看一下。考察这段代码:

List<Integer> li = new ArrayList<Integer>();
li.add(123);

// The line below will not compile.  But for the purposes of this
// thought-experiment, assume that it does compile and see how much
// trouble we get ourselves into.
List<Object> lo = li;  

// Now we can retrieve elements of the list as Object instead of Integer
Object number = lo.get(0);

// But what about this?
lo.add("hello world");

// If the line above is allowed then the line below throws ClassCastException
Integer i = li.get(1);  // Can't cast a String to Integer!


这就是为什么List<Integer>不是一个List<Object>的原因,虽然List<Integer>中所有的元素事实上是一个Object的实例。如果允许转换成List<Object>,那么转换后,理论上非整型的对象也将被允许添加到list中。

运行时类型安全

就像我们所见到的,一个List<X>不允许被转换为一个List<Y>,即使这个X能够被转换为Y。然而,一个List<X>能够被转换为一个List,这样您就可以通过继承的方法来做这样的事情。
这种将参数化类型转换为非参数化类型的能力对于向下兼容是必要的,但是它会在泛型所带来的类型安全体系上凿个漏洞:

// Here's a basic parameterized list.
List<Integer> li = new ArrayList<Integer>();

// It is legal to assign a parameterized type to a nonparameterized variable
List l = li;  

// This line is a bug, but it compiles and runs.
// The Java 5.0 compiler will issue an unchecked warning about it.
// If it appeared as part of a legacy class compiled with Java 1.4, however,
// then we'd never even get the warning.  
l.add("hello");

// This line compiles without warning but throws ClassCastException at runtime.
// Note that the failure can occur far away from the actual bug.
Integer i = li.get(0);


泛型仅提供了编译期的类型安全。如果您使用java5.0的编译器来编译您的代码并且没有得到任何警告,这些编译器的检查能够确保您的代码在运行期也是类型安全的。如果您获得了警告或者使用了像未经处理的类型那样修改您的集合的代码,那么您需要增加一些步骤来确保运行期的类型安全。您可以通过使用java.util.Collections中的checkedList()和checkedMap( )方法来做到这一步。这些方法将把您的集合打包成一个wrapper集合,从而在运行时检查确认只有正确类型的值能够被置入集合众。下面是一个能够补上类型安全漏洞的一个例子:

// Here's a basic parameterized list.
List<Integer> li = new ArrayList<Integer>();

// Wrap it for runtime type safety
List<Integer> cli = Collections.checkedList(li, Integer.class);

// Now widen the checked list to the raw type
List l = cli;  

// This line compiles but fails at runtime with a ClassCastException.
// The exception occurs exactly where the bug is, rather than far away
l.add("hello");


参数化类型的数组

在使用泛型类型的时候,数组需要特别的考虑。回忆一下,如果T是S的父类(或者接口),那么类型为S的数组S[],同时又是类型为T的数组T[]。正因为如此,每次您存放一个对象到数组中时,Java解释器都必须进行检查以确保您放入的对象类型与要存放的数组所允许的类型是匹对的。例如,下列代码在运行期会检查失败,抛出一个ArrayStoreException异常:

String[] words = new String[10];
Object[] objs = words;
objs[0] = 1;  // 1 autoboxed to an Integer, throws ArrayStoreException


虽然编译时obj是一个Object[],但是在运行时它是一个String[],它不允许被用于存放一个Integer.
当我们使用泛型类型的时候,仅仅依靠运行时的数组存放异常检查是不够的,因为一个运行时进行的检查并不能够获取编译时的类型参数信息。查看下列代码:

List<String>[] wordlists = new ArrayList<String>[10];
ArrayList<Integer> ali = new ArrayList<Integer>();
ali.add(123);
Object[] objs = wordlists;
objs[0] = ali;                       // No ArrayStoreException
String s = wordlists[0].get(0);      // ClassCastException!


如果上面的代码被允许,那么运行时的数组存储检查将会成功:没有编译时的类型参数,代码简单地存储一个ArrayList到一个ArrayList[]数组,非常正确。既然编译器不能阻止您通过这个方法来战胜类型安全,那么它转而阻止您创建一个参数化类型的数组。所以上述情节永远不会发生,编译器在第一行就开始拒绝编译了。

注意这并不是一个在使用数组时使用泛型的全部的约束,这仅仅是一个创建一个参数化类型数组的约束。我们将在学习如何写泛型方法时再来讨论这个话题。


类型参数通配符
假设我们需要写一个方法来显示一个List中的元素。[3]在以前,我们只需要象这样写段代码:

public static void printList(PrintWriter out, List list) {
    for(int i=0, n=list.size(); i < n; i++) {
        if (i > 0) out.print(", ");
        out.print(list.get(i).toString());
    }
}


在Java5.0中,List是一个泛型类型,如果我们试图编译这个方法,我们将会得到unchecked警告。为了解决这些警告,您可能需要这样来修改这个方法:

public static void printList(PrintWriter out, List<Object> list) {
    for(int i=0, n=list.size(); i < n; i++) {
        if (i > 0) out.print(", ");
        out.print(list.get(i).toString());
    }
}


这段代码能够编译通过同时不会有警告,但是它并不是非常地有效,因为只有那些被声明为List<Object>的list才会被允许使用这个方法。还记得么,类似于List<String>和List<Integer>这样的List并不能被转型为List<Object>。事实上我们需要一个类型安全的printList()方法,它能够接受我们传入的任何List,而不关心它被参数化为什么。解决办法是使用类型参数通配符。方法可以被修改成这样:

public static void printList(PrintWriter out, List<?> list) {
    for(int i=0, n=list.size(); i < n; i++) {
        if (i > 0) out.print(", ");
        Object o = list.get(i);
        out.print(o.toString());
    }
}


这个版本的方法能够被编译过,没有警告,而且能够在任何我们希望使用的地方使用。通配符“?”表示一个未知类型,类型List<?>被读作“List of unknown”
作为一般原则,如果类型是泛型的,同时您并不知道或者并不关心值的类型,您应该使用“?”通配符来代替一个未经处理的类型。未经处理的类型被允许仅是为了向下兼容,而且应该只能够被允许出现在老的代码中。注意,无论如何,您不能在调用构造器时使用通配符。下面的代码是非法的:
List<?> l = new ArrayList<?>();

创建一个不知道类型的List是毫无道理的。如果您创建了它,那么您必须知道它将保持的元素是什么类型的。您可以在随后的方法中不关心元素类型而去遍历这里list,但是您需要在您创建它的时候描述元素的类型。如果你确实需要一个List来保持任何类型,那么您只能这么写:

List<Object> l = new ArrayList<Object>();


从上面的printList()例子中,必须要搞清楚List<?>既不是List<Object>也不是一个未经处理的List。一个使用通配符的List<?>有两个重要的特性。第一,考察类似于get()的方法,他们被声明返回一个值,这个值的类型是类型参数中指定的。在这个例子中,类型是“unknown”,所以这些方法返回一个Object。既然我们期望的是调用这个object的toString()方法,程序能够很好的满足我们的意愿。

第二,考察List的类似add()的方法,他们被声明为接受一个参数,这个参数被类型参数所定义。出人意料的是,当类型参数是未确定的,编译器不允许您调用任何有不确定参数类型的方法——因为它不能确认您传入了一个恰当的值。一个List(?)实际上是只读的——既然编译器不允许我们调用类似于add(),set(),addAll()这类的方法。

界定通配符
让我们在我们原来的例子上作些小小的稍微复杂一点的改动。假设我们希望写一个sumList()方法来计算list中Number类型的值的合计。在以前,我们使用未经处理的List,但是我们不想放弃类型安全,同时不得不处理来自编译器的unchecked警告。或者我们可以使用List<Number>,那样的话我们就不能调用List<Integer>、List<Double>中的方法了,而事实上我们需要调用。如果我们使用通配符,那么我们实际上不能得到我们期望的类型安全,我们不能确定我们的方法被什么样的List所调用,Number?还是Number的子类?甚至,String?这样的一个方法也许会被写成这样:

public static double sumList(List<?> list) {
    double total = 0.0;
    for(Object o : list) {
        Number n = (Number) o;  // A cast is required and may fail
        total += n.doubleValue();
    }
    return total;
}


要修改这个方法让它变得真正的类型安全,我们需要使用界定通配符(bounded wildcard),能够确保List的类型参数是未知的,但又是Number或者Number的子类。下面的代码才是我们想要的:

public static double sumList(List<? extends Number> list) {
    double total = 0.0;
    for(Number n : list) total += n.doubleValue();
    return total;
}


类型List<? extends Number>可以被理解为“Number未知子类的List”。理解这点非常重要,在这段文字中,Number被认为是其自身的子类。

注意,这样的话,那些类型转换已经不再需要了。我们并不知道list中元素的具体类型,但是我们知道他们能够向上转型为Number,因此我们可以把他们从list中把他们当作一个Number对象取出。使用一个for/in循环能够稍微封装一下从list中取出元素的过程。普遍性的原则是当您使用一个界定通配符时,类似于List中的get()方法的那些方法将返回一个类型为上界的值。因此如果我们在for/in循环中调用list.get(),我们将得到一个Number。在前一节说到使用通配符时类似于list.add()这种方法中的限制依然有效:举个例子来说,如果编译器允许我们调用这类方法,我们就可以将一个Integer放到一个声明为仅保持Short值的list中去。

同样可行的是使用下界通配符,不同的是用super替换extends。这个技巧在被调用的方法上有一点不同的作用。在实际应用中,下界通配符要比上界通配符用得少。我们将在后面的章节里讨论这个问题。


脚注

[1] 在本章中,我会坚持用术语”泛型类型”来指一个声明一个或多个类型变量的类型,用”参数化的类型”来指由实际类型参数来替换其类型变量的泛型类型。然而,在一般情况下,这种区别并不明显,并且这些术语有时通用。
[2] 在撰写本文时候,javac并不支持@SuppressWarnings 的注解。期望在Java 5.1中得到支持。
[3] 本节所示的3个printList()方法忽略了这样一个事实,即java.util 中List的所有实现类都有一个可用的toString()方法。还要注意这些方法假定List实现RandomAccess并在LinkedList实例中只提供了很差的运行效率。
David Flanagan是众多O'Reilly书籍的作者。这些书包括《Java in a Nutshell》,《Java Examples in a Nutshell》,《Java Foundation Classes in a Nutshell》,《JavaScript: The Definitive Guide》,《JavaScript Pocket Reference》。

在Java in a Nutshell, 5th Edition中查看目录信息。

posted @ 2009-12-14 22:53 ammay 阅读(384) | 评论 (0)编辑 收藏

泛型

泛型是 Sun 公司发布的 JDK 5.0 中的一个重要特性,它的最大优点是提供了程序的类型安全同可以向后兼容。为了帮助读者更好地理解和使用泛型,本文通过一些示例从基本原理,重要概念,关键技术,以及相似技术比较等多个角度对 Java 语言中的泛型技术进行了介绍,重点强调了泛型中的一些基本但又不是很好理解的概念。

为了避免和 C++ 中的模板混淆,本文简要介绍了 Java 中的泛型和 C++ 中的模板的主要区别,希望这种比较能够帮助读者加深对泛型的理解。

引言

很多 Java 程序员都使用过集合(Collection),集合中元素的类型是多种多样的,例如,有些集合中的元素是 Byte 类型的,而有些则可能是 String 类型的,等等。Java 语言之所以支持这么多种类的集合,是因为它允许程序员构建一个元素类型为 Object 的 Collection,所以其中的元素可以是任何类型。

当使用 Collection 时,我们经常要做的一件事情就是要进行类型转换,当转换成所需的类型以后,再对它们进行处理。很明显,这种设计给编程人员带来了极大的不便,同时也容易引入错误。

在很多 Java 应用中,上述情况非常普遍,为了解决这个问题,使 Java 语言变得更加安全好用,近些年的一些编译器对 Java 语言进行了扩充,使 Java 语言支持了"泛型",特别是 Sun 公司发布的 JDK 5.0 更是将泛型作为其中一个重要的特性加以推广。

本文首先对泛型的基本概念和特点进行简单介绍,然后通过引入几个实例来讨论带有泛型的类,泛型中的子类型,以及范化方法和受限类型参数等重要概念。为了帮助读者更加深刻的理解并使用泛型,本文还介绍了泛型的转化,即,如何将带有泛型的 Java 程序转化成一般的没有泛型的 Java 程序。这样,读者对泛型的理解就不会仅仅局限在表面上了。考虑到多数读者仅仅是使用泛型,因此本文并未介绍泛型在编译器中的具体实现。Java 中的泛型和 C++ 中的模板表面上非常相似,但实际上二者还是有很大区别的,本文最后简单介绍了 Java 中的泛型与 C++ 模板的主要区别。





回页首


泛型概览

泛型本质上是提供类型的"类型参数",它们也被称为参数化类型(parameterized type)或参量多态(parametric polymorphism)。其实泛型思想并不是 Java 最先引入的,C++ 中的模板就是一个运用泛型的例子。

GJ(Generic Java)是对 Java 语言的一种扩展,是一种带有参数化类型的 Java 语言。用 GJ 编写的程序看起来和普通的 Java 程序基本相同,只不过多了一些参数化的类型同时少了一些类型转换。实际上,这些 GJ 程序也是首先被转化成一般的不带泛型的 Java 程序后再进行处理的,编译器自动完成了从 Generic Java 到普通 Java 的翻译。具体的转化过程大致分为以下几个部分:

  • 将参数化类型中的类型参数"擦除"(erasure)掉;
  • 将类型变量用"上限(upper bound)"取代,通常情况下这些上限是 Object。这里的类型变量是指实例域,本地方法域,方法参数以及方法返回值中用来标记类型信息的"变量",例如:实例域中的变量声明 A elem;,方法声明 Node (A elem){};,其中,A 用来标记 elem 的类型,它就是类型变量。
  • 添加类型转换并插入"桥方法"(bridge method),以便覆盖(overridden)可以正常的工作。

转化后的程序和没有引入泛型时程序员不得不手工完成转换的程序是非常一致的,具体的转化过程会在后面介绍。GJ 保持了和 Java 语言以及 Java 虚拟机很好的兼容性,下面对 GJ 的特点做一个简要的总结。

  • 类型安全。 泛型的一个主要目标就是提高 Java 程序的类型安全。使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果没有泛型,那么类型的安全性主要由程序员来把握,这显然不如带有泛型的程序安全性高。
  • 消除强制类型转换。泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会。
  • 向后兼容。支持泛型的 Java 编译器(例如 JDK5.0 中的 Javac)可以用来编译经过泛型扩充的 Java 程序(GJ 程序),但是现有的没有使用泛型扩充的 Java 程序仍然可以用这些编译器来编译。
  • 层次清晰,恪守规范。无论被编译的源程序是否使用泛型扩充,编译生成的字节码均可被虚拟机接受并执行。也就是说不管编译器的输入是 GJ 程序,还是一般的 Java 程序,经过编译后的字节码都严格遵循《Java 虚拟机规范》中对字节码的要求。可见,泛型主要是在编译器层面实现的,它对于 Java 虚拟机是透明的。
  • 性能收益。目前来讲,用 GJ 编写的代码和一般的 Java 代码在效率上是非常接近的。 但是由于泛型会给 Java 编译器和虚拟机带来更多的类型信息,因此利用这些信息对 Java 程序做进一步优化将成为可能。

以上是泛型的一些主要特点,下面通过几个相关的例子来对 Java 语言中的泛型进行说明。





回页首


带有泛型的类

为了帮助大家更好地理解 Java 语言中的泛型,我们在这里先来对比两段实现相同功能的 GJ 代码和 Java 代码。通过观察它们的不同点来对 Java 中的泛型有个总体的把握,首先来分析一下不带泛型的 Java 代码,程序如下:


1	interface Collection {
            2	   	public void add (Object x);
            3		public Iterator iterator ();
            4	}
            5
            6	interface Iterator {
            7	   	public Object next ();
            8		public boolean hasNext ();
            9	}
            10
            11	class NoSuchElementException extends RuntimeException {}
            12
            13	class LinkedList implements Collection {
            14
            15		protected class Node {
            16		   	Object elt;
            17			Node next = null;
            18			Node (Object elt) { this.elt = elt; }
            19		}
            20
            21		protected Node head = null, tail = null;
            22
            23		public LinkedList () {}
            24
            25		public void add (Object elt) {
            26			if (head == null) { head = new Node(elt); tail = head; }
            27			else { tail.next = new Node(elt); tail = tail.next; }
            28		}
            29
            30		public Iterator iterator () {
            31
            32			return new Iterator () {
            33				protected Node ptr = head;
            34				public boolean hasNext () { return ptr != null; }
            35				public Object next () {
            36					if (ptr != null) {
            37						Object elt = ptr.elt; ptr = ptr.next; return elt;
            |-------10--------20--------30--------40--------50--------60--------70--------80--------9|
            |-------- XML error:  The previous line is longer than the max of 90 characters ---------|
            38					} else throw new NoSuchElementException ();
            39				}
            40			};
            41		}
            42	}
            

接口 Collection 提供了两个方法,即添加元素的方法 add(Object x),见第 2 行,以及返回该 CollectionIterator 实例的方法 iterator(),见第 3 行。Iterator 接口也提供了两个方法,其一就是判断是否有下一个元素的方法 hasNext(),见第 8 行,另外就是返回下一个元素的方法 next(),见第 7 行。LinkedList 类是对接口 Collection 的实现,它是一个含有一系列节点的链表,节点中的数据类型是 Object,这样就可以创建任意类型的节点了,比如 Byte, String 等等。上面这段程序就是用没有泛型的传统的 Java 语言编写的代码。接下来我们分析一下传统的 Java 语言是如何使用这个类的。

代码如下:


1	class Test {
            2		    public static void main (String[] args) {
            3			// byte list
            4			LinkedList xs = new LinkedList();
            5			xs.add(new Byte(0)); xs.add(new Byte(1));
            6			Byte x = (Byte)xs.iterator().next();
            7			// string list
            8			LinkedList ys = new LinkedList();
            9			ys.add("zero"); ys.add("one");
            10			String y = (String)ys.iterator().next();
            11			// string list list
            12			LinkedList zss = new LinkedList();
            13			zss.add(ys);
            14			String z = (String)((LinkedList)zss.iterator().next()).iterator().next();
            |-------10--------20--------30--------40--------50--------60--------70--------80--------9|
            |-------- XML error:  The previous line is longer than the max of 90 characters ---------|
            15			// string list treated as byte list
            16			Byte w = (Byte)ys.iterator().next(); // run-time exception
            17			}
            18	}
            

从上面的程序我们可以看出,当从一个链表中提取元素时需要进行类型转换,这些都要由程序员显式地完成。如果我们不小心从 String 类型的链表中试图提取一个 Byte 型的元素,见第 15 到第 16 行的代码,那么这将会抛出一个运行时的异常。请注意,上面这段程序可以顺利地经过编译,不会产生任何编译时的错误,因为编译器并不做类型检查,这种检查是在运行时进行的。不难发现,传统 Java 语言的这一缺陷推迟了发现程序中错误的时间,从软件工程的角度来看,这对软件的开发是非常不利的。接下来,我们讨论一下如何用 GJ 来实现同样功能的程序。源程序如下:


1	interface Collection<A> {
            2		public void add(A x);
            3		public Iterator<A> iterator();
            4	}
            5
            6	interface Iterator<A> {
            7		public A next();
            8		public boolean hasNext();
            9	}
            10
            11	class NoSuchElementException extends RuntimeException {}
            12
            13	class LinkedList<A> implements Collection<A> {
            14		protected class Node {
            15			A elt;
            16			Node next = null;
            17			Node (A elt) { this.elt = elt; }
            18		}
            19
            20		protected Node head = null, tail = null;
            21
            22		public LinkedList () {}
            23
            24		public void add (A elt) {
            25			if (head == null) { head = new Node(elt); tail = head; }
            26			else { tail.next = new Node(elt); tail = tail.next; }
            27		}
            28
            29		public Iterator<A> iterator () {
            30			return new Iterator<A> () {
            31				protected Node ptr = head;
            32				public boolean hasNext () { return ptr != null; }
            33				public A next () {
            34					if (ptr != null) {
            35						A elt = ptr.elt; ptr = ptr.next; return elt;
            |-------10--------20--------30--------40--------50--------60--------70--------80--------9|
            |-------- XML error:  The previous line is longer than the max of 90 characters ---------|
            36					} else throw new NoSuchElementException ();
            37				}
            38			};
            39	 	}
            40	}
            

程序的功能并没有任何改变,只是在实现方式上使用了泛型技术。我们注意到上面程序的接口和类均带有一个类型参数 A,它被包含在一对尖括号(< >)中,见第 1,6 和 13 行,这种表示法遵循了 C++ 中模板的表示习惯。这部分程序和上面程序的主要区别就是在 Collection, Iterator, 或 LinkedList 出现的地方均用 Collection<A>, Iterator<A>, 或 LinkedList<A> 来代替,当然,第 22 行对构造函数的声明除外。

下面再来分析一下在 GJ 中是如何对这个类进行操作的,程序如下:


1	class Test {
            2		public static void main (String [] args) {
            3			// byte list
            4			LinkedList<Byte> xs = new LinkedList<Byte>();
            5			xs.add(new Byte(0)); xs.add(new Byte(1));
            6			Byte x = xs.iterator().next();
            7			// string list
            8			LinkedList<String> ys = new LinkedList<String>();
            9			ys.add("zero"); ys.add("one");
            10			String y = ys.iterator().next();
            11			// string list list
            12			LinkedList<LinkedList<String>>zss=
            newLinkedList<LinkedList<String>>();
            13			zss.add(ys);
            14			String z = zss.iterator().next().iterator().next();
            15			// string list treated as byte list
            16			Byte w = ys.iterator().next(); // compile-time error
            17		}
            18	}
            

在这里我们可以看到,有了泛型以后,程序员并不需要进行显式的类型转换,只要赋予一个参数化的类型即可,见第 4,8 和 12 行,这是非常方便的,同时也不会因为忘记进行类型转换而产生错误。另外需要注意的就是当试图从一个字符串类型的链表里提取出一个元素,然后将它赋值给一个 Byte 型的变量时,见第 16 行,编译器将会在编译时报出错误,而不是由虚拟机在运行时报错,这是因为编译器会在编译时刻对 GJ 代码进行类型检查,此种机制有利于尽早地发现并改正错误。

类型参数的作用域是定义这个类型参数的整个类,但是不包括静态成员函数。这是因为当访问同一个静态成员函数时,同一个类的不同实例可能有不同的类型参数,所以上述提到的那个作用域不应该包括这些静态函数,否则就会引起混乱。





回页首


泛型中的子类型

在 Java 语言中,我们可以将某种类型的变量赋值给其父类型所对应的变量,例如,String 是 Object 的子类型,因此,我们可以将 String 类型的变量赋值给 Object 类型的变量,甚至可以将 String [ ] 类型的变量(数组)赋值给 Object [ ] 类型的变量,即 String [ ] 是 Object [ ] 的子类型。

上述情形恐怕已经深深地印在了广大读者的脑中,对于泛型来讲,上述情形有所变化,因此请广大读者务必引起注意。为了说明这种不同,我们还是先来分析一个小例子,代码如下所示:


1			List<String> ls = new ArrayList<String>();
            2			List<Object> lo = ls;
            3	lo.add(new Integer());
            4	String s = ls.get(0);
            

上述代码的第二行将 List<String> 赋值给了 List<Object>,按照以往的经验,这种赋值好像是正确的,因为 List<String> 应该是 List<Object> 的子类型。这里需要特别注意的是,这种赋值在泛型当中是不允许的!List<String> 也不是 List<Object> 的子类型。

如果上述赋值是合理的,那么上面代码的第三行的操作将是可行的,因为 loList<Object>,所以向其添加 Integer 类型的元素应该是完全合法的。读到此处,我们已经看到了第二行的这种赋值所潜在的危险,它破坏了泛型所带来的类型安全性。

一般情况下,如果 A 是 B 的子类型,C 是某个泛型的声明,那么 C<A> 并不是 C<B> 的子类型,我们也不能将 C<A> 类型的变量赋值给 C<B> 类型的变量。这一点和我们以前接触的父子类型关系有很大的出入,因此请读者务必引起注意。





回页首


泛化方法和受限类型参数

在这一部分我们将讨论有关泛化方法(generic method )和受限类型参数(bounded type parameter)的内容,这是泛型中的两个重要概念,还是先来分析一下与此相关的代码。


1	interface Comparable<A> {
            2		public int compareTo(A that);
            3	}
            4
            5	class Byte implements Comparable<Byte> {
            6		private byte value;
            7		public Byte(byte value) {this.value = value;}
            8		public byte byteValue() {return value;}
            9		public int compareTo(Byte that) {
            10			return this.value - that.value;
            11		}
            12	}
            13
            14	class Collections {
            15		public static <A implements Comparable<A>>
            16	            A max (Collection<A> xs) {
            17			    	Iterator<A> xi = xs.iterator();
            18			    	A w = xi.next();
            19			    	while (xi.hasNext()) {
            20						A x = xi.next();
            21						if (w.compareTo(x) < 0) w = x;
            22					}
            23					return w;
            24		}
            25	}
            

这里定义了一个接口 Comparable<A>,用来和 A 类型的对象进行比较。类 Byte 实现了这个接口,并以它自己作为类型参数,因此,它们自己就可以和自己进行比较了。

第 14 行到第 25 行的代码定义了一个类 Collections,这个类包含一个静态方法 max(Collection<A> xs),它用来在一个非空的 Collection 中寻找最大的元素并返回这个元素。这个方法的两个特点就是它是一个泛化方法并且有一个受限类型参数。

之所以说它是泛化了的方法,是因为这个方法可以应用到很多种类型上。当要将一个方法声明为泛化方法时,我们只需要在这个方法的返回类型(A)之前加上一个类型参数(A),并用尖括号(< >)将它括起来。这里的类型参数(A)是在方法被调用时自动实例化的。例如,假设对象 m 的类型是 Collection<Byte>,那么当使用下面的语句:


Byte x = Collections.max(m);
            

调用方法 max 时,该方法的参数 A 将被推测为 Byte。

根据上面讨论的内容,泛化方法 max 的完整声明应该是下面的形式:


            < A >  A max (Collection<A> xs) {
            max 的方法体
            }
            

但是,我们见到的 max 在 < A > 中还多了 "implements Comparable<A>" 一项,这是什么呢?这就是我们下面将要谈到的"受限的类型参数"。在上面的例子中,类型参数 A 就是一个受限的的类型参数,因为它不是泛指任何类型,而是指那些自己和自己作比较的类型。例如参数可以被实例化为 Byte,因为程序中有 Byte implements Comparable<Byte> 的语句,参见第 5 行。这种限制(或者说是范围)通过如下的方式表示,"类型参数 implements 接口",或是 "类型参数 extend 类",上面程序中的"Byte implements Comparable<Byte>"就是一例。





回页首


泛型的转化

在前面的几部分内容当中,我们介绍了有关泛型的基础知识,到此读者对 Java 中的泛型技术应该有了一定的了解,接下来的这部分内容将讨论有关泛型的转化,即如何将带有泛型的 Java 代码转化成一般的没有泛型 Java 代码。其实在前面的部分里,我们或多或少地也提到了一些相关的内容,下面再来详细地介绍一下。

首先需要明确的一点是上面所讲的这种转化过程是由编译器(例如:Javac)完成的,虚拟机并不负责完成这一任务。当编译器对带有泛型的 Java 代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种字节码可以被一般的Java虚拟机接收并执行,这种技术被称为擦除(erasure)。

可见,编译器可以在对源程序(带有泛型的 Java 代码)进行编译时使用泛型类型信息保证类型安全,对大量如果没有泛型就不会去验证的类型安全约束进行验证,同时在生成的字节码当中,将这些类型信息清除掉。

对于不同的情况,擦除技术所执行的"擦除"动作是不同的,主要分为以下几种情况:

  • 对于参数化类型,需要删除其中的类型参数,例如,LinkedList<A> 将被"擦除"为 LinkedList;
  • 对于非参数化类型,不作擦除,或者说用它自己来擦除自己,例如 String 将被"擦除"为 String;
  • 对于类型变量(有关类型变量的说明请参考"泛型概览"相关内容),要用它们的上限来对它们进行替换。多数情况下这些上限是 Object,但是也有例外,后面的部分将会对此进行介绍。

除此之外,还需要注意的一点是,在某些情况下,擦除技术需要引入类型转换(cast),这些情况主要包括:

情况 1. 方法的返回类型是类型参数;

情况 2. 在访问数据域时,域的类型是一个类型参数。

例如在本文"带有泛型的类"一小节的最后,我们给出了一段测试程序,一个 Test 类。这个类包含以下几行代码:


8			LinkedList<String> ys = new LinkedList<String>();
            9			ys.add("zero"); ys.add("one");
            10			String y = ys.iterator().next();
            这部分代码转换后就变成了如下的代码:
            8	       	LinkedList ys = new LinkedList();
            9           ys.add("zero"); ys.add("one");
            10	String y = (String)ys.iterator().next();
            

第 10 行的代码进行了类型转换,这是因为在调用 next() 方法时,编译器发现该方法的返回值类型是类型参数 A(请参见对方法 next() 的定义),因此根据上面提到的情况 1,需要进行类型转换。

上面介绍了泛型转化中的擦除技术,接下来,我们讨论一下泛型转化中的另外一个重要问题--桥方法(bridge method)。

Java 是一种面向对象的语言,因此覆盖(overridden)是其中的一项重要技术。覆盖能够正常"工作"的前提是方法名和方法的参数类型及个数完全匹配(参数的顺序也应一致),为了满足这项要求,编译器在泛型转化中引入了桥方法(bridge method)。接下来,我们通过一个例子来分析一下桥方法在泛型转化中所起的作用。在本文"泛化方法和受限类型参数"一小节所给出的代码中,第 9 行到第 11 行的程序如下所示:


    9		public int compareTo(Byte that) {
            10			return this.value - that.value;
            11		}
            这部分代码经过转化,就变成了下面的样子:
            9		public int compareTo(Byte that) {
            10			return this.value - that.value;
            11		}
            12  public int compareTo(Object that){
            13			return this.compareTo((Byte)that);
            14		}
            

第 12 行的方法 compareTo(Object that) 就是一个桥方法,在这里引入这个方法是为了保证覆盖能够正常的发生。我们在前面提到过,覆盖必须保证方法名和参数的类型及数目完全匹配,在这里通过引入这个"桥"即可达到这一目的,由这个"桥"进行类型转换,并调用第 9 行参数类型为 Byte 的方法 compareTo(Byte that),需要注意的一点是这里的 "Object" 也并不一定是完全匹配的类型,但由于它是 Java 语言中类层次结构的根,所以这里用 "Object" 可以接受其他任何类型的参数。

根据面向对象的基本概念,我们知道,重载(overloading)允许桥方法和原来的方法共享同一个方法名,正如上面例子所显示的那样,因此桥方法的引入是完全合法的。一般情况下,当一个类实现了一个参数化的接口或是继承了一个参数化的类时,需要引入桥方法。

到此,我们对泛型中的子类型,带有泛型的类,泛化方法,受限类型参数以及泛型的转化进行了简要的介绍,下面部分将结合这些技术对前面提到的例子进行一下总结,以便能够帮助读者更深刻更全面地理解泛型。

首先来分析一下本文提到的那个 Collection 的例子。这里先是定义了两个接口 CollectionIterator,然后又定义了一个对接口 Collection 的一个实现 LinkedList。根据上面所介绍的对泛型的转化过程,这段代码转化后的 Java 程序为:


1	interface Collection {
            2	   	public void add (Object x);
            3	        public Iterator iterator ();
            4	}
            5
            6	interface Iterator {
            7	       	public Object next ();
            8	        public boolean hasNext ();
            9	}
            10
            11	class NoSuchElementException extends RuntimeException {}
            12
            13	class LinkedList implements Collection {
            14
            15		protected class Node {
            16	       	Object elt;
            17	        Node next = null;
            18	        Node (Object elt) { this.elt = elt; }
            19		}
            20
            21		protected Node head = null, tail = null;
            22
            23		public LinkedList () {}
            24
            25		public void add (Object elt) {
            26	       	if (head == null) {
            27		       	head = new Node(elt); tail = head;
            28			} else {
            29		       	tail.next = new Node(elt); tail = tail.next;
            30			}
            31		}
            32
            33		public Iterator iterator () {
            34	       	return new Iterator () {
            35		       	protected Node ptr = head;
            36	            public boolean hasNext () { return ptr != null; }
            37		       	public Object next () {
            38			       	if (ptr != null) {
            39				       	Object elt = ptr.elt; ptr = ptr.next; return elt;
            40					} else {
            41				       		throw new NoSuchElementException ();
            42						}
            43				}
            44			};
            45		}
            46	}
            

通过分析上述代码,我们不难发现,所有参数化类型 Collection, Iterator 和 LinkedList 中的类型参数 "A" 全都被擦除了。另外,剩下的类型变量 "A" 都用其上限进行了替换,这里的上限是 Object,见黑体字标出的部分,这是转化的关键部分。

下面我们分析一下在介绍有关泛化方法(generic method)和受限类型参数(bounded type parameter)时举的那个例子,该段 GJ 代码经过转换后的等价 Java 程序如下所示:


1	interface Comparable {
            2		public int compareTo(Object that);
            3	}
            4
            5	class Byte implements Comparable {
            6		private byte value;
            7		public Byte(byte value) {this.value = value;}
            8		public byte byteValue(){return value;}
            9		public int compareTo(Byte that) {
            10			return this.value - that.value;
            11		}
            12      public int compareTo(Object that){
            13			return this.compareTo((Byte)that);
            14		}
            15	}
            16
            17	class Collections {
            18		public static Comparable max(Collection xs){
            19			Iterator xi = xs.iterator();
            20			Comparable w = (Comparable)xi.next();
            21			while (xi.hasNext()) {
            22				Comparable x = (Comparable)xi.next();
            23				if (w.compareTo(x) < 0) w = x;
            23			}
            24			return w;
            25		}
            26	}
            

同样请读者注意黑体字标出的部分,这些关键点我们在前面已经介绍过了,故不赘述。唯一需要注意的一点就是第 18,20,22 行出现的Comparable。在泛型转化中,类型变量应该用其上限来替换,一般情况下这些上限是 "Object",但是当遇到受限的类型参数时,这个上限就不再是 "Object" 了,编译器会用限制这些类型参数的类型来替换它,上述代码就用了对 A 进行限制的类型 "Comparable" 来替换 A。

桥方法的引入,为解决覆盖问题带来了方便,但是这种方法还存在一些问题,例如下面这段代码:


1	interface Iterator<A> {
            2		public boolean hasNext ();
            3		public A next ();
            4	}
            5	class Interval implements Iterator<Integer> {
            6		private int i;
            7		private int n;
            8		public Interval (int l, int u) { i = l; n = u; }
            9		public boolean hasNext () { return (i <= n); }
            10		public Integer next () { return new Integer(i++); }
            11	}
            

根据以上所讲的内容,这部分代码转换后的 Java 程序应该是如下这个样子:


1	interface Iterator {
            2
            3		public boolean hasNext ();
            4		public Object next ();
            5
            6	}
            7
            8	class Interval implements Iterator {
            9
            10		private int i;
            11		private int n;
            12		public Interval (int l, int u) { i = l; n = u; }
            13		public boolean hasNext () { return (i <= n); }
            14		public Integer next%1% () { return new Integer(i++); }
            15		// bridge
            16		public Object next%2%() { return next%1%(); }
            17
            18	}
            

相信有些读者已经发现了这里的问题,这不是一段合法的 Java 源程序,因为第 14 行和第 16 行的两个 next() 有相同的参数,无法加以区分。代码中的 %1% 和 %2% 是为了区分而人为加入的,并非 GJ 转化的结果。

不过,这并不是什么太大的问题,因为 Java 虚拟机可以区分这两个 next() 方法,也就是说,从 Java 源程序的角度来看,上述程序是不正确的,但是当编译成字节码时,JVM 可以对两个 next() 方法进行识别。这是因为,在 JVM 中,方法定义时所使用的方法签名包括方法的返回类型,这样一来,只要 GJ 编译出的字节码符合Java字节码的规范即可,这也正好说明了 GJ 和 JVM 中字节码规范要求的一致性!

最后,值得一提的是,JDK 5.0 除了在编译器层面对 Java 中的泛型进行了支持,Java 的类库为支持泛型也做了相应地调整,例如,集合框架中所有的标准集合接口都进行了泛型化,同时,集合接口的实现也都进行了相应地泛型化。





回页首


Java 中的泛型与 C++ 模板的比较

GJ 程序的语法在表面上与 C++ 中的模板非常类似,但是二者之间有着本质的区别。

首先,Java 语言中的泛型不能接受基本类型作为类型参数――它只能接受引用类型。这意味着可以定义 List<Integer>,但是不可以定义 List<int>。

其次,在 C++ 模板中,编译器使用提供的类型参数来扩充模板,因此,为 List<A> 生成的 C++ 代码不同于为 List<B> 生成的代码,List<A> 和 List<B> 实际上是两个不同的类。而 Java 中的泛型则以不同的方式实现,编译器仅仅对这些类型参数进行擦除和替换。类型 ArrayList<Integer> 和 ArrayList<String> 的对象共享相同的类,并且只存在一个 ArrayList 类。





回页首


总结

本文通过一些示例从基本原理,重要概念,关键技术,以及相似技术比较等多个角度对 Java 语言中的泛型技术进行了介绍,希望这种介绍方法能够帮助读者更好地理解和使用泛型。本文主要针对广大的 Java 语言使用者,在介绍了泛型的基本概念后,重点介绍了比较底层的泛型转化技术,旨在帮助读者更加深刻地掌握泛型,笔者相信这部分内容可以使读者避免对泛型理解的表面化,也所谓知其然更知其所以然。



参考资料



关于作者

 

周晶,2006年4月毕业于北京航空航天大学计算机学院,获计算机硕士学位。主要研究领域为高级编译技术,Java虚拟机技术。 beyond.zhou@gmail.com



posted @ 2009-12-14 22:52 ammay 阅读(190) | 评论 (0)编辑 收藏

泛型

Jdk1.5中的新特性 --泛型 (详细版)

本来只转载了个链接,和一个简单的使用程序,但昨天不小心看到有人批判jdk1.5,先说java要强制转型不好的问题没解决,
容器不能放基类型不好,接着说泛型没用。而恰恰Jdk1.5中解决了这些问题,所以感叹之余,把这篇文章改一下,详细的说说泛型。

一,Java中的泛型:
在Java中能使用到泛型的多是容器类,如各种list map set,因为Java是单根继承,所以容器里边可以放的
内容是任何Object,所以从意义上讲原本的设计才是泛型。但用过Java的人是否感觉每次转型很麻烦呢?
而且会有些错误,比如一个容器内放入了异质对象,强制转型的时候会出现cast异常。而这中错误在编译器是
无从发现的。所以jdk1.5中提供了泛型,这个泛型其实是向c++靠拢了.好,我们先看几个实例再细说原理。

二,泛型的用法:(多个实例)

1 实例A
2 ArrayList < String >  strList  =   new  ArrayList < String > ();
3 strList.add( " 1 " );
4 strList.add( " 2 " );
5 strList.add( " 3 " );
6 // 关键点(1) 注意下边这行,没有强制转型
7 String str  =  strList.get( 1 );
8 // 关键点(2)然後我们加入,这个时候你会发现编译器报错,错误在编译器被发现,错误当然是发现的越早越好
9 strList.add( new  Object());


1 实例B
2 ArrayList < Integer >  iList  =   new  ArrayList < Integer > ();
3 // 关键点(3) 注意直接把整数放入了集合中,而没有用Integer包裹
4 iList.add( 1 );
5 iList.add( 2 );
6 iList.add( 3 );
7 // 关键点(4)同样直接取出就是int
8 int  num  =  iList.get( 1 );


1 实例C
2 // 关键点(5)展示一下key-value的时候要怎么写,同时key和value也可以是基本类型了。
3 HashMap < Integer,Integer >  map  =   new  HashMap < Integer,Integer > ();
4 map.put( 1 11 );
5 map.put( 2 22 );
6 map.put( 3 33 );
7 int  inum  =  map.get( 1 );
8


三,看完了实例了,详细来说说为什么吧
首先jdk1.5中的泛型,第一个解决的问题,就是Java中很多不必要的强制转型了,具体的实现,我们以ArrayList
为例,下边是ArrayList中的片断代码:

 1ArrayList类的定义,这里加入了<E>
 2public class ArrayList<E> extends AbstractList<E>
 3        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
 4
 5//get方法,返回不再是Object 而是E
 6public E get(int index) {
 7    RangeCheck(index);
 8    return elementData[index];
 9}

10//add方法,参数不再是Object 而是E
11public boolean add(E o) {
12    ensureCapacity(size + 1);  // Increments modCount!!
13    elementData[size++= o;
14    return true;
15}

16


四,Boxing 和UnBoxing
看到上边的关键点(3)和(4)是否感觉惊奇呢,因为Java中烦人的除了强制转型,另一个就是基础类型了
放入容器的时候要包装,取出了还要转回。Jdk1.5中解决了这个问题.如上边的使用方法

五,泛型的生命周期(使用注意事项)
如果我们试着把ArrayList<String> list的内容序列化,然後再读取出来,在使用的过程中会发现出错,
为什么呢?用Stream读取一下回来的数据,你会发现<String>不见了,list变成了普通的ArrayList,而不是
参数化型别的ArrayList了,为什么会这样呢 ?见下边的比较

六,C++的泛型和Java的泛型
在泛型的实现上,C++和Java有着很大的不同,
Java是擦拭法实现的
C++是膨胀法实现的
因为Java原本实现就是泛型的,现在加入型别,其实是"窄化",所以采用擦拭法,在实现上,其实是封装了原本的
ArrayList,这样的话,对于下边这些情况,Java的实现类只有一个。

1ArrayList<Integer>  .;   public class ArrayList
2ArrayList<String>  ..;   --同上--
3ArrayList<Double>  ..;   --同上--
4而C++采用的是膨胀法,对于上边的三种情况实际是每一种型别都对应一个实现,实现类有多个
5list<int> li;                class list; //int 版本
6list<string> ls;             class list; //string 版本
7list<double> ld;             class list; //double 版本    

这就造成了,在序列化后,Java不能分清楚原来的ArrayList是
ArrayList<Integer>还是ArrayList

七,题外话,在很多东西的实现上C++和Java有很多不同
例如运算符的问题i=i++问题,详细看这里
例如在C++中能很好实现的double-checked locking单态模式,在Java中几乎很难实现 详细看这里
还有就是上边提到的泛型实现上。

八,Jdk 1.5加入了不少新东西,有些能很大的提高开发质量,例如Jdk1.4 ,Jdk.15中StringBuffer的不同
因为从1。4转入1。5不久,所以慢慢会发一些在1。5的使用过程中发现的东西。

最后,我们还可以自己写类似ArrayList这样的泛型类,至于如何自定义泛型类,泛型方法请参见候捷先生的文章

posted @ 2009-12-14 22:51 ammay 阅读(168) | 评论 (0)编辑 收藏

导航

统计

常用链接

留言簿

随笔档案

文章分类

文章档案

搜索

最新评论

阅读排行榜

评论排行榜