代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 }zLe;1Tx
$8_*LR$
hc0VS3 k)
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 P?]q*KViM
:I<%.|8
8eOQRC33
*bv
Iqa
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 L/<Up
m^]/
/j
{-17;M$
g08=D$P
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 -t*C-C'"|
Y71b
Lg
JanLJe)
cs@5K$v
一、常见错误1# :多次拷贝字符串 rt~X(S
pF"z)E|^
by8d18:it
o5Qlp5`:u
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 )]qFI"B7
M6DyOe<
G9VzVx#T#
{bc<0
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: .v;2Q7X
?pQ, 5+8
}T(|\
X
70KXBu<6
String s = new String ("Text here"); ?0_i{BvN
tbOe,-U-@
R9=,T0Y
p
jv_sRV
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: xR1g
c+4SGWmO
+m>Kb edl
GD< Afni
String temp = "Text here"; *r9I
1W
String s = new String (temp); \nxt\KD
<T0-m?D_$
[;I.aT}R!;
_BW$?:)9
但是这段代码包含额外的String,并非完全必要。更好的代码为: "Ny_RF
a`|/*{
OpH9sBnA
W%1fm/G0
String s = "Text here"; AKAAb~{
0/] @#G2
AHZ6
Q g"{F},4
二、常见错误2#: 没有克隆(clone)返回的对象 W/?D}#e<4
L<Lu;KnY6
rxDule3m
v3]q2*`G#
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: E176O[(V=
Nw|Lrn*h!
rp1u
G_AAE#r`
import java.awt.Dimension; possM'vC
/***Example class.The x and y values should never*be negative.*/ &"^A
public class Example{ t-E'foYfr`
private Dimension d = new Dimension (0, 0); gXH89n
public Example (){ } 8n&" ,)U
EkTen:{G
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ vDBnWA
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ ~*2PmD"+:
if (height < 0 || width < 0) ff:&MsA|,
throw new IllegalArgumentException(); Q&F@[k
d.height = height; $6'xRUx X
d.width = width; W
tzV|e,
} '0o`<xW
S2<(n,"
public synchronized Dimension getValues(){ 0kCUz
// Ooops! Breaks encapsulation _k
j51=
return d; LI
nN-b#
} F;~ #\X
} k)4|%
*dK A/.g
}xdI{E1 q)
X=.+XP]
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: H=yD}!j
G&Cl:CtC
C]r$
N?pD"re)6
Example ex = new Example(); oW/&X5
Dimension d = ex.getValues(); [e&$4l IS
d.height = -5; s lPFDBx
d.width = -10; BtqJkdK!;1
;V%lFP3#
f}+G;a9Nj
@nZFw.
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 cF/FretoO
^|sQkufo
?29
KvT;#]
(p2\H>pTr
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 ?>AhC{
K=B[MT#V{2
ucA6s:!={
1C|j<w=i
更好的方式是让getValues()返回拷贝: ]1Q\wsB
h.7 1O"N
gK%&VzG4
G! 87F/
public synchronized Dimension getValues(){ IO6i
return new Dimension (d.x, d.y); eg,S(;VEt
} lYZHM,"
>ZNL
pJQ
f0^s*V+
c}{e,t
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 tHu8|JrH+
&[s^`e
Y.hrU*[J0
+"p",Z
三、常见错误3#:不必要的克隆 bMv9f
J
L4[bm[x
4wBCs0NIm
`9wz:s QtP
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: =1esUO[nx
qi)(\
c?opVbJB\
d[o =
/*** Example class.The value should never * be negative.*/ >T(f
public class Example{ IC{>q3
private Integer i = new Integer (0); {QhvHV
public Example (){ } D!X{9q}S1
-iW[cj
R`$
/*** Set x. x must be nonnegative* or an exception will be thrown*/ wLgRI$_Dm
public synchronized void setValues (int x) throws IllegalArgumentException{ =tog<7
if (x < 0) c`t1:%S
throw new IllegalArgumentException(); 4 5Ql7~
i = new Integer (x); {`3;Pd`
} De^is^{
@lj
public synchronized Integer getValue(){ Ia(A&Za
// We can’t clone Integers so we makea copy this way. v
h%\ " h
return new Integer (i.intValue()); Z4(2&t^
} nrf%/L
} j$L<9(DoR
xw=B4u'z
A2+t`[w
6}|vfw
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 jV7q)\uu^
^QnVYTM
+0=RC^
F.\]Hqq
方法getValue()应该被写为: ++kiCoC
,)Q mQ^/
r1=Zoxc=w
;=n7 Z
public synchronized Integer getValue(){ JEP"2M N,
// ’i’ is immutable, so it is safe to return it instead of a copy. fN K~z*
return i; N..u<06j/
} 2`Pk@,:_
Lc.7:r
Us%VBq
/g8yc'{p
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: j"NqNv
fx}R7GN2
bqe;) A7
lLg23k{'
?Boolean s@q54
?Byte zcNV<tx
?Character (nc fR
?Class [XQNgSy?z
?Double )kd)v4#
?Float %r>vZ/>a
?Integer w?5b: W,
?Long /vQ^>2X%
?Short |Jq/kmn
?String >kB?C!\
?大部分的Exception的子类 Ti'O 2k
ck@[% ?
oOD|FrlY
5q)Eed
四、常见错误4# :自编代码来拷贝数组 {<]abO
<<`."RY#0
RSnK`N\9jb
/stED{j,
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: }5]NUxQ_
*in_Zt3
HK-?<$Yc
l,/5$JGnk
public class Example{ $@U`zy"Y
private int[] copy; @vv`86bm
/*** Save a copy of ’data’. ’data’ cannot be null.*/ UtWoSFZ'o!
public void saveCopy (int[] data){ %v|,-B7Yx
copy = new int[data.length]; \(z)]D
for (int i = 0; i < copy.length; ++i) gr2zt&Z4
copy = data; ,sc>~B@Q
} iA < EJ
} 'WOWm$2
1XZ&X]
-p)HH@6a
wHY;Y-(ZT
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: e)iVX<qb
D!-zQ`^
<Nw?9P
W35nnBU
void saveCopy (int[] data){ Zkz:h7GUG-
try{ @&~BGh
copy = (int[])data.clone(); I|PiZ1]2Y
}catch (CloneNotSupportedException e){ bWyXDsr+
// Can’t get here. "Fke(?X'
} {66vdAu&h<
} 'shOSB
?Cu$qE!h)[
D,)~j6OG8
BHU[Rz7x
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: p1&d@PF&&
d_yqmx?w
bcZHFX
`Yut1N
static int[] cloneArray (int[] data){ p"X\]g^jA>
try{ VJNPs6
return(int[])data.clone(); L,l+1`Jz
}catch(CloneNotSupportedException e){ ;m&f Vp
// Can’t get here. Jsw<,uTD
} A1Zu^_y'
} I,#U
_
\"lzmxe0p
JLeV@NO
G%6wk=IH
这样的话,我们的saveCopy看起来就更简洁了: [OT@gp:
>!oN+8[~
T"0a&.TLj
g3qtWS
void saveCopy (int[] data){ ^ ]B&7\w"t
copy = cloneArray ( data); Ii
K&v<(]
} ;;U2I5 M7
&,#VhT![
P"% /
5 i#B?+Y
五、常见错误5#:拷贝错误的数据 c8yD-U/-
9sRP8Nj|
A]n!d}?
#{]=>n)j
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: Vxw?"mhP
*Lufz-[1
`t8e2?GH
6qw_ |A&g
import java.awt.Dimension; [Y:HVr,
/*** Example class. The height and width values should never * be --]\z* x
negative. */ ~#-`Qh
public class Example{ "zv+|_ZAfd
static final public int TOTAL_VALUES = 10; $]hf2Yr(
private Dimension[] d = new Dimension[TOTAL_VALUES]; ElYHA
public Example (){ } fG.w;Aemv5
NyGF57v[M
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ bLUn0)c
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ hMD yE.X-
if (height < 0 || width < 0) D_8hn3FH
throw new IllegalArgumentException(); k4`v(au^
if (d[index] == null) 9np<r82
d[index] = new Dimension(); W]R5\G*
d[index].height = height; gG$o8c-
d[index].width = width; R
vY`9D
} /wK7l-S
public synchronized Dimension[] getValues() hqE#BnQxP,
throws CloneNotSupportedException{ ;J`X0Vl$
return (Dimension[])d.clone(); GnLh qm"\
} ^yb_aC w
} yn=1b:kid
fW\u*dMMZE
5Q^~Z},
Q647a}
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: }x8fXdd
PzF)Vg
p;'vOb
0zfrx-'zN
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ Le}q>>o;q
Dimension[] copy = (Dimension[])d.clone(); H37Z\xS
for (int i = 0; i < copy.length; ++i){ ?Jma^ S
// NOTE: Dimension isn’t cloneable. O/5W-u
if (d != null) mki=.l$O
copy = new Dimension (d.height, d.width); Kp99y
} 9R E;50h
return copy; ?e ~* ,6
}
O35f5Kz
:3G9YjzC}
G/D{K$=t~
Mu:H'$"'H
在克隆原子类型数据的多维数组的时候,也会犯类似的错误。原子类型包括int,float等。简单的克隆int型的一维数组是正确的,如下所示: jALo;PDJ
`q/y|/v<
}\irr9,
y"]> Rr
public void store (int[] data) throws CloneNotSupportedException{ U%#=d@?
this.data = (int[])data.clone(); ZuE0'9
// OK 2ru6bIb;
} Ex Qld
j9qN!.~mM
b/G0EcRw+
9V;m;sz
拷贝int型的二维数组更复杂些。Java没有int型的二维数组,因此一个int型的二维数组实际上是一个这样的一维数组:它的类型为int[]。简单的克隆int[][]型的数组会犯与上面例子中getValues()方法第一版本同样的错误,因此应该避免这么做。下面的例子演示了在克隆int型二维数组时错误的和正确的做法: ,iHt*SZ,*
>B9rr0d0
XrvrN^'
?K]k(ZV_+Y
public void wrongStore (int[][] data) throws CloneNotSupportedException{ R@EFG%|`_
this.data = (int[][])data.clone(); // Not OK! Vt&I[osC
} *r_.o;6
public void rightStore (int[][] data){ SrKF\h%/+
// OK! QoW3*1o
this.data = (int[][])data.clone(); \jfW$TtZm
for (int i = 0; i < data.length; ++i){ jXdn4m/O
if (data != null) iUO5hdOM
this.data = (int[])data.clone(); l%)XPb2$J
} cbIW>IbM
} :Rq D0>1
*R:nB)(6<
m]%cNxS
|[V(u
=];FojC6I
六、常见错误6#:检查new 操作的结果是否为null 1HZexV
.!`j3W]
^.4<#Qs
NfSe(rd
Java编程新手有时候会检查new操作的结果是否为null。可能的检查代码为: NT nn!k
Wl,yznT
Xu
T|vh
a(
qw
Integer i = new Integer (400); G%P]qi
if (i == null) 'dg OE
throw new NullPointerException(); F%Xq}LMd
(O&b:D/Y
;uJVY)7a
x_Z~k
检查当然没什么错误,但却不必要,if和throw这两行代码完全是浪费,他们的唯一功用是让整个程序更臃肿,运行更慢。 6ZM<M7(V
@3G3l|~>
K>q,?x b
$@<