代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 y`T--v3mI
,5`."-0}
,$ho2R),Fn
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 &8o :
LJ:mJ#
_?*rtDzIM
V%VrAi.
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 IH*U!_ `
zLE>kK
dY4 8S{
&T5fH!?4
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 UA1]o5K
z|taa;iM
\fkS_r, i
w+URCj
一、常见错误1# :多次拷贝字符串 f]{1ZU%4
g!~-^_F
5( mCBH
5e~ j
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 $X{B*
WF
.rD#1)O
35-DnTv
O?+tY
y?
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: )Gu0i7iN
L<{OBuR
G!>
iqG
JI{OGr
String s = new String ("Text here"); 3N)Ycf8
P@o,4\;K
_>Pe]3
`2Z4#$.
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: UpE1PLZlB
DkF@XK0c3
XSL
t;zL:
Azdz3/
String temp = "Text here"; Wfi:wCqZG
String s = new String (temp); 5 O{Ip-
P?yOLG+)l)
>qh>Qm8w
c)n0D=
但是这段代码包含额外的String,并非完全必要。更好的代码为: xC=3|,U
"'&>g4F`o
OoU '86)
*h5ld P
String s = "Text here"; *1 J#Mdd
eKU@>5
gpO_0U4lQ]
%i]uW\~U
二、常见错误2#: 没有克隆(clone)返回的对象 fjz2m
GakmROZ@9
f6aT[Nw<
<(6-9(zHa
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: L`VQ{|&3V
)ZuQ;p
zei9,^
C
}fa%JN %E
import java.awt.Dimension; 6LF^[b/u
/***Example class.The x and y values should never*be negative.*/ ys"mP*wD
public class Example{ BW(DaNt^
private Dimension d = new Dimension (0, 0); (<:rKp
public Example (){ } V+"*A
VgC9'"|
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ }IalgQ(i
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ |WwFE|<
if (height < 0 || width < 0) +oKpA\mz
throw new IllegalArgumentException(); Zia|`}peW
d.height = height; s+\qie
d.width = width; T\$^>@
} ]@j"0F/`
JNA}EY^2I.
public synchronized Dimension getValues(){ O]4
x;`)
// Ooops! Breaks encapsulation i+
&lMgh
return d; '%|20j
} w
_6Y+
} S|5lx7
FoelOq6
%` uRUex
gm%bxr@X~
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: Q17o5##x7
fKK-c9F
Z?j='/u>@
5Z>pa`_$2
Example ex = new Example(); 2KNKdV3NK
Dimension d = ex.getValues(); C9;X6
d.height = -5; u g$\&rM>
d.width = -10; OB
I8~k
+>9^])K|
~[/c'3+4qn
FSZoT!
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 uJ5%JB("E
';T5[l,
+AC-f2
^HN
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 JX,#W!d
`]I5WTt*X
'L+BkE6+%
vz_g2.7l\
更好的方式是让getValues()返回拷贝: IqJ=\
Y>!W&Gtu
#=~1hk
r^tXr[}
public synchronized Dimension getValues(){ ``)1`wx$
return new Dimension (d.x, d.y); W%Nu]9T
} ,(kXF:
T9v#Jb6
bFxJ|
F3|pS:
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 z ex.0OT;
F?AfB[PM
"?(Fb_}i
Saq>o.
三、常见错误3#:不必要的克隆 nVA'O
bh6wI%8H
8GRrf2
6e-h;ylS
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: $P9$ ,w4
KK3xz*W0
)$N{(Cke2T
G9":z|
/*** Example class.The value should never * be negative.*/ DH*|>m&
public class Example{ >w# 3fTJ
private Integer i = new Integer (0); !td.ks0
public Example (){ } &Zy=vk*
!PTbR4s
/*** Set x. x must be nonnegative* or an exception will be thrown*/ !fjU?_[S
public synchronized void setValues (int x) throws IllegalArgumentException{ -"fq34v
if (x < 0) n|2-bRK-
throw new IllegalArgumentException(); KKJ [
i = new Integer (x); `mTxtuid{
} mzR
@P$:36
6U3@-+lF
public synchronized Integer getValue(){ {NqGWkGt*b
// We can’t clone Integers so we makea copy this way. 0|vWwZq
return new Integer (i.intValue()); R@aT=\u+
} =9MH
} BV:,bS
=qQQ^`^F'~
)O(Gw-jWE
$466?
oI
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 Zul32]1r
D4-U[l+K>
lY?d*qED
'F~SNIay
方法getValue()应该被写为: a{.n(M
EmoU7iy
)fr\V."
m,q<R1
public synchronized Integer getValue(){ x" T^>Q
// ’i’ is immutable, so it is safe to return it instead of a copy. E#]%e^
return i; _9
O'
} %/C[\wp81
<a3XV
h2<$L
7!)%%K.z6
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: ~'mhC46d
((q(Q9(F
|sAg@kM
06;{2&ju<
?Boolean C[,-1e?
?Byte @ U|u _S@
?Character .[A S
?Class $sJfxh
r
?Double |XZf:}q5:
?Float "TI?
qoz
?Integer ;& +75n
?Long WKML#U]5T
?Short LOzKpvGl
?String %9M49s
?大部分的Exception的子类 (1vS)v
$L
,//=yW
nX'.'3
&9tsk#bA.g
四、常见错误4# :自编代码来拷贝数组 +=4b5*+qG
!vw0Y,F&
\PJ89u0
$_kU)<e3
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: Sa5 y7
Y.J$f<[R
L zC~> Uj
g=Jfp$*[
public class Example{ g^FH[(P[G
private int[] copy; jL&F7itP
/*** Save a copy of ’data’. ’data’ cannot be null.*/ 3E-&8x7uYR
public void saveCopy (int[] data){ 8qveKS]vZ
copy = new int[data.length]; pz+#1=b]
for (int i = 0; i < copy.length; ++i) hrK^oa_[W
copy = data; TzJN,]F!M
} pm+[,u!i
} fmh]Y/UC
|EunDb[Y
he@swE&
e6Y0G,K
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: vSh)r 9
9L,T @#7
rcCMx"L=
gC_U7a w
void saveCopy (int[] data){ ?FyA2q!
try{ Sj\8$QIXC
copy = (int[])data.clone(); Yhfk{ CI
}catch (CloneNotSupportedException e){ 1ARIZ;H
// Can’t get here. *&s_u)b
} :v`o="
} r.[k D"l
MeC@+@C
HXX"B,N
H`sV\'`!}
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: JXrMtSp\
v2NzPzzyb
bA:abO
i{.!1i:
static int[] cloneArray (int[] data){ Y&nY]VV
try{ U<$ |ET'
return(int[])data.clone(); YRFM1?*
}catch(CloneNotSupportedException e){ o 0B`~7(
// Can’t get here. Ad[-YT
} hq|/XBd||
} ]*).3<Lw
2]|+.9B
x(A.^Yz
PM{kiz^
这样的话,我们的saveCopy看起来就更简洁了: B--`=@IRf"
b@Fa|>"_
C
7v
8
&W1c#]q@r
void saveCopy (int[] data){ j:g/[_0s
copy = cloneArray ( data); [dL#0~CL$
} 5bk5EE`
~e|~c<!z8@
7C=t19&R'
]O^!P,l)"
五、常见错误5#:拷贝错误的数据 E$gcd#rT
vb# d%1b5
h<[ o;E
P:=3;d{v
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: YQ&Xd/z-
$%LjIeVA5
uCx\Bt"VI
t<rhrW75P
import java.awt.Dimension; AkGCIn3
/*** Example class. The height and width values should never * be 2%QY~Ku~
negative. */ 1Nv_;p.{
public class Example{ 0e&Vvl4DK
static final public int TOTAL_VALUES = 10; =F6J%$
private Dimension[] d = new Dimension[TOTAL_VALUES]; ))<3+^S0V\
public Example (){ } ~)ls.NXI
&{99Owqg
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ Ao2t=vg
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ D3$}S{Yw1
if (height < 0 || width < 0) u9 J;OsnHK
throw new IllegalArgumentException(); 5F$W^N
if (d[index] == null) [/xw5rO%
d[index] = new Dimension(); U\P ;,o
d[index].height = height; jT%k{"+>+?
d[index].width = width; r\Zz=~![<
} npZ=x-ce
public synchronized Dimension[] getValues() {x
s{
throws CloneNotSupportedException{ Zj%l (OVq
return (Dimension[])d.clone(); }sZme3*J[
} Q
u{#4qToA
} 1jpcoJ@s
zrri&QDF<
qdWsP9}q
q<dZy? f
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: %&0_0BU
b$[O^p9x
B/7c`V
@Pb!:HeJE
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ 1)U%p
Dimension[] copy = (Dimension[])d.clone(); l*rli[No
for (int i = 0; i < copy.length; ++i){ =y.? =`"
// NOTE: Dimension isn’t cloneable.
$ac
VJI?
if (d != null) r&U