代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 ]9S`[c$
Ca $c;
" B@jfa%
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 pyW u9
BZF,=v
]R_G{%
cQFR]i
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 chAan~r[*
1=Ilej1
f8:$G.}i
p`+VrcCBOd
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 uiBTnG"
I*1S/o_xI
Eo{EKI1
RFG$X-.e
一、常见错误1# :多次拷贝字符串 "6I[4U"@
&(&
!g0cC.'
XSB8z
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 GF--riyfB
iY.eJlfH
KC&`x|
<Ns &b.\h6
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: >v0 :qN7|
{&nV4c$v
\/Ij7nD`l%
ZxS&4>.
String s = new String ("Text here"); 3DoRE2}
~/`X*n&
WSI
Xj5R
(Imp
$
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: IG / $!*E
=wA5P@
Rk<%r k
DA
LQ<iF
String temp = "Text here"; 9)yG.9d1
String s = new String (temp); Ob(leL>ow
bx(w:]2
M@^U0
?
=&0U`P$`
但是这段代码包含额外的String,并非完全必要。更好的代码为: o1YU_k<#
xVR:;
Jy[
_9h.Gt
}~*rx7p
String s = "Text here"; lvufk VG|
XN;/nU
6D9o08
E8tD)=1
二、常见错误2#: 没有克隆(clone)返回的对象 y-cw~kNPP3
[(cL/_
,z66bnjO
(G5xkygR9
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: OKQLv+q5K)
!s-/0ugZ
w<d*#$[,*
Y(GW0\<
import java.awt.Dimension; SLA#= K
/***Example class.The x and y values should never*be negative.*/ >}F? <JB
public class Example{ L<@&nx
private Dimension d = new Dimension (0, 0);
~R!gJTO9
public Example (){ } #K`B<2+T
Bz]J=g7
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ $GF&x>]]
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ @Qo,p
if (height < 0 || width < 0) A1<k1[5fJ
throw new IllegalArgumentException(); MYTS3(
d.height = height; `D)S-7BR
d.width = width; KF$ %q((
} R ]=SWE}U
MhH);fn
public synchronized Dimension getValues(){ 5<r)+?!n
// Ooops! Breaks encapsulation apaIJ+^[
return d; \UtS>4w\
} )[DpK=[N^p
} ;xW{Ehq-h
Mw|SH;nM
#KJZR{
N<bD
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: n1)'cS5}
gX"T*d>y
Y~GUR&ww0n
w)<4>(D
Example ex = new Example();
?zE<
Dimension d = ex.getValues(); 4[H,3}p9H
d.height = -5; -wIM0YJ
d.width = -10; Y\>\[*.v
!47A$sQ
bBFwx @
;8EjjF [>
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 )]]|d
auA.6DQ
s7Qyfe&>
n +dJc
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 eH
`t \n
%o-jwr}O{
7NUenCdc
|QqWVelc
更好的方式是让getValues()返回拷贝: Wbs^(iUU}
Ei|0L$NCg
cqHw^{'8
vK`S!7x'&
public synchronized Dimension getValues(){ I tgH>L'
return new Dimension (d.x, d.y); Qf~| S9,
} ]kH}lr
yG
;<VR2U`
intvlki]be
"9u-lcQ\
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 67,3i~
I`#EhH
p1uN]T7>
=jBL'|k5
三、常见错误3#:不必要的克隆 :X|AW?*
AYYRxhv_,
7ozYq_ $
TwwIt5_fN
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: 1+FYjh!2t
]t7ClT)n!
w=gQ3j#s
1y(iE C
/*** Example class.The value should never * be negative.*/ ] :GfOgo
public class Example{ 6e&g$R
v
private Integer i = new Integer (0); (S 3jZ
public Example (){ } `-5cQ2>"
hX %s]"
/*** Set x. x must be nonnegative* or an exception will be thrown*/ TR|;,A[%v#
public synchronized void setValues (int x) throws IllegalArgumentException{ 4KZ SL:A
if (x < 0) >5df@_'
throw new IllegalArgumentException(); )e#fj+>x)
i = new Integer (x); `GP3D~
} 7ia"u+Y
S{Rh'x\B
public synchronized Integer getValue(){ H.)fOctbO
// We can’t clone Integers so we makea copy this way. IS .g);Gj
return new Integer (i.intValue()); U=M#41J
} 2kC^7ZAwu
} Za34/ro/T
-wBnwn-
Y<de9Z@
IZ|c<#r6
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 dV$3u"9
"C?:T'dW
\8
g.
1k0^6gE|
方法getValue()应该被写为: ?V.ig
W6hNJb
R<_mK33hd
h#v L5At
public synchronized Integer getValue(){ 3s#|Y,{?6R
// ’i’ is immutable, so it is safe to return it instead of a copy. !Q[;5Lqt
return i; W&WB@)ie
} m]E o(P4+
,&-S?|
BllDWKb
<r@bNx@T
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: ryz/rf
]cS&8{ ^2
IQo]9Lx
=H
L9Z
?Boolean iM4mkCdOO
?Byte @F>[DW]O
?Character nm<L&11
?Class p, !1 3X
?Double (Be$$W
?Float J!ln=h
?Integer |Tj`qJGVw
?Long L;L2j&i%v)
?Short 9Kq<\"7Bmz
?String ?<-wHj)
?大部分的Exception的子类 Y=PzN3
y-D>xV)n
L;
@aE[#z
F%w\D9+P
四、常见错误4# :自编代码来拷贝数组 E
`?S!*jm
&;'w8_K"^
JkRGt Yq
9)8*FahW
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: hB?U5J
wn&[1gBxM
kO
/~i
H0 {Mlu9
public class Example{ bWhJ^LD
private int[] copy; s{b0#[
/*** Save a copy of ’data’. ’data’ cannot be null.*/ >1_Dk7E0D
public void saveCopy (int[] data){ 2l]C55p)s
copy = new int[data.length]; :-W$PIBe
for (int i = 0; i < copy.length; ++i) clij|?O
copy = data; pT|./ Fe
} H&"_}
} I^6c0`
&Z;Eu'ia
5%vP~vy_}
sE(X:[Am
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: .D>A'r8U
D'U\]'.
+H5 jRw
F#zQQ)(Pf
void saveCopy (int[] data){ i4 y(H
try{ Lh8#I&x
copy = (int[])data.clone(); PX+"" #
}catch (CloneNotSupportedException e){ p\4h$."
// Can’t get here. Br_3qJNVP
} 2b{@]Fp
} ylo]`Nq
TXY
AX!Md:s
t!+%g) @
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: 7$E2/@f
%3#b6m~
q[T_*X3o
EbHUGCMO
static int[] cloneArray (int[] data){ 7`j|tb-
try{ 0B#rqTEKu
return(int[])data.clone(); mP`,I"u
}catch(CloneNotSupportedException e){ #t5JUi%in*
// Can’t get here. <"j"h=tm}
} _dH[STT
} |\yDgs%EGy
[kU[}FT
gwkZk-f\p
S1 R #]
这样的话,我们的saveCopy看起来就更简洁了: g[uE@Gaj&
x<)!$cg
?CL z@u~
"N=&4<]I5
void saveCopy (int[] data){ :6HiP&<
copy = cloneArray ( data); z^SN#v$
} Au\=ypK
K~9 jin
am)J'i,
r(`8A:#d
五、常见错误5#:拷贝错误的数据 jHUz`.8B
:Kt mSY
cqU$gKT
1bFEx_
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: GtGyY0
k_.j%
]c~ rPi
n^I|}u\
import java.awt.Dimension; ^O,6(@>
/*** Example class. The height and width values should never * be xq#]n^
negative. */ )2*|WHO
public class Example{ $$)<(MP3
static final public int TOTAL_VALUES = 10; .WPuQZ!
private Dimension[] d = new Dimension[TOTAL_VALUES]; )Uoe~\
public Example (){ } /Wta$!X{-
P89Dg/P
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ :W1tIB
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ )G F
if (height < 0 || width < 0) hyr5D9d
throw new IllegalArgumentException(); _^,[wD
if (d[index] == null) LXOF{FG
d[index] = new Dimension(); +eVpMD(
l
d[index].height = height; 3mnL V*aRt
d[index].width = width; J>&dWKM3
} ~>wq;T:=
public synchronized Dimension[] getValues() +O%a:d%
throws CloneNotSupportedException{ Qr xO
erp
return (Dimension[])d.clone(); 4'u|L&ow
} .x9nWa
} YH:W]
r>D[5B
!{|yAt9kP
x,@O:e
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: o2t@-dNi
DrYoC7
9Y*Vz QE
".7KEnx
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ DNTRLIKa
Dimension[] copy = (Dimension[])d.clone(); 34&$_0zn
for (int i = 0; i < copy.length; ++i){ {pi67"mYp
// NOTE: Dimension isn’t cloneable. B3i=pcef
if (d != null) q'U-{~q%
copy = new Dimension (d.height, d.width); 'e8d["N
} @a{v>)
return copy; S@rsQ@PA
} IcNI uv
l.LFlwt
!&:.Uh
p>hCh5
在克隆原子类型数据的多维数组的时候,也会犯类似的错误。原子类型包括int,float等。简单的克隆int型的一维数组是正确的,如下所示: -_H2FlB
6!Ap;O^*
d+wNGN
R;I-IZS:
public void store (int[] data) throws CloneNotSupportedException{ P+h<{%:*
this.data = (int[])data.clone(); _jI)!rfb
// OK eWt>^]H~
} ~O3uje_
"NI>HO.U
d4rJ?qw
_}%#Yz
拷贝int型的二维数组更复杂些。Java没有int型的二维数组,因此一个int型的二维数组实际上是一个这样的一维数组:它的类型为int[]。简单的克隆int[][]型的数组会犯与上面例子中getValues()方法第一版本同样的错误,因此应该避免这么做。下面的例子演示了在克隆int型二维数组时错误的和正确的做法: f0s<Y
^IegR>
[!|d[
!t
[%'!v
public void wrongStore (int[][] data) throws CloneNotSupportedException{ &JzF
this.data = (int[][])data.clone(); // Not OK! &-.eu
} 97=YFK~*
public void rightStore (int[][] data){ 'c\TMb.
// OK! b|C,b"$N0
this.data = (int[][])data.clone(); XdXS^QA.s
for (int i = 0; i < data.length; ++i){ ^i,0n}>
if (data != null) %EhU!K#[
this.data = (int[])data.clone(); x#xO {
} 6P[O8
} /[|md0,
;$&5I9N
t7`Pw33#kY
a!]QD`
2ZFKjj
六、常见错误6#:检查new 操作的结果是否为null T<~[vjA
iZqFVr&JF
o+WrIAR
Rhxm)5 +
Java编程新手有时候会检查new操作的结果是否为null。可能的检查代码为: loVvr"&g
6je%LHhL
BN>$LL
AG!a=ufc0
Integer i = new Integer (400); @9Pn(fd]
if (i == null) aLo>Yi
throw new NullPointerException(); YedipYG9;
q|_ 5@Ly
!ES#::;z?
LR?#H)$
检查当然没什么错误,但却不必要,if和throw这两行代码完全是浪费,他们的唯一功用是让整个程序更臃肿,运行更慢。 vnOF$6n
rMFf8D(Y
(N>ew)Ke
CX2q7azG
C/C++程序员在开始写java程序的时候常常会这么做,这是由于检查C中malloc()的返回结果是必要的,不这样做就可能产生错误。检查C++中new操作的结果可能是一个好的编程行为,这依赖于异常是否被使能(许多编译器允许异常被禁止,在这种情况下new操作失败就会返回null)。在java 中,new 操作不允许返回null,如果真的返回null,很可能是虚拟机崩溃了,这时候即便检查返回结果也无济于事。 :JG}%
*j; r|P;g
七、常见错误7#:用== 替代.equals YuW\GSV00
])";Z
在Java中,有两种方式检查两个数据是否相等:通过使用==操作符,或者使用所有对象都实现的.equals方法。原子类型(int, flosat, char 等)不是对象,因此他们只能使用==操作符,如下所示: YQd&rkr
bI0+J)
~Am
%%$
17i@GnbNb
int x = 4; .j@n6RyN
int y = 5; @ dU3d\!}
if (x == y) OnPLz"-
System.out.println ("Hi"); hA19:H=7R0
// This ’if’ test won’t compile. m!>'}z
if (x.equals (y)) bWzc=03
System.out.println ("Hi"); h |
R$3+ 01j|
d-2I_ )9
:fQ*'m,
对象更复杂些,==操作符检查两个引用是否指向同一个对象,而equals方法则实现更专门的相等性检查。 ~./u0E
I z@x^s
FnU;n
fmyS#
6"
更显得混乱的是由java.lang.Object 所提供的缺省的equals方法的实现使用==来简单的判断被比较的两个对象是否为同一个。 dfd%A"
I
B{u.Yc:
F?4'>ZW
*qOCo_=P8
许多类覆盖了缺省的equals方法以便更有用些,比如String类,它的equals方法检查两个String对象是否包含同样的字符串,而Integer的equals方法检查所包含的int值是否相等。 ;a77YLTQ
&3/H
P)*<]
YLd%"H $n
<qiap2
大部分时候,在检查两个对象是否相等的时候你应该使用equals方法,而对于原子类型的数据,你用该使用==操作符。 #FM 'S|
E8 )*HOT_T
30-wTcG
_!Q\Xn
八、常见错误8#: 混淆原子操作和非原子操作 -$p-o
Z)
a{6|[aR
AFA*_9Ut
aM1JG$+7 G
Java保证读和写32位数或者更小的值是原子操作,也就是说可以在一步完成,因而不可能被打断,因此这样的读和写不需要同步。以下的代码是线程安全(thread safe)的: cHd39H9
b%$C!Tq'
|"*:ZSj
JFkjpBS
public class Example{ aDEP_b;
private int value; // More code here... M:M<bz Vu
public void set (int x){ ~hX'FV
// NOTE: No synchronized keyword >b${rgCvQ
this.value = x; cK/odOi
} >QPS0Vx[
} \'b-;exH
c9k,Dc
B75SLK:h=
c9={~
不过,这个保证仅限于读和写,下面的代码不是线程安全的: Q&;qFv5-l
Q:=/d$*xd
~+ur*3X
/PS]AM
public void increment (){ sP8B?Tn1W
// This is effectively two or three instructions: ^ 9E(8DD
// 1) Read current setting of ’value’. !(o2K!v0
// 2) Increment that setting. D/>5\da+y
// 3) Write the new setting back. a-=apD1RvG
++this.value; w+D5a
VJ
} |U0@(H
4\RuJx
)QT+;P.
r}bKVne
在测试的时候,你可能不会捕获到这个错误。首先,测试与线程有关的错误是很难的,而且很耗时间。其次,在有些机器上,这些代码可能会被翻译成一条指令,因此工作正常,只有当在其它的虚拟机上测试的时候这个错误才可能显现。因此最好在开始的时候就正确地同步代码: 6U]7V
6<6_W#
V&85<Y%Nl|
s*Ll\#
public synchronized void increment (){ ],4LvIPD
++this.value; [V~bo/n
} |-<L :%
0^^i=iE-u
('oUcDOFTS
J ASn\z
九、常见错误9#:在catch 块中作清除工作 ?a(3~dh|
ay.IKBXc
$r_ gFv
g#*N@83C
一段在catch块中作清除工作的代码如下所示: aKO@_R,:
N<%,3W_-_
: Tl?yGF
N<WFe5
OutputStream os = null; tDVdl^#
try{ Uk4">]oct
os = new OutputStream (); 8&bj7w,K
// Do something with os here. X'<xw
os.close(); mYvm_t9
}catch (Exception e){ 1C{n\_hR
if (os != null) +J9lD`z
os.close(); &B
C#u.^!
} +f+yh0Dj
MN4}y5
\h4y,sl
[x,&Gwa
尽管这段代码在几个方面都是有问题的,但是在测试中很容易漏掉这个错误。下面列出了这段代码所存在的三个问题: K<(RVh
[OSUARm
v
29oEkaX2o
]Re<7_xt
1.语句os.close()在两处出现,多此一举,而且会带来维护方面的麻烦。 xOlkG*3c
g11K?3*%Q
g(^l>niF:
=\.|'
2.上面的代码仅仅处理了Exception,而没有涉及到Error。但是当try块运行出现了Error,流也应该被关闭。 w8Yff[o
:h@:F7N _
?9cy5z[
b :00w["
3.close()可能会抛出异常。 JZ
[&:
E%N]t} }[
98"N UT
QkbN2mFv%
上面代码的一个更优版本为: !/SFEL@_B
;iVyJZI
2_C.-;!
+Gko[<
OutputStream os = null; 4(]k=c1<
try{ @U5o;X!qU
os = new OutputStream (); &[uGfm+@
// Do something with os here. CDhk!O..
}finally{ 5o*x?P!$
if (os != null) %qMk&1
os.close(); .67W\p
} "]<Ut{Xb
.xx9tP}Xy
@B6[RZ R
[sBD|P;M
这个版本消除了上面所提到的两个问题:代码不再重复,Error也可以被正确处理了。但是没有好的方法来处理第三个问题,也许最好的方法是把close()语句单独放在一个try/catch块中。 HO>uS>+
DWG}}vN:&
AyUVsIuPT=
vjb{h'v
十、常见错误10#: 增加不必要的catch 块 :Pv{E
jsj" W&J
LCtm@oN
Ue7~rPdlR
一些开发者听到try/catch块这个名字后,就会想当然的以为所有的try块必须要有与之匹配的catch块。 4Bz~_
Y]PZ| G)
d{&z^
4-MA!&
C++程序员尤其是会这样想,因为在C++中不存在finally块的概念,而且try块存在的唯一理由只不过是为了与catch块相配对。 +?8nY.~,'
o,L !F`W
WW.=>]7;
J7H1<\=cJb
增加不必要的catch块的代码就象下面的样子,捕获到的异常又立即被抛出: G+ToZ&f@
e=U7w7(s9
Yi:+,-Fso
qXW5_iX
try{ P;GUGG*W
// Nifty code here chy7hPxC;
}catch(Exception e){ )u$A!+fo
throw e; N.]8qzW
}finally{ =B\?(
// Cleanup code here hn-S$3')`
} ;rX4${h
X!m/I
i$q
ty ~U~
^t"\PpmK<d
不必要的catch块被删除后,上面的代码就缩短为: AbB%osz}Ed
>. A{=?
2&M
8Wb#
UX6-{
RP
try{ 28-@Ga4
// Nifty code here *k/_p^
}finally{ OtJS5A
// Cleanup code here iMSS8J
} # 8A|-u=3
6gv.n
(Q@+W|~
U;_;_
常见错误11#;没有正确实现equals,hashCode,或者clone 等方法 g)zy^aDf
I$YF55uB
n%Fa;!S
\(Iy>L.
方法equals,hashCode,和clone 由java.lang.Object提供的缺省实现是正确的。不幸地是,这些缺省实现在大部分时候毫无用处,因此许多类覆盖其中的若干个方法以提供更有用的功能。但是,问题又来了,当继承一个覆盖了若干个这些方法的父类的时候,子类通常也需要覆盖这些方法。在进行代码审查时,应该确保如果父类实现了equals,hashCode,或者clone等方法,那么子类也必须正确。正确的实现equals,hashCode,和clone需要一些技巧。 ^;'3(m=
n`6vM4rM)
v^vEaB
)gE:@3
小结 5i0<BZDTef
GB0] |z5
[mhY_Hmz]
\ZA%"F){
我在代码审查的时候至少遇到过一次这些错误,我自己也犯过其中的几个错误。好消息是只要你知道你在找什么错误,那么代码审查就很容易管理,错误也很容易被发现和修改。即便你找不到时间来进行正规的代码审查,以自审的方式把这些错误从你的代码中根除会大大节省你的调试时间。花时间在代码审查上是值得的。 pJqayzV
)|:|.`H