代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 k3#wLJ
mfz"M)1p1
8,H~4Ce3
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 w7r'SCVh3+
1Lc8fP$
0a@c/XGBp
CxkMhd8qz
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 nqrDT1b**
>I|<^$/
1B(G]o_>!
zv,\@Z9.($
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 /RMer
Xj
SbCJ|z#?
-GFwFkWm
vyujC`61d
一、常见错误1# :多次拷贝字符串 n~.% p
[Zh2DNp
k5q(7&C
]M uF9={
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 URk$}_39
GG*BN<(>!
u!M&;QL
"7:u0p!
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: KjC[q
F~%|3a$Y
ML"_CQlE7
waBRQh
String s = new String ("Text here"); @\+%GDv
";o~&8?)
{rz>^
'r6 cVBb}
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: \Ec
X!aC
~R)1nN|
X"wFQa
1T:)Zv'
String temp = "Text here"; _@7(g(pY 3
String s = new String (temp); { qjUI
>=bt
X,&`WPA:S
z_'dRw
但是这段代码包含额外的String,并非完全必要。更好的代码为: \G]K,TG
e5QOB/e&
]Kof sU_{
3Sk5I%
String s = "Text here"; EkDws`@
GpScc'a7
makaI0M
U-ERhm>uk
二、常见错误2#: 没有克隆(clone)返回的对象 kja4!_d
6V+V
zDo
F_K
ShsJ_/C2
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: N!]PIWnC
,nI_8r"M>
]Qh[%GD
$3lt{ %
import java.awt.Dimension; <1TlW
~q<
/***Example class.The x and y values should never*be negative.*/ !,I7 ?O
public class Example{ ZBPd(;"x+
private Dimension d = new Dimension (0, 0); LAj}kW~
public Example (){ } Oib[\O7[z
bN]\K/
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ O}e|P~W
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ ^
sS>Mts
if (height < 0 || width < 0) w{RNv%hJ$=
throw new IllegalArgumentException(); r4;^c}
d.height = height; "0!~g/X`rK
d.width = width; dBsRm{aS
} v`@5enr
?.]o_L_K
public synchronized Dimension getValues(){ /j`i/Ha1
// Ooops! Breaks encapsulation Og_2k
~
return d; f34_?F<h
} 6s> sj7
} ~JIywzcf8
5`(((_Um+
Uf=vs(
3| GNi~
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: ,w,ENU0~f
[c,|Lw4
K-N]h
A9NOeE
Example ex = new Example(); + 8MW$ m$
Dimension d = ex.getValues(); H(
d.height = -5; =1%zI%
d.width = -10; d/"gq}NT
R>Z,TQU
SD)5?{6<
aS c#&{
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 A@9U;8k
&*Q|d*CP
7}. #Z
>1#DPU(g
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 yBpW#1=
$q4 XcIX 7
67Af} >Q
)->-~E}p9
更好的方式是让getValues()返回拷贝: _lP4ez
Y
Ukk-(gjX
s:-8 Z\,
GN"M:L^k`
public synchronized Dimension getValues(){ 6ON
return new Dimension (d.x, d.y); jx^|2
} *+_fP |cv
;t.SiA
QO1A976o
hNu>s
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 dSA
[3V
WZ-4^WM=!
DDqC}l_
D#vn {^c8O
三、常见错误3#:不必要的克隆 tJ(c<:zD
@d8&3@{R^
-D.BJ(
EM>c%BH<N
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: eONeWY9
BN<#x@m$]
V0SW 5
m
=)"NE>
/*** Example class.The value should never * be negative.*/ ,kGw;8X
public class Example{ UUdu;3E=5
private Integer i = new Integer (0); pq/FLYiv
public Example (){ } Thht_3_C,f
v*C+U$_3\1
/*** Set x. x must be nonnegative* or an exception will be thrown*/ /-G qG)PX
public synchronized void setValues (int x) throws IllegalArgumentException{ !`O_VV`/@
if (x < 0) G#9o?
throw new IllegalArgumentException(); }J'5EAp
i = new Integer (x); H{Y5YTg]
} O+{pF.P#V
o{S}e!Vb
public synchronized Integer getValue(){ W<cW;mO
// We can’t clone Integers so we makea copy this way. tk3<sr"IQ
return new Integer (i.intValue()); iOX4Kl
} 886 ('
} thlpj*|
teQaHe#
~P"!DaAf
B BApL{
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 hy!'Q>[`
tF;& x
g
,oB k>
6N)<
o ;U
方法getValue()应该被写为: aPY>fy^8D
82Z[eo
ei|*s+OZu
"c !oOaA
public synchronized Integer getValue(){ kMJQeo79
// ’i’ is immutable, so it is safe to return it instead of a copy. (>+k 3
return i; 5tgILxSK
} Hb@G*L$
4$q)e<-
e GqvnNv
NbQMWU~7
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: -Fok%iQ'5
,
$D&WH
`ykMh>*{
C-:SQf
?Boolean N18diP[C
?Byte Nw3I
?Character 2EqsfU*
I
?Class =yhn8t7@]
?Double N,sqr k]
?Float H8o%H=I%
?Integer 8 /RfNGY
?Long >2/wzsW
?Short QBPvGnb
?String ^ T:qT*v
?大部分的Exception的子类 5u
u2 _B_L
3wa<,^kqy
r:8]\RU
5.C[)`_
四、常见错误4# :自编代码来拷贝数组 P98X[0&
:yO,
==e#CSJq
sJHy=z0m
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: wk@(CKQzI,
yTq(x4]
kj<D 4)
g.`t!6Hc
public class Example{ wCC~tuTpr
private int[] copy; &\6`[# bT
/*** Save a copy of ’data’. ’data’ cannot be null.*/ }
{gWTp
public void saveCopy (int[] data){ 3>@qQ_8%~
copy = new int[data.length]; _?(hWC"0
for (int i = 0; i < copy.length; ++i) }Nd`;d
copy = data; >m_p\$_
} ;SlS!6.W-
} S'%cf7Z
t\|K"
g_Dt} !A\B
thZ@BrO#
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: HA3SQ
C}8e<[})
>gOI]*!5
!+|N<`
void saveCopy (int[] data){ C$..w80/1
try{ (61twutC
copy = (int[])data.clone(); K+\0}qn
}catch (CloneNotSupportedException e){ Y=WN4w
// Can’t get here. qY~$wVY(
} hO<w]jV,
} M;vlQ"Yl'
(HV~ '5D
He71h(BHm
{,-5k.P[
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: ?C>VB+X}y
m^oi4mV
:86luLFm
VqO<+~M,E
static int[] cloneArray (int[] data){ Qdx`c^4m
try{ d;jJe0pH
return(int[])data.clone(); zhvk%Y:
}catch(CloneNotSupportedException e){ TLL[F;uZ
// Can’t get here. Lugk`NUvF
} Eztz~oFo
} E_gDwWot
M;TfD
m|cWX"#g
b\|p
这样的话,我们的saveCopy看起来就更简洁了: -JQg ~1
6<Pg>Bg
M?4r 5R
<11Tqb
void saveCopy (int[] data){ 5t5S{aCDr
copy = cloneArray ( data); t ]I(98pY
} s3E~X
pv?17(w(\
`@.s!L(V
J0*]6oD!
五、常见错误5#:拷贝错误的数据 (h>X:!
~ew**@N
]gZ8b-
2O
Iv|WeSL.
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: 0*:hm%g
.lF\b A|
qpwh #^2
I&NpN~AU
import java.awt.Dimension; .gkPG'm[
/*** Example class. The height and width values should never * be H{A| ~V)
negative. */ _Il9s#NA%
public class Example{ ch8w'
static final public int TOTAL_VALUES = 10; ${?ex nb$
private Dimension[] d = new Dimension[TOTAL_VALUES]; 3;l>x/amk
public Example (){ } 6ewOZ,"j"4
^FMa8;'o
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ tfKeo|DM"
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ F)iGD~
if (height < 0 || width < 0) e%c5OZ3~
throw new IllegalArgumentException(); bq&S?! =s
if (d[index] == null) ?N?pe}
d[index] = new Dimension(); 8\.1m9&r>o
d[index].height = height; XM@i|AK
M0
d[index].width = width; ]j$p _s>
} m^x\@!N:(
public synchronized Dimension[] getValues() xv% USm
throws CloneNotSupportedException{ `FB?cPR
return (Dimension[])d.clone(); Qp.!U~
} 2hC$"Dfp
} ~_j%nJ
&2
m"7 R
4O
n_&)VF#n(
%s :
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: A-Pwi.$
NEou2y+}
qVe6RpS
4NR5?s
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ 5a|m}2IX
Dimension[] copy = (Dimension[])d.clone(); 8lGgp&ey
for (int i = 0; i < copy.length; ++i){ (Dh;=xG
// NOTE: Dimension isn’t cloneable. S!!\!w>N
if (d != null) kDP^[V
P+
copy = new Dimension (d.height, d.width); L?C~
qS2g
} @=#s~ 3
return copy; Z*aU2Kr`;
} `"":
St&H