JDK 1.5版本包含了Java语法方面的主要改进。
自从Java 1.0版本首次受到开发人员欢迎以来,Java语言的语法就没有发生过太大的变化。虽然1.1版本增加了内部类和匿名内部类,1.4版本增加了带有新的assert关键字的assertion(断定)功能,但Java语法和关键字仍然保持不变--像编译时常量一样处于静态。它将通过J2SE 1.5(代号Tiger)发生改变。
过去的J2SE版本主要关注新类和性能,而Tiger的目标则是通过使Java编程更易于理解、对开发人员更为友好、更安全来增强Java语言本身,同时最大限度地降低与现有程序的不兼容性。该语言中的变化包括generics(泛化)、autoboxing、一个增强的“for”循环、 typesafe enums(类型安全的枚举类型)、一个静态导入工具(static import facility)和varargs。
通过generics来改进类型检查
generics使你能够指定一个集合中使用的对象的实际类型,而不是像过去那样只是使用Object。generics也被称为“参数化类型”,因为在generics中,一个类的类型接受影响其行为的类型变量。
generics并不是一个新概念。C++中有模板,但是模板非常复杂并且会导致代码膨胀。C++编码人员能够仅使用C++模板,通过一些小的技巧来执行阶乘函数,然后看着编译器生成C++源代码来处理模板调用。Java开发人员已经从C++语言中学到了很多关于generics的知识,并经过了足够长时间的实践,知道如何正确使用它们。Tiger的当前计划是从健壮的Generic Java (GJ)方案演变而来的。GJ方案的口号是“使Java的类型简化、再简化。”
为了了解generics,让我们从一个不使用generics的例子开始。下面这段代码以小写字母打印了一个字符串集合:
//获得一个字符串集合
public void lowerCase(Collection c) {
Iterator itr = c.iterator();
while (itr.hasNext()) {
String s = (String) itr.next();
System.out.println(s.toLowerCase());
}
}
这个方法不保证只接收字符串。编程人员负责记住传给这个方法什么类型的变量。Generics通过显式声明类型来解决这个问题。Generics证明并执行了关于集合包含什么东西的规则。如果类型不正确,编译器就会产生一个错误。在下面的改写代码中,注意Collection和Iterator是如何声明它们只接收字符串对象的:
public void lowerCase(
Collection<String> c) {
Iterator<String> itr = c.iterator();
while (itr.hasNext()) {
System.out.println(
itr.next().toLowerCase());
}
}
现在,该代码包含了更强大的类型,但它仍然包含许多键盘类型。我们将在后面加以介绍。注意,你可以存储类型参数的任何子类型。接下来,我们将使用这个特性draw()一个形状集合。
// 获得孩子集合...
public void drawAll(Collection<Shape> c) {
Iterator<Shape> itr = c.iterator();
while (itr.hasNext()) {
itr.next().draw();
}
}
尖括号中的值被称为类型变量。参数化类型能够支持任何数量的类型变量。例如,java.util.Map就支持两个类型变量--一个用于键类型,一个用于值类型。下面的例子使用了一个带有指向一列元素对象的字符串查找键的map:
public static void main(String[] args) {
HashMap<String, List<Element>> map =
new HashMap<String,
List<Element>>();
map.put("root",
new ArrayList<Element>());
map.put("servlet",
new LinkedList<Element>());
}
这个类定义声明了它支持多少个类型变量。类型参数的数量必须精确地与所期望的相匹配。而且,类型变量一定不能是原始类型(primitive types)。
List<String, String> // takes one
List<int> // 无效的,原始类型
即使在期望使用一个普通类型(raw type)的时候,你也可以使用一个参数化类型。当然,你也可以反过来做,但这么做会收到一条编译时警告:
public static void oldMethod(List list) {
System.out.println(list.size());
}
public static void main(String[] args) {
List<String> words =
new ArrayList<String>();
oldMethod(words); // 没问题
}
这就实现了轻松的向后兼容:接受一个原始列表的老方法能够直接接受一个参数化List<String>。接受参数化List<String>的新方法也能够接受一个原始列表,但是因为原始列表不声明或执行相同的类型约束,所以这个动作会触发一个警告。可以保证的是:如果在编译时你没有得到名为unchecked(未检查)的警告,那么在运行时编译器生成的强制类型转换(cast)将不会失败。
有趣的是,参数化类型和普通类型被编译为相同的类型。没有专门的类来指定这一点,使用编译器技巧就可以完成这一切。instanceof检查可以证明这一点。
words instanceof List // true
words instanceof ArrayList //true
words instanceof ArrayList<String> // true
这个检查产生了一个问题:“如果它们是相同的类型,这种检查能起多大作用?”这是一条用墨水而不是用血写的约束。这段代码将产生一个编译错误,因为你不能向List<String>中添加新的Point:
List<String> list =
new ArrayList<String>();
list.add(new Point()); // 编译错误
但是这段代码被编译了!
List<String> list =
new ArrayList<String>();
((List)list).add(new Point());
它将参数化类型强制转换为一个普通类型,这个普通类型是合法的,避免了类型检查,但正如前面所解释的那样,却产生了一个调用未检查的警告:
warning: unchecked call to add(E) as a member of the raw type
java.util.List
((List)list).add(new Point());
^
写一个参数化类型
Tiger提供了一个写参数化类型的新语法。下面显示的Holder类可以存放任意引用类型。这样的类很便于使用,例如,通过引用语义支持CORBA传递,而不需要生成单独的Holder类:
public class Holder<A> {
private A value;
Holder(A v) { value = v; }
A get() { return value; }
void set(A v) { value = v; }
}
使用一个参数化的Holder类型,你能够安全地得到和设置数据,而不需进行强制类型转换:
public static void main(String[] args) {
Holder<String> holder =
new Holder<String>("abc");
String val = holder.get(); // "abc"
holder.set("def");
}
“A”类型参数名可以是任何标准的变量名。它通常是一个单一的大写字母。你也可以声明类型参数必须能够扩展另一个类,如下所示:
// 也可以
public class Holder<C extends Child>
关于是否能够声明任何其他的类型参数仍然存在争议。你对generics了解的越深,你需要的特殊规则就越多,但是特殊规则越多,generics就会越复杂。
Tiger中设计用来保存线程局部变量(thread local variable)的核心类java.lang.ThreadLocal,将可能变得与下面这个 Holder类的作用类似:
public class ThreadLocal<T> {
public T get();
public void set(T value);
}
我们也将看见java.lang.Comparable的变化,允许类声明与它们相比较的类型:
public interface Comparable<T> {
int compareTo(T o);
}
public final class String implements Comparable<String> {
int compareTo(String anotherString);
}
Generics不仅仅用于集合,它们有更为广泛的用途。例如,虽然你不能基于参数化类型(因为它们与普通类型没有什么不同)进行捕捉(catch),但是你可以抛出(throw)一个参数化类型。换句话说,你可以动态地决定throws语句中抛出什么。
下面这段令人思维混乱的代码来自generics规范。该代码通过扩展Exception的类型参数E定义了一个 Action接口。Action类有一个抛出作为E 出现的任何类型的run()方法。然后,AccessController类定义一个接受Action<E>的静态exec()方法,并声明exec()抛出E。声明该方法自身是参数化的需要该方法标记(method signature)中的特殊<E extends Exception>。
现在,事情变得有点棘手了。main()方法调用在Action 实例(作为一个匿名内部类实现)中传递的AccessController.exec()方法。该内部类被参数化,以抛出一个 FileNotFoundException。main()方法有一个捕捉这一异常类型的catch语句。如果没有参数化类型,你将不能确切地知道run()会抛出什么。有了参数化类型,你能够实现一个泛化的Action类,其中run()方法可以任意实现,并可以抛出任意异常(Exception):
interface Action<E extends Exception> {
void run() throws E;
}
class AccessController {
public static <E extends Exception>
void exec(Action<E> action) throws E {
action.run();
}
}
public class Main {
public static void main(String[] args) {
try {
AccessController.exec(
new Action<FileNotFoundException>() {
public void run()
throws FileNotFoundException {
// someFile.delete();
}
});
}
catch (FileNotFoundException f) { }
}
}
协变返回类型
下面进行一个随堂测验:下面的代码是否能够成功编译?
class Fruit implements Cloneable {
Fruit copy() throws
CloneNotSupportedException {
return (Fruit)clone();
}
}
class Apple extends Fruit
implements Cloneable {
Apple copy()
throws CloneNotSupportedException {
return (Apple)clone();
}
}
答案:该代码在J2SE 1.4中不能编译,因为改写一个方法必须有相同的方法标记(包括返回类型)作为它改写的方法。然而,generics有一个叫做协变返回类型的特性,使上面的代码能够在Tiger中进行编译。该特性是极为有用的。
例如,在最新的JDOM代码中,有一个新的Child接口。Child有一个detach()方法,返回从其父对象分离的Child对象。在Child接口中,该方法当然返回Child:
public interface Child {
Child detach();
// etc
}
当Comment类实现detach()时,它总是返回一个Comment,但如果没有协变返回类型,该方法声明必须返回Child:
public class Comment {
Child detach() {
if (parent != null)
parent.removeContent(this);
return this;
}
}
这意味着调用者一定不要将返回的类型再向下返回到一个Comment。协变返回类型允许Comment 中的detach()返回 Comment。只要Comment是Child的子类就行。除了能够返回Document的DocType和能够返回Element的EntityRef,该特性对立刻返回Parent 的Child.getParent()方法也能派上用场。协变返回类型将确定返回类型的责任从类的用户(通过强制类型转换确认)转交给类的创建者,只有创建者知道哪些类型彼此之间是真正多态的。 这使应用编程接口(API)的用户使用起来更容易,但却稍微增加了API设计者的负担。
Autoboxing
Java有一个带有原始类型和对象(引用)类型的分割类型系统。原始类型被认为是更轻便的,因为它们没有对象开销。例如,int[1024]只需要4K存储空间,以及用于数组自身的一个对象。然而,引用类型能够在不允许有原始类型的地方被传递,例如,传递到一个List。这一限制的标准工作场景是在诸如list.add(new Integer(1))的插入操作之前,将原始类型与其相应的引用类型封装(box或wrap)在一起,然后用诸如((Integer)list.get(0)).intValue()的方法取出(unbox)返回值。
新的 autoboxing特性使编译器能够根据需要隐式地从int转换为Integer,从char 转换为Character等等。auto-unboxing进行相反的操作。在下面的例子中,我不使用autoboxing计算一个字符串中的字符频率。我构造了一个应该将字符型映射为整型的Map,但是由于Java的分割类型系统,我不得不手动管理Character和Integer封箱转换(boxing conversions)。
public static void countOld(String s) {
TreeMap m = new TreeMap();
char[] chars = s.toCharArray();
for (int i=0; i < chars.length; i++) {
Character c = new Character(chars[i]);
Integer val = (Integer) m.get(c);
if (val == null)
val = new Integer(1);
else
val = new Integer(val.intValue()+1);
m.put(c, val);
}
System.out.println(m);
}
Autoboxing使我们能够编写如下代码:
public static void countNew(String s) {
TreeMap<Character, Integer> m =
new TreeMap<Character, Integer>();
char[] chars = s.toCharArray();
for (int i=0; i < chars.length; i++) {
char c = chars[i];
m.put(c, m.get(c) + 1); // unbox
}
System.out.println(m);
}
这里,我重写了map,以使用generics,而且我让autoboxing给出了map能够直接存储和检索char 和int值。不幸的是,上面的代码有一个问题。如果m.get(c)返回空值(null),会发生什么情况呢?怎样取出null值?在抢鲜版(early access release)(参见下一步)中,取出一个空的Integer 会返回0。自抢鲜版起,专家组决定取出null值应该抛出一个NullPointerException。因此,put()方法需要被重写,如下所示:
m.put(c, Collections.getWithDefault(
m, c) + 1);
新的Collections.getWithDefault()方法执行get()函数,在该方法中,如果值为空值,它将返回期望类型的默认值。对于一个int类型来说,则返回0。
虽然autoboxing有助于编写更好的代码,但我的建议是谨慎地使用它。封箱转换仍然会进行并仍然会创建许多包装对象(wrapper-object)实例。当进行计数时,采用将一个int与一个长度为1的的 int数组封装在一起的旧方法更好。然后,你可以将该数组存储在任何需要引用类型的地方,获取intarr[0]的值并使用intarr[0]++递增。你甚至不必再次调用put(),因为会在适当的位置产生增量。使用这一方法和其他一些方法,你能够更有效地进行计数。使用下面的算法,执行100万个字符的对时间会从650毫秒缩短为30毫秒:
public static void countFast(String s) {
int[] counts = new int[256];
char[] chars = s.toCharArray();
for (int i=0; i < chars.length; i++) {
int c = (int) chars[i];
counts[c]++; // no object creation
}
for (int i = 0; i < 256; i++) {
if (counts[i] > 0) {
System.out.println((char)i + ":"
+ counts[i]);
}
}
}
在C#中,我们可以看到一个类似但稍微不同的方法。C#有一个统一的类型系统,在这个系统中值类型和引用类型都扩展System.Object。但是,你不能直接看到这一点,因为C#为简单的值类型提供了别名和优化。int是System.Int32的一个别名,short是System.Int16的一个别名,double是System.Double的一个别名。在C#中,你能够调用“int i = 5; i.ToString();”,它是完全合法的。这是因为每个值类型都有一个在它被转换为引用类型时创建的相应隐藏引用类型(在值类型被转换为一个引用类型时创建的)。
int x = 9;
object o = x; //创建了引用类型
int y = (int) o;
当基于一个不同的类型系统时,最终结果与我们在J2SE 1.5中看到的非常接近。
对于循环的增强
还记得前面的这个例子么?
public void drawAll(Collection<Shape> c) {
Iterator<Shape> itr = c.iterator();
while (itr.hasNext()) {
itr.next().draw();
}
}
你再也不用输入这么多的文字了!这里是Tiger版本中的新格式。
public void drawAll(Collection<Shape> c) {
for (Shape s : c) {
s.draw();
}
}
你可以阅读这样一段代码“foreach Shape s in c”。我们注意到设计者非常聪明地避免添加任何新的关键字。考虑到很多人都用“in”来输入数据流,我们对此应该感到非常高兴。编译器将该新的语法自动扩展到其迭代表中。
for (Iterator<Shape> $i = c.iterator();
$i.hasNext(); ) {
Shape s = $i.next();
s.draw();
}
你可以使用该语法来对普通(raw,非参数化的)类型进行迭代,但是编译器会输出一个警告,告诉你必须的类型转换可能会失败。你可以在任何数组和对象上使用“foreach”来实现新的接口java.lang.Iterable。
public interface Iterable<T> {
SimpleIterator<T> iterator();
}
public interface SimpleIterator<T> {
boolean hasNext();
T next();
}
java.lang中的新的接口避免对java.util的任何语言依赖性。Java语言在java.lang之外必须没有依赖性。要注意通过next()方法来更巧妙地使用协变返回类型。需要说明的一点是,利用该“foreach”语法和SimpleIterator接口,就会丧失调用iterator.remove()的能力。如果你还需要该项能力,则必须你自己迭代该集合。
与C#对比一下,我们会看到相似的语法,但是C#使用“foreach”和“in”关键字,从最初版本开始它们就被作为保留字。
// C#
foreach (Color c in colors) {
Console.WriteLine(c);
}
C#的“foreach”对任何集合(collection)或者数组以及任何可列举的实现都有效。我们再一次看到了在Java和C#之间的非常相似之处。
(类型安全的枚举类型)typesafe enum
enums 是定义具有某些命名的常量值的类型的一种方式。你在C,C++中已经见过它们,但是显然,它们曾经在Java中不用。现在,经过了八年之后,Java重又采用它们,并且大概比先前的任何语言都使用得更好。让我们首先来看看先前我们是如何解决enum问题的?不知道你有没有编写过如下代码?
class PlayingCard {
public static final int SUIT_CLUBS
= 0;
public static final int SUIT_DIAMONDS
= 1;
public static final int SUIT_HEARTS
= 2;
public static final int SUIT_SPADES
= 3;
// ...
}
这段代码很简单也很常见,但是它有问题。首先,它不是类型安全的(typesafe)。可以给一个方法传递文字“5”来获取一个suit,并且将被编译。同时,这些值用这些常量直接被编译成每个类。Java通过这些常量进行这种"内联"(inlining)来达到优化的目的,但其风险在于,如果对这些值重新排序并且只重新编译该类,则其他类将会错误地处理这些suits。 而且,该类型是非常原始的,它不能被扩展或者增强,同时如果你输出这些值中的一个,你只会得到一个含意模糊的整型量,而不是一个好记的有用名字。这种方法非常简单,这也正是我们为什么要这样做的原因,但是它并不是最好的方法。所以,也许需要尝试一下下面的这个方法:
class PlayingCard {
class Suit {
public static final Suit CLUBS
= new Suit();
public static final Suit DIAMONDS
= new Suit();
public static final Suit HEARTS
= new Suit();
public static final Suit SPADES
= new Suit();
protected Suit() { }
}
}
它是类型安全的(typesafe)且更加具有可扩展性,并且,属于面向对象设计的类。然而,这样简单的一种方法并不支持序列化,没有合法值的列表,无法将这些值排序,并且,不能作为一个有意义的字符串来打印一个值。你当然可以添加这些特性,Josh Bloch在他的Effective Java一书中(第五章,第21条)为我们准确展示了如何解决这些问题。然而,你最终得到的是几页蹩脚的代码。
Java新的enum特性具有一个简单的单行语法:
class PlayingCard {
public enum Suit { clubs,
diamonds, hearts, spades }
}
被称之为enum(枚举)类型的该suit,对应于每个enum常量都有一个成员项。每个enum类型都是一个实际类,它可以自动扩展新类java.lang.Enum。编译器赋予enum类以有意义的String()、 hashCode(), 和equals() 方法, 并且自动提供Serializable(可序列化的)和Comparable(可比较的)能力。令人高兴地是enum类的声明是递归型的:
public class Enum<E extends Enum<E>>
implements Comparable<E>,
Serializable {
使用最新的enum类型可以提供很多好处:包括:比整型操作(int operations)更好的性能,编译时更好的类型安全性,不会被编译到客户端并且可以被重新命名和排序的常量,打印的值具有含意清晰的信息,能够在集合(collections)甚至switch中被使用,具有添加域(fields)和方法的能力,以及实现任意接口的能力。
每个enum具有一个字符串名字和一个整型顺序号值:
out.println(Suit.clubs); // "clubs"
out.println(Suit.clubs.name); // "clubs"
out.println(Suit.clubs.ordinal); // 0
out.println(Suit.diamonds.ordinal); // 1
Suit.clubs == Suit.clubs // true
Suit.clubs == Suit.diamonds // false
Suit.clubs.compareTo(Suit.diamonds) // -1
enum可以拥有构造器和方法。甚至一个main()方法都是合法的。下面的例子将值赋给罗马数字:
public enum Roman {
I(1), V(5), X(10), L(50), C(100),
D(500), M(1000);
private final int value;
Roman(int value) { this.value = value; }
public int value() { return value; }
public static void main(String[] args) {
System.out.println(Roman.I);
}
}
非常奇怪的是,不能将序列数值赋给一个enum,比如说“enum Month{jan=1,feb=2,….}”。 然而,却可以给enum常量添加行为。比如说,在JDOM中,XMLOutputter支持数种空白处理方法。如果JDOM是参照J2SE1.5构建的,那么这些方法就可以用一个enum来定义,并且enum类型本身可以具有这种处理行为。不管这种编码模式是不是会被证明是有用的,我们都会逐渐了解它。肯定这是一个异常有趣的概念。
public abstract enum Whitespace {
raw {
String handle(String s) { return s; }
},
trim {
String handle(String s) {
return s.trim(); }
},
trimFullWhite {
String handle(String s) {
return s.trim().equals("") ? "":s; }
};
abstract String handle(String s);
public static void main(String[] args) {
String sample = " Test string ";
for (Whitespace w : Whitespace.VALUES)
System.out.println(w + ": '"
+ w.handle(sample) + "'");
}}
很少有公开的出版物谈及Java新的enum。enum常量名是不是都应该都大写?对于常量来说,这是一个标准,但是规范指出小写名称“于更好的字符串格式,一般应该避免使用定制的toString方法。”另外,名字和顺序号该是域还是方法?这是封装方法一再引起争论的问题。在向J2SE1.5添加关键字方面,该特性也落了一个不太好的名声。令人伤心的是,它还是一个通常被用作存储Enumerator(计数器)实例的词。如果你已经在你的代码中使用了“enum”,那么在你为J2SE 1.5的应用编译之前,必须修改它。现在,你已得到了充分的警示。
让我们看一下C#,所有的enum都扩展成System.Enum。每个enum都具有可以被赋值的整型(或者字节型或者其他类型)值。enum还拥有静态方法,以便从字符串常量来初始化enum,获取有效值列表,从而,可以看到某个值是不是被支持。通过使用[flags]属性来标记一个enum,你可以确保值支持位屏蔽,并且系统负责打印被屏蔽的值的有用输出:
// C#
[Flags]
public enum Credit : byte {
Visa = 1, MC = 2, Discover = 4 }
Credit accepted = Credit.Visa | Credit.MC;
c.WriteLine(accepted); // 3
c.WriteLine(accepted.Format());//"Visa|MC"
静态导入
静态导入使得我们可以将一套静态方法和域放入作用域(scope)。它是关于调用的一种缩写,可以忽略有效的类名。比如说,对Math.abs(x)的调用可以被简单地写成 abs(x)。为了静态地导入所有的静态域和方法,我们可以使用“import static java.lang.Math”,或者指定要导入的具体内容,而使用“import static java.lang.System.out”--在这里没有什么令人激动的新特性,只是缩写而已。它让你可以不用Math而来完成math(数字计算)。
import static java.lang.Math.*;
import static java.lang.System.out;
public class Test {
public static void main(String[] args) {
out.println(abs(-1) * PI);
}
}
注意,“static”关键字的重用是为了避免任何新的关键词。语言越成熟,对于“static”关键词的使用就越多。如果在静态成员之间发生冲突的话,就会出现含混的编译错误,这一点跟类的导入一样。是否将java.lang.Math.* 作为固有的导入引发了一定的争论,不过在获知其将会触发含混的错误之后,这种争论不会再发生了。
Varargs
“varargs”表示“参数的变量”,存在于C语言中,并且支持通用的printf()和scanf()函数。 在Java中,我们通过编写一些接受Object[]、List、Properties(属性)的方法以及可以描述多个值的其它简单数据结构--比如说,Method.invoke(Object obj,Object[] args--来模拟这一特性。)。这要求调用程序将数据封装到这种单一的容器结构中。varargs允许调用程序传递值的任意列表,而编译器会为接收程序将其转化为数组。其语法就是在参数声明中的参数名之后添加“...”,以便使其成为vararg。它必须是最后一个参数--比如说,编写一个sum()函数以便将任意数量的整数相加:
out.println(sum(1, 2, 3));
public static int sum(int args...) {
int sum = 0;
for (int x : args) { sum += x; }
return sum;
}
在抢鲜版本中,vararg符号使用方括号,就像sum(int[] args...)。然而,在之后的讨论中,根据James Gosling的提议,方括号被去掉了。在这里的例子中,我们不使用方括号,但是如果你需要在抢鲜版本中使用这些代码的话,就需要将方括号添加上去。借助autoboxing,可以通过接受一个Object args…,可以接受任何类型的参数,包括原始类型。这与printf()类型的一些方法一样,它们接受任何数量的所有类型的参数。实际上,该Tiger版本可以使用这一特性通过format方法(其行为与printf()一样)来提供一个Formattable(可格式化)的接口。这是我们在以后的文章中将要讨论的话题。目前,我们只编写简单的printf():
public static void printf(String fmt,
int args...) {
int i = 0;
for (char c : fmt.toCharArray()) {
out.print(c == '%' ? args[i++] : c);
}
}
public static void main(String[] args) {
printf("My values are % and %
",
1, 2);
}
在Tiger版本中,你会发现采用一个新格式的invoke()函数: invoke(Object obj, Object args...). 这看上去更加自然。结论
J2SE1.5版努力使Java的编程更加简便、安全和更加富有表现力。这些特性和谐完美的被结合在一起。如果你跟我一样,总是喜欢用老的“for”循环,你肯定希
望你拥有Tiger。然而,需要记住的是,该规范并没有最终完成,很多地方还需要修改。管理这些变化的专家小组(JSR-14,JSR-175以及JSR-201)会在2003年
年末的beta版本发布之前,以及预期在2004年发布最终版发布之前,会做出很多修改。然而,Sun表达了对JavaOne的信心,认为总体上主要原则不会改变太多。
如果你想体验一下,那么你可以从下面的站点获取抢鲜版本。 从中你会找到可能从任何一个预览版软件都会遇到的错误,但是也会看到很多激动人心的新特性。
我强烈建议你尝试一下。