代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 E>"SC\#7
^Je*k)COn
D9n+eZ
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 9YBlMf`KEf
9,}Z1 f\%
0+A#k7c6p
f1d<xGx
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 _ CzAv%
aecvz0}@R
C{6m?6
qtP*O#1q
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 uYd_5
nw
g~OG~g@
x+1-^XvK
LC0-O1
一、常见错误1# :多次拷贝字符串 |J^I8gx+
hiWs:Yq
ZjnWbnW
Z,F1n/7
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 r&XxF>
:vC+}.{p
*mN8Qd
;47 =x1ji
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: " &mwrjn"T
5%DHF-W)
8JO(P0aT
n|PW^kOE/
String s = new String ("Text here"); 9|9/8a6A
>DW%i\k1V~
li~=85 J
[,|4%Y
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: .O
PBET(gv
"2I{T
#Vm)wH3
R7x*/?
String temp = "Text here"; }5?|iUH|
String s = new String (temp); b+71`aD0
W#9LK
Jj
/NVyzM51V
zG&yu0;D6
但是这段代码包含额外的String,并非完全必要。更好的代码为: 57$/Dn
;ZZmX]kz,M
<XnxAA
QwI HEmdM
String s = "Text here";
1_LGlu~&
C,{ Ekbg
)/{~&LU
8sL+ik"
二、常见错误2#: 没有克隆(clone)返回的对象 j*_#{niy:
"%=K_WJ?
4o@^._-R
yLt>OA<X
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: VO*fC
]Vf2Mn=]"
ab<7jfFIa
77G4E ,]
import java.awt.Dimension; Ude)$PAe%
/***Example class.The x and y values should never*be negative.*/ 1,6Y)_
public class Example{ ?/KkN3Y_j[
private Dimension d = new Dimension (0, 0); H"|oI|~
public Example (){ } ;{g>Z|
A@ w9_qo
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ v<?k$ e5
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ PO=A^ b
if (height < 0 || width < 0) 8noo^QO
throw new IllegalArgumentException(); pz/vvH5
d.height = height; 75']fFO@!
d.width = width; ;B"S*wYMN
} hHsO?([99
{^K&9sz
public synchronized Dimension getValues(){ e73zpF
// Ooops! Breaks encapsulation HOVzpj
return d; p2m`pT
} Wt!NLlN8
} E%)3{#.z
o31pF
wpm $?X
4[K6 ZDBU
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: 5VlF\-
V j_z"t7q
T'VKZ5W
)`m/vYKWL
Example ex = new Example(); qTnk>g_oS&
Dimension d = ex.getValues(); K.6xNQl{}
d.height = -5; :D=y<n;S+
d.width = -10; _ud!:q
Eb\SK"8
IN!IjInaT@
$
?YSAD1
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 %XZdz=B
0I>[rxal
%>:d5"&Lbs
9 N@N U:M+
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 k#/%#rQM
s|C4Jy_
EA!I&
mBq
,SoqVboRl
更好的方式是让getValues()返回拷贝: &n&ndq
QdP)-Fx
<(2,@_~@r
'FGf#l<
public synchronized Dimension getValues(){ 8x<; AL|`
return new Dimension (d.x, d.y); irzWk3@:
} o!|TCwt
n6
AP6PK7
b/'RJQSAc
q,_ 1?A)
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 /%h<^YDBf
ITEd[
@^d
:8Jn?E (36
>*[Bq;
三、常见错误3#:不必要的克隆 7_AcvsdW
4[m4u6z=
%!Ak]|[7
ik|iAWy
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: 'B$qq[l]S
E.OL_ \
q|ww fPez7
R9V v*F]m@
/*** Example class.The value should never * be negative.*/ 5y|/}D>
public class Example{ (]p,Z<f
private Integer i = new Integer (0); ,;-55|o\V
public Example (){ } ]abox%U=%
9WsGoZPn
/*** Set x. x must be nonnegative* or an exception will be thrown*/ `Ui|T
public synchronized void setValues (int x) throws IllegalArgumentException{ /YH5s=
if (x < 0) Qxh 1I?h
throw new IllegalArgumentException(); =lqGt.x
i = new Integer (x); j`kw2(
} X{bqG]j
06S-3bis
public synchronized Integer getValue(){ N6_<[`
// We can’t clone Integers so we makea copy this way. A!j6JY.w
return new Integer (i.intValue()); gdyP,zMD7
} tV,Y38e
} `O|PP3S
(E(kw="
&B5@\Hd;
)6:nJ"j#
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 g{?]a'?
{(!j6|jK
y9L:2f\
Wo+'j $k
方法getValue()应该被写为: 5//.q;z
2Aq%;=+*
X"qC&oZmf
:TzHI
public synchronized Integer getValue(){ d*xKq"+
&E
// ’i’ is immutable, so it is safe to return it instead of a copy. C~dD'Tq]
return i; i@}/KT
} U[UjL)U
W{2(fb
Q>}*l|Ci
I`e|[k2
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: [#emm1k
3<nd;@:-
%}asw/WiUa
{qHf%y&[
?Boolean U`fxe`nVa
?Byte ]Kb3'je
?Character A!Ls<D.
?Class ~L.)<{?
?Double >
%U
?Float H,H=y},
?Integer wLf=a^c#
?Long _n;V iQMu
?Short 3G7Qo
?String jI(}CT`g
?大部分的Exception的子类 y84=Q
)q48cQ
?lYi![.o
w1+xlM,,9
四、常见错误4# :自编代码来拷贝数组 r-$SF5uv
iCYo?>
^Pk-<b4}
tOK lCc
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: Wl:vO^
>}~Pu|
_S
b4$-?f?V
_ ecKX</Q
public class Example{ qh)o44/
$
private int[] copy; SDTX3A1
/*** Save a copy of ’data’. ’data’ cannot be null.*/ )J"Lne*"
public void saveCopy (int[] data){ xxh(VQdg
copy = new int[data.length]; U`es
n?m!
for (int i = 0; i < copy.length; ++i) MDCK@?\
copy = data; l`s_#3
} E}V8+f54S
} d?)C} 2
SqhG\qE{Qj
u^T{sQ"_
OJUH".o
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: 4epE!`z_&
:b&O{>M]Y
5X5 &(S\
8uR4ZE*
void saveCopy (int[] data){ _T 5ZL
try{ bt/u^E
copy = (int[])data.clone(); }-:s9Lt
}catch (CloneNotSupportedException e){ OA??fb,b
// Can’t get here. tU02t#8
} !dVth)UV
} IG1+_-H:
!`yg bI.
3rEBG0cf]
ugtb`d{ Sl
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: u~,@Zg87
5__8+R
<B*}W2\
%{*}KsS`p
static int[] cloneArray (int[] data){ TlD)E
try{ 9WaKs d f
return(int[])data.clone(); |5
sI=?p&t
}catch(CloneNotSupportedException e){ (#WE9~Sru
// Can’t get here. 1)8;9
Ba:
} 6Hz45
} D_%y&p?<Ls
%.kJ@@_e
g_\U-pzr
6_a42#
这样的话,我们的saveCopy看起来就更简洁了: s5X .(;+
\7QAk4I~
R <+K&_
!tkP!%w
void saveCopy (int[] data){ 2G'Au} q0n
copy = cloneArray ( data); wD-(3ZVd4
} aO9a G*9T
Z?H#=|U
,ufB*[~
GVT+c@Gx
五、常见错误5#:拷贝错误的数据 X0Q};,
_
13M
URbu=U
DS,"^K
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: R&13P&:g
v*+.;60_
_e<3 g9bj
p.9VyM
import java.awt.Dimension; Tz H*?bpP
/*** Example class. The height and width values should never * be S.bB.<
negative. */ 8S_i;
public class Example{ 8v7;{4^
static final public int TOTAL_VALUES = 10; _u$X.5Q;
private Dimension[] d = new Dimension[TOTAL_VALUES]; io_4d2uBh
public Example (){ } _q >>]{5
J+3PUfg>@R
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ 20G..>zW
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ \Lxsg!wtJ
if (height < 0 || width < 0) Y]ML-smN
throw new IllegalArgumentException(); Sq,ZzMw
if (d[index] == null) s7?Q[vN
d[index] = new Dimension(); t1,sG8Z
d[index].height = height; \e%H5Wx
d[index].width = width; \vVGfG?6
} zmH 8#
public synchronized Dimension[] getValues() hm=E~wv'L
throws CloneNotSupportedException{ ;6g &_6
return (Dimension[])d.clone(); <QGf9{m
} Omkl|l9
} wV- kB4^4
&BnK[Q8X
F.)b`:g
6$qn'K$
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为:
SqL8MKN)
5`oVyxJ<
}R#YO$J7
a $pxt!6
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ <4,n6$E
Dimension[] copy = (Dimension[])d.clone(); |cwGc\ES
for (int i = 0; i < copy.length; ++i){ 1*{` .
// NOTE: Dimension isn’t cloneable. |tC`rzo
if (d != null) tL68
u[
copy = new Dimension (d.height, d.width); U$R+&@;
} './j<2|;U
return copy; `a}!t=~#w
} qkpnXQ
tgn_\ - +
@#q>(Ox%
f!;4-.p`
在克隆原子类型数据的多维数组的时候,也会犯类似的错误。原子类型包括int,float等。简单的克隆int型的一维数组是正确的,如下所示: *Z"9Q X
W-9^Ncp
0;,4.hsh
bq5tEn
public void store (int[] data) throws CloneNotSupportedException{ &DC
o;Ij;
this.data = (int[])data.clone(); KlbL<9P>
// OK 1df}gG
} +$Q33@F5l
/an$4?":~
2fp\s5%J}
WyH2` xxX
拷贝int型的二维数组更复杂些。Java没有int型的二维数组,因此一个int型的二维数组实际上是一个这样的一维数组:它的类型为int[]。简单的克隆int[][]型的数组会犯与上面例子中getValues()方法第一版本同样的错误,因此应该避免这么做。下面的例子演示了在克隆int型二维数组时错误的和正确的做法: $Yh7N5XH,
FCv3ZF?K
sr!m
*6%!i7kr
public void wrongStore (int[][] data) throws CloneNotSupportedException{ Wu]Dpe
this.data = (int[][])data.clone(); // Not OK! b&s"/Y89
} Vt-D8J\A
0
public void rightStore (int[][] data){ kIS_6!
// OK! "'
g*_
this.data = (int[][])data.clone(); e*w2u<HP
for (int i = 0; i < data.length; ++i){ au'Zjj/Ai5
if (data != null) ?9#}p
this.data = (int[])data.clone(); 6;g_}Zx
} NLHF3h=?1p
} !\.%^LK1
[!E pv<G
8KKI.i8`
F+r3~T%
9$7tB
六、常见错误6#:检查new 操作的结果是否为null MY11 5%
t(FIBf3
0q`n] NM
<%fcs"Mb
Java编程新手有时候会检查new操作的结果是否为null。可能的检查代码为: 4J3cQ;z
X_Vj&{
Op^r }7
$OK}jSH*v)
Integer i = new Integer (400); %lsk>V
if (i == null) a=3?hVpB
throw new NullPointerException(); /*DC`,q
J{"<Hgb
YK Nz[x$|
Jwzkd"D
检查当然没什么错误,但却不必要,if和throw这两行代码完全是浪费,他们的唯一功用是让整个程序更臃肿,运行更慢。 <igsO
]F[ V6`H
;E0Xn-o_
\Ub=Wm\
C/C++程序员在开始写java程序的时候常常会这么做,这是由于检查C中malloc()的返回结果是必要的,不这样做就可能产生错误。检查C++中new操作的结果可能是一个好的编程行为,这依赖于异常是否被使能(许多编译器允许异常被禁止,在这种情况下new操作失败就会返回null)。在java 中,new 操作不允许返回null,如果真的返回null,很可能是虚拟机崩溃了,这时候即便检查返回结果也无济于事。 4%do.D*
Y@'ug N|[C
七、常见错误7#:用== 替代.equals ydFZ$W_}w
Q%6Lc.i
在Java中,有两种方式检查两个数据是否相等:通过使用==操作符,或者使用所有对象都实现的.equals方法。原子类型(int, flosat, char 等)不是对象,因此他们只能使用==操作符,如下所示: Ht.0ug
O?|st$g
$ftcYBZa
[ix45xu7
int x = 4; .iFd
int y = 5; |7XV!D!\g
if (x == y) hawE2k0p(
System.out.println ("Hi"); S~auwY ,<
// This ’if’ test won’t compile. 6A$
\I44
if (x.equals (y)) };%l <Ui;
System.out.println ("Hi"); FFGG6r
5yO%| )
NsYeg&>`
v^_OX$=,
对象更复杂些,==操作符检查两个引用是否指向同一个对象,而equals方法则实现更专门的相等性检查。 _bp9UJ
NWCJ|
Wt2+D{@8
V<f76U)
更显得混乱的是由java.lang.Object 所提供的缺省的equals方法的实现使用==来简单的判断被比较的两个对象是否为同一个。 i}mvKV?!|1
(~t/8!7N
^|KX)g
yeQ6\yi
许多类覆盖了缺省的equals方法以便更有用些,比如String类,它的equals方法检查两个String对象是否包含同样的字符串,而Integer的equals方法检查所包含的int值是否相等。 i6F`KF'i&
?rqU&my S
bN-ljw0&
<=KtRE>$
大部分时候,在检查两个对象是否相等的时候你应该使用equals方法,而对于原子类型的数据,你用该使用==操作符。 5N=QS1<$5
?ysC7((
'Dl31w%:
bbevy!m
八、常见错误8#: 混淆原子操作和非原子操作 {1
fva^O
qH(3Z^ #.|
:p^7XwX%w
X.V6v4
Java保证读和写32位数或者更小的值是原子操作,也就是说可以在一步完成,因而不可能被打断,因此这样的读和写不需要同步。以下的代码是线程安全(thread safe)的: lc%2fVG-e
JGjqBuz#A*
u
Ey>7I
]AjDe]
public class Example{ 6{/HNEI*1
private int value; // More code here... rap`[O|l=
public void set (int x){ 8t3,}}TJ
// NOTE: No synchronized keyword "0al"?
this.value = x; G[7Z5)2B
} Ph(bgQg
} k`H#u, &