代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 qEuO@oE
)
[?xT
iMt3h8
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 xu_,0ZT]{
4w~%MZA^
;1[Z&Uv8
`<yQ`Y_X
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 nj0sh"~+
(M"rpG>L
~5`oNa
2mnAL#
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 ^P^%Q)QXl
e*qGrg (E
M,S'4Szuk
$%q=tn'EX
一、常见错误1# :多次拷贝字符串 nX 9]dz
(5 @H
;xe.0j0h
BO#tn{(#
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 SF&2a(~s
5e$1KN`
vjS=ZinN"
Lj(cCtb)
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: Jn^b}bk t
Hc=QSP
ghWWJx9
t+}wTis
String s = new String ("Text here"); Bp_R"DS7A
7]xDMu'^&f
i?Pnyi
,bXZ<RY$
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: C= V2Y_j
1Vdi5;dn
8'zZVX D<
y7M{L8{0
String temp = "Text here"; z,4mg6gt
String s = new String (temp); <|4$TH^t
>P:X\5Oj
hK{H7Ey*
xsB0LUt
但是这段代码包含额外的String,并非完全必要。更好的代码为: vo`&
'"fJA/O
q6)fP4MQ]
i-Rn,}v
String s = "Text here"; 6ki2/ Q
@]vY[O!&;
EM*I%|n@m
>i,_qe?V:w
二、常见错误2#: 没有克隆(clone)返回的对象 1*9.K'
4_kN';a4Q
tLWw<)t
N=BG0t$
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: (_zlCHB
A vq+s.h
k[D_L`
GeTk/tU
import java.awt.Dimension; ,< x/
/***Example class.The x and y values should never*be negative.*/ *u1q7JFQk
public class Example{ ~_vSMX
private Dimension d = new Dimension (0, 0); )rK2%\Z
public Example (){ } \~ChbPnc
\"oZ\_
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ OALNZKP
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ }I&.xzJ
if (height < 0 || width < 0) ZrTB%
throw new IllegalArgumentException(); ? +L,
d.height = height; \]V:>=ry>
d.width = width; qK a}O*
} GYfOwV!zB
[|OII!"
public synchronized Dimension getValues(){ teg5g|*
// Ooops! Breaks encapsulation HCs^?s8Pp
return d; +QU>D:l
} JP5e=Z<
} E(P
6s;LZ
3&+dyhL'w
Z5>~l
?b,>+v-w::
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: &2y4k"B&)
}yEV&&
@
w'2FYe{wj
RJ{$`d
Example ex = new Example(); ki9&AFs2X
Dimension d = ex.getValues(); !k)6r6
d.height = -5; '\I(n|\
d.width = -10; 2+gbMd4n
p H y
C7FQc{
y4Jc|)
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 I_ mus<sE
IC0L&;En
dT|f<E/P
CaJ-oy8
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 V:kRr cX
=0=#M(w
CJ\a7=*i
iYStl
更好的方式是让getValues()返回拷贝: `F7]M
=\oH=
f
}tW-l*\U
%+(AKZu:
public synchronized Dimension getValues(){ B$_4ul\)
return new Dimension (d.x, d.y); ,x8;| o5
} I9S;t_Z<
OOqT 0wN
il5C9ql$
JXK\mah
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 X&pYLm72;
N `|A
'Rn-SD~gIr
pbzFzLal
三、常见错误3#:不必要的克隆 8}B
:5NMgR.d
UphTMyn3
y|5s
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: r)iEtT!p*
~T1W-ig4[*
m^D'p
DXLXGvcM
/*** Example class.The value should never * be negative.*/ :<qe2Z5k
public class Example{ *,\"}x*
private Integer i = new Integer (0); @V%\Gspv
public Example (){ } qT$k%(
:\OSHs<M
/*** Set x. x must be nonnegative* or an exception will be thrown*/ q-JTGCFl
public synchronized void setValues (int x) throws IllegalArgumentException{ #d-({blo<
if (x < 0) 1>J.kQR^
throw new IllegalArgumentException(); $D^\[^S
i = new Integer (x);
IOl_J>D]F
} +~^S'6yB
n[3z_QI
public synchronized Integer getValue(){ Qg*\aa94
// We can’t clone Integers so we makea copy this way. 0\dmp'j]
return new Integer (i.intValue()); .EKlw##
} m-AF&( ;K
} x0
)V
o]r
"I.6/9
h6h6B.\Ld
Ei4^__g\'
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 <7^|@L
6
%Rk|B`ST
u&:N`f
=l`)b
方法getValue()应该被写为: NI V}hf YF
#fuUAbU0X
v"G1vSx)BT
iq; |
i!
public synchronized Integer getValue(){ 75# 8P?i
// ’i’ is immutable, so it is safe to return it instead of a copy. g&$=Y7G
return i; tIuM9D{P
} *2/Jg'de
]cp b;UfM
Z=JKBoAY
1sqE/-v1_^
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: P(D>4/f3"
%B%_[<B
LZykc
c9g
OyTK,i<n
?Boolean -r\jIO_
?Byte >yO/p(/;jR
?Character {iD/0q
?Class <]rayUyaf
?Double l/N<'T_G
?Float ZJ/528Ju
?Integer J>Ar(p
?Long /q9I^ ztV
?Short A,~3oQV
?String B7%,D}
?大部分的Exception的子类 FuHBzBoM=
%ih\|jRt
>]h{[kU %4
51k}LH
四、常见错误4# :自编代码来拷贝数组 ._}}@V_/
<o(;~
t<!m4Yd|#
fd)8lK[KJ"
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: R]"Zv'M(AM
qed_ PsI
7
Lm9I
(?qCtLZ
public class Example{ Sy8t2lk
private int[] copy; =3bk=vy
/*** Save a copy of ’data’. ’data’ cannot be null.*/ ;8]HCC@:
public void saveCopy (int[] data){ s%jBIeh
copy = new int[data.length]; J
n.7W5v
for (int i = 0; i < copy.length; ++i) iXWHI3
copy = data; uKJ:)oyaCP
} 4$Ai!a
} B{Cm`f8E
R$:-~<O
@@Q4{o
zIc6L3w$
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: DsdM:u*s
fQoAdw
b^W&-Hh
IL@yGuO,
void saveCopy (int[] data){ !:+U-mb*
try{ tV++QC7@L
copy = (int[])data.clone(); P
y'BMk
}catch (CloneNotSupportedException e){ Z518J46o
// Can’t get here. [+[W\6
} y_WC"
} <-`bWz=+
ufL,Kq4
g#I`P&
;j0.#P:a
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: 7F"ljkN1S
48xgl1R(j
7'wpPXdY1
4!!|P
static int[] cloneArray (int[] data){ maap X/J
try{ G@s:|oe
return(int[])data.clone(); voZaJ2ho/O
}catch(CloneNotSupportedException e){ k=)U
// Can’t get here. Sm/8VSY
} BbB3#/g
} 0]>bNbLB"
!{&r|6
x.1=QF{!
=]@Bc
7@
这样的话,我们的saveCopy看起来就更简洁了: Zr}>>aIJ]k
N<JI^%HBgP
UN?tn}`!
D4$b-?y
void saveCopy (int[] data){ %<yW(s9{
copy = cloneArray ( data); r`"_D%kc
} ev&l=(hY
Rxy|Ag/I;V
kH 9k<{
}wf8y
五、常见错误5#:拷贝错误的数据 sX?arI=_U
~D5
-G?%$"
'&CZ%&(Gw
0hS&4nW
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: IR/S`HD_
K E\>T:
oypLE=H
u8"s#%>Ny
import java.awt.Dimension; |1wZ`wGZ:L
/*** Example class. The height and width values should never * be ],c0nz^%BR
negative. */ Kj0)/Fjl+
public class Example{ % 3#g-
static final public int TOTAL_VALUES = 10; v=^^Mr"Z^
private Dimension[] d = new Dimension[TOTAL_VALUES]; VmQ^F|
{
public Example (){ } wo9R:kQ
3r%v@8)!b
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ 9No6\{[M
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ n[/D>Pi
if (height < 0 || width < 0) l"8g9z
throw new IllegalArgumentException(); 88u[s@
if (d[index] == null) thPAD+u.3
d[index] = new Dimension(); %Vo'\|
d[index].height = height; $Y/z+ea
d[index].width = width; 5T/+pC$e=
} XzAXcxC6G
public synchronized Dimension[] getValues() pll5m7[
throws CloneNotSupportedException{ Z{3=.z{&^=
return (Dimension[])d.clone(); y95
#t
} TrDTay
} IiKU=^~w
B)k/]vz)*D
!5 S#
e\z,^
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: 0Y`+L6&UX
%YxKWZ/?
u9_?c
G-
eI|FrBq%
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ z{.&sr>+v
Dimension[] copy = (Dimension[])d.clone(); D*L@I@
[
for (int i = 0; i < copy.length; ++i){ Fmn_fW6
// NOTE: Dimension isn’t cloneable. tdU'cc?M
if (d != null) qLBQ!>lR
copy = new Dimension (d.height, d.width); 8Ogg(uS70'
} Ez
<YD
return copy; kU:Q&[/jzH
} jhT/}"v
z%fjG} z
i(rYc
?(s9dS,7wZ
在克隆原子类型数据的多维数组的时候,也会犯类似的错误。原子类型包括int,float等。简单的克隆int型的一维数组是正确的,如下所示: |QrVGm@2
!le#7Kii
Lh+7z>1
)~)T[S
public void store (int[] data) throws CloneNotSupportedException{ 8hV4l'Pa72
this.data = (int[])data.clone(); :|l0x a
// OK /p-k'387
} @V4nc
'o.
xfUV'=~(
*o=Z~U9z
x>i =
拷贝int型的二维数组更复杂些。Java没有int型的二维数组,因此一个int型的二维数组实际上是一个这样的一维数组:它的类型为int[]。简单的克隆int[][]型的数组会犯与上面例子中getValues()方法第一版本同样的错误,因此应该避免这么做。下面的例子演示了在克隆int型二维数组时错误的和正确的做法: 8U#14U5rS
*`s*l+0b
Mf5kknYuL9
w^~s4Q_>>
public void wrongStore (int[][] data) throws CloneNotSupportedException{ ,*$Y[UT
this.data = (int[][])data.clone(); // Not OK! m%U=:u7#M
} .:-*89c
public void rightStore (int[][] data){ o &b\bK%E
// OK! '<"%>-^Gn
this.data = (int[][])data.clone();
i[/1AI
for (int i = 0; i < data.length; ++i){ "97sH_
,
if (data != null) $hM9{
this.data = (int[])data.clone(); Kd}%%L
} .Sm 8t$
} RaiYq#X/
x!4<ff.
2Z(?pJyDM
_Wp,
z`
Nj;(QhYZ
六、常见错误6#:检查new 操作的结果是否为null g6q[
I8
j1JdG<n
\KEmfCx'n
r*7J#M /
Java编程新手有时候会检查new操作的结果是否为null。可能的检查代码为: SM}&
@cJ
NR^Z#BU
&sq q+&ao
c:DV8'fT
Integer i = new Integer (400); <95*z @
if (i == null) +C$wkx]
throw new NullPointerException(); ZU:c[`
V" 5rIk
2 $Z4 >!
ZB}zT9JaE
检查当然没什么错误,但却不必要,if和throw这两行代码完全是浪费,他们的唯一功用是让整个程序更臃肿,运行更慢。 (Q"s;g
3qfQlqJ&3
7n#Mh-vq
ipiS=
C/C++程序员在开始写java程序的时候常常会这么做,这是由于检查C中malloc()的返回结果是必要的,不这样做就可能产生错误。检查C++中new操作的结果可能是一个好的编程行为,这依赖于异常是否被使能(许多编译器允许异常被禁止,在这种情况下new操作失败就会返回null)。在java 中,new 操作不允许返回null,如果真的返回null,很可能是虚拟机崩溃了,这时候即便检查返回结果也无济于事。 i .?l\
CwF=@:*d
七、常见错误7#:用== 替代.equals uN&49o
`)jAdad-s
在Java中,有两种方式检查两个数据是否相等:通过使用==操作符,或者使用所有对象都实现的.equals方法。原子类型(int, flosat, char 等)不是对象,因此他们只能使用==操作符,如下所示: $nthMx$
mqQ//$Y
<XpG5vV
AQ-R^kT
int x = 4; O sIvW'$\
int y = 5; !O,`Z`T?
if (x == y) )q+;+J`>
System.out.println ("Hi"); E-rGOm" m
// This ’if’ test won’t compile. =HoA2,R)
if (x.equals (y)) M/6q
^*
System.out.println ("Hi"); `?"[u"*
*fDhNmQ `
L{1PCs36c
.|6Wmn-uS
对象更复杂些,==操作符检查两个引用是否指向同一个对象,而equals方法则实现更专门的相等性检查。 k1^&;}/f:
F-?s8RD
][Cg8
cj3P]2B#
更显得混乱的是由java.lang.Object 所提供的缺省的equals方法的实现使用==来简单的判断被比较的两个对象是否为同一个。 }
AHR7mu=
Daf;;
w
~<_PjV
:2&W9v
许多类覆盖了缺省的equals方法以便更有用些,比如String类,它的equals方法检查两个String对象是否包含同样的字符串,而Integer的equals方法检查所包含的int值是否相等。 /vPcg
6[%4Q[
!_1RQ5]^
vP&JL~
大部分时候,在检查两个对象是否相等的时候你应该使用equals方法,而对于原子类型的数据,你用该使用==操作符。 d>Np; "
]+78
"(
\R#OJ=F
cCy*?P@
八、常见错误8#: 混淆原子操作和非原子操作 !vSj1w
dBp)6ok#c
[%6"UH
r
x_KJCU
Java保证读和写32位数或者更小的值是原子操作,也就是说可以在一步完成,因而不可能被打断,因此这样的读和写不需要同步。以下的代码是线程安全(thread safe)的: v+2t;PJd2
7gbu7"Qc
Pu|3_3^
z/S}z4o/
public class Example{ xcl8q:
private int value; // More code here... TqXB2`7Ri
public void set (int x){ #ruL+-8!<
// NOTE: No synchronized keyword ^&8xfI6?
this.value = x; w`K=J!5y2g
} -6>T0-
} 7%^/Jm
^5*9BwH`
||kUi=5
|Xk>a7X
不过,这个保证仅限于读和写,下面的代码不是线程安全的: odpjEeQC
vZt48g
6<C|O-
_QOZ`st
public void increment (){ S_56!
// This is effectively two or three instructions: -]~vEfq+T
// 1) Read current setting of ’value’. v|u[BmA)*k
// 2) Increment that setting. s(Llz]E~ZX
// 3) Write the new setting back. 2_Otv2
++this.value; rXu^]CK
*G
} -eoXaP{[
G$$y\e$
q'[q]
2i_k$-
在测试的时候,你可能不会捕获到这个错误。首先,测试与线程有关的错误是很难的,而且很耗时间。其次,在有些机器上,这些代码可能会被翻译成一条指令,因此工作正常,只有当在其它的虚拟机上测试的时候这个错误才可能显现。因此最好在开始的时候就正确地同步代码: sDnXgCcS!
=6:>C9
EG[Rda
5ki<1{aVtZ
public synchronized void increment (){ .a`(?pPr,
++this.value; DNl'}K1W
} P<A_7Ho
K_sHZ
%gE*x
#
'&hk?
九、常见错误9#:在catch 块中作清除工作 =;?afUj
Dm0Ts~
YE5B^sQ1
)Cfk/OnRd
一段在catch块中作清除工作的代码如下所示: :N
~A7@
of k@.TmO
>")<pUQ
UiLiy?EJ
OutputStream os = null; 9d_
Zdc
try{ (Ld,<!eN0
os = new OutputStream (); =9AX\2w*H;
// Do something with os here. ic(`E v
os.close(); #XPY\n^k
}catch (Exception e){ _gl7Ma
if (os != null) 6wY6*R
os.close(); I@/+=
} L&L