代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 $P3nP=mf
LS'=>s"
'QF>e
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 7g9 ^Jn
[<QWTMjR
H%]ch6C
|U
$-d^ZJ
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 <>s\tJ
lvi:I+VgA
'OCo1|iK~
M7,MxwZ0k
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 4sjr\9IDC
}8 _9V|E
(B<AK4G
rrYp^xLa`
一、常见错误1# :多次拷贝字符串 !`o:+Gg@
ZnLk :6'
[t{#@X
7"p s#)O
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 XWpnZFjE
~79Qg{+]N
ue<<Y"NR
vfJk?
(
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: }c ;um
E^a`IA
)O C[;>F7
K"j=_%{
String s = new String ("Text here"); 92VX5?Cyg
Gcz@ze
q-
(NZno
B@inH]wq
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: jDXGm[U
rq["O/2
2Q|*xd4B^
^jjJM| a
String temp = "Text here"; D*'M^k|1
String s = new String (temp); x9A
ZS#e)[
O>M*mTM
h!av)nhM
'8kjTf#g<l
但是这段代码包含额外的String,并非完全必要。更好的代码为: \Rqh|T<D
|#:dC #
.y9rM{h}b
=GKYroNM
String s = "Text here"; &d3 '{~:
u;ooDIq@
XW_xNkpL5c
Bi:wP/>v
二、常见错误2#: 没有克隆(clone)返回的对象 ^@lg5d3F
a {$k<@Ww
8~(+[[TQ@
S{i@=:
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: y<%.wM]-J
1 lCikS^c
-s%-*K+,W
= #2qX>?
import java.awt.Dimension; agm5D/H]:
/***Example class.The x and y values should never*be negative.*/ 'h6}cw+K
public class Example{ <&s)k
private Dimension d = new Dimension (0, 0); xT?} wF
public Example (){ } |;u%JW$4
A='+tJa
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ ->2wrOH|H
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ (<R\
if (height < 0 || width < 0) W;oU +z^t$
throw new IllegalArgumentException(); dFP-(dX#
d.height = height; u4,X.3V]A
d.width = width; ?V)C9@bp
} {&}/p-S
'=,rb
public synchronized Dimension getValues(){ $K.%un Gm
// Ooops! Breaks encapsulation }d3N`TT
return d; 4 ^~zN"6]
} %f_OP$;fc
} A6UdWK
+.(}u ,:8
|Iok(0V
O})u'
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: :O'C:n<g
E7NbPNd
yg-FJ/
Dj
]Hgg
Example ex = new Example(); ?F87C[o
Dimension d = ex.getValues(); tk)>CK11
d.height = -5; @Tfwh/UN
d.width = -10; ELrZ8&5G
]Z$TzT&@%
4&oXy,8LC
N?=qEX|R
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 mzV"G>,o
*pb:9JKi
`b.o&t$L
b1+hr(kMRM
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 f05"3L:
wCU&Xb$F
Cwsoz
ZO%fS'n
更好的方式是让getValues()返回拷贝: Z.aLk4QO@
])QO%
4kaE}uKU
@!":(@3[
public synchronized Dimension getValues(){ R?bn,T>
return new Dimension (d.x, d.y); ~.W=
} lRv#1'Y
8!uL-_ Bn
^Cc8F3os=
;zZ ,3pl-E
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 Mm5U`mB
Q3BLL`W~
N
/sEec
?z5ne??
三、常见错误3#:不必要的克隆 J})$
$^vp'^uW>
N#RD:"RS!
9raHSzK@d
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: @)OnIQN~
Q\o$**+{
W+d9cM=
w69>tC
/*** Example class.The value should never * be negative.*/ ;OQ'B=uK
public class Example{ Jw:Fj{D
private Integer i = new Integer (0); pAJ=f}",]E
public Example (){ } y3={NB+
k_*XJ <S!Y
/*** Set x. x must be nonnegative* or an exception will be thrown*/ B^i mG
public synchronized void setValues (int x) throws IllegalArgumentException{ j<l#qho{h
if (x < 0) 0NL :z1N-h
throw new IllegalArgumentException(); hi ;WFyJTu
i = new Integer (x); E/wQ+rv
} ERp:EZ'
E1c>nrnh*
public synchronized Integer getValue(){ u;+%Qh
// We can’t clone Integers so we makea copy this way. $:f.Krj
return new Integer (i.intValue()); zQL!(2
} r +p@X
} tXf}jU}
z3^RUoGU
WdTbt
$"Y3mD}?L
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 V.K70)]
n\Z^K
n!UMU ^
=gW"#ZjL){
方法getValue()应该被写为: 3
R5%N
~
9M1a*frxZ
wD<vg3e[H
~8jThi
U
public synchronized Integer getValue(){ D-Bv(/Pz]$
// ’i’ is immutable, so it is safe to return it instead of a copy. Jq#[uX
return i; *4|9&PNLE
} PzIy">plm
5c<b|
"%:7j!#X|I
\#
7@a74
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: i'M^ez)u
Qe2m8
pZu?V"R
*mf}bTiS
?Boolean 5+y@ ]5&g
?Byte Q8 -3RgAw
?Character D8k*0ei&
?Class qzz[y#q(
?Double jZa25Z00
?Float q|n97.vD
?Integer ])N|[ |$
?Long `ifb<T
?Short h^['rmd
?String XXXljh6
?大部分的Exception的子类 :L]-'\y
,`D/sNP,q
vAi"$e
{G Ub'J
四、常见错误4# :自编代码来拷贝数组 Lqg]Fd
lxm*;?j`W
ah 4kA LO
buRhQ"
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: A)OdQFet(
u06tDJ[
U%Dit
$RpFxi
public class Example{ DD2adu^
private int[] copy; /^d. &@*
/*** Save a copy of ’data’. ’data’ cannot be null.*/ \.5F](:
public void saveCopy (int[] data){ :}^Rs9 '
copy = new int[data.length]; b([:,T7
for (int i = 0; i < copy.length; ++i) 3b#L17D3_
copy = data; +IvNyj|
} <sa #|Y$
} 1d`cTaQ-
kl| g
F@g17 aa
4/b(Y4$,[r
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: HB%K|&!+
uG4$2
>Q&CgGpW$
9p5= _
void saveCopy (int[] data){ wc"9A~
try{ `q^(SM
copy = (int[])data.clone(); 64SW
}catch (CloneNotSupportedException e){ PVhik@Yoh
// Can’t get here. '[%jjUU
} |0lLl^zp
} nQ|GqU\oA
?fB5t;~E
*E.LP1xP
)5U!>,fT
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: \]t]#D>0
l/[pEUYU
b)d^ `J
~H7!MC~K
static int[] cloneArray (int[] data){ nMkOUW:T!
try{ JT}.F!q6E
return(int[])data.clone(); uN8/Q2
}catch(CloneNotSupportedException e){ :Pc(DfkS
// Can’t get here. 36nyu_h:R
} '|_/lz$h
} UAdz-)$
k; ;viT
B4IBuS
iM"asEU
这样的话,我们的saveCopy看起来就更简洁了: GKCM|Y
Vn^)
^}hJL7O'
@'
d6iYk_
void saveCopy (int[] data){ \Yd4gaY\o
copy = cloneArray ( data); Nfg{,/O
} JwB"\&'1ZS
d @m\f
76_<xUt{
yWNOG 2qAP
五、常见错误5#:拷贝错误的数据 S#mK
Pi+3
P8<hvMF
%Uf'+!4l`
[z2eCH
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: ?.Q3 pUT
Z&-tMai;
cW; H!:&
G0Hs,B@5?
import java.awt.Dimension; g>yry}>04%
/*** Example class. The height and width values should never * be &8n?
negative. */ "oe!M'aj`1
public class Example{ O:._W<
static final public int TOTAL_VALUES = 10; @`S.@^%7fO
private Dimension[] d = new Dimension[TOTAL_VALUES]; hXc}r6<B
public Example (){ } |kc@L`7s
+j.qZ8
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ t0.;nv@A0
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ b)`pZiQP
if (height < 0 || width < 0) ?2ItTrlB
throw new IllegalArgumentException(); W+\?~L.
if (d[index] == null) O@wK[(w^
d[index] = new Dimension(); yPN+W8}f
d[index].height = height; W~yLl%
d[index].width = width; zqf[Z3
} T
pD;
public synchronized Dimension[] getValues() |mOMRP#'
throws CloneNotSupportedException{ 8SZK:VE@
return (Dimension[])d.clone(); *QE"K2\5
} d8o ewkiR
} ^BiPLQ
G,|KL" H6
-?z\5z
/?P!.!W&
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: |z*>ixK
mf9hFy*<4
WqQU@sA
7 >bMzdH
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ iD714+N(
Dimension[] copy = (Dimension[])d.clone(); V&iS~V0.
for (int i = 0; i < copy.length; ++i){ |IN[uQ
// NOTE: Dimension isn’t cloneable. AG>\aV"b
if (d != null) `[Sl1saZ$S
copy = new Dimension (d.height, d.width); S/7l/DFb
} +GeWg`
\=
return copy; H%z/v|e6
} &a6,ln:P
?4[NNL
oj@g2H5P
O|e}
在克隆原子类型数据的多维数组的时候,也会犯类似的错误。原子类型包括int,float等。简单的克隆int型的一维数组是正确的,如下所示: PIxjM>
`HyF_m>\
b 4OnZ;FI
J|5Ay1eF-
public void store (int[] data) throws CloneNotSupportedException{ esI'"hVJ
this.data = (int[])data.clone(); ,Xtj;@~-
// OK AY88h$a
} #&BS
?@
y/tSGkMv
nNQ-"t
m9t$h
拷贝int型的二维数组更复杂些。Java没有int型的二维数组,因此一个int型的二维数组实际上是一个这样的一维数组:它的类型为int[]。简单的克隆int[][]型的数组会犯与上面例子中getValues()方法第一版本同样的错误,因此应该避免这么做。下面的例子演示了在克隆int型二维数组时错误的和正确的做法: H+x#gK2l
4Jykos2
Y.-S=Y
no&-YktP}
public void wrongStore (int[][] data) throws CloneNotSupportedException{ T8Na]V5
this.data = (int[][])data.clone(); // Not OK! JC2*$qu J
} X=,6d9,
public void rightStore (int[][] data){ Nfaf;;J}
// OK! LGVlc@0'
this.data = (int[][])data.clone(); fRNP#pi0u
for (int i = 0; i < data.length; ++i){ IaasHo\
if (data != null) ti2
this.data = (int[])data.clone(); =/}X$,@2
} HeozJ^u\?
} mb{q(WEPP
@GeHWv
<5IQc[3]aP
Uk'U?9O
a+
GJVJ
六、常见错误6#:检查new 操作的结果是否为null ir&.Z5=
[r9d<Zi}{
y6%<zhs
,LUTHWEo"I
Java编程新手有时候会检查new操作的结果是否为null。可能的检查代码为: ch })ivFP[
9g]M4*?C9P
"8/dD]=f^a
mi^hvks<
Integer i = new Integer (400); mH\@QdF
if (i == null) 1;&T^Gdj
throw new NullPointerException(); h(N=V|0
+tUQ
S#2[%o
'5rUe\k
检查当然没什么错误,但却不必要,if和throw这两行代码完全是浪费,他们的唯一功用是让整个程序更臃肿,运行更慢。 %VJW@S>j/
QO,+ps<
%?=)!;[
RL&lKHA
C/C++程序员在开始写java程序的时候常常会这么做,这是由于检查C中malloc()的返回结果是必要的,不这样做就可能产生错误。检查C++中new操作的结果可能是一个好的编程行为,这依赖于异常是否被使能(许多编译器允许异常被禁止,在这种情况下new操作失败就会返回null)。在java 中,new 操作不允许返回null,如果真的返回null,很可能是虚拟机崩溃了,这时候即便检查返回结果也无济于事。 XTo8,'UaP
d)KF3oA
七、常见错误7#:用== 替代.equals i!,HB|wQ
v8'5pLt"
在Java中,有两种方式检查两个数据是否相等:通过使用==操作符,或者使用所有对象都实现的.equals方法。原子类型(int, flosat, char 等)不是对象,因此他们只能使用==操作符,如下所示: ;J=:IEk
.
#U}q 7X
%),!2_ x~
JXm?2/
int x = 4; d+5:Qrr
int y = 5; D^$OCj\
if (x == y) w~N-W8xNR
System.out.println ("Hi"); _]o5R7[MQ
// This ’if’ test won’t compile. X4Xf2aXI
if (x.equals (y)) O?E6xc<8
System.out.println ("Hi"); eq hAus?)
Euu
,mleM
SRf5W'4y
* nCx[
对象更复杂些,==操作符检查两个引用是否指向同一个对象,而equals方法则实现更专门的相等性检查。 tcOnM w
$?f]ZyZr.
!nzGH*td
oAz<G
更显得混乱的是由java.lang.Object 所提供的缺省的equals方法的实现使用==来简单的判断被比较的两个对象是否为同一个。 #LWg" i
B.K4!/cF
2AK}D%jfc
kqf8=y
许多类覆盖了缺省的equals方法以便更有用些,比如String类,它的equals方法检查两个String对象是否包含同样的字符串,而Integer的equals方法检查所包含的int值是否相等。 O`(U/?
=4> @8=JA
yVYkuO
V6*?$o
大部分时候,在检查两个对象是否相等的时候你应该使用equals方法,而对于原子类型的数据,你用该使用==操作符。 *"T+G*~
TQ-KkH}y
\Tkp
e &Rb
八、常见错误8#: 混淆原子操作和非原子操作 6[+j'pW?
(9'be\
L*^
V5^-
!gJzg*{u@
Java保证读和写32位数或者更小的值是原子操作,也就是说可以在一步完成,因而不可能被打断,因此这样的读和写不需要同步。以下的代码是线程安全(thread safe)的: rKIRNc#d
C P&o%Uc*
LG6I_[
-TZ^ ~s
public class Example{ z Lw(@&
private int value; // More code here... 8]]@S"ZM,\
public void set (int x){ ArX]L$D
// NOTE: No synchronized keyword xT=ySa$|>
this.value = x; KBj@V6Q
} 0%H24N
9.
} |0]YA
hXTYTbTX
GGM5m|4
zzE]M}s
不过,这个保证仅限于读和写,下面的代码不是线程安全的: c/RT0xql*
c_DaNEfaY
G<fS(q
`#p< rfe
public void increment (){ NfqJ=9
// This is effectively two or three instructions: <(?'
s9
// 1) Read current setting of ’value’. )w3
,
// 2) Increment that setting. fFHK:n`
// 3) Write the new setting back. V8T#NJ
++this.value; J@gm@ jLc
} 1q`k}KMy
SdSgn |S
(gDQ\t@3-
ph+M3q(z
在测试的时候,你可能不会捕获到这个错误。首先,测试与线程有关的错误是很难的,而且很耗时间。其次,在有些机器上,这些代码可能会被翻译成一条指令,因此工作正常,只有当在其它的虚拟机上测试的时候这个错误才可能显现。因此最好在开始的时候就正确地同步代码: =-m(\}
6+?wnp-
X&.:H~xS+
<OIUyZS
public synchronized void increment (){ XJ O[[G`
++this.value; _hWuAJ9Qy
} 3l$E8?[Zwi
",QYDFFeF
{=qEBbM
ETxp#PZ
九、常见错误9#:在catch 块中作清除工作 ovbEmb
dB@FI
{x9j_/R
e|JIrOnc
一段在catch块中作清除工作的代码如下所示: v`
$%G
nPcxknl(pd
dDo6fP2
_TrZ'iL}T
OutputStream os = null; 7MoR9,(
try{ 5
>'66gZ
os = new OutputStream (); )O9f hj)
// Do something with os here. t@6w$5:}
os.close(); 1*L^^%w
}catch (Exception e){ tg3zXJ4k_
if (os != null) pL8H8kn
os.close(); )U]:9)
} 2G
ZF/9}
$,.3&zsy
4Q@\h=r
D/e&7^iK
尽管这段代码在几个方面都是有问题的,但是在测试中很容易漏掉这个错误。下面列出了这段代码所存在的三个问题: ; 4l-M2
s:3aRQ%
"oHp.$+K
)y(oHRCp->
1.语句os.close()在两处出现,多此一举,而且会带来维护方面的麻烦。 1]Gf)|
ijE<spG
J_|7$
l/
F|6
nwvgq
2.上面的代码仅仅处理了Exception,而没有涉及到Error。但是当try块运行出现了Error,流也应该被关闭。 J`4Z<b53
0T(O'v}.
cD5w| rm?i
vz- 9<w;>a
3.close()可能会抛出异常。 y:~eU
L^6"'#
eR7qE) h
?Y%}(3y
上面代码的一个更优版本为: uFz/PDOZ@
0K&_D)
:sU!PF[<
cLn; ,u4
OutputStream os = null; 9*BoYFw92*
try{ jMTRcj];(
os = new OutputStream (); o1
jk=
// Do something with os here. NAJ '><2
}finally{ lB=(8.
if (os != null) KrJ 5"1=
os.close(); ThjUiuWe
} sq6>DuBZz
izXbp02
=g/4{IL%
Ii|uGxEc
这个版本消除了上面所提到的两个问题:代码不再重复,Error也可以被正确处理了。但是没有好的方法来处理第三个问题,也许最好的方法是把close()语句单独放在一个try/catch块中。 W^^K0yn`@
bjuYA/w<
&,^mM'
C
CR%D\I$o
十、常见错误10#: 增加不必要的catch 块 J>><o:~@
wYZy e^7
_2NN1/F5
=n> iQS
一些开发者听到try/catch块这个名字后,就会想当然的以为所有的try块必须要有与之匹配的catch块。 $5ZR[\$
j kSc&
~D<7W4c
E~'q?LJOB
C++程序员尤其是会这样想,因为在C++中不存在finally块的概念,而且try块存在的唯一理由只不过是为了与catch块相配对。 7bctx_W&6
{CW1t5$*
,Y`'myL8W
3 %z
增加不必要的catch块的代码就象下面的样子,捕获到的异常又立即被抛出: O]c=Yyl
`6|i&w:b
d\v$%0
b{Z^)u2X
try{ xR\D(FLVS
// Nifty code here
m"96:v
}catch(Exception e){ }rO?5
throw e; 5oVLv4Z9u
}finally{ RpBiE8F4
// Cleanup code here $KoPGgC[
} x, G6\QmA
59&T