代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 -x?Z2EA!
L=(-BYS
l0&Fm:))k
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 {aE[h[=r
u6C_*i{2
fw %p_Cm
fRNj *bIV
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 BB}WfA
@3n!5XM{EE
ivo3pibk%
2I:P}!
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 LJrH_h8C
0+mR
y57
9fp"r,aHN&
m{>1#1;$t
一、常见错误1# :多次拷贝字符串 Z|K HF"
|QS|\8g{0V
Rk9n,"xpv
yz [pF
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 aG1Fj[,
q}i#XQU
T4x%3-4;
.XgY&5Qk
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: wPU5L*/*i
Y6wr}U
$mxG-'x%K
:V(C+bm *
String s = new String ("Text here"); WvU[9ME^)
%:C6\4
a;$V;3C{b&
WX Fm'5Vr
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: W~H`{x%Av>
/[c_,G""
/J}G{Y
|n
Qi\]='C
String temp = "Text here"; g_4%M0&AX
String s = new String (temp); x)80:A}
`n,RC2yo
h.-L_!1B7
&. _"rhz
但是这段代码包含额外的String,并非完全必要。更好的代码为: `K VSYC
39^+;Mev
=U84*HAv
$`OyGeq"T
String s = "Text here"; {"jtR<{)
@o[ZJ4>*
m
70r'b]
Q'U!
二、常见错误2#: 没有克隆(clone)返回的对象 gZHgL7@
N5 sR
AXcmN
mBIksts5h
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: P^o@x,V!&
U/FysN_N!
ttr`
!ak760*A
import java.awt.Dimension; e!Z}aOeE
/***Example class.The x and y values should never*be negative.*/ M_0f{
public class Example{ [Zdrm:=]L
private Dimension d = new Dimension (0, 0); 8XV RRk
public Example (){ } 6b*xhu\
`C_qqf
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ i^WY/ OhL
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ 'xd8rN%T
if (height < 0 || width < 0) Xcfd]29
throw new IllegalArgumentException(); L0VZ>!*o
d.height = height; H8g6ZCU~
d.width = width; h5P ]`r
} vo Et\H
yIiVhI?X
public synchronized Dimension getValues(){ 62;xK-U
// Ooops! Breaks encapsulation nK< v
return d; (e_<~+E
} %i7U+v(d
} UNSXr`9
y?cN
0.m-}
G9&2s%lu.e
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: I>rTqOK
IqlCl>_j
[qY yr
BN(=LQ2["
Example ex = new Example(); 1z|bQ,5
Dimension d = ex.getValues(); 7Z9'Y?[m
d.height = -5; yC
?p,Ci,
d.width = -10; =LY`K#
9PV]bt,
_KloX{a
KKQT?/ {b
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 oFp1QrI3k8
U6|T<bsOl
l4mRNYv)z
ZweAY.]e
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 D,*|:i
kE6/d,
RU#}!Kq
*]/iL#
更好的方式是让getValues()返回拷贝: Slo^tqbG
pC,Z=+:
J e|
3ouy-SQ
public synchronized Dimension getValues(){ gdSqG2/&
return new Dimension (d.x, d.y); >+<b_q|P
} rYV]<[?~7
aZo}Ix:/
%Un wh1VG
7f3,czW
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 4n.JRR&;
PN99 R]K0g
P3!@}!r8
tf54EIy5Y
三、常见错误3#:不必要的克隆 Q"NZE
f.j<VKF}
3S#p4{3
A|K=>7n]U
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: h$sOJs~6h
!\VEUF,K?
s%rmfIp"
MrUjqv6a[
/*** Example class.The value should never * be negative.*/ =!DX,S7
public class Example{ u,:hT]
~+
private Integer i = new Integer (0); GL>YJ%
public Example (){ } Yx,E5}-
zC:Pg4=w]
/*** Set x. x must be nonnegative* or an exception will be thrown*/ =mX26l`B
public synchronized void setValues (int x) throws IllegalArgumentException{ o=!_.lDF:
if (x < 0) %R?WkG
throw new IllegalArgumentException(); &=S:I!9;;
i = new Integer (x); `, ]ui*
} 1D)0\#><
hMz)l\0
public synchronized Integer getValue(){ &2.DZ),L
// We can’t clone Integers so we makea copy this way. y4@gw.pt
return new Integer (i.intValue()); K2Ro0
} D=%1?8K
} ^uG^>Om*
y5*zyd
]8"U)fzmc.
(#6Fg|f4Y
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 aeNbZpFQ
czT2f
bbjEQby
o,?G(
方法getValue()应该被写为: OqRRf
]zAwKuIK
u{HO6s\S
p<\!{5:
public synchronized Integer getValue(){ &N= vs
// ’i’ is immutable, so it is safe to return it instead of a copy. kf<c[ su
return i; CvZ\Z472.j
} N3lz-vP-
%A3m%&(m&%
WB_BEh[>j
OXpN8Dh5
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: LibQlNW\
IS!OO<
WC=d@d)M
Vh;|qF 9
?Boolean ~uq010lMno
?Byte `YwJ.E
?Character }%PK %/ zI
?Class o_b3G
?Double |ssl0/nk
?Float >r\GB#\5
?Integer
mT -[I<
?Long $aU.M3
?Short .Mb0++% W
?String 7BINqVS&
?大部分的Exception的子类 =Yl ea,S
dR_6j}
'
=5B
smQl^
6a
四、常见错误4# :自编代码来拷贝数组 Nr]Fh
Sx
J0Y8#z
HnjA78%i
\1<|X].jNY
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: !"yr;t>|Zb
7T6Zlp
,W[J@4.
?Be}{Qqlg
public class Example{ G9Kck|50
private int[] copy; uxDM
#
/*** Save a copy of ’data’. ’data’ cannot be null.*/ A/:_uqm4
public void saveCopy (int[] data){ (K8Ob3zN_
copy = new int[data.length]; ?ZGsh7<k
for (int i = 0; i < copy.length; ++i) U$OI]Dd9
copy = data; R ai
04
} H#V&5|K%
} >EFWevT{
p[xGL }
+\
yZ[g2*1L
N>*+Wg$Ne
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: U/kQw rM
zdU46|!u
AIn/v`JeX
EZjtZMnj
void saveCopy (int[] data){ h/{1(c}
try{ >P@VD"U
copy = (int[])data.clone(); T^`; wD
}catch (CloneNotSupportedException e){ li\=mH,Wr
// Can’t get here. lqMr@
:t
} 6i+,/vr
} -3)jUzD
[|c%<|d2
j-R*!i
y2jw3R
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: itirh"[
,>b>I#{
*IWW,@0
WG6
0
static int[] cloneArray (int[] data){ 2YKa <?_
try{
&qdhxc4
return(int[])data.clone(); A&Aj!#
}catch(CloneNotSupportedException e){ S :}"gwFM
// Can’t get here. &*7KQd
} 9NU0K2S
} Kw?3joy
/u.ZvY3,
3BCD0
%8
#6ePwd
这样的话,我们的saveCopy看起来就更简洁了: _ pz}
LOi}\O8
wxc#)W
I-r+1gty
void saveCopy (int[] data){ wz69Yw7
copy = cloneArray ( data); OrM1eP"I
} 3Y2~HuM
<C(o0u&/
OHpV%8`
B T"R"w
五、常见错误5#:拷贝错误的数据 +ppA..1
a=j'G]=
u)<s*jk
-c0ypz
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: 7>j~;p{
5a_8`csu
PgK7CG7G
y-bUVw!Y
import java.awt.Dimension; ?hkOL$v<9}
/*** Example class. The height and width values should never * be n8F5z|/
negative. */ "t.`/4R2w
public class Example{ q{Z#}|km#
static final public int TOTAL_VALUES = 10; m?<E >-bI
private Dimension[] d = new Dimension[TOTAL_VALUES]; ~o%igJ
}.C
public Example (){ } xH*X5?
HVHv,:bPo
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ qJdlZW<
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ )'U0n`=
if (height < 0 || width < 0) A/'po_'uy
throw new IllegalArgumentException(); ]1<GZ`
if (d[index] == null) 9/(jY$Ar
d[index] = new Dimension(); 3)W zX
d[index].height = height; h5@GeYda
d[index].width = width; gd*Gn"
} b@;Wh-{d
public synchronized Dimension[] getValues() [TFJb+N&
throws CloneNotSupportedException{ X^ Is-[OvE
return (Dimension[])d.clone(); P7.bn
} PY^#hC5:
} !>`Fg>uy
JaRsm'SIk~
n^T,R
kUgfFa#_
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: ~.%HZzR6&
<ErX<(0`ig
)|lxzlk
pqfX}x
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ R^*baiXVI
Dimension[] copy = (Dimension[])d.clone(); }LT&BNZj
for (int i = 0; i < copy.length; ++i){ dg24h7|]
// NOTE: Dimension isn’t cloneable. %A$&9c%
if (d != null) 9dhEQ=K{3
copy = new Dimension (d.height, d.width); 8XB[CbO
} ^'V :T Y
return copy; rKrHd
} f
5v&4
?@.v*'qR
Jo\P,-\(
h<Aq|*
在克隆原子类型数据的多维数组的时候,也会犯类似的错误。原子类型包括int,float等。简单的克隆int型的一维数组是正确的,如下所示: ;ItH2Lw<&
K"0IW A
;v:(
{?H5Pw>{%h
public void store (int[] data) throws CloneNotSupportedException{ ;KlYiu
this.data = (int[])data.clone(); XnQR(r)pR2
// OK Ku75YFO,5
} W#p7M[
-[=eVS.2%
CBEf;Ig
r0XEB,}
拷贝int型的二维数组更复杂些。Java没有int型的二维数组,因此一个int型的二维数组实际上是一个这样的一维数组:它的类型为int[]。简单的克隆int[][]型的数组会犯与上面例子中getValues()方法第一版本同样的错误,因此应该避免这么做。下面的例子演示了在克隆int型二维数组时错误的和正确的做法: 2jFuF71
u
S1O-Q>
@x}"aJgl
kyJbV[o<#
public void wrongStore (int[][] data) throws CloneNotSupportedException{ yWi-ic
[n
this.data = (int[][])data.clone(); // Not OK! DW. w=L|5R
} RSp wU;o6z
public void rightStore (int[][] data){ -!j6&
// OK! q<dG}aj
this.data = (int[][])data.clone(); cs+3&T:,*
for (int i = 0; i < data.length; ++i){ eThaH0
if (data != null) $eYL|?P50h
this.data = (int[])data.clone(); <e2l@@#oy
} 1 ~zjsi
} lT|Gkm<G
l_^SU8i57
1[!v{F%]
!!y]pMjJa@
t}YcB`q)
六、常见错误6#:检查new 操作的结果是否为null ?*fY$93O
\VNu35* J|
7FG;fJ;&NZ
S(zp_
Java编程新手有时候会检查new操作的结果是否为null。可能的检查代码为: E~%n-A
h1w({<q*ov
l6/VJ~(}'
K92j BR
Integer i = new Integer (400); m4mE7Wn.3
if (i == null) O[Vet/^)
throw new NullPointerException(); MuoE~K2
<\^0!v
QqA=QTZ}
v'W{+>.
检查当然没什么错误,但却不必要,if和throw这两行代码完全是浪费,他们的唯一功用是让整个程序更臃肿,运行更慢。 l P F326e
i2,4:M)CV
1RRE{]2v#
VeYT[Us"
C/C++程序员在开始写java程序的时候常常会这么做,这是由于检查C中malloc()的返回结果是必要的,不这样做就可能产生错误。检查C++中new操作的结果可能是一个好的编程行为,这依赖于异常是否被使能(许多编译器允许异常被禁止,在这种情况下new操作失败就会返回null)。在java 中,new 操作不允许返回null,如果真的返回null,很可能是虚拟机崩溃了,这时候即便检查返回结果也无济于事。 X Q#K1Z
0gd`W{YP
七、常见错误7#:用== 替代.equals wFJf"@/vJ
3p0v
在Java中,有两种方式检查两个数据是否相等:通过使用==操作符,或者使用所有对象都实现的.equals方法。原子类型(int, flosat, char 等)不是对象,因此他们只能使用==操作符,如下所示: >h\y1IrAaG
Eomfa:WL
7D6`1&
{&=+lr_h?
int x = 4; YB 38K(
int y = 5; TN(Vzs%
if (x == y) xyp{_ MZ
System.out.println ("Hi"); 8xPt1Sotq[
// This ’if’ test won’t compile. hNN>Pd~;
if (x.equals (y)) EeW
,-I
System.out.println ("Hi"); -S'KxC
!5`MiH
.-d'*$
yJ
xXe3E&
对象更复杂些,==操作符检查两个引用是否指向同一个对象,而equals方法则实现更专门的相等性检查。 mZ+!8$1X
@^{`!>Vt
XO+BZB`F
M/N8bIC! Q
更显得混乱的是由java.lang.Object 所提供的缺省的equals方法的实现使用==来简单的判断被比较的两个对象是否为同一个。 vO}r(kNJ
PG&t~4QM`
XF!L.' zH
|P
>"a`
许多类覆盖了缺省的equals方法以便更有用些,比如String类,它的equals方法检查两个String对象是否包含同样的字符串,而Integer的equals方法检查所包含的int值是否相等。 ,md_eGF
fiGTI}=P
K:,V>DL
xfYKUOp/
大部分时候,在检查两个对象是否相等的时候你应该使用equals方法,而对于原子类型的数据,你用该使用==操作符。 PkvW6,lS
;4nY{)bD
>y3FU1w5d
>q"dLZ
八、常见错误8#: 混淆原子操作和非原子操作 [wGj?M}
%K6veB{M
7%*#M#(T
&jE\D^>ko
Java保证读和写32位数或者更小的值是原子操作,也就是说可以在一步完成,因而不可能被打断,因此这样的读和写不需要同步。以下的代码是线程安全(thread safe)的: I!lDKS,b
Cv**iW
1}(22Q;
TeHJj`rdAU
public class Example{ O~3
A>j
private int value; // More code here... u{sHuVl
public void set (int x){ Ku(YTXtK
// NOTE: No synchronized keyword 1oQw)X
this.value = x; /<rvaR
} J"`VA_[
} @<\oM]jX
(GJtTp~2C4
_Mw3>GNl
D2$9$xeR
不过,这个保证仅限于读和写,下面的代码不是线程安全的: UB$}`39@
w!F>fcm
s<I)THC
AO-5>r
public void increment (){ IMf|/a9-
// This is effectively two or three instructions: 8 v/H;65
// 1) Read current setting of ’value’. %U\,IO `g
// 2) Increment that setting. lw@Yn>eza
// 3) Write the new setting back. 3&hR#;,"X
++this.value; zp}7p~#k^
} p<5]QV7st
\<7Bx[/D4
/Hr|u
B2;P%B
在测试的时候,你可能不会捕获到这个错误。首先,测试与线程有关的错误是很难的,而且很耗时间。其次,在有些机器上,这些代码可能会被翻译成一条指令,因此工作正常,只有当在其它的虚拟机上测试的时候这个错误才可能显现。因此最好在开始的时候就正确地同步代码: uo"<}>iJ
\Zj%eW!m
H*=cw<
}z`x-(V
public synchronized void increment (){ hb`9Vn\-E
++this.value; \|PiQy*_?
} Z@bgJL83
-CvmZ:n
dbf<k%i6
]s\r3I]
九、常见错误9#:在catch 块中作清除工作 z !K2UTX
7HPwlS
jSI1tW8
(TZK~+]@sb
一段在catch块中作清除工作的代码如下所示: "qmSwdM
*C_A(n5"V
mskG2mA
4.O) /0sU
OutputStream os = null; XZE(& (s
try{ G5}_NS/
os = new OutputStream (); b}!
cEJY
// Do something with os here. "wcaJ;Os
os.close(); +~8Lc'0aA
}catch (Exception e){ 8zK#./0\
if (os != null) K[T0);hZR
os.close(); VVJ0?G
(?
} j7}mh
,=)DykP
zluq2r
\BHZRytQF
尽管这段代码在几个方面都是有问题的,但是在测试中很容易漏掉这个错误。下面列出了这段代码所存在的三个问题: ,rB(WKU
/YJo"\7
01.q9AGy
GfONm6A
1.语句os.close()在两处出现,多此一举,而且会带来维护方面的麻烦。 a\P :jgF
+XWTu!
?_eLrz4>L^
FB6Lz5:Vf
2.上面的代码仅仅处理了Exception,而没有涉及到Error。但是当try块运行出现了Error,流也应该被关闭。 <*5S7)]BP
wB)y@w4k
;[y( 14g
gj^)T_E_
3.close()可能会抛出异常。 F_@B ` ,
e{x>u(
ZqclmCi
SeHrj&5U
上面代码的一个更优版本为: S{^x]h|?
bxE~tsM"@Y
aL(G0@(
j4XVk@'OX
OutputStream os = null; ka_m
Q<{9
try{ #9GfMxH
os = new OutputStream (); f ,e]jw@
// Do something with os here. vHi%UaD-y
}finally{ ]
(e ,J
if (os != null) utck{]P
os.close(); tA1?8`bQ
} wDvu2iC=
u!X~!h-6~
[RBSUOF
"(=g7,I4
这个版本消除了上面所提到的两个问题:代码不再重复,Error也可以被正确处理了。但是没有好的方法来处理第三个问题,也许最好的方法是把close()语句单独放在一个try/catch块中。 o*K7(yUL4
0>Y3xNb
|k}<Zz1UM
8g-u
十、常见错误10#: 增加不必要的catch 块 %n$f#Ml_r
g 4+K"Q/M
An_(L*Qz
`:&RB4Z
一些开发者听到try/catch块这个名字后,就会想当然的以为所有的try块必须要有与之匹配的catch块。 N82 6xvA
lf"w/pb'
EjfQF C
EV6R[2kl
C++程序员尤其是会这样想,因为在C++中不存在finally块的概念,而且try块存在的唯一理由只不过是为了与catch块相配对。 b
ri[&=
i*$+>3Q-
&4OOW;,?<
L}
R"1O
增加不必要的catch块的代码就象下面的样子,捕获到的异常又立即被抛出: HzM\<YD
pCt2-aam
i ;B^I8
5WI
bnV@
try{ d>[i*u,]/
// Nifty code here b36{vcs~
}catch(Exception e){ 2)IM<rf'^
throw e; #?)6^uTW
}finally{ j \rGU){
// Cleanup code here 0er|QC
} p@pb[Bx~[
+pYgh8w@
w10~IP
|47t+[b
不必要的catch块被删除后,上面的代码就缩短为: > %KEMlKZ
"E+;O,N-
w6Gez~8
/T6bc^nOW
try{ *Xnf}Ozx
// Nifty code here ?=lb@U
}finally{ U-DQ?OtmC@
// Cleanup code here |mMsU,*gB
} R+.4|1p
k2Cq9kQ q
XoD:gf
^?{&v19m
常见错误11#;没有正确实现equals,hashCode,或者clone 等方法 B-g-T>8
'jO2pH/%
_N;@jq\q
+C\79,r
方法equals,hashCode,和clone 由java.lang.Object提供的缺省实现是正确的。不幸地是,这些缺省实现在大部分时候毫无用处,因此许多类覆盖其中的若干个方法以提供更有用的功能。但是,问题又来了,当继承一个覆盖了若干个这些方法的父类的时候,子类通常也需要覆盖这些方法。在进行代码审查时,应该确保如果父类实现了equals,hashCode,或者clone等方法,那么子类也必须正确。正确的实现equals,hashCode,和clone需要一些技巧。 QyCrz{/
TDw~sxtv&
E^J &?-
}@LIb<Y
小结 0V6, &rTF
Xr^ 5Th\
rhLhFN{h
@(L}:]{@
我在代码审查的时候至少遇到过一次这些错误,我自己也犯过其中的几个错误。好消息是只要你知道你在找什么错误,那么代码审查就很容易管理,错误也很容易被发现和修改。即便你找不到时间来进行正规的代码审查,以自审的方式把这些错误从你的代码中根除会大大节省你的调试时间。花时间在代码审查上是值得的。 A< .5=E,/
L:C/PnIV