代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 c17_2 @N
K Pt5=a
Pb?$t
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 Olh<,p+x
/4g1zrU
"f "6]y
o| #Qu8Lk
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 c&AygqN
(CsD*U`h
hS)'a^FV
S4G^z}{_
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 *QLI3B9V
DpUbzr41+k
{vuZ{IJa
;j^H)."A\
一、常见错误1# :多次拷贝字符串 E=>FjCsu<-
f6p-s
y>
&Rvm>TC=
*q()f\
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 r7b1-
5*1D$mxD"
C}_ ojcR
k&,~qoU
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: 7PtN?;rP
F;+|sMrq
3+ @<lVew6
mT.u0KUIy
String s = new String ("Text here"); DP3PYJ%+B
8xccp4
Ib+Y~
XYR
V+VkY3
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: 4<k9?)~(J
FLGk?.x$\
#MRMNL@
%`&2+\`
String temp = "Text here"; [uI|DUlI6o
String s = new String (temp); Bh;7C@dq
8C67{^`::
w-Da~[J
vTJ}8
但是这段代码包含额外的String,并非完全必要。更好的代码为: ~])t 6i
"
N9 <w U
80Gn%1A9
QWzB6H]
String s = "Text here"; Sgp;@4`M
=Ur}~w&H8
HbXPok
Z@]e{zO
二、常见错误2#: 没有克隆(clone)返回的对象 .
r[Hu40p
DV<` K$ET
]Bjyi[#bg
XpBj%e:
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: d`
jjGEj
(]Y 5eM
m<j8cJ(
K95p>E`9e
import java.awt.Dimension; SjwyLc
/***Example class.The x and y values should never*be negative.*/ cp#JBHO
public class Example{ P!+'1KR
private Dimension d = new Dimension (0, 0); _nbBIaHN{
public Example (){ } :'~Y
f;1K5Y
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ /.Ww6a~
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ >g+?Oebgw
if (height < 0 || width < 0) Y#u}tE
d
throw new IllegalArgumentException(); SVO 3821
d.height = height; :=wTvz
d.width = width; }j*KcB_
} ^eR%N8Z
j^^Ap
public synchronized Dimension getValues(){ =jX8.K4]
// Ooops! Breaks encapsulation 1:f9J
return d; L1Iz<>
} }>VG~u8
} E#ul IgD
n&-qaoNl
/J:bWr
9Hc$G{[a
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: $!8-? ?ML
^(|vsFzn
,'p2v)p^4
\H=&`?
Example ex = new Example(); (UU(:/
Dimension d = ex.getValues(); iy 14mh\ ~
d.height = -5; A7%:05
d.width = -10; UG'9*(*
XVvK2(
5ZMR,SZhC
$CY't'6Hn
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 -5I2ga
~:3QBMk::
HA2k[F@3^
,]+z)
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 59O?_F9
)0Me?BRp
\ aHVs
20Z8HwQi
更好的方式是让getValues()返回拷贝: 0o9 3iu=&
Kd=%tNp
? P(
ZA
K)\M5id]
public synchronized Dimension getValues(){ dVsE^jsL
return new Dimension (d.x, d.y); $D}{]MN.
} /XhIx\40l
{|1Y:&M?
.8y3O]
lsy?Ac
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 GQ9\'z#+
1$%V{4bJ
+eX@U;J,g
4)U.5FBk
)
三、常见错误3#:不必要的克隆 V\^EfQ
}(1JaG
~fT_8z
m<0&~rg
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: /C4^<k\
<K8\n^i~c
sK7+Q
`kU/NKq
/*** Example class.The value should never * be negative.*/ \U[{z&]~
public class Example{ Dg}
Ka7H
private Integer i = new Integer (0); 69J4=5lX
public Example (){ } nSkPM5\TI
%YSu8G_t
/*** Set x. x must be nonnegative* or an exception will be thrown*/ C@bm
public synchronized void setValues (int x) throws IllegalArgumentException{ \o/n
if (x < 0) /6h(6 *JI
throw new IllegalArgumentException(); CC@.MA@9N
i = new Integer (x); Xt#4/>dlR
} DXa-rk8
9Iz%ht
public synchronized Integer getValue(){ Sp^9&^
// We can’t clone Integers so we makea copy this way. "V$Bnz\n
return new Integer (i.intValue()); `g6h9GC6
} Ygl%eP%Z
} }C#;fp"L
UGuxV+Nwf
Fm #w2o
.F(i/)vaq|
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 ^1L>l9F
MHsc+gQiz
iTV) NsC}
$pFo Rv
方法getValue()应该被写为: _<NMyRJo
w);6K[+;
Vgyew9>E
6p?JAT5
public synchronized Integer getValue(){ ,I_^IitN
// ’i’ is immutable, so it is safe to return it instead of a copy. Hf vTxaK
return i; Ie4 hhW
} S}ECW,K
!Z5[QNVaV
%LZ({\5K#f
jMN[J|us51
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: aBw2f[mo
8kIR y
(>I`{9x>6
gW1b~(
fD
?Boolean 3Bx:Ntx<
?Byte mUz\ra;z
?Character *L'>U[Pl7
?Class p$x{yz3
?Double Kbb78S30
?Float <@Fy5k-%.
?Integer @bnG:np
?Long MiRdX#+Y
?Short zWb4([P;
?String v^_mFp-}\
?大部分的Exception的子类 a
/:@"&Y
"n=vN<8(o
?l $Nf@-
3^Q]j^e4Ny
四、常见错误4# :自编代码来拷贝数组 )5.C]4jol
W5' 3$,X9
,E$@=1)
A?oXqb
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: u]ZqOJXxu
KV*xApb9y
v
(2GX
DS%\SrC
public class Example{ fVM`-8ZTq
private int[] copy; 2AVa(
/*** Save a copy of ’data’. ’data’ cannot be null.*/ ?^EXTU85`"
public void saveCopy (int[] data){ X K5<Tg
copy = new int[data.length]; 6Kj'ZyVL
for (int i = 0; i < copy.length; ++i) rX; Ys2vQ*
copy = data; \^V`ds*.
} Zxb_K
} fI7j):h;
|P.6<
i9D0]3/>
k,uK6$Z
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: q;:6_Qr
2EK%N'H
$
A9%UhV
@YH+cG|
void saveCopy (int[] data){ nWvuaQ0}
try{ ,=
&B28Qe)
copy = (int[])data.clone(); IB`>'~s&A
}catch (CloneNotSupportedException e){ 7lo|dg80
// Can’t get here. D>!6,m2
} pW]4bx@E
} gXH[$guf
kGUJ9Du
vw)7 !/#
5c;h&
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: Zv_jy@k
C P3<1~
er.CDKD%L
\)48904^
static int[] cloneArray (int[] data){ 0liR
try{ U5]pi+r
return(int[])data.clone(); t
nS+5F
}catch(CloneNotSupportedException e){ _7D _72
// Can’t get here. i0s6aAhgJ
} 2nFy`|aA%
} Y=
7%+WyD
G8I Y#
T'fcc6D5p
Z.wA@ ~e
这样的话,我们的saveCopy看起来就更简洁了: zLD|/`
O3.C:?;x
b`_w])Y@
&VBd~4|p
void saveCopy (int[] data){ 5`<eKwls
copy = cloneArray ( data); s:AkkkF
} V
>,Z-&.%
<q,+ON\'
Cj*-[EL<
dtAbc7
五、常见错误5#:拷贝错误的数据
pAu72O?
M-
0i7%
)=Q)BN[
&-1./?
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: @wq#>bm
e0;
cMzkL%
M/*NM= -a
import java.awt.Dimension; `E\imL
/*** Example class. The height and width values should never * be |7^^*UzSK:
negative. */ .!}hhiF,Z
public class Example{ /i)Hb`(S
static final public int TOTAL_VALUES = 10; IOK}+C0e
private Dimension[] d = new Dimension[TOTAL_VALUES]; Uw<&Wm`'
public Example (){ } x>~p;z#VX
~B$b)`*
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ Y1dVM]l
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ B/"2.,
if (height < 0 || width < 0) _iEj
throw new IllegalArgumentException(); gq5qRi`q
if (d[index] == null) $A$@|]}p
d[index] = new Dimension(); +3,|"g::
d[index].height = height; #~Q8M*~@
d[index].width = width; Fpt-V
} &&L"&Rc
public synchronized Dimension[] getValues() ,eQ[Fi!!
throws CloneNotSupportedException{ :ZxLJK9x1
return (Dimension[])d.clone(); d/7l efF
} (}:C+p
'I
} :Au /2
hFvi5I-b
@rb l^
Z v0C@r
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: h<+|x7u
cywg[
Q&M'=+T
/9Ilo\MdD
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ k*-NsNPw$
Dimension[] copy = (Dimension[])d.clone(); 3hq1yyec
for (int i = 0; i < copy.length; ++i){ ~k'V*ERNSj
// NOTE: Dimension isn’t cloneable. (3*UPZv
if (d != null) &2EBk= X
copy = new Dimension (d.height, d.width); yoqa@ V
} ODf4+& u
return copy; *(cU]NUH_
} cbKL$|
!ax;5 @J
gUB{Bh($Y
K%}}fw2RMN
在克隆原子类型数据的多维数组的时候,也会犯类似的错误。原子类型包括int,float等。简单的克隆int型的一维数组是正确的,如下所示: ,M3z!=oIGn
z#<P}}
tiLu75vj
'Zk<l#"}
public void store (int[] data) throws CloneNotSupportedException{ eSl-9
^
this.data = (int[])data.clone(); HBLWOQab
// OK F?Or;p5`Y
} (OQ?<'Qa
G5Q!L;3HZ
jiIST^Zq#t
][Y^-Ak1
拷贝int型的二维数组更复杂些。Java没有int型的二维数组,因此一个int型的二维数组实际上是一个这样的一维数组:它的类型为int[]。简单的克隆int[][]型的数组会犯与上面例子中getValues()方法第一版本同样的错误,因此应该避免这么做。下面的例子演示了在克隆int型二维数组时错误的和正确的做法: SvK1.NUa
)Mzt3u
W'_/6_c$!
r@T| e
public void wrongStore (int[][] data) throws CloneNotSupportedException{ EaS~`
this.data = (int[][])data.clone(); // Not OK! f|xLKcOP
} =hw^P%Zn
public void rightStore (int[][] data){ 9u wL{P&
// OK! 4FA|[An
this.data = (int[][])data.clone(); [V@yRWI
for (int i = 0; i < data.length; ++i){ T{*^_
if (data != null) 1a9w(X
this.data = (int[])data.clone(); MB:n~>ga
} #Y[H8TW
} J"[3~&em
=8{*@>CX
N"DY?6
a]1i/3/
!=[uT+v
六、常见错误6#:检查new 操作的结果是否为null 7tH]*T9e>
CKTrZxR"
qmmv7==
BV9 *s
Java编程新手有时候会检查new操作的结果是否为null。可能的检查代码为:
qtSs)n
9y"TDo
p
q-!WQ
lSc,AOXp
Integer i = new Integer (400); |l90g|isJ
if (i == null) /BzA(Ic/
throw new NullPointerException(); (Cj,\r
6MrKi|'X@
|}qjqtZ
a@|.;#FF
检查当然没什么错误,但却不必要,if和throw这两行代码完全是浪费,他们的唯一功用是让整个程序更臃肿,运行更慢。 \;
bWh
g'G8 3F
3kLOoL?
- s|t^
C/C++程序员在开始写java程序的时候常常会这么做,这是由于检查C中malloc()的返回结果是必要的,不这样做就可能产生错误。检查C++中new操作的结果可能是一个好的编程行为,这依赖于异常是否被使能(许多编译器允许异常被禁止,在这种情况下new操作失败就会返回null)。在java 中,new 操作不允许返回null,如果真的返回null,很可能是虚拟机崩溃了,这时候即便检查返回结果也无济于事。 ~eo^`4O{{
@
t@|q
七、常见错误7#:用== 替代.equals >rwYDT#m]
Js}tZ\+P75
在Java中,有两种方式检查两个数据是否相等:通过使用==操作符,或者使用所有对象都实现的.equals方法。原子类型(int, flosat, char 等)不是对象,因此他们只能使用==操作符,如下所示: 0|2%# E
+ x_wYv
ci7~KewJ*
R^9"N?Q7;`
int x = 4; ida*]+ ~
int y = 5; 11*"d#
if (x == y) |h1^Gv
System.out.println ("Hi"); tL8't]M,
// This ’if’ test won’t compile. g)M#{"H
if (x.equals (y)) w2)/mSnu
System.out.println ("Hi"); -fM1$/]
}W
"(cYN_
h}6b&m
y@9Y,ZR*
对象更复杂些,==操作符检查两个引用是否指向同一个对象,而equals方法则实现更专门的相等性检查。 H!JWc'(<$
EHWv3sR-
DN|vz}s
-IvL+}K
更显得混乱的是由java.lang.Object 所提供的缺省的equals方法的实现使用==来简单的判断被比较的两个对象是否为同一个。 $i&\\QNn
eH=c|m]!P
\|!gPc%s
S 1ibw \'
许多类覆盖了缺省的equals方法以便更有用些,比如String类,它的equals方法检查两个String对象是否包含同样的字符串,而Integer的equals方法检查所包含的int值是否相等。 ,iOZ|
'aPCb`^;w
=[(%n94
m9g^ -X
大部分时候,在检查两个对象是否相等的时候你应该使用equals方法,而对于原子类型的数据,你用该使用==操作符。 =n
}Yqny
f)tc 4iV
~\bHfiIDy
Fhi5LhWe+.
八、常见错误8#: 混淆原子操作和非原子操作 `Y\QUj
7S2c|U4IM
N K"%DU<
l-=e62I{=|
Java保证读和写32位数或者更小的值是原子操作,也就是说可以在一步完成,因而不可能被打断,因此这样的读和写不需要同步。以下的代码是线程安全(thread safe)的: E<a.LW@
(qk5f`O
M;@Ex`+?i
|
W?[,|e
public class Example{ ZW2s[p r
private int value; // More code here... [5LMt*Y
public void set (int x){ (v|`LmV
// NOTE: No synchronized keyword MVuP
|&:n
this.value = x; "sIN86pCs
} ypT9 8
} u p~@?t2
jhcuK:`L
wKrdcWI,Z
/p[y1
不过,这个保证仅限于读和写,下面的代码不是线程安全的: 7?]!Ecr"
)Jz !Ut
0&o
WfTg
o(nHB
g
public void increment (){ 9>zDJx
// This is effectively two or three instructions: 8"pA9Mr
// 1) Read current setting of ’value’. u
dUXc6U
// 2) Increment that setting. T@>63
// 3) Write the new setting back. Q5T(nEA
++this.value; ,"C&v~
} ^B6`e^<
|>[X<>m
SJF 2k[da
~:s!].H
在测试的时候,你可能不会捕获到这个错误。首先,测试与线程有关的错误是很难的,而且很耗时间。其次,在有些机器上,这些代码可能会被翻译成一条指令,因此工作正常,只有当在其它的虚拟机上测试的时候这个错误才可能显现。因此最好在开始的时候就正确地同步代码: Z0z)
L]a|vp
YL!oF^XO
*q[^Q'jnN
public synchronized void increment (){ Y/!0Q6<[2Y
++this.value; iQ0&