java,php,asp.net,linux,javascript,mysql,mssql,oracle,编程
java编程
java编程
Java代码的常见错误
Java代码
代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查—由同事们寻找代码中的错误—所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了
11
个java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。
一、常见错误
1
# :多次拷贝字符串
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作:
String s =
new
String (
"Text here"
);
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码:
String temp =
"Text here"
;
String s =
new
String (temp);
但是这段代码包含额外的String,并非完全必要。更好的代码为:
String s =
"Text here"
;
二、常见错误
2
#: 没有克隆(clone)返回的对象
封装(encapsulation)是面向对象编程的重要概念。不幸的是,java为不小心打破封装提供了方便——java允许返回私有数据的引用(reference)。下面的代码揭示了这一点:
import
java.awt.Dimension;
/***Example class.The x and y values should never*be negative.*/
public
class
Example{
private
Dimension d =
new
Dimension (
0
,
0
);
public
Example (){ }
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/
public
synchronized
void
setValues (
int
height,
int
width)
throws
IllegalArgumentException{
if
(height <
0
|| width <
0
)
throw
new
IllegalArgumentException();
d.height = height;
d.width = width;
}
public
synchronized
Dimension getValues(){
// Ooops! Breaks encapsulation
return
d;
}
}
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码:
Example ex =
new
Example();
Dimension d = ex.getValues();
d.height = -
5
;
d.width = -
10
;
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。
更好的方式是让getValues()返回拷贝:
public
synchronized
Dimension getValues(){
return
new
Dimension (d.x, d.y);
}
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。
三、常见错误
3
#:不必要的克隆
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对:
/*** Example class.The value should never * be negative.*/
public
class
Example{
private
Integer i =
new
Integer (
0
);
public
Example (){ }
/*** Set x. x must be nonnegative* or an exception will be thrown*/
public
synchronized
void
setValues (
int
x)
throws
IllegalArgumentException{
if
(x <
0
)
throw
new
IllegalArgumentException();
i =
new
Integer (x);
}
public
synchronized
Integer getValue(){
// We can’t clone Integers so we makea copy this way.
return
new
Integer (i.intValue());
}
}
这段代码是安全的,但是就象在错误
1
#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。
方法getValue()应该被写为:
public
synchronized
Integer getValue(){
// ’i’ is immutable, so it is safe to return it instead of a copy.
return
i;
}
java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括:
·Boolean
·Byte
·Character
·Class
·Double
·Float
·Integer
·Long
·Short
·String
·大部分的Exception的子类
四、常见错误
4
# :自编代码来拷贝数组
java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成:
public
class
Example{
private
int
[] copy;
/*** Save a copy of ’data’. ’data’ cannot be null.*/
public
void
saveCopy (
int
[] data){
copy =
new
int
[data.length];
for
(
int
i =
0
; i < copy.length; ++i)
copy[i] = data[i];
}
}
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是:
void
saveCopy (
int
[] data){
try
{
copy = (
int
[])data.clone();
}
catch
(CloneNotSupportedException e){
// Can’t get here.
}
}
如果你经常克隆数组,编写如下的一个工具方法会是个好主意:
static
int
[] cloneArray (
int
[] data){
try
{
return
(
int
[])data.clone();
}
catch
(CloneNotSupportedException e){
// Can’t get here.
}
}
这样的话,我们的saveCopy看起来就更简洁了:
void
saveCopy (
int
[] data){
copy = cloneArray ( data);
}
五、常见错误
5
#:拷贝错误的数据
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差:
import
java.awt.Dimension;
/*** Example class. The height and width values should never * be
negative. */
public
class
Example{
static
final
public
int
TOTAL_VALUES =
10
;
private
Dimension[] d =
new
Dimension[TOTAL_VALUES];
public
Example (){ }
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */
public
synchronized
void
setValues (
int
index,
int
height,
int
width)
throws
IllegalArgumentException{
if
(height <
0
|| width <
0
)
throw
new
IllegalArgumentException();
if
(d[index] ==
null
)
d[index] =
new
Dimension();
d[index].height = height;
d[index].width = width;
}
public
synchronized
Dimension[] getValues()
throws
CloneNotSupportedException{
return
(Dimension[])d.clone();
}
}
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为:
public
synchronized
Dimension[] getValues()
throws
CloneNotSupportedException{
Dimension[] copy = (Dimension[])d.clone();
for
(
int
i =
0
; i < copy.length; ++i){
// NOTE: Dimension isn’t cloneable.
if
(d !=
null
)
copy[i] =
new
Dimension (d[i].height, d[i].width);
}
return
copy;
}
在克隆原子类型数据的多维数组的时候,也会犯类似的错误。原子类型包括
int
,
float
等。简单的克隆
int
型的一维数组是正确的,如下所示:
public
void
store (
int
[] data)
throws
CloneNotSupportedException{
this
.data = (
int
[])data.clone();
// OK
}
拷贝
int
型的二维数组更复杂些。java没有
int
型的二维数组,因此一个
int
型的二维数组实际上是一个这样的一维数组:它的类型为
int
[]。简单的克隆
int
[][]型的数组会犯与上面例子中getValues()方法第一版本同样的错误,因此应该避免这么做。下面的例子演示了在克隆
int
型二维数组时错误的和正确的做法:
public
void
wrongStore (
int
[][] data)
throws
CloneNotSupportedException{
this
.data = (
int
[][])data.clone();
// Not OK!
}
public
void
rightStore (
int
[][] data){
// OK!
this
.data = (
int
[][])data.clone();
for
(
int
i =
0
; i < data.length; ++i){
if
(data !=
null
)
this
.data[i] = (
int
[])data[i].clone();
}
}
六、常见错误
6
#:检查
new
操作的结果是否为
null
java编程新手有时候会检查
new
操作的结果是否为
null
。可能的检查代码为:
Integer i =
new
Integer (
400
);
if
(i ==
null
)
throw
new
NullPointerException();
检查当然没什么错误,但却不必要,
if
和
throw
这两行代码完全是浪费,他们的唯一功用是让整个程序更臃肿,运行更慢。
C/C++程序员在开始写java程序的时候常常会这么做,这是由于检查C中malloc()的返回结果是必要的,不这样做就可能产生错误。检查C++中
new
操作的结果可能是一个好的编程行为,这依赖于异常是否被使能(许多编译器允许异常被禁止,在这种情况下
new
操作失败就会返回
null
)。在java 中,
new
操作不允许返回
null
,如果真的返回
null
,很可能是虚拟机崩溃了,这时候即便检查返回结果也无济于事。
七、常见错误
7
#:用== 替代.equals
在java中,有两种方式检查两个数据是否相等:通过使用==操作符,或者使用所有对象都实现的.equals方法。原子类型(
int
, flosat,
char
等)不是对象,因此他们只能使用==操作符,如下所示:
int
x =
4
;
int
y =
5
;
if
(x == y)
System.out.println (
"Hi"
);
// This ’if’ test won’t compile.
if
(x.equals (y))
System.out.println (
"Hi"
);
对象更复杂些,==操作符检查两个引用是否指向同一个对象,而equals方法则实现更专门的相等性检查。
更显得混乱的是由java.lang.Object 所提供的缺省的equals方法的实现使用==来简单的判断被比较的两个对象是否为同一个。
许多类覆盖了缺省的equals方法以便更有用些,比如String类,它的equals方法检查两个String对象是否包含同样的字符串,而Integer的equals方法检查所包含的
int
值是否相等。
大部分时候,在检查两个对象是否相等的时候你应该使用equals方法,而对于原子类型的数据,你用该使用==操作符。
八、常见错误
8
#: 混淆原子操作和非原子操作
java保证读和写
32
位数或者更小的值是原子操作,也就是说可以在一步完成,因而不可能被打断,因此这样的读和写不需要同步。以下的代码是线程安全(thread safe)的:
public
class
Example{
private
int
value;
// More code here...
public
void
set (
int
x){
// NOTE: No synchronized keyword
this
.value = x;
}
}
不过,这个保证仅限于读和写,下面的代码不是线程安全的:
public
void
increment (){
// This is effectively two or three instructions:
// 1) Read current setting of ’value’.
// 2) Increment that setting.
// 3) Write the new setting back.
++
this
.value;
}
在测试的时候,你可能不会捕获到这个错误。首先,测试与线程有关的错误是很难的,而且很耗时间。其次,在有些机器上,这些代码可能会被翻译成一条指令,因此工作正常,只有当在其它的虚拟机上测试的时候这个错误才可能显现。因此最好在开始的时候就正确地同步代码:
public
synchronized
void
increment (){
++
this
.value;
}
九、常见错误
9
#:在
catch
块中作清除工作
一段在
catch
块中作清除工作的代码如下所示:
OutputStream os =
null
;
try
{
os =
new
OutputStream ();
// Do something with os here.
os.close();
}
catch
(Exception e){
if
(os !=
null
)
os.close();
}
尽管这段代码在几个方面都是有问题的,但是在测试中很容易漏掉这个错误。下面列出了这段代码所存在的三个问题:
1
.语句os.close()在两处出现,多此一举,而且会带来维护方面的麻烦。
2
.上面的代码仅仅处理了Exception,而没有涉及到Error。但是当
try
块运行出现了Error,流也应该被关闭。
3
.close()可能会抛出异常。
上面代码的一个更优版本为:
OutputStream os =
null
;
try
{
os =
new
OutputStream ();
// Do something with os here.
}
finally
{
if
(os !=
null
)
os.close();
}
这个版本消除了上面所提到的两个问题:代码不再重复,Error也可以被正确处理了。但是没有好的方法来处理第三个问题,也许最好的方法是把close()语句单独放在一个
try
/
catch
块中。
十、常见错误
10
#: 增加不必要的
catch
块
一些开发者听到
try
/
catch
块这个名字后,就会想当然的以为所有的
try
块必须要有与之匹配的
catch
块。
C++程序员尤其是会这样想,因为在C++中不存在
finally
块的概念,而且
try
块存在的唯一理由只不过是为了与
catch
块相配对。
增加不必要的
catch
块的代码就象下面的样子,捕获到的异常又立即被抛出:
try
{
// Nifty code here
}
catch
(Exception e){
throw
e;
}
finally
{
// Cleanup code here
}
不必要的
catch
块被删除后,上面的代码就缩短为:
try
{
// Nifty code here
}
finally
{
// Cleanup code here
}
常见错误
11
#;没有正确实现equals,hashCode,或者clone 等方法
方法equals,hashCode,和clone 由java.lang.Object提供的缺省实现是正确的。不幸地是,这些缺省实现在大部分时候毫无用处,因此许多类覆盖其中的若干个方法以提供更有用的功能。但是,问题又来了,当继承一个覆盖了若干个这些方法的父类的时候,子类通常也需要覆盖这些方法。在进行代码审查时,应该确保如果父类实现了equals,hashCode,或者clone等方法,那么子类也必须正确。正确的实现equals,hashCode,和clone需要一些技巧。
转载请注明:
http://www.pmjava.com/?thread-62957-1.html
posted on 2009-06-20 03:01
rrong_m
阅读(1325)
评论(0)
编辑
收藏
Powered by:
BlogJava
Copyright © rrong_m
<
2009年6月
>
日
一
二
三
四
五
六
31
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
1
2
3
4
5
6
7
8
9
10
11
导航
BlogJava
首页
新随笔
聚合
管理
统计
随笔 - 38
文章 - 115
评论 - 0
引用 - 0
常用链接
我的随笔
我的文章
我的评论
我的参与
随笔档案
2009年7月 (1)
2009年6月 (42)
文章分类
j2ee
(rss)
java(4)
(rss)
javascript(8)
(rss)
文章档案
2009年6月 (110)
java编程
java php asp.net编程
java php asp.net编程
搜索
积分与排名
积分 - 72514
排名 - 762
最新评论
阅读排行榜
1. hibernate映射clob blob字段(4530)
2. java去标点符号 正则(3469)
3. java substring截取字符串 汉字和字母(2983)
4. java获取google 的简单天气预报(2003)
5. 递归删除父节点及所有子节点(1656)
评论排行榜
1. 博客搬家(0)
2. sql版 魔方算法(0)
3. mysql 保留字(0)
4. java去标点符号 正则(0)
5. java全角半角转换(0)