代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 e0v&wSi
[#lPT'l
&&_W,id`
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 =qIJXV
zVl(?b&CF
u^!-Z)W
y])xP%q2O
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 k3S**&i!CR
pg4M$;ED
FjkE^o>
>"zSW?
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 1ub03$pL;
h=d&@k\g
4;w_o9o
f{*G%
一、常见错误1# :多次拷贝字符串 ]E[Mv}
=
gmJJ(}HVz
#G)ZhgB^
`S$BBF;
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 -qid.
'hU&$lgMF
a l#yc
*(D_g!a
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: CFRo>G
9)l[$X
>qcir~ &
iCc@N|~
String s = new String ("Text here"); PS(LD4mD
xU67ztS'E'
@-!w,$F)%d
2)4{
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: A6TNtXk
96MRnj*Y[
`(*5yX C
a)y8MGx?
String temp = "Text here"; /oe="/y6
String s = new String (temp); 7/Ve=7]
1eiH%{w
i]9SCO
Hr96sN.R
但是这段代码包含额外的String,并非完全必要。更好的代码为: "}Ya.
el+euOV
7th&C,c&
~3/>;[!
String s = "Text here"; 0($MN]oZa
15Yy&9D
.i[Tp6'%,
o6B!ikz 8
二、常见错误2#: 没有克隆(clone)返回的对象 sx*(JM}Be
s{$c 8
LLaoND6
o*5|W9
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: 0r:8ni%cL
]<++w;#+x
ph^qQDA
B-r9\fi,
import java.awt.Dimension; r95$B6
/***Example class.The x and y values should never*be negative.*/ -I\_v*nA
public class Example{ mIl^
private Dimension d = new Dimension (0, 0); bLaD1rnGi
public Example (){ } l3l[jDa, 2
Q0ev*MS9Z
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ #t3ju^ |?
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ #l8CUg~Uj
if (height < 0 || width < 0) 9Tjvc! 4_b
throw new IllegalArgumentException(); BXyZn0k
d.height = height; ];zi3oS^
d.width = width; o8Q(,P
} !7^fji
i"sVk8+o!
public synchronized Dimension getValues(){ C.pNDpx-
// Ooops! Breaks encapsulation "6Ly?'HK
return d; G8akMd]2
} $\m=-5 0-
} y~p7&^FeR
F}i rCi47c
!Y`nKC(=z
Z*s/%4On
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: @: %}clZ
tEBf2|<
+>c)5Jih
!=k\Rr@qx
Example ex = new Example(); cs~
}k7><
Dimension d = ex.getValues(); _;X# &S(q-
d.height = -5; UmInAH4
d.width = -10; R1J"QU
0&-!v?6)
eJ2[=L'
SQa.xLU
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 B)ynF?"
bpKMQrwd
< ~x5{p
FW[<;$
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 ^>[DG]g
Es[?yft2Q<
*R1x^t+)
!>9*$E
|
更好的方式是让getValues()返回拷贝: *"j_3vAx
G0y%_"[
B^$l]cvZ
SZvw>=)a
public synchronized Dimension getValues(){ jVk|(
return new Dimension (d.x, d.y); ^x:4%%Q]l
} B]Yj"LM)
>:Q:+R;3o
s( 2=E|
|~v($ c
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 j!:U*}f
#@lr$^M
-v >BeVF
cGOE $nL
三、常见错误3#:不必要的克隆 <Hm:#<\
?CL1^N%
pB?a5jpA
OkA-=M)RI:
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: *% uv7G@%N
MeP U`M--
OdwSNG
+<bq@.x
/*** Example class.The value should never * be negative.*/ McH*J j
public class Example{ D95$
private Integer i = new Integer (0); .'D+De&y
public Example (){ } POUB{ba
^D oJ='&
/*** Set x. x must be nonnegative* or an exception will be thrown*/ BFj@Z'7P
public synchronized void setValues (int x) throws IllegalArgumentException{ Yg2z=&p-{"
if (x < 0) .B#Lt,m
throw new IllegalArgumentException(); C'7W50b
i = new Integer (x); :qgdn,Me
} wrGd40
?R"5 .3
public synchronized Integer getValue(){ ,<pql!B-
// We can’t clone Integers so we makea copy this way. Q+dBSKSK
return new Integer (i.intValue()); bs%]xf
~D;
} 69yTGUG3
} N]+x@M @^3
#Yj0'bgK
%z8@;
=p&6A^
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 Er{[83
CdTmL{Y1
$V`O%Sz
Ldir'FW
方法getValue()应该被写为: ?xUz{O0/
.7E-
>{Lfrc1
sY1@ch"
public synchronized Integer getValue(){ ;M4N=G Wd4
// ’i’ is immutable, so it is safe to return it instead of a copy. y^M'&@F
return i; Y5ebpw+B-
} pok,`yW\
V~e1CZ(2X
<| Z0|sel
,EwJg69
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: ;J?^M!l2=
3%|<U51
l\$_t2U
\Xxx5:qM
?Boolean FopD/D{
?Byte <w{W1*R9
?Character q. BqOa:
?Class EY2s${26%
?Double B#EF/\5
?Float t*.v!
?Integer du'$JtZo
?Long 9R.tkc|K
?Short 9Cf^Q3)5o
?String kQVl8KS
?大部分的Exception的子类 1{";u"q
<!DOCvd
8'g/WZY~~
Z.<1,EKi=
四、常见错误4# :自编代码来拷贝数组 z^B!-FcIz>
+H="5uO<
)](8{}wo
O@E&lP6
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: r=@h}TKv{I
bIWcL$}4Q
pLyX9C
$8_*LR$
public class Example{ o/=K:5
private int[] copy; $I1p"6
/*** Save a copy of ’data’. ’data’ cannot be null.*/ \?qXscq
public void saveCopy (int[] data){ _}JygOew
copy = new int[data.length]; rRC3^X`u
for (int i = 0; i < copy.length; ++i) .iew5.eB+
copy = data; zq1&MXR)l
} ;'J L$=
} ?kS5=&<
hb?
|fi
JZP2NB_xt
-*yj[?6
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: Iun!rv
*q8W;WaL
+[~\\X
4S"K%2'O
void saveCopy (int[] data){ 2sittP
try{ 06Uxd\E~
copy = (int[])data.clone(); ;iS}<TA
}catch (CloneNotSupportedException e){ U35}0NT _
// Can’t get here. wu
3uu1J
} V TEyqo2
} Saz+GQ G
#3/l4`/j
_f34p:B%s
!+fHdB
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: UI;!_C_
<w2Nh eM 3
9lA YCsX
?hDEFW9&^x
static int[] cloneArray (int[] data){ Ud{-H_m+
try{ c#{<|
.
return(int[])data.clone(); F1%'
zsv
}catch(CloneNotSupportedException e){ !uHI5k,f
// Can’t get here. #UXmTrZ.
} -F5U.6~`!
} ) mv}u~
z':>nw
x!"!oJG^k
\
2".Kb@=
这样的话,我们的saveCopy看起来就更简洁了: (iWNvVGS
Po^2+s(fY
n\cP17dr
88G[XkL$2
void saveCopy (int[] data){ OWq~BZ{
copy = cloneArray ( data); `yC
R.3+
} w;#9 hW&
\LM'KD pP_
7Uj[0Awn
j j$'DZk
五、常见错误5#:拷贝错误的数据 u $sX6
w y
Le3
X.UIFcK^
(Yw5X_|
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: xX"?3%y>
Tmw
:w~
.s2d
^5;Y
import java.awt.Dimension; 1/#N{rZ
/*** Example class. The height and width values should never * be eY&UFe
negative. */ ~:+g+Mf~[
public class Example{ E+ 7S:B
static final public int TOTAL_VALUES = 10; /H3,v8J@
private Dimension[] d = new Dimension[TOTAL_VALUES]; n+!.0d}6
public Example (){ } 1 1p\
z
.R-:vU880
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ H!45w;,I
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ N!Y'W)i16
if (height < 0 || width < 0) ZFdQZ=.'
throw new IllegalArgumentException(); crhck'?0
if (d[index] == null) UiYA#m
d[index] = new Dimension(); {1SxM /
d[index].height = height; Bj]0Cz
d[index].width = width; H=yD}!j
} WO}JIExy
public synchronized Dimension[] getValues() N?pD"re)6
throws CloneNotSupportedException{ mIr{Wocx
return (Dimension[])d.clone(); <o]tW4\(R
} 0kiW629o
} '2.F-~
:+R||qi
U9N}6a=
'Y&yt"cs
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: !w/~dy
K=B[MT#V{2
O~t5qnu/}
M_XZOlW5
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ B2,!
0Re
Dimension[] copy = (Dimension[])d.clone(); MA1,;pv6
for (int i = 0; i < copy.length; ++i){ ,*%8*]<=
// NOTE: Dimension isn’t cloneable. @D60
if (d != null) ,g6.d#c
copy = new Dimension (d.height, d.width); _5X}&>>lhF
} f0^s*V+
return copy; zg5u
} &[s^`e
J#X 7Ss
p 3_Q
m_]"L
在克隆原子类型数据的多维数组的时候,也会犯类似的错误。原子类型包括int,float等。简单的克隆int型的一维数组是正确的,如下所示:
NOQgkN
}$UuYO/i
GUcuD^Fe
DD-DY&2R
public void store (int[] data) throws CloneNotSupportedException{ APLu?wy7s5
this.data = (int[])data.clone(); ->j9(76 "
// OK ,3Wa~\/Q
} Q%
dpGI
d-?~O~qD|!
C'=C^X%
;nC+Kz:
拷贝int型的二维数组更复杂些。Java没有int型的二维数组,因此一个int型的二维数组实际上是一个这样的一维数组:它的类型为int[]。简单的克隆int[][]型的数组会犯与上面例子中getValues()方法第一版本同样的错误,因此应该避免这么做。下面的例子演示了在克隆int型二维数组时错误的和正确的做法: yDyq. -Q
.KB*u*h
.<j8>1
A2+t`[w
public void wrongStore (int[][] data) throws CloneNotSupportedException{ t>25IJG
this.data = (int[][])data.clone(); // Not OK! f?{Y<M~]
} OsK=% aDpj
public void rightStore (int[][] data){ -bj1y2)n
// OK! ;=n7 Z
this.data = (int[][])data.clone(); Xpa;F$VI
for (int i = 0; i < data.length; ++i){ da-3hM!u+
if (data != null) ^Krkf4fO
this.data = (int[])data.clone(); ^%<v| Y(X
} bqe;) A7
} Nh+$'6yT%
@lJGdp
Y] n^(V
4,aBNuxWd
M)LdGN?$
六、常见错误6#:检查new 操作的结果是否为null Dnp^yqz*
.oe,#1Qh{
zO+nEsf^O
:WxMv~e{U
Java编程新手有时候会检查new操作的结果是否为null。可能的检查代码为: M.128J+xfS
'!Sj]+
2+^#<Uok
|fJ,+)_(
Integer i = new Integer (400); UtWoSFZ'o!
if (i == null) x/Ds`\
throw new NullPointerException(); x x
'XR'zK
O-G@To3\
ooD/QZUE
xp39TiXJ*
检查当然没什么错误,但却不必要,if和throw这两行代码完全是浪费,他们的唯一功用是让整个程序更臃肿,运行更慢。 kO5KZ;+N-
NT-du$!u
r>G$u
hrW.TwK
C/C++程序员在开始写java程序的时候常常会这么做,这是由于检查C中malloc()的返回结果是必要的,不这样做就可能产生错误。检查C++中new操作的结果可能是一个好的编程行为,这依赖于异常是否被使能(许多编译器允许异常被禁止,在这种情况下new操作失败就会返回null)。在java 中,new 操作不允许返回null,如果真的返回null,很可能是虚拟机崩溃了,这时候即便检查返回结果也无济于事。 yrIT4y
I#mT#xs6
七、常见错误7#:用== 替代.equals :*8@MjZ4
S\f^y8*<
在Java中,有两种方式检查两个数据是否相等:通过使用==操作符,或者使用所有对象都实现的.equals方法。原子类型(int, flosat, char 等)不是对象,因此他们只能使用==操作符,如下所示: SZ0Zi\W
F>}).qx
%"z W]
L,l+1`Jz
int x = 4; dxU[>m;
int y = 5; dk.da&P
if (x == y) &zm5s*yNt
System.out.println ("Hi"); q)%C|
// This ’if’ test won’t compile. 6^wiEnA
if (x.equals (y)) $Qm;F%
>
System.out.println ("Hi");
t,H,*2
[oYe/<3
P EbB0GL
A]n!d}?
对象更复杂些,==操作符检查两个引用是否指向同一个对象,而equals方法则实现更专门的相等性检查。 Cf+O7Y`^
Vk-_v5
;lK2]
[Y:HVr,
更显得混乱的是由java.lang.Object 所提供的缺省的equals方法的实现使用==来简单的判断被比较的两个对象是否为同一个。 $4L=Dg
8ZahpB
*JDc1$H0
S#""((U$
许多类覆盖了缺省的equals方法以便更有用些,比如String类,它的equals方法检查两个String对象是否包含同样的字符串,而Integer的equals方法检查所包含的int值是否相等。 kQ:2 @SOm
i? a]v 5
|Rl|Th
jRBx7|ON
大部分时候,在检查两个对象是否相等的时候你应该使用equals方法,而对于原子类型的数据,你用该使用==操作符。 ^ZV xBQKg
KYeA=
?r@ZTuq#
yn=1b:kid
八、常见错误8#: 混淆原子操作和非原子操作 -Pvt+I>
`6M(`*Up
*}0Q S@FN
#VrT)po+
Java保证读和写32位数或者更小的值是原子操作,也就是说可以在一步完成,因而不可能被打断,因此这样的读和写不需要同步。以下的代码是线程安全(thread safe)的: 8G@FX $$Q
H;Bj\-Pa
>:K3y$]_
q!7\`>.2:{
public class Example{ !YoKKG~_0
private int value; // More code here... 1ri#hm0x\
public void set (int x){ }^ iE|YKz
// NOTE: No synchronized keyword g-]td8}#
this.value = x; kiECJ@5p
} NR3IeTd
} )-sEm`(`I9
vdo[qk\C
\k* ]w_m-
Pgo5&SQb
不过,这个保证仅限于读和写,下面的代码不是线程安全的: PJ_|=bn
rXaL1`t*
P_Zo}.{
h(zi$V
public void increment (){ 1"e=Zqn$)
// This is effectively two or three instructions: ~7=,)Q
// 1) Read current setting of ’value’. 00Rk %QV
// 2) Increment that setting. tF'67,~W
// 3) Write the new setting back. vXf#gX!Y
++this.value; .5T7O_%FP
} X(1.Hjh
_l Jj 6=
WRnUF[y+)
BE U[M
在测试的时候,你可能不会捕获到这个错误。首先,测试与线程有关的错误是很难的,而且很耗时间。其次,在有些机器上,这些代码可能会被翻译成一条指令,因此工作正常,只有当在其它的虚拟机上测试的时候这个错误才可能显现。因此最好在开始的时候就正确地同步代码: 1"k
+K~:
0r@rXwz
G
cbal:q
~q9RZ#g13J
public synchronized void increment (){ 4gZN~_AI<
++this.value; D QRt\!
} 0q3:"X
<9Chkb|B
Ne4A
qzG'Gz{{qu
九、常见错误9#:在catch 块中作清除工作 :')<|(Zy
D?E5p.!A
Wl,yznT
Xu
T|vh
一段在catch块中作清除工作的代码如下所示: ="4jk=on
G%P]qi
'dg OE
C/cyqxVl}
OutputStream os = null; c=K M[s.
try{ d,>l;l
os = new OutputStream (); V2bod=&Lc
// Do something with os here. ~:0h o
os.close(); .=NK^
}catch (Exception e){ I7TMv.
if (os != null) '3xSzsDn
os.close(); x^
Wgo`v)
} ,p2
Di
duM>(y
M\GS&K$lq
UOwj"#
尽管这段代码在几个方面都是有问题的,但是在测试中很容易漏掉这个错误。下面列出了这段代码所存在的三个问题: 8?(4E 'vf
}{ P}P}
Rw7Q[I5z%
w?R6$n`
1.语句os.close()在两处出现,多此一举,而且会带来维护方面的麻烦。 4f1*?HX&
!nd*U}q
RS93_F8
"'8$hV65.p
2.上面的代码仅仅处理了Exception,而没有涉及到Error。但是当try块运行出现了Error,流也应该被关闭。 vbWX`skU
U@*z#T#"m
Ufk7%`
*s/F4?*
3.close()可能会抛出异常。 d2(n3Xf
2
o.Mh/D0
*L!R4;ubE
n.T
[a
上面代码的一个更优版本为: y K{~
P--#5W;^oB
/f2*J
t4Z.b 5g
OutputStream os = null; cBAA32wf
try{ m3,v&Z
os = new OutputStream (); r+U-l#Q
// Do something with os here. KUp
lN1Sy
}finally{ SAqX[c
if (os != null) 6dNo!$C^
os.close(); ;+5eE`]a/L
} d)
> if<o
4A*'0!H
:|Z*aI]9
Nc7YMxk'H
这个版本消除了上面所提到的两个问题:代码不再重复,Error也可以被正确处理了。但是没有好的方法来处理第三个问题,也许最好的方法是把close()语句单独放在一个try/catch块中。 .IgCC_C9
Hu;#uAnxQ
a([cuh.
ruA!+@or
十、常见错误10#: 增加不必要的catch 块 S4\T (
hxv/285B
u=4tW:W,
9SU;c l
一些开发者听到try/catch块这个名字后,就会想当然的以为所有的try块必须要有与之匹配的catch块。 .qHgQ_%
r..Rh9v/=E
;MO
%))
i
JQS@2=A
C++程序员尤其是会这样想,因为在C++中不存在finally块的概念,而且try块存在的唯一理由只不过是为了与catch块相配对。 :0]KIybt
vm Hf$rq
tn}9(Oa)
vb$k/8JK
增加不必要的catch块的代码就象下面的样子,捕获到的异常又立即被抛出: N (43+
@NNN&%
m7d? SU
(l$bA_F\
try{ X09&S4
// Nifty code here x&7!m
}catch(Exception e){
]@<O!fS
throw e; Bq\%]2;eo{
}finally{ ? 1_*ct=g9
// Cleanup code here }3QEclZr
} yYW>)
w
5,- +&;
z S^:Ng5
d#A.A<p*
不必要的catch块被删除后,上面的代码就缩短为: m. XLpD
Xp%JPI {
RCsd
j]jwQRe
try{ 5Zh
/D0!|
// Nifty code here j{nL33T%
}finally{ )WD<Q x&