代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 4!96k~d}
S&[9Vb
EASmB
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 ; 5[W*,7s
z`Nss
o=
$II~tO
)~nieQEZQ
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 rADzJ#CU\
w0H#M)c
:1bDkoK
Kf$(7FT'`
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 L5|g\Y`
fsnZHL}=n
J
48$l(l3
#D{Eq8dp
一、常见错误1# :多次拷贝字符串 9Nv?j=*$
X$P(8'[9A
v*As:;D_
~mK+Q%G5
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 Gp)J[8j
K:AP 0Te
Nx*1m
BC
;qWSfCt/^
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: "VoufXM:
;g2UIb?{6
OkT@ _U
]Z85%q^`
String s = new String ("Text here"); _]D
6m2R
!
jDopE0L
0sme0"Sl
9pS:#hg
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: Sx0{]1J
@k'V`ZQF
j]R[;8g
TVSCjI
String temp = "Text here"; BYa#<jXtAT
String s = new String (temp); a+~b3
k:@N6K/$P^
alNn(0MG
%Kp^wf#o9
但是这段代码包含额外的String,并非完全必要。更好的代码为: :kwDa
a
E
GZiWBr
1:@ScHS
$T7 qd
String s = "Text here"; Nvh&=%{g
>w.%KVBJ
Z6Kp-z(l3
@B(E&
二、常见错误2#: 没有克隆(clone)返回的对象 F:Ps>
L=C#E0{i
:!?Fq/!
El
:%\hGy
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: #mK?:O\-1
`GCK%evLG
uf (_<~
hJk:&!M=T
import java.awt.Dimension; %4YSuZg
/***Example class.The x and y values should never*be negative.*/ Vw`Q:qo0:b
public class Example{ Pv\8 \,B9
private Dimension d = new Dimension (0, 0); %,ScGQE
public Example (){ } u3wd~.
Rxlv:
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ V U5</si+
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ zx.SRs$
if (height < 0 || width < 0) "sY}@Q7
throw new IllegalArgumentException(); b+hN\/*]
d.height = height; @qx$b~%
d.width = width; 8ZCA
vEy
} ]gaeN2
[*0M$4
public synchronized Dimension getValues(){ '#,C5*`
// Ooops! Breaks encapsulation WQD:~*C:
return d; 6uUn
} HNj;_S
} fM*?i"j;Y
5tZ0zr
,\#s_N7
qcQq.cS_'N
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: U^U
hZ!
-:J<JX)o
DVKb`KJ"
`R.Pz _oe
Example ex = new Example(); hk
S:_e=
Dimension d = ex.getValues(); UTN[!0[
d.height = -5; .P?n<n#
d.width = -10; g)|vS>^~
k"/Rjd(;
"*W# z
[fo#){3K
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 3MKu!
ucU7
@j
N`N?1!fM<}
CQrP%}`r
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 *W>, 98
-"H0Qafm
19!;0fe=
"5sA&^_#_
更好的方式是让getValues()返回拷贝: T.-tV[2
;)P=WS:=
IA]wO%c
Cx.##n0
public synchronized Dimension getValues(){ ^=1u2YdVw
return new Dimension (d.x, d.y); -o!bO9vC
} LEOa=(mN\
8pftc) k
qfxEo76'
L%QRWhB
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 &?Q^i">cZ
`ah|BV
"zCT S
tLq]#9kL
三、常见错误3#:不必要的克隆 6iF&!Fd>J
ki/Cpfq40*
(uhE'IQ{(
X7`-dSVE
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: X-,oL.:c
@7.7+blS"H
!y'>sAf
Ht\2 IP
/*** Example class.The value should never * be negative.*/ "Jg.)1Jw
public class Example{ 9PV+Kr!c5I
private Integer i = new Integer (0); k_zn>aR$F
public Example (){ } [^6z>
Iwh0PfWJ
/*** Set x. x must be nonnegative* or an exception will be thrown*/ g;nLR<]
public synchronized void setValues (int x) throws IllegalArgumentException{ -o{ x
;:4
if (x < 0) ) jvI Nb
throw new IllegalArgumentException(); =NI?Jk*iAq
i = new Integer (x); 1,Mm+_)B
} hiA\~}sl n
UL>2gl4s/
public synchronized Integer getValue(){ >w,jaQ
// We can’t clone Integers so we makea copy this way. M+HhTW;I=
return new Integer (i.intValue()); =l${p*ABQ
} Wi>m}^}9
} %N`_g' r!
6akI5\b
$?]`2*i
*FZav2]-
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 4#]g852
8~s0%%{,M
d,Oagx
WVOj;c
方法getValue()应该被写为: %iEdU V\$
]7yxXg
3(,m(+J[S
tY!l}:E[
public synchronized Integer getValue(){ udBIEW,`
// ’i’ is immutable, so it is safe to return it instead of a copy. J[hmY= ,
return i; 'g'RXC}D>
} c_M[>#`
jWi~Q o+
BmccSC;o4
:
xggo
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: x|dP-E41\
qBh@^GxY),
o$+R
-1v9
?Boolean &ni#(
?Byte 6DK).|@$r
?Character ^,AE;ZT7
?Class Q@>1z*'I
?Double Iz. h
?Float cg17e
?Integer -$0}rfX
?Long ?~t5>PEonv
?Short <g;,or#$
?String e!gNd>b {
?大部分的Exception的子类 {f)aFGp
Kl%[f jI)
dg|x(p#
SOM? 0.
四、常见错误4# :自编代码来拷贝数组 T#E$sZ
3\
Mt+!1{
<HN+pi
yI#qkl-
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: jl(D;JnF
E QU@';~8
GIc q|Pe
zuW4gJ
public class Example{ YI"!&a'yj
private int[] copy; q0Q[]|L
/*** Save a copy of ’data’. ’data’ cannot be null.*/ "RK"Pn+
public void saveCopy (int[] data){ Mog [,{w
copy = new int[data.length]; C,W_0=!e
for (int i = 0; i < copy.length; ++i) A:GqR;;"x>
copy = data; HJ]e%og
} jdu6P+_8n
} W,Q>3y*
A^X\
7sOAaWx
rA B=H*|6
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: wbKJ:eWgt
,&=7ir14>R
Xn%7{%;h
%H"
void saveCopy (int[] data){ 5CN=a2&
try{ C=q&S6/+
copy = (int[])data.clone(); h'=)dFw7
}catch (CloneNotSupportedException e){ { >izfG,\
// Can’t get here. g_P98_2f.k
} y'odn ;
} mhhc}dS(H
N~CQh=<
|^UQVNJ
JWg.0d$hM
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: fg#e*7Odn
uKM` umE
{S9gOg
3?"gfw W
static int[] cloneArray (int[] data){ iBbaHU*V
try{ $3>Rw/,
return(int[])data.clone(); %po;ih$jr*
}catch(CloneNotSupportedException e){ S}U_uZ$b
// Can’t get here. Y 'X!T8
} IO"P /Q
} ciml:"nQ
c|9g=DjK
a]V8F&)g#
h~Z &L2V
这样的话,我们的saveCopy看起来就更简洁了: zc;kNkV#1Y
1)
2-UT
V
)oXJL
^$O(oE(D
void saveCopy (int[] data){ __$ ;Z
copy = cloneArray ( data); |mn} wNUN]
} ri59LY y=
*kK +Nvt8s
l9eTghLi
.U|'KCM9m
五、常见错误5#:拷贝错误的数据 9(S=0<
';Nc;9
JJWPte/
r`6f
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: NdLe|L?c
R"O%##Ws
">1wPq&
M*3G
import java.awt.Dimension; 8Y RT0/V
/*** Example class. The height and width values should never * be WR#h~N
9c
negative. */ zzI,iEG
public class Example{ 9M9Fif.
static final public int TOTAL_VALUES = 10; F#<:ZByjJ@
private Dimension[] d = new Dimension[TOTAL_VALUES]; lg$aRqI29
public Example (){ } qtZzJ>Y
C}xfo}i
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ KP0(w(q
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ ~b)X:ku
if (height < 0 || width < 0) NwYQ6VEA
throw new IllegalArgumentException(); oz{X"jfu
if (d[index] == null) Ar/P%$Zfq
d[index] = new Dimension(); LsIZeL^
d[index].height = height; hkb\GcOj
d[index].width = width; }DjVZ48
} vqf}(/.D
public synchronized Dimension[] getValues() $+44US
throws CloneNotSupportedException{ gE@Pb
return (Dimension[])d.clone(); dS 4/spNq
} FN!?o:|(
} _('
@'r
.@nfqv7{
B\rY\
PZV>A!7C8n
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: '\8YH+%It
[Ca''JqrA
l6WEx
-d
DIQ30(MS
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ DU"Gz!X]Jd
Dimension[] copy = (Dimension[])d.clone(); 2RNee@!JJP
for (int i = 0; i < copy.length; ++i){ p2b~k[
// NOTE: Dimension isn’t cloneable. L7rr/D
if (d != null) 5TuwXz1v
copy = new Dimension (d.height, d.width); [T7&)p
} $04lL/;
return copy; A#I&&qZ
} ^C^I
|/l] ]+
By7lSbj
vS5}OV
在克隆原子类型数据的多维数组的时候,也会犯类似的错误。原子类型包括int,float等。简单的克隆int型的一维数组是正确的,如下所示: }E(w@&
(_}q>3
B:v_5e\f@
C,>n
public void store (int[] data) throws CloneNotSupportedException{ 8NNh8k#6
this.data = (int[])data.clone(); yxpv;v:)=
// OK 5,f`5'$
} !0zcS7&P
!fAvxR
HX| p4-L
R -ek O7z
拷贝int型的二维数组更复杂些。Java没有int型的二维数组,因此一个int型的二维数组实际上是一个这样的一维数组:它的类型为int[]。简单的克隆int[][]型的数组会犯与上面例子中getValues()方法第一版本同样的错误,因此应该避免这么做。下面的例子演示了在克隆int型二维数组时错误的和正确的做法: )^qXjF
Z D"*fr
qlPIxd
cL4Go,)w
public void wrongStore (int[][] data) throws CloneNotSupportedException{ $RI$VyAjD
this.data = (int[][])data.clone(); // Not OK! _ti^i\8~
} X}3?k<m
public void rightStore (int[][] data){ Kzq^f=p
// OK! ynMYf
this.data = (int[][])data.clone(); OMjPC_
for (int i = 0; i < data.length; ++i){ Zi}h\R a
if (data != null) AtHkz|sl
this.data = (int[])data.clone(); o?M ;f\Fy
} TeZu*c
} Px?"5g#+
1nvT={'R
A~E S{Zkh
8irTGA
f&5S`}C
六、常见错误6#:检查new 操作的结果是否为null I'{Ctc
*<
fJgc"3
p(GI02|n
'M? ptu?f
Java编程新手有时候会检查new操作的结果是否为null。可能的检查代码为: "-Nyf
v4 rO 0y=C
8kU(>' ^_:
l>
H'PP~
Integer i = new Integer (400); i}>EGmv m
if (i == null) n9&fH
throw new NullPointerException(); [=cbzmX[
Y~^R^J
$;ny`^8
|p*cI @
检查当然没什么错误,但却不必要,if和throw这两行代码完全是浪费,他们的唯一功用是让整个程序更臃肿,运行更慢。
X_Lt{mf
d<OdQvW.
qu$FpOJ
kl1Q:
C/C++程序员在开始写java程序的时候常常会这么做,这是由于检查C中malloc()的返回结果是必要的,不这样做就可能产生错误。检查C++中new操作的结果可能是一个好的编程行为,这依赖于异常是否被使能(许多编译器允许异常被禁止,在这种情况下new操作失败就会返回null)。在java 中,new 操作不允许返回null,如果真的返回null,很可能是虚拟机崩溃了,这时候即便检查返回结果也无济于事。 {GT5
h|'|n/F
七、常见错误7#:用== 替代.equals _M7|:*
'cS| BT
在Java中,有两种方式检查两个数据是否相等:通过使用==操作符,或者使用所有对象都实现的.equals方法。原子类型(int, flosat, char 等)不是对象,因此他们只能使用==操作符,如下所示: X5+^b({
mhU=^/X
ircL/:
qPDRB.K|}
int x = 4; Xs$a^zZ
int y = 5; T&S=/cRBK}
if (x == y) ^e]O
>CJ
System.out.println ("Hi"); #>~A-k)
// This ’if’ test won’t compile. Q8l vwip
if (x.equals (y)) gxI/MD~!>
System.out.println ("Hi"); t Jtp1$h
&l-d_dh
HtE^7i*_
A`(Cuw-o
对象更复杂些,==操作符检查两个引用是否指向同一个对象,而equals方法则实现更专门的相等性检查。 6yYd~|T.Fl
n?q+:P
s`,g4ce`
{s6#h #U
更显得混乱的是由java.lang.Object 所提供的缺省的equals方法的实现使用==来简单的判断被比较的两个对象是否为同一个。 6=Q6J
Ax@7RJ||
c-.F{~
kMEXg zl
许多类覆盖了缺省的equals方法以便更有用些,比如String类,它的equals方法检查两个String对象是否包含同样的字符串,而Integer的equals方法检查所包含的int值是否相等。 3ErV" R4"$
2|RxowXZ"
^l
;Bo3^_
!_c6 `oW
大部分时候,在检查两个对象是否相等的时候你应该使用equals方法,而对于原子类型的数据,你用该使用==操作符。 z8D,[`
5mudww`
-g9CW[
"<&o;x<
八、常见错误8#: 混淆原子操作和非原子操作 b/#<::D `
L4u.cHJ}0
-s0J8b
wax^iL!
Java保证读和写32位数或者更小的值是原子操作,也就是说可以在一步完成,因而不可能被打断,因此这样的读和写不需要同步。以下的代码是线程安全(thread safe)的: _q@lP|
e2nZwPH
? )IH#kL
|D'!.$7%
public class Example{ F$:mGyl5_
private int value; // More code here... Q3t%JP>;g
public void set (int x){ RJT55Rv{
// NOTE: No synchronized keyword l9y %@7
this.value = x; :G^4/A_
} '}>8+vU`
} O7&OCo|b%>
vj#m#1\f
oc-o>H
j~;y~Cx?
不过,这个保证仅限于读和写,下面的代码不是线程安全的: l<"B[
G[zy sxd
mkBQTQGT
.rDao]K
public void increment (){ C<^S$
// This is effectively two or three instructions: "4*QA0As
// 1) Read current setting of ’value’. &s\,+d0
// 2) Increment that setting. ^b.fci{1m
// 3) Write the new setting back. <X97W\
++this.value; +@@( C9
} 5':j=KQE_
h=NXU9n%'
4dSAGLpp
vL "noLs
在测试的时候,你可能不会捕获到这个错误。首先,测试与线程有关的错误是很难的,而且很耗时间。其次,在有些机器上,这些代码可能会被翻译成一条指令,因此工作正常,只有当在其它的虚拟机上测试的时候这个错误才可能显现。因此最好在开始的时候就正确地同步代码: <`A!9+
zrtbk~v8y
8L@@UUjr
dW^#}kN7V
public synchronized void increment (){ ~ :B/`1[m
++this.value; 0 R&7vn
} 3`"k1W
}4Gn$'e
R3BK\kf&
hH?ke(&=f
九、常见错误9#:在catch 块中作清除工作 ) I.uqG
-fK_F6_\]
$7Lcn9?G
GL&rT&
一段在catch块中作清除工作的代码如下所示: p1ER<_fp
o3OJI_
v&
"KY]2v.
bG)6p05Oa
OutputStream os = null; <&t[E0mU
try{ SQw"mO
os = new OutputStream (); K~8!Gh{h]
// Do something with os here. .d4&s7n0
os.close(); <2+FE/3L
}catch (Exception e){ `
-<S13
if (os != null) z`8>$9
os.close(); V F"c}
} kf)s3I/`(
<|a9r: [
2l8z/o 7v
i}5+\t[Q
尽管这段代码在几个方面都是有问题的,但是在测试中很容易漏掉这个错误。下面列出了这段代码所存在的三个问题: wS:`c
J
F2=#\U$
QVN@B[9
$)(Zt^
1.语句os.close()在两处出现,多此一举,而且会带来维护方面的麻烦。 @Z~0!VY
Ti5"a<R4m6
1a},(ZcdX
.noY[P8i
2.上面的代码仅仅处理了Exception,而没有涉及到Error。但是当try块运行出现了Error,流也应该被关闭。 )q%DRLD'G
@hOY&
LFQPysC
j0e1CSE
3.close()可能会抛出异常。 6rAenK-%
Y3luU&'
w6k^|."
Fw"x4w
上面代码的一个更优版本为: dC">AW
IBv9xP]BZ
Sj4 @pMh4
[#2z=Xg
OutputStream os = null; 4f,%@s)zn
try{ }e,*'mCC*
os = new OutputStream (); 9kU|?JE
// Do something with os here. N8]d0
}finally{ )$FwB6^
if (os != null) gO!:WD
os.close(); *wz6 2p
} #!M;4~Sfx
HG})VPBa
mz .uK2l{
ob=IaZ@?
这个版本消除了上面所提到的两个问题:代码不再重复,Error也可以被正确处理了。但是没有好的方法来处理第三个问题,也许最好的方法是把close()语句单独放在一个try/catch块中。 9KZLlEk5O
g*:f#u5
e&="5.ik
_&F*4t!n_
十、常见错误10#: 增加不必要的catch 块 q?Mmkh)g
Y3f2RdGl
=)XC"kUp
Hh<}~s
一些开发者听到try/catch块这个名字后,就会想当然的以为所有的try块必须要有与之匹配的catch块。 G]fx3=
knu>{a}
?|we.{
k%ckV`y
C++程序员尤其是会这样想,因为在C++中不存在finally块的概念,而且try块存在的唯一理由只不过是为了与catch块相配对。 '\Hh
kR(hUc1O
v!?>90a
jQ?6I1o
增加不必要的catch块的代码就象下面的样子,捕获到的异常又立即被抛出: I =yy
I
q \\52:\
H9T'{R*FC
X9n},}bJ"
try{ cH\.-5NQ
// Nifty code here |=4imM7
}catch(Exception e){ `Jon^&^;|
throw e; 2UjQ!g`
}finally{ *.NVc
// Cleanup code here k:kx=K5=4
} ^0&
Ea[K$NC)#
o8ADAU"
\P0>TWE
不必要的catch块被删除后,上面的代码就缩短为: @,v.Y6Ge
PaYsn *{})
5J8U] :Y)
Qa=v }d-O
try{ gS4@3BOw&.
// Nifty code here {%3sj"suB
}finally{ f\gN+4)
// Cleanup code here `G^MTDp?L+
} VE5M}kDCZ
'}NQ`\k
&7t3D?K'qX
]l4#KI@
常见错误11#;没有正确实现equals,hashCode,或者clone 等方法 P_ x9:3
ey>V^Fj
r@Tq-o
0SLS;s.GX
方法equals,hashCode,和clone 由java.lang.Object提供的缺省实现是正确的。不幸地是,这些缺省实现在大部分时候毫无用处,因此许多类覆盖其中的若干个方法以提供更有用的功能。但是,问题又来了,当继承一个覆盖了若干个这些方法的父类的时候,子类通常也需要覆盖这些方法。在进行代码审查时,应该确保如果父类实现了equals,hashCode,或者clone等方法,那么子类也必须正确。正确的实现equals,hashCode,和clone需要一些技巧。 P mgTTI
.#y.:Pb|e
E%6}p++
7nAB^~)6l
小结 Z-,'M tD
k~ZE4^dM
9.qjEe
zQQ=8#]
我在代码审查的时候至少遇到过一次这些错误,我自己也犯过其中的几个错误。好消息是只要你知道你在找什么错误,那么代码审查就很容易管理,错误也很容易被发现和修改。即便你找不到时间来进行正规的代码审查,以自审的方式把这些错误从你的代码中根除会大大节省你的调试时间。花时间在代码审查上是值得的。 xA>O4SD
h*9s^`9)