代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 aMARZ)V
u]^N&2UW
b<I9 MR
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 a\uie$"cr]
aFiCZHohw
Hk_y/97OO
_zxLwU1(x
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 _?7#MWe&
.N4
5pNY)>]t=
V5"CSMe
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 d<w~jP\
?i\B^uB
BIaDY<j90
WzW-pV]
一、常见错误1# :多次拷贝字符串 `D(
xv
j;yf8Nf
q"KnLA(
D;GD<zC]
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 P=L$;xgp
iVtl72O
.o%^'m"=D[
HWns.[
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: {eJt,[Y *
E9j+o y
5\mTr)\R
uD_v!
String s = new String ("Text here"); 3OyS8`
&MmU
$FH18
q:>^ "P{
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: ;:S&F
p]D]:
Z}P
yAT^VRbv
s6*ilq1
String temp = "Text here"; hjZ}C+=O
String s = new String (temp); =*BIB5
9 A,Z|q/z5
;?fS(Vz~
7T``-:`[
但是这段代码包含额外的String,并非完全必要。更好的代码为: }/dRU${!
f8 /'%$N
I:&/`K4,x,
)O+}T5c=
String s = "Text here"; S 6,4PP
h-a!q7]l
8eIUsI.o
L(S'6z~_9
二、常见错误2#: 没有克隆(clone)返回的对象 <^.=>Q0S\
<>xJn{f0c
?,P3)&3g
ha@L94Lq
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: p=GWq(S6
0Q,g7K<d
b2(RpY2Y
c:S] R"
import java.awt.Dimension; w1x"
c>1C
/***Example class.The x and y values should never*be negative.*/ "@@I!RwA
public class Example{ sKz`aqI
private Dimension d = new Dimension (0, 0); j| Wv7
public Example (){ } MfO:m[s
1 Q;}zHd
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ qs\2Z@;
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ [%~
:@m
if (height < 0 || width < 0) (YBMsh
throw new IllegalArgumentException(); [L`ZE*z
d.height = height; ( m\$hX
d.width = width; 7qO a
;^T
} Tm^89I]L
#1c]PX
public synchronized Dimension getValues(){ *b *G2f^
// Ooops! Breaks encapsulation I
+5)Jau^S
return d; \}6;Kf}\
} Xdi:1wW@p
} "q}FPJ^l_N
7g=2Z[o
N#V.1<Y
eAUcv`[#p
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: !f>d_RG
nMDxH$O
r`&-9"+
o)6p A^+
Example ex = new Example(); )}Q(Tl\$
Dimension d = ex.getValues(); W)'*m-I
d.height = -5; i
8!zu!-0
d.width = -10; 1}hIW":3Sr
rG?>ltxB
g&dPd7
]&Z))H
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 64Lx-avf
z"D.Bm~ ]
!-s!f&_
u4z&!MT}
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 jF`BjxrG
ob+euCuJ
"~UUx"Y
3mPjpm
更好的方式是让getValues()返回拷贝: jq'!UN{
I%|>2}-_U
dd2[yKC`
HM>lg`S
public synchronized Dimension getValues(){ ?!qY,9lhH
return new Dimension (d.x, d.y); imQURC
} /iG*)6*^k
MO;X>D =
Qf@I)4'
rt
JtK6t
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 nRd)++
TW !&p"Us+
FP[!BUOf"
6c0>gUQx-
三、常见错误3#:不必要的克隆 T843":
,C3,TkA]
JNcYJ[wqv
4k./(f2+
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: lRh9j l
=4d (b ;
Bc3:}+l
@p\}p Y$T
/*** Example class.The value should never * be negative.*/ o\vBOp?hj
public class Example{ @F(mi1QO
private Integer i = new Integer (0); r$)w7Gk<
public Example (){ } boG_f@dv(
h gJ[LU| >
/*** Set x. x must be nonnegative* or an exception will be thrown*/ fNGZ o
public synchronized void setValues (int x) throws IllegalArgumentException{ E7-@&=]v
if (x < 0) g^zs,4pPU<
throw new IllegalArgumentException(); !!Z?[rj
i = new Integer (x); yDNOt C|
} QeF3qXI
K98i[,rP
public synchronized Integer getValue(){ KZPEG!-5
// We can’t clone Integers so we makea copy this way. ]qhPd_$?D'
return new Integer (i.intValue()); dH( ('u[
} `({T]@]V
} +}jJ&Z9)
S*yjee<@
v%Wx4v@%SE
tBpC: SG
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 +{/
RejQ5'Neh
ID/F
p,"g+ MwP
方法getValue()应该被写为: R:B^
.bio7c6
*2jK#9"MP
Yw0[[N<SW
public synchronized Integer getValue(){ =hl }.p
// ’i’ is immutable, so it is safe to return it instead of a copy. 7g3>jh
return i; {T].]7Z
} .`&F>o(A
$tl\UH7%2
NVRLrJWpp
r7qh>JrO
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: (y^vqMz
5eWwgA
-lv)tHs<
S{3nM<
?Boolean QE7V.
>J_p
?Byte 'F3)9&M
?Character &\_iOw8
?Class Vx-7\NB
?Double O+ICol
?Float }}<z/zN&^
?Integer o6p98Dpg
?Long "^Ybs'-
?Short Q%f|~Kl-hd
?String %.vVEy
?大部分的Exception的子类 B/n[m@O
M+x,opl
Nv"EV;$
QPq7R
四、常见错误4# :自编代码来拷贝数组 `He,p -
fj&i63?e
zen*PeIrA^
K^R,Iu/M
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: MuSUKBhM
^4o;$u4R
x|vqNZ\F
wiBVuj#
public class Example{ L{c q, jk
private int[] copy; >
%KuNy{
/*** Save a copy of ’data’. ’data’ cannot be null.*/ FKmFo^^0
public void saveCopy (int[] data){ znHnVYll(
copy = new int[data.length]; iya"ky~H
for (int i = 0; i < copy.length; ++i) I&YSQK:b
copy = data; B2O} 1.
} b3VS\[p
} 28=O03q
=xS+5(
[0D
Et
)[Yv?>ib
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: 7t*"%]o
{+@bZ}57
&/Q0
+U3m#Y )k
void saveCopy (int[] data){ NhYLtw^u
try{ Kx%Sku<F'
copy = (int[])data.clone(); rX1QMR7?
}catch (CloneNotSupportedException e){ Nj 00W1
// Can’t get here. S)/_muP
} jfqopiSi
} \~_9G{2?
DX|#
gUAm
WH\))y-
Alxx[l\<J
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: +) m_o"hl
L5\WpM=
%p0xM
>HS W]"k
static int[] cloneArray (int[] data){ g"> {9YE
try{ PKzyV ;
return(int[])data.clone(); :]1TGfS
}catch(CloneNotSupportedException e){ v@d
// Can’t get here. -Zz$~$
} lgtC |kM=
} W!&vul5
~7Tc$
"I
8M`#pN^
(#E.`e1#6
这样的话,我们的saveCopy看起来就更简洁了: [bk?!0]aV
d:U9pC$
c*@E_}C#
=Yt
R`
void saveCopy (int[] data){ E3iW-B8u8
copy = cloneArray ( data); 8UVmv=T
} ])!o5`ltZ
o$Z6zm xO
KPj\-g'A
|Xk4&sDrK
五、常见错误5#:拷贝错误的数据 !f+H,]D"
jYhB
+|
`u&Zrdr,
?CA,
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: ~'CE[G5
Oal3rb
9
o&`5
/Bm( `T
import java.awt.Dimension; :d#VE-e
/*** Example class. The height and width values should never * be %eO0wa$a
negative. */ b8LLr;oQw
public class Example{ !GNBDRr
static final public int TOTAL_VALUES = 10; 9^G/8<^^>
private Dimension[] d = new Dimension[TOTAL_VALUES]; eF3,2DDC
public Example (){ } Gnq?"</
vG"=h%
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ w_{wBL[3e
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ +P`*kj-P\
if (height < 0 || width < 0) 7w6cwHrL@
throw new IllegalArgumentException(); PU1Qsb5
if (d[index] == null) R'@9]99
d[index] = new Dimension(); A*/HjTX
d[index].height = height; @-!}BUs?
d[index].width = width; aD$v2)RR
} !74S
public synchronized Dimension[] getValues() 4QIX19{"
throws CloneNotSupportedException{ F6{bjv2A
return (Dimension[])d.clone(); PU^l.
} y3o25}"
} zp%Cr.)$
>^*+iEe
n+A?"`6*#
@3C>BLI8+
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: esMX-.8Cx
7B\Vs-d
zM(vr"U
yooX$
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ t~#zMUfac
Dimension[] copy = (Dimension[])d.clone(); O#S;q5L@
for (int i = 0; i < copy.length; ++i){ &N\jG373
// NOTE: Dimension isn’t cloneable. vRHd&0
if (d != null) K)DDk9*
copy = new Dimension (d.height, d.width); pw)||Q
} w"cZHm
return copy; |w<H!lGe!$
} g|vNhq0|i
~5CBEIF(NS
Z:sg}
-v]Sr33L
在克隆原子类型数据的多维数组的时候,也会犯类似的错误。原子类型包括int,float等。简单的克隆int型的一维数组是正确的,如下所示: 1Y7Eajt-5
iiS-9>]/
P;qN(2L/=<
=IQ5<;U3
public void store (int[] data) throws CloneNotSupportedException{ 3 I@}my1
this.data = (int[])data.clone(); rMLp-aR'
// OK { +
Zd*)M[
} e}
P I^bc
LIvFx|
+RYls|f
/tf5Bv'<
拷贝int型的二维数组更复杂些。Java没有int型的二维数组,因此一个int型的二维数组实际上是一个这样的一维数组:它的类型为int[]。简单的克隆int[][]型的数组会犯与上面例子中getValues()方法第一版本同样的错误,因此应该避免这么做。下面的例子演示了在克隆int型二维数组时错误的和正确的做法: y)W@{@{kl
~,oMz<iMV
_lGdUt 2
CsoiyY -2
public void wrongStore (int[][] data) throws CloneNotSupportedException{ I+[>I=ewa
this.data = (int[][])data.clone(); // Not OK! 1^7hf;|#g
} ,D]QxbwZ
public void rightStore (int[][] data){ 7b;I+q
// OK! +'I+o5*
this.data = (int[][])data.clone(); 8&?s#5zA
for (int i = 0; i < data.length; ++i){ *KV0%)}sbL
if (data != null) X.f>'0i
this.data = (int[])data.clone(); ][9%Kl*%@p
} {f2S/$q
} vdx0i&RiL
%.^_Ps0
1m:XR0 P
19Mu61
<SgM@0m
六、常见错误6#:检查new 操作的结果是否为null )4<__|52"1
R`DKu=
YaKeq5%y
x7:s]<kE
Java编程新手有时候会检查new操作的结果是否为null。可能的检查代码为: s;S?;(QI
VEqS;~[
?'@8kpb
3=FZ9>by
Integer i = new Integer (400); #h8Sq~0
if (i == null) EN;}$jZ>47
throw new NullPointerException(); 9F)z4
LcB]Xdsa(
F+,~v-
0=* 8
检查当然没什么错误,但却不必要,if和throw这两行代码完全是浪费,他们的唯一功用是让整个程序更臃肿,运行更慢。 ur$
_
K1r#8Q!t
ftvG\T f
juka0/
C/C++程序员在开始写java程序的时候常常会这么做,这是由于检查C中malloc()的返回结果是必要的,不这样做就可能产生错误。检查C++中new操作的结果可能是一个好的编程行为,这依赖于异常是否被使能(许多编译器允许异常被禁止,在这种情况下new操作失败就会返回null)。在java 中,new 操作不允许返回null,如果真的返回null,很可能是虚拟机崩溃了,这时候即便检查返回结果也无济于事。 6y+Kjd/D
\ 3wfwu.q
七、常见错误7#:用== 替代.equals #(#Wv?r6
:PIF07$xl
在Java中,有两种方式检查两个数据是否相等:通过使用==操作符,或者使用所有对象都实现的.equals方法。原子类型(int, flosat, char 等)不是对象,因此他们只能使用==操作符,如下所示: R7$:@<:g
=j5MFX.-o
B_nVP
s>;v!^N?u
int x = 4; TUV&vz{
int y = 5; -(bXSBs#
if (x == y) 5R@
System.out.println ("Hi"); Co (.:z~
// This ’if’ test won’t compile. {QHVo#
if (x.equals (y)) ,0*&OXt
System.out.println ("Hi"); *.sVr7=j
v,
n$^R
%QH)' GJQ
@ezH'y-v
对象更复杂些,==操作符检查两个引用是否指向同一个对象,而equals方法则实现更专门的相等性检查。 0E,8R{e
QMa;Gy
@wpN6 /
r=5{o1"
更显得混乱的是由java.lang.Object 所提供的缺省的equals方法的实现使用==来简单的判断被比较的两个对象是否为同一个。 y.e^h RKb
"U!AlZ`g
iF-6Y0~8
LR.Hh
许多类覆盖了缺省的equals方法以便更有用些,比如String类,它的equals方法检查两个String对象是否包含同样的字符串,而Integer的equals方法检查所包含的int值是否相等。 u.d).da
[5zx17'
C~nzH,5
$ACvV"b
大部分时候,在检查两个对象是否相等的时候你应该使用equals方法,而对于原子类型的数据,你用该使用==操作符。 qfL~Wp2E;
|]J>R
8P^ITL z%
0Sle
八、常见错误8#: 混淆原子操作和非原子操作 ,4zmb`dP<
[s"O mAy4
Y?
x,
3HLNCt09
Java保证读和写32位数或者更小的值是原子操作,也就是说可以在一步完成,因而不可能被打断,因此这样的读和写不需要同步。以下的代码是线程安全(thread safe)的: >TOu|r
J8S'/y(LE<
g .onTFwN
v@Gl|29_
public class Example{ )-0kb~;|
private int value; // More code here... q}~3C1
public void set (int x){ 19O,a#{KHf
// NOTE: No synchronized keyword V.O(S\
this.value = x; :p]'32FA!
} rM .|1(u
} o_@4Sl8
~i~7na|
v%"|WV[N
Sz^
veh?
不过,这个保证仅限于读和写,下面的代码不是线程安全的: R%Q@
62 O.?Ij
5&v~i\Q
O-LO/*5MI
public void increment (){ Min
^>
// This is effectively two or three instructions: -j73Wz
// 1) Read current setting of ’value’. }dp=?AFg
// 2) Increment that setting. A D1=[I3
// 3) Write the new setting back. o
Z%9_$Z
++this.value; ny`#%Vs
} Tav*+
W#KpPDgZE
7(qE0R&@
'yL%3h
_@
在测试的时候,你可能不会捕获到这个错误。首先,测试与线程有关的错误是很难的,而且很耗时间。其次,在有些机器上,这些代码可能会被翻译成一条指令,因此工作正常,只有当在其它的虚拟机上测试的时候这个错误才可能显现。因此最好在开始的时候就正确地同步代码:
c|M6<}
^:#D0[
(vb
SM}P
2
dAB-d:k
public synchronized void increment (){ ^c&L,!_)H
++this.value; B?4boF?~
} lEhk'/~
,wIONDnLZ
/% M/
i(iXD
九、常见错误9#:在catch 块中作清除工作 +tVaBhd!
JKGc3j,+#
hS)'a^FV
8>0e*jC
一段在catch块中作清除工作的代码如下所示: 7T1=q{#M
KTP8?Q"n0
dYL"h.x
D'J0wT#
OutputStream os = null; @>p<3_Y1
try{ 3wC' r
os = new OutputStream (); ynE)Xdh
// Do something with os here. m^bNuo
os.close(); sOU1n
}catch (Exception e){ 4|CtRF<L
if (os != null)
k~jP'aD
os.close(); jV4\A
} MBqt&_?K
>AcrG]
/7}It$|nhy
prTw'~(B
尽管这段代码在几个方面都是有问题的,但是在测试中很容易漏掉这个错误。下面列出了这段代码所存在的三个问题: Mdl{}P0)
)pq;*~IBI
-(f)6a+H
OoA|8!CFa
1.语句os.close()在两处出现,多此一举,而且会带来维护方面的麻烦。 v"#mzd.tW
pKit~A,Q
)i!o8YB
Sgp;@4`M
2.上面的代码仅仅处理了Exception,而没有涉及到Error。但是当try块运行出现了Error,流也应该被关闭。 OH">b6>\
UFp,a0|
Tl2C^j
0V'nK V"|
3.close()可能会抛出异常。 L{h%f4Du#
%Ti}CwI`
f!!V${)X
G>1eFBh }
上面代码的一个更优版本为: yIDD@j=l
UN]f"k&