代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 #9}KC 9f
\$^ z.
Z!G_" 3
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 " jn@S-
7oA$aJQ
"UKX~}8T
n|lXBCY7K
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 h'^7xDw
2/=CrK
)`F?{Sg
#Bj{
4OeV
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 LdR}v%EH
*ntq;]
4Cke(G
~cy/\/oO
一、常见错误1# :多次拷贝字符串 WRZi^B8@
`GC7o DL
irqlU
J)A1`(x&T
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 'e02rqip{
HKv:)h{?
#6fp"
H&E c*MT
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: dm,7OQ
,$Qa]UN5Q
QXishHk&
.x$+R%5U
String s = new String ("Text here"); J6Hw05%0=
.
l RW
]
M"{=z
?'CIt5n+\{
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: pA"x4\s
|4YDvDEJi
:N\*;>
!cE>L~cza
String temp = "Text here"; kLR4?tX!
String s = new String (temp); m46Q%hwV
sI/Hcm
\
lP
c,8)
oc?,8I[P5
但是这段代码包含额外的String,并非完全必要。更好的代码为: Ge@./SGT
d{hbgUSj
D#x D-c
~-GgVi*I
String s = "Text here"; *PMvA1eN=#
Mr<2I
oaHg6PT!
@Rj&9/\L
二、常见错误2#: 没有克隆(clone)返回的对象 =DvFY]9{
dl'pl
e{:P!r
aM
d,iW#,
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: Zq2dCp%
24Z7;'
Y"D'|i
~;aSX1
import java.awt.Dimension; '{\VOU
/***Example class.The x and y values should never*be negative.*/ Hhr/o~?;}#
public class Example{ "@Bc eD
private Dimension d = new Dimension (0, 0); Xlw&hKS
public Example (){ } ,G
e7
9(
cn v4!c0
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ gHQ[D|zu
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ :1q+[T/ @
if (height < 0 || width < 0) A1{P"p!
throw new IllegalArgumentException(); -_
.f&l8
d.height = height; bRJYw6oA<
d.width = width; ~1`.iA
} SOE#@{IXBa
a)MjX<y
public synchronized Dimension getValues(){ )W:`Q&/G
// Ooops! Breaks encapsulation lu`\6
return d; mG7Wu{~=U
} Z6!MX_ep
} UA!h[+Z
}C/u>89%q
C#emmg!a\
/YR*KxIx
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: i?z3!`m
Kw3fpNd
@SDsd^N{2P
El Z'/l*\
Example ex = new Example(); /v:g' #n
Dimension d = ex.getValues(); DOaEz?2)
d.height = -5; Vs]+MAL
d.width = -10; X |.'_6l.
Id
*Gs>4U
jx!)N>
pB@8b$8(Z
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 'BpK(PlUh
pNcNU[c
L=iaL[zdJ
+)^F9LPl
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 [N$da=`wv
:J@q
Xa
muQH!Q
8js5/G+
更好的方式是让getValues()返回拷贝: Z=sy~6m+v
$R2T)
im>Sxu@
;tf1#6{
public synchronized Dimension getValues(){ WiH%URFB
return new Dimension (d.x, d.y); m( C7Fa
} S]KcAz( fX
@BbZ(cZ*
d;Z<")
>T%Jlj3ZG
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 iJ~5A'?6
Dn) =V.
&9$0v" `H
fa=#S
三、常见错误3#:不必要的克隆 SDcxro|8i
Vl{CD>$,
/u<lh.
hPW
K7FuMB
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: },2-\-1
DIB Az s
W8,XSUl
hmtRs]7
/*** Example class.The value should never * be negative.*/ _U1~^ucV
public class Example{ `)`_G!a
private Integer i = new Integer (0); J#L-Slav%
public Example (){ } o$'Fz[U
@CP"AYB #
/*** Set x. x must be nonnegative* or an exception will be thrown*/
jC*(ZF1B
public synchronized void setValues (int x) throws IllegalArgumentException{ q]0a8[]3
if (x < 0) (ivV [
throw new IllegalArgumentException(); 82&JYx
i = new Integer (x); V5i_\A
} D7X-|`kH
#StD]d
public synchronized Integer getValue(){ X"(!\{ySI;
// We can’t clone Integers so we makea copy this way. I--WS[
return new Integer (i.intValue()); *;7&
} r62x*?/
} ;Z-Cn.
NZe3
m
xB68RQe)
!3DWz6u
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 U;?%rM6
LbJtU!
P|v ;'9
O4@Ki4f3A%
方法getValue()应该被写为: Wcz{": [
oIt.Pc~;'#
Ig'Y]%Z0
K)]7e?:Wu
public synchronized Integer getValue(){ FZ #ngrT
// ’i’ is immutable, so it is safe to return it instead of a copy. WVftLIJ
return i; r[eZV"
} U_ V0
8d-; ;V
25l6@7q.
1T%Y:0
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: G#HbiVH9
H.7gSB 1
Z9i,#/
L4zSro:Si
?Boolean ldM [8
?Byte 3Ym5SrKK
?Character w^ui%9
&6H
?Class K-)*S\<}
?Double 5hB&]6n
?Float ~B:Lai4"
?Integer %+w>`k3(N
?Long req=w;E:
?Short ?f1%)]>
?String YdV5\!
?大部分的Exception的子类 j^1T3 +
tRS^|??
Ve2z= 6(
,YSQog
四、常见错误4# :自编代码来拷贝数组 k1L GT&
}Tu_?b`RUm
nqBZp N^
bFVz ;
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: -]Z!_[MlDF
vROl}s;
8doT`rI1
UX41/# 4
public class Example{ .Y&_k
private int[] copy; U#-&%|b$
/*** Save a copy of ’data’. ’data’ cannot be null.*/ ~1S7\e7{
public void saveCopy (int[] data){ A~ '2ki5$g
copy = new int[data.length]; `kwyF27v]
for (int i = 0; i < copy.length; ++i) *na7/ysT<
copy = data; ynw^nmM
} E,xCfS)
} xii*"n ~
zr&K0a{hc
L-Xd3RCD
iEr|?,
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: 7_S+/2}U*
$P^=QN5Bb
<.l5>mgkCw
Y3-Tg~/~W
void saveCopy (int[] data){ eoR@5OA&
try{ mZ/?uPIa
copy = (int[])data.clone(); ,'Y*e[
}catch (CloneNotSupportedException e){ 6"|PJ_@P
// Can’t get here. |E53
[:p
} 6aM`qz)
} lDe9EJR
#Q^mdv?
Cs^o- g!L
HNY{%D
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: '$
s:cS`=
(dpBGt@
L0UAS'hf
-njxc{b
static int[] cloneArray (int[] data){ vO]gj/SaT
try{ ,T|iA/c
return(int[])data.clone(); k|BY 7C
}catch(CloneNotSupportedException e){ Xvi{A]V
// Can’t get here. 8ji!FZf
} ,G"?fQ7z R
} m]Z+u e
>7vSN<w~m
-hQ=0h~\B.
$
ohwBv3S
这样的话,我们的saveCopy看起来就更简洁了: ^dZ,Itho
g|"z'_
>Eik>dQ a
HjGT{o
void saveCopy (int[] data){ /p<mD-:.M
copy = cloneArray ( data); ^P"t
"
} a+A/l
2}[rc%tV:?
$]|_xG-6{
q1r\60M
五、常见错误5#:拷贝错误的数据 tK g%5;v
*%B%BJnX
mrFMdpaHl%
cAVe(:k)
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: 6jCg7Su]
;NRm ,
Jfo|/JQ
)lB-D;3[_
import java.awt.Dimension; |g8
]WFc
/*** Example class. The height and width values should never * be g\rujxHlH
negative. */ .a;-7|x
public class Example{ I #1_
static final public int TOTAL_VALUES = 10; * fSa8CV
private Dimension[] d = new Dimension[TOTAL_VALUES]; }9Y='+.%^
public Example (){ } ~`*:E'/5k]
U!3nn#!yE
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ 6XFO@c}d
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ dMRwQejY{7
if (height < 0 || width < 0) /PPk
p9H{
throw new IllegalArgumentException(); #kLM=a/_NO
if (d[index] == null) g0g/<Tv[
d[index] = new Dimension(); d`({z]W;
d[index].height = height; *'d5~dz=
d[index].width = width; IdzF<>;W
} &bBp`h
public synchronized Dimension[] getValues() h=`rZC
throws CloneNotSupportedException{ lba*&j]w=
return (Dimension[])d.clone(); j|lg&kN
} eC[g"Ef
} o|^0DYb
1 68U-<
F
b`V.
oJ6
d:
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: J)'6 z
:JW~$4
"q#(}1Zd
Bfi9%:eG
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ KC }B\~ +
Dimension[] copy = (Dimension[])d.clone(); ~+CNED0z+
for (int i = 0; i < copy.length; ++i){ 8f8+3
// NOTE: Dimension isn’t cloneable. KO{}+~,.6
if (d != null) Kz$Ijj
copy = new Dimension (d.height, d.width); +Tq
_n@
} ip1jY!
return copy; bpUN8BI[T
} ;pAkdX&b