Java 5 新特性






3.64/5 (7投票s)
本文讨论了 Java 5 中的新特性,并探讨了如何从中受益。在第一部分中,我们将介绍自动装箱和 foreach。
摘要
Java 5 有一些不错的新特性。本文将讨论这些特性,并探讨如何从中受益。在第一部分中,我们将介绍自动装箱和 foreach
。
Java 5 新特性
Java 语言在 1.5 版本(或称 Java 5)中添加了许多有趣的特性。语言级别的特性包括自动装箱、foreach
、枚举、可变参数、注解、静态导入和泛型。
这些特性中的大多数都可以被认为是进步。它们通过减少冗长的语法和使代码更直观来提高开发人员的生产力。
下图显示了我对这些特性实用性的看法
左侧的特性(foreach
、可变参数、自动装箱、枚举)非常好。注解非常有用,但我们还需要弄清楚何时何地正确使用它。静态导入只提供了微不足道的价值收益(也可能导致糟糕的代码),而另一个特性则相当糟糕。
自动装箱
Java 有两类“公民”:对象和原始类型。你无法在代码中很好地混合使用它们。如果你想编写一个可以接受任何参数类型的通用 API 怎么办?这在 Reflection API 中实现。让我们在这里编写一个对 Reflection 的 invoke()
方法的拙劣模仿
package com.agiledeveloper;
class A {}
class B {}
public class Test
{
public static void foo1(A obj)
{
System.out.println("foo called with " + obj);
}
public static void foo2(A obj1, B obj2)
{
System.out.println("foo2 called with " + obj1 + " and " + obj2);
}
// Poor imitation of refelction API to illustrate the point
public static void invoke(String method, Object[] args)
{
if (method.equals("foo1"))
{
foo1((A) args[0]);
}
if (method.equals("foo2"))
{
foo2((A) args[0], (B) args[1]);
}
}
public static void main(String[] args)
{
invoke("foo1", new Object[]{new A()});
invoke("foo2", new Object[]{new A(), new B()});
}
}
在 main()
中,我先用 foo1
调用 invoke()
,然后用 foo2
调用。为了发送任意数量的参数,invoke()
接受一个对象数组。将 A
的对象或 B
的对象发送到此方法没有问题。但是,让我们添加另一个方法
public static void foo3(int value)
{
System.out.println("foo3 called witih " + value);
}
我们如何使用 invoke()
方法调用此方法?invoke()
期望一个对象数组,而 foo3()
期望一个 int
。在 Java 的早期版本中,你必须将 int
包装成一个对象并将其发送到 invoke()
方法。包装 int
的标准对象是 Integer
。同样,我们有用于 double
的 Double
、用于 char
的 Character
等等。虽然 Integer.class
代表 Integer
类,但 Integer.TYPE
代表此类的包装器所服务的 int
原始类型。
这种手动装箱和手动拆箱的方法有相当多的缺点
- 这导致代码混乱
- 需要开发人员做更多的工作
- 看起来不自然
- 这让新手程序员感到困惑
让我们修改 invoke()
方法,看看我们将如何调用它
public static void invoke(String method, Object[] args)
{
if (method.equals("foo1"))
{
foo1((A) args[0]);
}
if (method.equals("foo2"))
{
foo2((A) args[0], (B) args[1]);
}
if (method.equals("foo3"))
{
foo3(((Integer) (args[0])).intValue());
}
}
并且,对它的调用将如下所示
invoke("foo3", new Object[]{new Integer(3)}); // Until Java 1.4
请注意,您如何将值 3 放入(或装箱)到 Integer
对象中。同样,在 invoke()
中调用 foo3()
时,您必须解箱该值。
在 Java 5 中,自动装箱和自动拆箱功能旨在减轻这种混乱。虽然这仍然在底层发生,但在源代码级别,我们不必这样做。这是修改后的代码
invoke("foo3", new Object[] {3});
上面的代码在编译时会被转换,以便值 3 会自动装箱成一个 Integer
对象。
让我们考虑另一个例子——我们将添加一个方法 foo4()
,如下所示
public static Integer foo4(Integer v)
{
System.out.println("foo4 called with " + v.intValue());
return new Integer(5);
}
现在,这是使用 Java 5 自动装箱/拆箱功能调用它的方法
int someValue = foo4(4);
System.out.println("Result of call to foo4 " + someValue);
如您所见,这段代码看起来更自然,更简洁。
优点和缺点
- 请注意,装箱和拆箱仍然在底层发生。看看生成的字节码
92: iconst_4
93: invokestatic #31;
//Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
96: invokestatic #33;
//Method foo4:(Ljava/lang/Integer;)Ljava/lang/Integer;
99: invokevirtual #23;
//Method java/lang/Integer.intValue:()I
102: istore_1
这段源代码呈现出一种具有欺骗性的外观。它会产生性能影响,特别是当你调用计算密集型方法时。
Integer
对象为 null
,将其赋值给 int
将导致抛出运行时异常 – NullPointerException
。==
的含义。对于对象,您比较的是身份;对于原始类型,您比较的是值。在自动拆箱的情况下,会发生基于值的比较。foreach
循环是一种控制结构,自我们开始编程以来就一直存在。你最常使用的旧式循环语法是
for(int i = 0; i < arr.length; i++)
{
... = arr[i] ...
}
虽然这种结构很简单,但即使我们不需要索引 i
,我们也被迫使用它。
如果您想遍历一个集合,例如 ArrayList
怎么办?我们会做类似的事情
for(Iterator iter = lst.iterator(); iter.hasNext(); )
{
System.out.println(iter.next());
}
不是很优雅,不是吗?在 for
语句中,最后一个 ;
之后什么都没有。你必须在循环体内调用 next()
方法才能前进到下一个元素,同时获取要处理的元素。
Java 5 中引入的 foreach
进一步简化了循环。让我们看看如何使用旧方法和新方法进行循环的示例
package com.agiledeveloper;
import java.util.ArrayList;
import java.util.Iterator;
public class Test
{
public static void main(String[] args)
{
String[] messages = {"Hello", "Greetings", "Thanks"};
for (int i = 0; i < messages.length; i++)
{
System.out.println(messages[i]);
}
for (String msg : messages)
{
System.out.println(msg);
}
ArrayList lst = new ArrayList();
lst.add(1);
lst.add(4.1);
lst.add("test");
for (Iterator iter = lst.iterator(); iter.hasNext();)
{
System.out.println(iter.next());
}
for (Object o : lst)
{
System.out.println(o);
}
ArrayList<Integer> values = new ArrayList<Integer>();
values.add(1);
values.add(2);
int total = 0;
for (int val : values)
{
total += val;
}
System.out.println("Total : " + total);
}
}
foreach
被巧妙地引入,以避免引入任何新关键字。你会这样读
for (String msg : messages)
意为“对于 消息中的每个字符串 msg”。设计者没有发明另一个关键字“foreach”,而是决定使用旧的“for”。此外,“in”可能在现有代码中用于字段、变量或方法。为了避免踩到你的痛处,他们决定改用 :
。在 foreach
的循环中,msg
代表数组 String[]
中的一个 String
元素。
如你所见,循环要简单得多,更优雅(暂时忽略丑陋的 :
),也更容易编写。但是,这里到底发生了什么?我们可以再次求助于字节码来理解。
代码
for (String msg : messages)
{
System.out.println(msg);
}
翻译为
51: aload_1
52: astore_2
53: aload_2
54: arraylength
55: istore_3
56: iconst_0
57: istore 4
59: iload 4
61: iload_3
62: if_icmpge 85
65: aload_2
66: iload 4
68: aaload
69: astore 5
71: getstatic #6;
//Field java/lang/System.out:Ljava/io/PrintStream;
74: aload 5
76: invokevirtual #7;
//Method java/io/PrintStream.println:(Ljava/lang/String;)V
79: iinc 4, 1
82: goto 59
对于数组,foreach
只是简单地转换为我们熟悉的 for
循环。让我们看看 foreach
会发生什么
for (Object o : lst)
{
System.out.println(o);
}
其中 lst
是一个 ArrayList
。 इसका字节码是
157: aload_2
158: invokevirtual #22;
//Method java/util/ArrayList.iterator:()Ljava/util/Iterator;
161: astore_3
162: aload_3
163: invokeinterface #18, 1;
//InterfaceMethod java/util/Iterator.hasNext:()Z
168: ifeq 190
171: aload_3
172: invokeinterface #19, 1;
//InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
177: astore 4
179: getstatic #6; //Field java/lang/System.out:Ljava/io/PrintStream;
182: aload 4
184: invokevirtual #20;
//Method java/io/PrintStream.println:(Ljava/lang/Object;)V
187: goto 162
如您所见,在这种情况下,foreach
转换为遍历 Iterator
的 for
循环。
所以,foreach
仅仅是一个语法糖,在编译时会转换为我们传统的循环。
在上面的例子中,我将 foreach
用于 String[]
和 ArrayList
。如果你想将 foreach
用于你自己的类怎么办?你可以这样做,前提是你的类实现了 Iterable
接口。这非常简单。为了让 foreach
工作,你必须返回一个 Iterator
。当然,你知道 Collection
提供了迭代器。但是,如果你想在自己的类上使用 foreach
,期望你实现更庞大的 Collection
接口是不公平的。Iterable
接口只有一个方法
/** Implementing this interface allows an object to be the target of
* the "foreach" statement.
*/
public interface Iterable<T> {
/**
* Returns an iterator over a set of elements of type T.
*
* @return an Iterator.
*/
Iterator<T> iterator();
}
我们来试一试
//Wheel.java
package com.agiledeveloper;
public class Wheel
{
}
//Car.java
package com.agiledeveloper;
import java.util.Iterator;
import java.util.ArrayList;
public class Car implements Iterable<Wheel>
{
ArrayList<Wheel> wheels = new ArrayList<Wheel>();
public Car()
{
for(int i = 0; i < 4; i++)
{
wheels.add(new Wheel());
}
}
public Iterator<Wheel> iterator()
{
return wheels.iterator();
}
}
现在,我可以在 Car
对象上使用 foreach
,如下所示
Car aCar = new Car();
for(Wheel aWheel : aCar)
{
// aWheel ...
}
优点和缺点
foreach
看起来更简单、更优雅,也更容易使用。- 但是,您不能一直使用它。如果您需要访问集合中的索引(用于设置或更改值)或者您想要访问迭代器(例如,用于删除元素),那么您就不能使用
foreach
。 - 如果您的类没有实现
Iterable
接口,您的类的用户将无法在您的对象上使用foreach
。
结论
Java 5 具有一些不错的新特性。在第一部分中,我们讨论了其中两个:自动装箱和 foreach
。这些特性消除了代码中的混乱,使代码更容易理解。然而,它们仅仅是语法糖,并不能提高代码的性能。我们将在本文的后续部分讨论其余的特性。