代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 LQjsOo
<ZB1Vi9}8
?@V[#.
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 FHV-BuH5
E4hLtc^
+
y{N-+10z
q&d~
\{J
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 |7zd%!
3$X'Y]5a
HbW0wuI
'}$Dgp6e
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 G\(|N9^:
8(* [Fe9
F8apH{&t
[]D@Q+1
一、常见错误1# :多次拷贝字符串 2p"WTd
^yOZArc'r
F;]%V%F.X
Phke`3tth
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 O?|gp<=d
f!JS= N?3
#f+$Ddg*
=kuMWaD
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: QqU!Najf
[KxF'm z9
C9t4#"
S9#)A->
String s = new String ("Text here"); SCz318n
%Z1N;g0
_F`lq_C
bcYF\@};
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: [ 1u-Q%?#
Gn&4V}F
!@v7Zu43,
p3^m9J
String temp = "Text here"; ynrT a..
String s = new String (temp); }+sT4'Ah>
Er{>p|n=
yNTK .
15sp|$&`
但是这段代码包含额外的String,并非完全必要。更好的代码为: /~<@ *-'
r3PT1'P?L
&c,kQo+pA
VzVc37Z>6
String s = "Text here"; T~='5iy|
4H/fP]u
5^x1cUB]
y_?Me]
二、常见错误2#: 没有克隆(clone)返回的对象 j?+X\PtQ
-jiG7OL
%QP0
Pjc
Tx +
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: .qZI$
l.
O`<KwUx !
j{Q9{}<e
>=-(UA
import java.awt.Dimension; BSVxN
/***Example class.The x and y values should never*be negative.*/ BT"XT5@
public class Example{ PAM}*'
private Dimension d = new Dimension (0, 0); |/)${*a4n
public Example (){ } :n-]>Q>5=k
;4pYK@9w_
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ ~5oPpTAe
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ G2T|RT$_K
if (height < 0 || width < 0) gp\<p-}
throw new IllegalArgumentException(); {.INnFGP@)
d.height = height; k1D@fiz
d.width = width; ]@u6HH~^
} :w^Ed%>y7
#e$5d>j(
public synchronized Dimension getValues(){ ]'=)2
.}
// Ooops! Breaks encapsulation VB*oGG
return d; ?snp8W-WB
} \}|o1Xh2
} Sxh]R+Xb
|0f>aZ
e-EUf
q}?4f*WC
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: ys kO
Fkd+pS\9g~
fNW"+ <W
0a XPPnuX
Example ex = new Example(); ]Yn_}Bq
Dimension d = ex.getValues(); Y<%@s}zc
d.height = -5; aq@8"b(.
d.width = -10; #$8% w
LF& z
@y\XR
,1+y/{S
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 _dhgAx-H)h
#;2n;.a
)O@]uY
M# %a(Y3K)
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 S;286[oq@
Rx=>6,)'
]z/8KL
kZGRxp9
更好的方式是让getValues()返回拷贝: Tq[kl'_
lSVp%0jR
yj.7'{mA
!`Hd-&}bYz
public synchronized Dimension getValues(){ fy@<&U5rg
return new Dimension (d.x, d.y); J`].:IOh
} "ozr+:#\
t^G"f;Ra+
&keR~~/
2Tp1n8FV
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 M:[ %[+6
_)>_{Pm
U"^kH|
#PH~1`vl
三、常见错误3#:不必要的克隆 UKT%13CO4U
FW G6uKv
3@$,s~+ 3
?FpWvyz|
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: .ufTQ?Fe
(jRm[7H
AW!?"xdZ
ij( B,Y
/*** Example class.The value should never * be negative.*/ TU,s*D&e
public class Example{ @v)p<r^M">
private Integer i = new Integer (0); @] DVD
public Example (){ } }o?AP vd
q(.sq12<<W
/*** Set x. x must be nonnegative* or an exception will be thrown*/ E%,^Yvh/
public synchronized void setValues (int x) throws IllegalArgumentException{ FE (ev 9@
if (x < 0) f=r<nb'H
throw new IllegalArgumentException(); %4,O 2\0?&
i = new Integer (x); pm
9"4 z
} YA_c
N5p/@
"8x8UgG
public synchronized Integer getValue(){ ~5%W:qwQ
// We can’t clone Integers so we makea copy this way. Vr`R>S,-
return new Integer (i.intValue()); NflD/q/ L
} ;S^'V
} 0uOkMuy<
rrBsb -
QSdHm
vyK7I%T'R
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 (3Two}
t!W(_8j
>_-s8t=|
p93r'&Q
方法getValue()应该被写为: t\k$};qJ
#~2%)
7XTkX"zKj
4C61GB?Vy
public synchronized Integer getValue(){ NV72
// ’i’ is immutable, so it is safe to return it instead of a copy. z<U-#k7nz
return i;
!sQY&*
} {GK;63`1
j<VFn~*_
aW)-?(6>
jET{Le8i
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: [65`$x-
p.v0D:@&
Q kEvw<
8D3OOab
?Boolean )NXmn95
?Byte K/j3a[.
?Character Zw5Ni Xj
?Class bpJ(XN}E
?Double [q)8N
?Float Ln')QN
?Integer Ui_8)z _
?Long la0BiLzb]
?Short &:9cAIe]H
?String =.f-w0V
?大部分的Exception的子类 ;c-(ObSm
#~}nFY.
Wuc S:8#|
e6R}0w~G
四、常见错误4# :自编代码来拷贝数组 .h@rLorm>
"7'J&^|
nm5cpnNl
~dgDO:)
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: ?I_s0k I
QdH\LL^8R4
V:In>u$QJ!
qT{U(
public class Example{ ]'!f28Ng-
private int[] copy; 0%&1\rm+j
/*** Save a copy of ’data’. ’data’ cannot be null.*/ g]<4&)~
public void saveCopy (int[] data){ l&OKBUG
copy = new int[data.length]; [842&5Pd?
for (int i = 0; i < copy.length; ++i) h)ECf?r<
copy = data; QRc{vUR&
} =9y[1t
} LSa,1{
/32Fy`KV
X@+{5%
A-Sv;/yD_
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: QUq_:t+Dv
h58`XH
&zl|87M
twL3\
}N/B
void saveCopy (int[] data){ BgurzS4-
try{ j"ThEx0
copy = (int[])data.clone(); Y;dz,}re
}catch (CloneNotSupportedException e){ 2iY3Lsna
// Can’t get here. mXRB7k
} B:b5UD
} ZXqSH${Tp
3m]4=
\8)U!9,$nn
lP[w?O
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: @TLS<~
QwNly4
!O+)sbd<
q
MfT>rH
static int[] cloneArray (int[] data){ J`peX0Stl
try{ %+@O#P
return(int[])data.clone(); ypbe!Y<i]
}catch(CloneNotSupportedException e){ q}`${3qQ3
// Can’t get here. D"Bl:W'?j
} /7aBDc-v
} yh Yb'GK
MW! srTQ_
7L`A{L
y?[ v=j*U
这样的话,我们的saveCopy看起来就更简洁了: <{dVKf,e
+6sy-<ZL:
Ed0QQyC@9
Eza`Z`
^el
void saveCopy (int[] data){ oI0M%/aM
copy = cloneArray ( data); [>+4^&
} (7mAt3n
k
T%.8'9
%824Cqdc
iqC|G/
五、常见错误5#:拷贝错误的数据 RY]#<9>M
`>7;!
}6p@lla,%]
03|PYk 6EW
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: \l'm[jy>
eV2W{vuI
TTeH`
n&{Dq}q
import java.awt.Dimension; {'XggI%
/*** Example class. The height and width values should never * be 6.CbAi3Z
negative. */ _D+}q_
public class Example{ Nh8Q b/::
static final public int TOTAL_VALUES = 10; NTdixfR
private Dimension[] d = new Dimension[TOTAL_VALUES]; ]mo-rhDsM
public Example (){ } X\`_3=
K{x\4
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ g-Mj.owu=
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ o9|nJ;
if (height < 0 || width < 0) X^T:8npxt
throw new IllegalArgumentException(); q$ZHd
if (d[index] == null) S'|,oUWDb
d[index] = new Dimension(); e bm])~ZL
d[index].height = height; $*SW8'],`
d[index].width = width; AJf4_+He
} whmdcVh.
public synchronized Dimension[] getValues() Vr )<\h
throws CloneNotSupportedException{ b=g8eMm
return (Dimension[])d.clone(); 6DM$g=/'
} d:ARf
} O-ew%@_
E[2m&3&
N^#ZJoR
V^7V[(~`
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: bt"W(m&f
1e(E:_t
P?8GV%0$
H;?{BV
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ '{a/2
l
Dimension[] copy = (Dimension[])d.clone(); )LdP5z-
for (int i = 0; i < copy.length; ++i){ %@wJ`F2a_
// NOTE: Dimension isn’t cloneable. )jU)_To
if (d != null) A'j;\
`1
copy = new Dimension (d.height, d.width); `s"'r !
} 6 )Hwt_b
return copy; f* !j[U/r_
} =q>'19^Jx
>/:" D$
JI? rL
I, -hf=-
在克隆原子类型数据的多维数组的时候,也会犯类似的错误。原子类型包括int,float等。简单的克隆int型的一维数组是正确的,如下所示: VLS0XKI)
;Yx )tWQI
8}c$XmCM
?{\nf7Y
public void store (int[] data) throws CloneNotSupportedException{ E%+Dl=
this.data = (int[])data.clone(); 8I-u2Y$Sr
// OK `NnUyQ;T
} FYOD
Upn
,`wXg
us;YV<)d
y)F;zW<+
拷贝int型的二维数组更复杂些。Java没有int型的二维数组,因此一个int型的二维数组实际上是一个这样的一维数组:它的类型为int[]。简单的克隆int[][]型的数组会犯与上面例子中getValues()方法第一版本同样的错误,因此应该避免这么做。下面的例子演示了在克隆int型二维数组时错误的和正确的做法: _wC3kAO
?Eg(Gu.J
Q~814P8]
FqkDKTS\&
public void wrongStore (int[][] data) throws CloneNotSupportedException{ `sUZuWL_
this.data = (int[][])data.clone(); // Not OK! 7Ilm{@b=
} dA-2%uJ
public void rightStore (int[][] data){ }XZ'v_Ti
// OK! iDN;m`a
this.data = (int[][])data.clone(); X'wE7=29M
for (int i = 0; i < data.length; ++i){ |>27'#JC
if (data != null) V_>\9m
this.data = (int[])data.clone(); _,zA ^*b
} _]04lGx27
} Scp7X7{N
M^MdRu
l*ayd>`~x
;6gDV`Twy
jYx38_5e
六、常见错误6#:检查new 操作的结果是否为null 4,..kSA3iw
~u)}ScTp
g+DzscIT
9!f/aI
Java编程新手有时候会检查new操作的结果是否为null。可能的检查代码为: 0n@rLF
#%`|~%`{:
un shH <
So{x]x:f
Integer i = new Integer (400); 'Hc-~l>D
if (i == null) y]2qd35u_A
throw new NullPointerException(); D5$wTI
P.6nA^hXB
5 elw~u
n/DP>U$I&
检查当然没什么错误,但却不必要,if和throw这两行代码完全是浪费,他们的唯一功用是让整个程序更臃肿,运行更慢。 N<f"]
@WJgWJm
^=C{.{n
?bPRxR
C/C++程序员在开始写java程序的时候常常会这么做,这是由于检查C中malloc()的返回结果是必要的,不这样做就可能产生错误。检查C++中new操作的结果可能是一个好的编程行为,这依赖于异常是否被使能(许多编译器允许异常被禁止,在这种情况下new操作失败就会返回null)。在java 中,new 操作不允许返回null,如果真的返回null,很可能是虚拟机崩溃了,这时候即便检查返回结果也无济于事。 "XB[|#&
]NjX?XdX<
七、常见错误7#:用== 替代.equals O>SLOWgha
x6(~;J
在Java中,有两种方式检查两个数据是否相等:通过使用==操作符,或者使用所有对象都实现的.equals方法。原子类型(int, flosat, char 等)不是对象,因此他们只能使用==操作符,如下所示: q:l>O5
L/wD7/ODr
-0?~
7P"| J\
int x = 4; :Nu^
int y = 5; F\fWvXdW
if (x == y) 4/mig0"N.
System.out.println ("Hi"); >^%7@i:@U
// This ’if’ test won’t compile. 0%,!jW{`
if (x.equals (y)) z)'M k[
System.out.println ("Hi"); n_$
:7J
el2bd
:
xG}(5Tt
A{UULVp
对象更复杂些,==操作符检查两个引用是否指向同一个对象,而equals方法则实现更专门的相等性检查。 y(Y!?X I
{8 8 )~
;} und*q
kdCUORMK
更显得混乱的是由java.lang.Object 所提供的缺省的equals方法的实现使用==来简单的判断被比较的两个对象是否为同一个。 fYp'&Btb]x
D|@/yDQ
JmPHAUd
xm%Um\Pb7
许多类覆盖了缺省的equals方法以便更有用些,比如String类,它的equals方法检查两个String对象是否包含同样的字符串,而Integer的equals方法检查所包含的int值是否相等。 =jlt5 z
VGtC)mG8)
&Ts-a$Z7?S
O_$m!5ug
大部分时候,在检查两个对象是否相等的时候你应该使用equals方法,而对于原子类型的数据,你用该使用==操作符。 zV:pQRbt.
>"gf3rioW
W4[V}s5u
-cZDGt
八、常见错误8#: 混淆原子操作和非原子操作 :80Z6F.k`
OC1I&",Ai|
}-ftyl7
$SM#< @
Java保证读和写32位数或者更小的值是原子操作,也就是说可以在一步完成,因而不可能被打断,因此这样的读和写不需要同步。以下的代码是线程安全(thread safe)的: $tz;<M7B
)_{dWf1
ulu9'ch
~GTz:nC*
public class Example{ n9@ of
private int value; // More code here... f~Fm4>\(
public void set (int x){ 7s"<
'cx_F
// NOTE: No synchronized keyword 9UKp?SIF
this.value = x; hc~s"Atck
} w:s]$:MA8
} G:<`moKgL
hJwC~HG5
D_/^+H]1
+6UVn\9Q
不过,这个保证仅限于读和写,下面的代码不是线程安全的: At flf2 K
.jS~By|r
#k_HN}B
$Z|ffc1
public void increment (){ F_Y7@Ei/
// This is effectively two or three instructions: hQ]H
/+\
// 1) Read current setting of ’value’. JAAI_gSR3
// 2) Increment that setting. 1"/He ` 4
// 3) Write the new setting back. yyv8gH
++this.value; I*x[:)X8
} 9;Itqe{8w
Gqcq,_?gt
!,[C]Q1
Vnx,5E&
在测试的时候,你可能不会捕获到这个错误。首先,测试与线程有关的错误是很难的,而且很耗时间。其次,在有些机器上,这些代码可能会被翻译成一条指令,因此工作正常,只有当在其它的虚拟机上测试的时候这个错误才可能显现。因此最好在开始的时候就正确地同步代码: ?"zY"*>4
RQ'exc2x0
0GB:GBhZ
=i_-F$pV
public synchronized void increment (){ v3}L`dyh3
++this.value; Hu.t 3:w
} ]4h92\\965
SV:4GVf
ox:[f9.5
+x_Rfk$fb
九、常见错误9#:在catch 块中作清除工作 {.Z}5K
5WC+guK7
bhkUKxd
SG-'R1
J
一段在catch块中作清除工作的代码如下所示: }:u~K;O87
=
QQ5f5\l
Y^
kXSU
vFE;D@bz:
OutputStream os = null; v-yde>(
try{ }e2(T
os = new OutputStream (); PUo/J~ v
// Do something with os here. Q -MQ9'
os.close(); #+$G=pS'v
}catch (Exception e){ ?*?RP)V
if (os != null) S/Fkw4%
os.close(); 2>86oP&
} mjWU0Gh%*
yHHt(GM|o
#{k|I$
f>piHh?
尽管这段代码在几个方面都是有问题的,但是在测试中很容易漏掉这个错误。下面列出了这段代码所存在的三个问题: [%9noB
MF~H"D
n
(q{Ck#+
LbaK={tR
1.语句os.close()在两处出现,多此一举,而且会带来维护方面的麻烦。 @;<ht c
jV?
}9L^;
PQK(0iCo4
?T>'j mmV=
2.上面的代码仅仅处理了Exception,而没有涉及到Error。但是当try块运行出现了Error,流也应该被关闭。 z;A>9vQ_J
Vs%|pIV
QmLF[\Oo_
S+'rG+NJ
3.close()可能会抛出异常。 SfJ./ny
}?z@rt^
r *$Ner
n) k1
上面代码的一个更优版本为: ({JHZ6uZ
wY~&Q}U
*uo'VJI7_,
vC1v"L;[o/
OutputStream os = null; 4'-|UPhx
try{ OE4+GI.r-
os = new OutputStream (); ]8icBneA~'
// Do something with os here. ,y+$cM(
}finally{ :JfE QIN
if (os != null) DXa=|T
os.close(); F)+{AQL
} d}JP!xf%
% ]I ZLJ
&^}6
9
|1ST=O7.LH
这个版本消除了上面所提到的两个问题:代码不再重复,Error也可以被正确处理了。但是没有好的方法来处理第三个问题,也许最好的方法是把close()语句单独放在一个try/catch块中。 +)j1.X
h$.:Uj8/
_)]+hUwY
td4[[ /
十、常见错误10#: 增加不必要的catch 块 =JKv:</.G
cs1l~bl
6ezS {Q
Tszp3,]f
一些开发者听到try/catch块这个名字后,就会想当然的以为所有的try块必须要有与之匹配的catch块。 1j:Wh
*^RmjW1I
MXzVgy
"y_#7K
C++程序员尤其是会这样想,因为在C++中不存在finally块的概念,而且try块存在的唯一理由只不过是为了与catch块相配对。 %H]lGN)
X=Ys<TM,
G7)Fk%>
p=C%Hmd5E
增加不必要的catch块的代码就象下面的样子,捕获到的异常又立即被抛出: m;D- u>o
Wm);C~Le
u1z
mwY
IJy[
try{ J?Dq>%+^
// Nifty code here #
eCjn
}catch(Exception e){ *P 3V
throw e; :^Fh!br==
}finally{ oyNSh8c7c
// Cleanup code here C_4)=#@GU
} + +aL4:
)u/H>;L P
NvHJ3> "%
BWrv%7
不必要的catch块被删除后,上面的代码就缩短为: !2z?YZhu
: C b&v07
\mw(cM#:
-0_d/'d
try{ IBQ@{QB
// Nifty code here +&Hr4@pgW
}finally{ \MK*by
// Cleanup code here 6gT5O]]#o
} Pl<;[cB
u{FDdR9<
E[O<S B
I
zCOgBT~p
常见错误11#;没有正确实现equals,hashCode,或者clone 等方法 X^\>:<
t9Y=m6
cwm_nQKk
b:R-mg.VT{
方法equals,hashCode,和clone 由java.lang.Object提供的缺省实现是正确的。不幸地是,这些缺省实现在大部分时候毫无用处,因此许多类覆盖其中的若干个方法以提供更有用的功能。但是,问题又来了,当继承一个覆盖了若干个这些方法的父类的时候,子类通常也需要覆盖这些方法。在进行代码审查时,应该确保如果父类实现了equals,hashCode,或者clone等方法,那么子类也必须正确。正确的实现equals,hashCode,和clone需要一些技巧。 k51Eyy50(
fx@j?*Qb
+8v9flh
= <j"M85.
小结 N gLU$/y;
8ZCo c5
[tg^GOf '
H)aQ3T4N5
我在代码审查的时候至少遇到过一次这些错误,我自己也犯过其中的几个错误。好消息是只要你知道你在找什么错误,那么代码审查就很容易管理,错误也很容易被发现和修改。即便你找不到时间来进行正规的代码审查,以自审的方式把这些错误从你的代码中根除会大大节省你的调试时间。花时间在代码审查上是值得的。 8a_[B~
v3GwD00