Java 5 新特性:第二部分





4.00/5 (2投票s)
本文讨论 Java 5 中的两个新特性:可变参数(varargs)和静态导入(static import)。
摘要
在第一部分中,我们讨论了 Java 5 的自动装箱和 foreach
特性。在本第二部分中,我们将讨论可变参数和静态导入。由于枚举、注解和泛型等其他特性需要单独/特殊处理,我们将在其他文章中介绍它们。
可变参数(Varargs)
假设您想使用可变数量的参数调用一个方法。在 Java 1.4 或更早版本中,我们可用的选项是传递一个数组。让我们看一个简单的例子,如下所示:
public static int max(int[] values)
{
int max = values[0];
for(int aValue : values)
{
if (aValue > max) max = aValue;
}
return max;
}
我们可以通过传递具有不同数量值的数组来调用此方法,如下所示:
max(new int[] {1, 7, 2, 9, 8});
max(new int[]{8, 12, 87, 23, 1, 3, 6, 9, 37});
为了调用接受可变数量参数的方法,我们必须将参数打包成一个数组。虽然这可行,但不够优雅。Java 5 中引入的可变参数解决了这个优雅性问题。
在我们了解它之前,我们先谈谈 C++ 是如何处理这个问题的。在 C++ 中,您可以使用省略号(...)的概念向方法传递可变数量的参数。虽然这是一个有趣的概念,但它有两个显著问题。首先,在方法内部获取不同参数的代码并不简单。您必须使用一组特殊的函数(va_list
、va_args
等)从栈中提取参数。其次,没有确定的方法来确定栈上数据的类型。通常,我不鼓励开发人员在 C++ 中使用省略号,而是依赖运算符重载和连接。
Java 5 中的可变参数概念没有这些问题。它不仅优雅,而且类型非常安全。让我们以 max
方法为例,并修改它以使用可变参数。
public static int max(int... values)
请注意,我唯一更改的是将 int[]
更改为 int…
,并且我没有修改 max
方法的实现。如果方法在更改参数类型后仍然有效,那么新语法是什么?它与数组有什么关系?
类型后跟 ... 仅仅是一种语法糖——它只是一个数组。然而,编译器允许您向此方法传递数组或一组离散值。例如,现在我可以按如下方式调用修改后的 max
方法:
max(new int[] {1, 7, 2, 9, 8});
max(new int[]{8, 12, 87, 23, 1, 3, 6, 9, 37});
max(1, 7, 2, 9, 8);
max(8, 12, 87, 23, 1, 3, 6, 9, 37);
下面两行比上面两行更简洁。但是当您传递离散值时会发生什么?编译器只是将这些值“打包”成一个数组。所以,代码
max(1, 7);
被编译成
max(new int[] {1, 7});
正如您可以从以下字节码中看到的那样:
0: iconst_2
1: newarray int
3: dup
4: iconst_0
5: iconst_1
6: iastore
7: dup
8: iconst_1
9: bipush 7
11: iastore
12: invokestatic #2; //Method max:([I)I
在上面的例子中,我们传递了一个 int
类型的数组。如果我们想传递不同类型的数据怎么办?当然可以。请看下面的例子:
public static void print(Object... values)
{
for(Object obj : values)
{
System.out.printf("%s is of type %s\n", obj, obj.getClass().getName());
}
}
上面的代码接收一个 Object
类型的可变参数。您可以像下面这样用不同的类型调用它:
print(1, "test", 'a', 2.1);
此调用的输出是
1 is of type java.lang.Integer
test is of type java.lang.String
a is of type java.lang.Character
2.1 is of type java.lang.Double
如果您记住自动装箱,第一行应该不足为奇——这就是类型为 Integer
而不是 int
的原因。我们还使用了 printf
语句,它直接受益于可变参数的概念。
您不仅限于仅将可变参数作为参数;也就是说,如果您愿意,可以同时拥有常规参数和可变参数。但是,如果存在可变参数,它必须放在最后。换句话说,先放置您喜欢的任何常规参数,然后放置可变参数,如以下示例所示:
public static void print(String msg, Object... values)
优点和缺点
- 当您想传递可变数量的参数时,可变参数会派上用场。如果您需要这种灵活性和优雅性,请使用它。
- 如果您的可变参数是
Object
类型,您会失去一些编译时类型安全性。但是,如果它是特定类型(例如int…
),那么您就具有类型安全性。 - 如果您的应用程序只期望向方法传递三四个参数,那么就不要费心使用可变参数。
- 如果您不小心,可能会在方法重载时遇到问题,因为可变参数可能会增加方法重载时参数冲突的机会。
静态导入
导入语句允许您向编译器提供提示,说明您在代码中引用的是哪个类。它为您提供了使用类的短名称(如 JButton
)而不是长而完全限定的名称(如 javax.swing.JButton
)的便利。请记住,导入并不告诉编译器类在哪里(classpath 负责此项)。它只告诉编译器您要使用哪个类。
当然,如果多个包/命名空间中的类发生冲突,那么我们将收到错误。在这种情况下,您可以求助于使用类的完全限定名称。
虽然导入提供了便利,但它也有一个缺点。它没有清楚地向读者指出所使用的类来自何处。如果您的代码顶部有多个导入,那么您的代码阅读者可能需要一段时间才能弄清楚哪些类属于哪个包/命名空间。
静态导入是 Java 5 中的一个概念,不幸的是,它加剧了这个问题。让我们看看下面的代码:
package com.agiledeveloper.com;
public class Test
{
public static void main(String[] args)
{
System.out.println("Math.abs(-2) = " + Math.abs(-2));
System.out.println("Math.ceil(3.9) = " + Math.ceil(3.9));
}
}
通过这段代码,您可以很快地看出我正在调用 Math
类的 abs()
和 ceil()
方法。然而,对于这两个调用,我都必须输入 Math.
。假设您需要在代码中调用 abs()
函数十次。那么,您可以认为通过调用 abs()
而不是 Math.abs()
可以减少代码冗余。静态导入允许您这样做,如下所示:
package com.agiledeveloper.com;
import static java.lang.Math.abs;
import static java.lang.Math.ceil;
import static java.lang.Runtime.*;
public class Test
{
public static void main(String[] args)
{
System.out.println("Math.abs(-2) = " + abs(-2));
System.out.println("Math.ceil(3.9) = " + ceil(3.9));
System.out.printf("Free memory is = %d", getRuntime().freeMemory());
}
}
在上面的代码中,我们调用了 abs()
、ceil()
和 getRuntime()
,就好像它们是 Test
类的静态方法一样(但它们不是)。静态导入给我们一种错觉,认为我们已经将这些方法引入到当前命名空间中。
除了难以识别方法来源的问题,静态导入也有一些优点。例如,在 EasyMock 2.0 中,静态导入用于减少代码的冗长性。在使用模拟对象时,您会编写如下代码:
SomeInterface mock = (SomeInterface) createMock(SomeInterface.class);
expectCall(mock.foo()).andReturn(5);
replay(mock);
…
上面划线的方法都属于 EasyMock
类,可以在上面的代码中使用,前提是我们添加了
import static org.easymock.EasyMock.*;
优点和缺点
- 静态导入对于消除重复调用一组静态方法的繁琐很有用。
- 一个显著的缺点是它会使代码难以阅读——您可能很难弄清楚某些方法是从哪里来的。因此,我们建议您非常谨慎地使用静态导入,并且每个文件不超过一两个。使用多个静态导入不是一个好主意。
枚举、注解和泛型
枚举、注解和泛型的概念值得单独介绍。Java 5 中的枚举非常有趣,它是 Joshua Bloch 关于编写优秀枚举的建议的实现。
其他功能
在本文中,我们主要关注 Java 5 中的语言特性。Java 5 还有一些其他有趣的特性。下面列出了一些精选特性:
StringBuilder
StringBuffer
消除了对象创建开销,但有同步开销。StringBuilder
消除了这种开销。- 客户端与服务器端垃圾回收差异,更具适应性的回收。
- 改进的图像 I/O,以提高性能和内存使用。
- 使用共享存档减少应用程序启动时间和占用空间。
- 增强了线程优先级。
- 能够获取一个线程或所有线程的堆栈跟踪。
- 线程上的
UncoughtExceptionHandler
。 - 改进了致命异常的错误报告。
System.nanoTime()
提供纳秒级粒度的时间测量。ProcessBuilder
- 比
Runtime.exec()
启动进程更容易。 Formatter
和Formattable
提供以printf
风格格式化输出的能力。- Scanner 用于更容易地转换为基本类型——基于正则表达式。
java.lang.instrument
允许在运行时进行字节码增强以检测代码。- Collections Framework 拥有
Queue
、BlockingQueue
和ConcurrentMap
接口及其实现。一些类被修改以实现新接口。 - 反射 API 支持注解、枚举。Class 已泛型化。
System.getenv()
不再废弃!
结论
在 Java 5 特性文章的第二部分中,我们讨论了可变参数的优点,并展示了它如何使我们的代码优雅且更易于使用。它仅仅是一种语法糖,您可以将数组或离散值传递给方法。我们还研究了静态导入,我们建议您谨慎使用。
参考文献
- Java 开发者网络.
- 在“演示文稿”部分查找“Java 5 特性”示例和幻灯片.
- “Java 泛型的优缺点,第一部分、第二部分和第三部分。”
- “Java 5 中的枚举”,待定。
- “Java 5 中的注解”,待定。
- Joshua Bloch,Effective Java 编程语言指南,Addison-Wesley,2001。