42. 《CSDN 社区电子杂志——Java 杂志》
书写。例如:TestGen<String,String> t=new TestGen<String,String>();
3、 泛型中<K extends Object>,extends 并不代表继承,它是类型范围限制。
1.2. 泛型与数据类型转换
1.2.1. 消除类型转换
上面的例子大家看到什么了,数据类型转换的代码不见了。在以前我们经常要书写以下代码,
如:
//code list 5
import java.util.Hashtable;
class Test {
public static void main(String[] args) {
Hashtable h = new Hashtable();
h.put("key", "value");
String s = (String)h.get("key");
System.out.println(s);
}
}
这个我们做了类型转换,是不是感觉很烦的,并且强制类型转换会带来潜在的危险,系统可
能会抛一个 ClassCastException 异常信息。在 JDK5.0 中我们完全可以这么做,如:
//code list 6
import java.util.Hashtable;
class Test {
public static void main(String[] args) {
Hashtable<String,Integer> h = new Hashtable<String,Integer> ();
h.put("key", new Integer(123));
int s = h.get("key").intValue();
System.out.println(s);
}
}
这里我们使用泛化版本的 HashMap,这样就不用我们来编写类型转换的代码了,类型转换的过
程交给编译器来处理,是不是很方便,而且很安全。上面是 String 映射到 String,也可以将 Integer
映射为 String,只要写成 HashTable<Integer,String> h=new HashTable<Integer,String>();h.get(new
Integer(0))返回 value。果然很方便。
http://emag.csdn.net - 37 - 2005 年 1 月 1 期 总第 2 期
43. 《CSDN 社区电子杂志——Java 杂志》
1.2.2. 自动解包装与自动包装的功能
从上面有没有看到有点别扭啊,h.get(new Integer(123))这里的 new Integer(123);好烦的,在
JDK5.0 之前我们只能忍着了,现在这种问题已经解决了,请看下面这个方法。我们传入一个 int
这一基本型别,然后再将 i 的值直接添加到 List 中,其实 List 是不能储存基本型别的,List 中应该
存储对象,这里编译器将 int 包装成 Integer,然后添加到 List 中去。接着我们用 List.get(0);来检索
数据,并返回对象再将对象解包装成 int。恩,JDK5.0 给我们带来更多方便与安全。
//Code list 7
public void autoBoxingUnboxing(int i) {
ArrayList<Integer> L= new ArrayList<Integer>();
L.add(i);
int a = L.get(0);
System.out.println("The value of i is " + a);
}
1.2.3. 限制泛型中类型参数的范围
也许你已经发现在 code list 1 中的 TestGen<K,V>这个泛型类,其中 K,V 可以是任意的型别。也
许你有时候呢想限定一下 K 和 V 当然范围,怎么做呢?看看如下的代码:
//Code list 8
class TestGen2<K extents String,V extends Number>
{
private V v=null;
private K k=null;
public void setV(V v){
this.v=v;
}
public V getV(){
return this.v;
}
public void setK(K k){
this.k=k;
}
public V getK(){
return this.k;
}
public static void main(String[] args)
{
http://emag.csdn.net - 38 - 2005 年 1 月 1 期 总第 2 期
44. 《CSDN 社区电子杂志——Java 杂志》
TestGen2<String,Integer> t2=new TestGen2<String,Integer>();
t2.setK(new String("String"));
t2.setV(new Integer(123));
System.out.println(t2.getK());
System.out.println(t2.getV());
}
}
上边 K 的范围是<=String ,V 的范围是<=Number,注意是“<=”,对于 K 可以是 String 的,
V 当然也可以是 Number,也可以是 Integer,Float,Double,Byte 等。看看下图也许能直观些
图-1
A
A1 A2
A2 1 A2 2
请看上图 A 是上图类中的基类, A2 分别是 A 的子类, 有 2 个子类分别是 A2_1,
A1, A2 A2_2。
然后我们定义一个受限的泛型类 class MyGen<E extends A2>,这个泛型的范围就是上图中兰色部
分。
这个是单一的限制,你也可以对型别多重限制,如下:
class C<T extends Comparable<? super T> & Serializable>
我们来分析以下这句, extends Comparable 这个是对上限的限制,
T Comparable<? super
T>这个是下限的限制,Serializable 是第 2 个上限。一个指定的类型参数可以具有一个或多个
上限。具有多重限制的类型参数可以用于访问它的每个限制的方法和域。
1.2.4. 多态方法
//Code list 9
class TestGen {
<T extends Object> public static List<T> make(T first) {
return new List<T>(first);
}
}
http://emag.csdn.net - 39 - 2005 年 1 月 1 期 总第 2 期
57. 《CSDN 社区电子杂志——Java 杂志》
System.out.println((String) iter.next());
}
}
public void showAll (String[] sa) {
for (int i = 0; i < sa.length; i++) {
System.out.println(sa[i]);
}
}
这样的代码不仅显得臃肿,而且容易出错,我想我们大家在刚开始接触编程时,尤其是 C/C++
和 Java,可能多少都犯过以下类似错误的一种或几种:把 for 语句的三个表达式顺序弄错;第二个
表达式逻辑判断不正确(漏掉一些、多出一些、甚至死循环);忘记移动游标;在循环体内不小心
改变了游标的位置等等。为什么不能让编译器帮我们处理这些细节呢?在 5.0 中,我们可以这样写:
public void showAll (Collection c) {
for (Object obj : c) {
System.out.println((String) obj);
}
}
public void showAll (String[] sa) {
for (String str : sa) {
System.out.println(str);
}
}
这样的代码显得更加清晰和简洁,不是吗?具体的语法很简单:使用":"分隔开,前面的部分
写明从数组或 Collection 中将要取出的类型,以及使用的临时变量的名字,后面的部分写上数组或
者 Collection 的引用。加上泛型,我们甚至可以把第一个方法变得更加漂亮:
public void showAll (Collection<String> cs) {
for (String str : cs) {
System.out.println(str);
}
}
有没有发现:当你需要将 Collection<String>替换成 String[],你所需要做的仅仅是简单的把参
数类型"Collection<String>"替换成"String[]",反过来也是一样,你不完全需要改其他的东西。这在
J2SE(TM) 5.0 之前是无法想象的。
对于这个看上去相当方便的新语言元素,当你需要在循环体中访问游标的时候,会显得很别
扭:比方说,当我们处理一个链表,需要更新其中某一个元素,或者删除某个元素等等。这个时
http://emag.csdn.net - 52 - 2005 年 1 月 1 期 总第 2 期
58. 《CSDN 社区电子杂志——Java 杂志》
候,你无法在循环体内获得你需要的游标信息,于是需要回退到原先的做法。不过,有了泛型和
增强的 for 循环,我们在大多数情况下已经不用去操心那些烦人的 for 循环的表达式和嵌套了。毕
竟,我们大部分时间都不会需要去了解游标的具体位置,我们只需要遍历数组或 Collection,对吧?
1.6. 自动装箱/自动拆箱
所谓装箱,就是把值类型用它们相对应的引用类型包起来,使它们可以具有对象的特质,如
我们可以把 int 型包装成 Integer 类的对象,或者把 double 包装成 Double,等等。所谓拆箱,就是
跟装箱的方向相反,将 Integer 及 Double 这样的引用类型的对象重新简化为值类型的数据。
在 J2SE(TM) 5.0 发布之前,我们只能手工的处理装箱和拆箱。也许你会问,为什么需要装箱
和拆箱?比方说当我们试图将一个值类型的数据添加到一个 Collection 中时,就需要先把它装箱,
因为 Collection 的 add()方法只接受对象;而当我们需要在稍后将这条数据取出来,而又希望使用
它对应的值类型进行操作时,我们又需要将它拆箱成值类型的版本。现在,编译器可以帮我们自
动地完成这些必要的步骤。下面的代码我提供两个版本的装箱和拆箱,一个版本使用手工的方式,
另一个版本则把这些显而易见的代码交给编译器去完成:
public static void manualBoxingUnboxing(int i) {
ArrayList<Integer> aList = new ArrayList<Integer>();
aList.add(0, new Integer(i));
int a = aList.get(0).intValue();
System.out.println("The value of i is " + a);
}
public static void autoBoxingUnboxing(int i) {
ArrayList<Integer> aList = new ArrayList<Integer>();
aList.add(0, i);
int a = aList.get(0);
System.out.println("The value of i is " + a);
}
看到了吧,在 J2SE(TM) 5.0 中,我们不再需要显式的去将一个值类型的数据转换成相应的对
象,从而把它作为对象传给其他方法,也不必手工的将那个代表一个数值的对象拆箱为相应的值
类型数据,只要你提供的信息足够让编译器确信这些装箱/拆箱后的类型在使用时是合法的:比方
讲,如果在上面的代码中,如果我们使用的不是 ArrayList<Integer>而是 ArrayList 或者其他不兼容
的版本如 ArrayList<java.util.Date>,会有编译错误。
http://emag.csdn.net - 53 - 2005 年 1 月 1 期 总第 2 期
59. 《CSDN 社区电子杂志——Java 杂志》
当然,你需要足够重视的是:一方面,对于值类型和引用类型,在资源的占用上有相当大的
区别;另一方面,装箱和拆箱会带来额外的开销。在使用这一方便特性的同时,请不要忘记了背
后隐藏的这些也许会影响性能的因素。
1.7. 类型安全的枚举
在介绍 J2SE(TM) 5.0 中引入的类型安全枚举的用法之前,我想先简单介绍一下这一话题的背
景。
我们知道,在 C 中,我们可以定义枚举类型来使用别名代替一个集合中的不同元素,通常是
用于描述那些可以归为一类,而又具备有限数量的类别或者概念,如月份、颜色、扑克牌、太阳
系的行星、五大洲、四大洋、季节、学科、四则运算符,等等。它们通常看上去是这个样子:
typedef enum {SPRING, SUMMER, AUTUMN, WINTER} season;
实质上,这些别名被处理成 int 常量,比如 0 代表 SPRING,1 代表 SUMMER,以此类推。因
为这些别名最终就是 int,于是你可以对它们进行四则运算,这就造成了语意上的不明确。
Java 一开始并没有考虑引入枚举的概念,也许是出于保持 Java 语言简洁的考虑,但是使用 Java
的广大开发者对于枚举的需求并没有因为 Java 本身没有提供而消失,于是出现了一些常见的适用
于 Java 的枚举设计模式,如 int enum 和 typesafe enum,还有不少开源的枚举 API 和不开源的内部
实现。
我大致说一下 int enum 模式和 typesafe enum 模式。所谓 int enum 模式就是模仿 C 中对 enum
的实现,如:
public class Season {
public static final int SPRING = 0;
public static final int SUMMER = 1;
public static final int AUTUMN = 2;
public static final int WINTER = 3;
}
这种模式跟 C 中的枚举没有太多本质上的区别, 枚举的局限它基本上也有。 typesafe enum
C 而
模式则要显得健壮得多:
public class Season {
private final String name;
private Season(String name) {
http://emag.csdn.net - 54 - 2005 年 1 月 1 期 总第 2 期
60. 《CSDN 社区电子杂志——Java 杂志》
this.name = name;
}
public String toString() {
return name;
}
public static final Season SPRING = new Season("spring");
public static final Season SUMMER = new Season("summer");
public static final Season AUTUMN = new Season("autumn");
public static final Season WINTER = new Season("winter");
}
后一种实现首先通过私有的构造方法阻止了对该类的继承和显式实例化,因而我们只可能取
得定义好的四种 Season 类别,并且提供了方便的 toString()方法获取有意义的说明,而且由于这是
一个完全意义上的类,所以我们可以很方便的加入自己的方法和逻辑来自定义我们的枚举类。
最终,Java 决定拥抱枚举,在 J2SE(TM) 5.0 中,我们看到了这一变化,它所采用的设计思路
基本上就是上面提到的 typesafe enum 模式。它的语法很简单,用一个实际的例子来说,要定义一
个枚举,我们可以这样写:
public enum Language {CHINESE, ENGLISH, FRENCH, HUNGARIAN}
接下来我们就可以通过 Language.ENGLISH 来使用了。呃…这个例子是不是有点太小儿科了,
我们来看一个复杂点的例子。使用 Java 的类型安全枚举,我们可以为所有枚举元素定义公用的接
口,然后具体到每个元素本身,可以针对这些接口实现一些特定的行为。这对于那些可以归为一
类,又希望能通过统一的接口访问的不同操作,将会相当方便。通常,为了实现类似的功能,我
们需要自己来维护一套继承关系或者类似的枚举模式。这里借用 Java 官方网站上的一个例子:
public enum Operation {
PLUS { double eval(double x, double y) { return x + y; } },
MINUS { double eval(double x, double y) { return x - y; } },
TIMES { double eval(double x, double y) { return x * y; } },
DIVIDE { double eval(double x, double y) { return x / y; } };
// Do arithmetic op represented by this constant
abstract double eval(double x, double y);
}
在这个枚举中,我们定义了四个元素,分别对应加减乘除四则运算,对于每一种运算,我们
都可以调用 eval()方法,而具体的方法实现各异。我们可以通过下面的代码来试验上面这个枚举类:
public static void main(String args[]) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values()) {
http://emag.csdn.net - 55 - 2005 年 1 月 1 期 总第 2 期
82. 《CSDN 社区电子杂志——Java 杂志》
1.5. 再议 Annotation
在 EJB3 中,Annotation 把开发和部署的工作合在一起。但是在一些企业环境中,开发人员并
不控制诸如数据源名等(这些是部署部门和管理部门的工作),这样把数据源名写在 xml 中将比较
好。
Annotation 是本身静态的,一旦添加或者修改 annotation 都需要重新编译,在运行时读取,这
样就丧失了运行时配置的能力。因此 Annotations 不会取代 xml,它只是提供了另一种途径。并且
我相信 sun 公司将在未来提供一个方式可以在运行期更改 metadata。
关于这点 TSS 上有着很激烈的讨论,很多开发人员提出:利用 xml 来更改 annotation,并希望类似
的方式能被采纳为标准规范。比如使用如下格式:
<class name="org.hibernate.Item">
@Entity
@Table(name="AUCTION_ITEM")
<method sig="getBar()">@Transient</method>
<method sig="doSomething(int, String)">
@Tx(REQUIRES_NEW)
</method>
</class>
当然也有不同意见:But then, I think of "overriding" as a different problem to "xml deployment
descriptors", and so I think we need two solutions. I think Cedric and Bill are trying to kill two birds with
one stone, so maybe their proposals are better....
关于为 annotation 提供动态配置能力的问题,其中一个网友认为:Sun make it real pain to do the
deployment XML so that they can introduce annotation to fix it. The annotation can make
code/deployment coupling so strong that Sun can come out with a new way (annotation interceptor in jdk
1.6? :)) for fixing it. and the cycles goes on...这让我想起了类似的现象: 和 TagLib。
JSP 希望 Annotation
不会和 TagLib 有同样的命运。
Annotation 本身引入另一种类型的接口。在 EJB3 中确实使程序更加 POJO,也消除了一些接
口。并且编译后的代码也可以直接移植到另一个并不处理这些 annotations 的环境中(感谢 VM 在
加载类时并不检查那些 annotations 的 classes,甚至这些类不在 classpath 中)。然而代码也确实增加
了另一些接口。这个表现在编译期,如果没有这些 annotation classes,是编译不过的。
另一个问题(还好不算很重要),关于 annotation 的 namespace。在多层应用的系统中,可能会
出现同样的全局 annotation 的 namespace 冲突。比如一些公共的 annotation,如@Transaction,将会
http://emag.csdn.net - 77 - 2005 年 1 月 1 期 总第 2 期