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。这些特性消除了代码中的混乱,使代码更容易理解。然而,它们仅仅是语法糖,并不能提高代码的性能。我们将在本文的后续部分讨论其余的特性。

