用断言和泛型进行Java编程
从JDK1.4到即将到来的Java 8的lambdas,Java语言比一开始的时候已经进步了很多了。在“Java 101:Java的新时代”的接下来的几篇文章中会描述Java语言的一系列基本特性工具,这个星期就先从断言(assertions)和泛形(generics)开始。
Java语言是世界上最广泛使用的编程语言之一,因此,我们不难想像,一些对它的改变有时会备受争议。在“Java 101,Java的新时代”的接下来的几篇文章会集中在那些从Java 1.4到Java 8中添加的特性。我们的目的是介绍给你一系列的Java语言特性的工具,当然会附带一些例子说明他们怎么在Java程序中使用,并且为什么要那样用。我们也会探索其他的一些有争议的新特性,例如泛型和lambda(查看文章底部的我关于泛型擦除的讨论)。
第一篇文章中我们来谈谈断言(assertions),它是在Java 1.4的时候就引入的,另外还有泛型(generics),它是Java 5引入的少数重要的新特性之一。
Java 1.4中的断言(Assertions)
断言(Assertions)是在Java 1.4引入的,到现在仍然是Java语言最有用并且最重要的附加功能之一。断言(Assertions)主要用于在程序中判断结果是否正确。断言(Assertions)测试条件是否是true值(又叫布尔表达示),当条件为false时通知开发人员。使用断言可以在代码的正确性上极大地提升你的信心。
Java常见问答博客
你有关于Java编程的问题吗?可以从有经验的Java导师中获取可靠的答案。Jeff Friesen的Java常见问答博客每个星期都会更新,会集中在一些Java初学者和稍有经验的开发人员遇到的常见问题。
假设你已经启用它们用于程序测试,断言是一些可以在运行时执行的可编译的实体。你可以在bugs发生的地方设置断言,在出现问题的时候它就会告诉你,这可以极大地减少你调试有问题的程序的时间。
在Java 1.4之前,开发人员大部分都是使用注释来进行一些代码正确性的假设。虽然对代码进行文档注释很有用,但注释比起断言是稍逊一筹的,毕竟断言是测试和调试的机制。因为编译器忽略注释,没有办法用它们来实现bug的通知。在代码修改时,注释并没有修改的问题也是很常见的。
实现断言
断言是通过assert
表达式和java.lang.AssertionError
类来实现的。这个表达式以关键字assert
开头,后跟着一个布尔表达式。assert
表达式语法上表示如下:
1
|
assert
BooleanExpr;
|
如果BooleanExpr
为true,什么事都不会发生,执行会继续。但是,如果表达式为false,AssertionError
会初始化并被抛出,如清单1所示。
Listing 1. AssertDemo.java (version 1)
1
2
3
4
5
6
7
8
|
public
class
AssertDemo
{
public
static
void
main(String[] args)
{
int
x = -
1
;
assert
x >=
0
;
}
}
|
在Listing 1中的断言表明开发人员希望变量x包含一个大于等于0的值。然而,这显然是不正确的,这个断言表达式执行后会抛出AssertionError
。
编译清单1(javac AssertDemo.java
),开启断言并执行(java -ea AssertDemo
)。你应该可以看到下面的输出:
1
2
|
Exception
in
thread “main” java.lang.AssertionError
at AssertDemo.main(AssertDemo.java:6)
|
对于另外一种例子,不带-ea
(enable assertions)参数执行AssertDemo
将会没有任何输出。当断言没有启用时,尽管它们仍然存在classfile中,但它们并不会被执行。
使用断言来测试前置条件(preconditions)和后置条件(postconditions)
断言经常用于测试一个程序的前置条件和后置条件:
- 前置条件是在执行一些代码流程前必须为true的条件。前置条件保证调用方法和被调用方法保持一致的协议。
- 后置条件是在执行一些代码流程后必须为true的条件。后置条件保证被调用方法和调用方法保持一致的协议。
前置条件
你可以在需要的时候通过显示的检查和抛出异常限制在public构造函数和方法中执行前置条件。对于一些私有的帮助方法,你可以通过指定断言来执行前置条件。
Listing 3. AssertDemo.java (version 3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
import
java.io.FileInputStream;
import
java.io.InputStream;
import
java.io.IOException;
class
PNG
{
/**
* Create a PNG instance, read specified PNG file, and decode
* it into suitable structures.
*
* @param filespec path and name of PNG file to read
*
* @throws NullPointerException when <code>filespec</code>
is
* <code>null</code>
*/
PNG(String filespec)
throws
IOException
{
// Enforce preconditions in non-private constructors and
// methods.
if
(filespec ==
null
)
throw
new
NullPointerException(“filespec is
null
”);
try
(FileInputStream fis =
new
FileInputStream(filespec))
{
readHeader(fis);
}
}
private
void
readHeader(InputStream is)
throws
IOException
{
// Confirm that precondition is satisfied in private
// helper methods.
assert
is !=
null
: “
null
passed to is”;
}
}
public
class
AssertDemo
{
public
static
void
main(String[] args)
throws
IOException
{
PNG png =
new
PNG((args.length ==
0
) ?
null
: args[
0
]);
}
}
|
清单3中的是一个最小的读取和解码PNG(portable network graphics)图片文件的PNG
类。构造函数明确地把filespec
和null
进行对比,当这个参数包含null
时抛出NullPointerException
。这里最主要的目的是执行前置条件保证filespec
不包含null
。
指定assert filespec != null
并不大好;因为当断言被关闭时,构造函数的Javadoc中提到的前置条件并不被推荐。(实际上,还是可以接受的,因为FileInputStream()
会抛出NullPointerException
,但你不应该依赖这些没有强制文档的行为。)
然而,assert
在private readHeader()
帮助方法中是合适的,它会完全读取和解码PNG文件的8位头。传递一个非空值的前置条件会一直是通过的。
后置条件
后置条件通常是通过断言来指定的。不管方法(构造函数)是否是public。请看清单 4
Listing 4. AssertDemo.java (version 4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
public
class
AssertDemo
{
public
static
void
main(String[] args)
{
int
[] array = {
20
,
91
, -
6
,
16
,
0
,
7
,
51
,
42
,
3
,
1
};
sort(array);
for
(
int
element: array)
System.out.printf(“%d “, element);
System.out.println();
}
private
static
boolean
isSorted(
int
[] x)
{
for
(
int
i =
0
; i < x.length-
1
; i++)
if
(x[i] > x[i+
1
])
return
false
;
return
true
;
}
private
static
void
sort(
int
[] x)
{
int
j, a;
// For all integer values except the leftmost value ...
for
(
int
i =
1
; i < x.length; i++)
{
// Get integer value a.
a = x[i];
// Get index of a. This is the initial insert position, which is
// used if a is larger than all values in the sorted section.
j = i;
// While values exist to the left of a’s insert position and the
// value immediately to the left of that insert position is
// numerically greater than a’s value ...
while
(j >
0
&& x[j-
1
] > a)
{
// Shift left value—x[j-1]—one position to its right —
// x[j].
x[j] = x[j-
1
];
// Update insert position to shifted value’s original position
// (one position to the left).
j—;
}
// Insert a at insert position (which is either the initial insert
// position or the final insert position), where a is greater than
// or equal to all values to its left.
x[j] = a;
}
assert
isSorted(x): “array not sorted”;
}
}
|
清单4展示了使用插入排序算法来排序int值数组的sort帮助方法。我在sort()返回前使用了assert来检查x被排序的后置条件。
清单4的例子展示了断言的一个重要特点,就是它们执行的时候代价比较高。因此,断言在生产代码中一般是禁用的。在清单4中,isSorted()
必须搜索整个数组,当数组非常大时,这将会非常耗时。
Java 5的泛型
除了Java并发工具(2013年6月修订)外,Java 5添加了8个语言特性:泛型,类型安全枚举,注解,自动装箱和拆箱,增强的循环,静态导入,可变参数和协变返回类型。我会在接下来的两篇文章中介绍所有Java 5的特性,我们先从泛型开始。
泛型是一系列的语言特性,它允许类型或方法能在获取编译时类型安全的同时操作不同类型的对象。泛型针对的是运行过程中由于代码非类型安全抛出java.lang.ClassCastExceptions
异常的问题。
Java类库的泛型
尽管泛型在Java Collection Framework中广泛使用,但他们并不专有。其他的Java基础类库包括
java.lang.Class
,java.lang.Comparable
,java.lang.ThreadLocal
和java.lang.ref.WeakReference
也有使用它。
考虑下面的代码块,它展示了泛型引入前在Java代码中很常见的类型安全问题:
1
2
3
|
List doubleList =
new
LinkedList();
doubleList.add(
new
Double(
3.5
));
Double d = (Double) doubleList.iterator().next();
|
尽管上面程序的目的只是为了保存java.lang.Double
对象到list
,但没有任何东西可以阻止保存其他类型的对象。例如,你可以指定doubleList.add("hello");
来添加一个java.lang.String
对象。然而,当保存其他类型的对象时,最后一行(Double)的转换操作会在遇到一个非Double的对象时抛出ClassCastException
。
因为这种类型安全的问题直到运行才会被检测出来,开发人员可能发现不了问题,而遗留到了客户那里。很显然,由编译器来检测问题是更好的。泛型帮助编译器,使之可以让开发人员标记list需要包含哪种特定类型的对象,如下:
1
2
3
|
List<Double> doubleList =
new
LinkedList<Double>();
doubleList.add(
new
Double(
3.5
));
Double d = doubleList.iterator().next();
|
List<Double>
现在读做Double
类型的List
。List是一个泛型接口,表示为List,当在操作具体的对象时接收一个Double
类型参数。编译器现在可以在添加对象到list时保证正确性—例如,这个list只能保存Double
对象。这种类型保证使得不再需要(Double)转换。
深入泛型类
泛型是一个类或接口通过一个形参列表引入一系列的参数类型,而这些形参列表是通过尖括号内的逗号分割的list来指定的。泛型类型遵循下面的语法:
1
2
3
4
5
6
7
8
9
|
class
identifier<formalTypeParameterList>
{
// class body
}
interface
identifier<formalTypeParameterList>
{
// interface body
}
|
Java集合框架提供了许多泛型类型和他们参数列表的例子。例如,java.util.Set<E>
是一个以作为形参列表,E作为list唯一类型参数。java.util.Map<K,V>
则是另外一个例子。
命名类型参数
Java编程规范规定类型参数的名称必须是单个大写字母,例如E表示元素(element),K表示键(key),V表示值(value),T表示类型(type)。如果可能,禁止使用无意义的如“P”——
java.util.List<E>
表示一系列元素,但List它又表示什么呢?