代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 ,3,(/%=k
3$n O@rOS
^V?W'~
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 A>Y#-e;<d
sKniqWi
sMDHg
',l}$]y5
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 nl/~7({
n1Fp$9%
Mrly(*!U"@
s;Sv@=\
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 RA3!k&8?#
!WgVk7aP`
py%_XL=w,
^77X?nDz=h
一、常见错误1# :多次拷贝字符串 `&/~%>
zcCGREe=
.[:2M9Rx
'nP;IuMP
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 |S.;']t+
ykBq?Vr
+2Wijrn
Kq&JvY^
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: %(d0`9
bSm*/Q
R.)w
l
3x3 =ke!
String s = new String ("Text here"); CZ|h` ";P2
,cwjieM
~`!{5:v
" ,>,t_J
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: m5kt
O^EU
)^[PW&=W|x
5K[MKfT
CMviR<.
String temp = "Text here"; K<~J*k<v
String s = new String (temp); ^w+)A;?W
.NRSBk
6[S-%|f
M 0Vs9K=
但是这段代码包含额外的String,并非完全必要。更好的代码为: x>eV$UJ
%?i~`0-:n%
y25L`b
nf)y_5y
String s = "Text here"; ! ~3zp L
]>H'CM4JR
c]>LL(R-7)
:7DXLI|L#?
二、常见错误2#: 没有克隆(clone)返回的对象 2Nvb Q 3c5
&+Yoob]P
Uk<2XGj
0.!!rq,
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: /;9iDjG
U%H6jVE
@; 0t+
&KPJB"0L
import java.awt.Dimension; xQ~N1Y2W
/***Example class.The x and y values should never*be negative.*/ HqoCl
public class Example{ j@C*kj;-
private Dimension d = new Dimension (0, 0); b1Fd]4H3P
public Example (){ } ql2O%B.6?
^Y?Y5`!Q
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ KreF\M%Ke
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ &N:`Rler
if (height < 0 || width < 0) 84jA)
throw new IllegalArgumentException(); *e%(J$t
d.height = height; a1 .+L
d.width = width; c!Wj^
} t;L7H E@Y
}KFM8CbS
public synchronized Dimension getValues(){ o*Kl`3=]
// Ooops! Breaks encapsulation ' T%70)CM~
return d; 5KRI}f
} ) ={
H
} D9;s%
k\A[p\
P2=u-{?~
rO7[{<97m
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: Rb
l4aB+
mt*/%>@7R
+hUz/G+3
4">C0m;ks
Example ex = new Example(); aa,^+^J
Dimension d = ex.getValues(); DxJY{e9
d.height = -5; >%i]p
d.width = -10; -cs$E2
-
Rn5{s3?F~2
8rpr10;U
$0P7^4)w:
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 thrv_^A
;noZmPa
i f !
-Ta|
qQa
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 |P(8T'
tde&w=ec
jzAXC^FS
Fd1jElt
更好的方式是让getValues()返回拷贝: ;QqC c!b
3R6=C~
;$/]6@bqB
;Wedj\Kkp
public synchronized Dimension getValues(){ [kL`'yi
return new Dimension (d.x, d.y); z\+Ug9Of
} 2b^E8+r9
6![}Jvu>
lCiRvh1K
8;5@5Au
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 /nz J`d
EN;4EC7tE
f=T&$tZ<
%%qg<iO_
三、常见错误3#:不必要的克隆 :[!b";pR
-E>LB\[t)
E<r<ObeRv`
cxc-|Xori
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: ?eOw8Rom
3%YDsd vQx
s>ohXISB[
]@
N::!m
/*** Example class.The value should never * be negative.*/ R7 ^f|/l
public class Example{ dr^MW?{a\
private Integer i = new Integer (0); ]!v\whZ>
public Example (){ } oN&U@N/>aU
^\7GFpc
/*** Set x. x must be nonnegative* or an exception will be thrown*/ -I.BQ
public synchronized void setValues (int x) throws IllegalArgumentException{ \vF*n Z5/
if (x < 0) ujh`&GiB+
throw new IllegalArgumentException(); I
T gzD"d
i = new Integer (x); (gjCm0#_%
} ?v}S9z
`P?!2\/
public synchronized Integer getValue(){ W
![*0pL
// We can’t clone Integers so we makea copy this way. &FY7
D<
return new Integer (i.intValue()); }E#1Z\)
} h]DzX8r}
} DT3koci(
C(&3L[
9@wmngvM*Y
T~)R,OA7m
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 )uC5
FZx.Yuv
!XQ)>T^G5
)M_|r2dDq3
方法getValue()应该被写为: lD6PKZ\RIj
lt& c/xi_
J7p?9
0`Y"xN`'i
public synchronized Integer getValue(){ Y InPmR
// ’i’ is immutable, so it is safe to return it instead of a copy. s_cur-
return i; zszx~LSvIT
} *}=z^;_oq
RKb (
iyP0;$
ZG8Xr"
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: 2q ~y\fe
S3ZIC\2
{ZKXT8'
\?"p]&2UcB
?Boolean cBQ+`DXn5c
?Byte 3 uJ?;
?Character f{)n xd
>#
?Class Xp0S
?Double [ps5
?Float 'Xu3]'m*
?Integer r>g5_"FL
?Long .~fov8
?Short KN".0WU
?String *{dMo,.eI
?大部分的Exception的子类 Y'76! Y
n a+P|'6
U3BhoD#f\
zo:NE00
四、常见错误4# :自编代码来拷贝数组 G(gZL%M6
rY295Q
1!Afq}|
&{g y{npQ
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: [" sm7yQ
sf8F h
_&aPF/
mb*|$ysPx
public class Example{ <KFl4A~
private int[] copy; :*MR$Jf
/*** Save a copy of ’data’. ’data’ cannot be null.*/ O3tw@ &k
public void saveCopy (int[] data){ *vOk21z77d
copy = new int[data.length]; UdcrX`^.
for (int i = 0; i < copy.length; ++i) .,\^{.E
copy = data; =Y5_@}\0
} Nf%/)Tk
} 7yUX]95y8
=b#:j:r
1 Q*AQYVY
| z?c>.
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: {\gpXVrn_
=s&ycc;-5}
rUZ09>nDy
:Z+Jt=;
void saveCopy (int[] data){ %H4>k#b@$
try{ <[?ZpG
copy = (int[])data.clone(); 1AQy8n*
}catch (CloneNotSupportedException e){ -|I_aOC@
// Can’t get here. ^.c<b_(=h
} ef2)k4)"
} !o\e/HGc!
lYS*{i1^ '
t&~*!w!+jH
kAq#cLprG
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: eVj7%9
Sl'{rol'
U#|6n ,
O]KQ]zN
static int[] cloneArray (int[] data){ slaH 2}$xR
try{ ]q/USVj{
return(int[])data.clone(); I.it4~]H
}catch(CloneNotSupportedException e){ a|z@5r%
// Can’t get here. %DM0Z8P$B-
} V=<AI.Z:w
} "[0.a\ d<
L~C:1VG5
}=|plz}
Y|buQQ|
这样的话,我们的saveCopy看起来就更简洁了: w-3Lw<
?in)kL
<6+T&Ov6
@hy~H?XN
void saveCopy (int[] data){ L%+mD$@u
copy = cloneArray ( data); HlEHk'
} A9MTAm{
M 0$E_*
ij1YV2v
V[n,fEPBr
五、常见错误5#:拷贝错误的数据 jB`:(5%RO
wo;OkJKF
sKk+^.K}|
3-'3w ,
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: j4u
["O3
=.w~qL
'V&Tlw|
i! gS]?*DH
import java.awt.Dimension; AH_qZTv0{Q
/*** Example class. The height and width values should never * be XlnSh<e
negative. */ Mmg~Fn
public class Example{ BGHZL~
static final public int TOTAL_VALUES = 10; P](8Qrl
private Dimension[] d = new Dimension[TOTAL_VALUES]; 20}w.V
public Example (){ } <@Vf:`a!P>
)sNPWn8<Uy
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ %NM={X|'
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ |>Fz:b d
if (height < 0 || width < 0) %k~ezn
throw new IllegalArgumentException(); 4c[/%e:\-
if (d[index] == null) jw`05rw:
d[index] = new Dimension(); PQa0m)H@
d[index].height = height; crN*eFeW
d[index].width = width; n oM=8C&U
} pb)kN%
public synchronized Dimension[] getValues() tP"6H-)X&
throws CloneNotSupportedException{ SN]g4}K-
return (Dimension[])d.clone(); &AWrM{e
} +vxOCN4}v
} x/wgD'?
Z4rk$K'=1w
Cs
y,3XG
+I3O/=)
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: \P")Eh =d
<i]0EE}%
^;rjs|`K#
S[zGA<}
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ ?$T ^L"~
Dimension[] copy = (Dimension[])d.clone(); vhAgX0k
for (int i = 0; i < copy.length; ++i){ "j8)l4}
// NOTE: Dimension isn’t cloneable. H%gAgXHn
if (d != null) m C`*#[
copy = new Dimension (d.height, d.width); M=lU`Sm
} wDGb h=
return copy; }z$_!)/i
} 0y+^{@lU
y_w
<3
*YL86R+U
'Grii,
在克隆原子类型数据的多维数组的时候,也会犯类似的错误。原子类型包括int,float等。简单的克隆int型的一维数组是正确的,如下所示: 6u lx0$[
6/5,n0
xr7}@rq"U<
Sk/@w[
public void store (int[] data) throws CloneNotSupportedException{ 2l+L96
this.data = (int[])data.clone(); `[g$EXX
// OK Nw$OJ9$L>
} [rWBVfm
, ?U)mYhI
4=/jh:h
P.C?/7$7Z+
拷贝int型的二维数组更复杂些。Java没有int型的二维数组,因此一个int型的二维数组实际上是一个这样的一维数组:它的类型为int[]。简单的克隆int[][]型的数组会犯与上面例子中getValues()方法第一版本同样的错误,因此应该避免这么做。下面的例子演示了在克隆int型二维数组时错误的和正确的做法: 1n[)({OQ
yxHo0U
vI0,6fOd6
wbrOL(q.m
public void wrongStore (int[][] data) throws CloneNotSupportedException{ z k/`Uz
this.data = (int[][])data.clone(); // Not OK! +pwTM]bV
} \t&! &R#
public void rightStore (int[][] data){ _.Hj:nFHz
// OK!
Um{) ?1
this.data = (int[][])data.clone(); sTl^j gV7j
for (int i = 0; i < data.length; ++i){ $"W[e"Q
if (data != null) 6qFzo1LO
this.data = (int[])data.clone(); 8wvHg_U6W
} @n?"*B
} gv>DOez/
<O0tg[ub
8sG?|u
bE.<vF&
Ig}hap]G
六、常见错误6#:检查new 操作的结果是否为null
x _>1x#
r=0PW_r:
t!Cz;ajNi
hdx_Tduue
Java编程新手有时候会检查new操作的结果是否为null。可能的检查代码为: iL);bv W
6E-eD\?I&
slPr^)
,HTwEq>-G
Integer i = new Integer (400); @i!+Z
if (i == null) 2PyuM=(Wt
throw new NullPointerException(); bWp:!w#K
yO7y`;Q(sF
9)v]jk
Es7
c2YdU
检查当然没什么错误,但却不必要,if和throw这两行代码完全是浪费,他们的唯一功用是让整个程序更臃肿,运行更慢。 IC'+{3.m8
\O"H#gt
e8("G[P>
F$tzsz,9n
C/C++程序员在开始写java程序的时候常常会这么做,这是由于检查C中malloc()的返回结果是必要的,不这样做就可能产生错误。检查C++中new操作的结果可能是一个好的编程行为,这依赖于异常是否被使能(许多编译器允许异常被禁止,在这种情况下new操作失败就会返回null)。在java 中,new 操作不允许返回null,如果真的返回null,很可能是虚拟机崩溃了,这时候即便检查返回结果也无济于事。 hGY-d}npAJ
oQ
r.cKD ?
七、常见错误7#:用== 替代.equals / ^d9At614
8QJr!#u
在Java中,有两种方式检查两个数据是否相等:通过使用==操作符,或者使用所有对象都实现的.equals方法。原子类型(int, flosat, char 等)不是对象,因此他们只能使用==操作符,如下所示: JseKqJ?g
#;a+)~3*O
E/oLE^yL
ai$l7]7
int x = 4; [k\VUg:P
int y = 5; j[:70%X
if (x == y) DSRc4|L
System.out.println ("Hi"); W`9{RZ'
// This ’if’ test won’t compile. ,dQ*0XO!
if (x.equals (y)) \-]Jm[]^
System.out.println ("Hi"); I2CI9,0
^2$b8]q
fDns r"T
/} PdO
对象更复杂些,==操作符检查两个引用是否指向同一个对象,而equals方法则实现更专门的相等性检查。 /ojwOJ
9D\E0YG X/
.jqil0#)Y"
4h!yh2c..
更显得混乱的是由java.lang.Object 所提供的缺省的equals方法的实现使用==来简单的判断被比较的两个对象是否为同一个。 33lh~+C
jm<^WQ%Cc
]@z!r2[
L@[}sMdq(
许多类覆盖了缺省的equals方法以便更有用些,比如String类,它的equals方法检查两个String对象是否包含同样的字符串,而Integer的equals方法检查所包含的int值是否相等。 B">Ko3
t=Rl`1=(K
s-Gd{=%/q
o'$-
大部分时候,在检查两个对象是否相等的时候你应该使用equals方法,而对于原子类型的数据,你用该使用==操作符。 GPh;r7xg6
n-M6~
&&<l}E
U~yPQ8jD
八、常见错误8#: 混淆原子操作和非原子操作 t&43)TPb.
sYXLVJ>b
<ndY6n3
Z<d=v3q
Java保证读和写32位数或者更小的值是原子操作,也就是说可以在一步完成,因而不可能被打断,因此这样的读和写不需要同步。以下的代码是线程安全(thread safe)的: \Kui`X
Z:h'kgG &
C~>0K,C0^
sSKD"
public class Example{ NKFeND
private int value; // More code here... /<T{g0s
public void set (int x){ u{uqK7]+
// NOTE: No synchronized keyword Xl<*Fn?
this.value = x; y]R+/
} :_HdOm
} `j'1V1
3dZj<(.
R!"`Po
/:
-&b#+
不过,这个保证仅限于读和写,下面的代码不是线程安全的: *=O3kUoL
>u0XV "g$
M).CyY;bm
'?I3&lYz{
public void increment (){ 8]#J_|A6Z
// This is effectively two or three instructions: MR4k#{:w
// 1) Read current setting of ’value’. FnOahLS
// 2) Increment that setting. 3e-E/6zH6
// 3) Write the new setting back. .*"KCQGOgM
++this.value; 2Gj)fMK38
} 8yOhKEPX
=_N$0
#Ddo` >`&
`l70i2xcj
在测试的时候,你可能不会捕获到这个错误。首先,测试与线程有关的错误是很难的,而且很耗时间。其次,在有些机器上,这些代码可能会被翻译成一条指令,因此工作正常,只有当在其它的虚拟机上测试的时候这个错误才可能显现。因此最好在开始的时候就正确地同步代码: H;AMRL o4z
|eye) E:
/Wl8Jf7'
dqgr98
public synchronized void increment (){ )Xt#coagS
++this.value; 2*gB ~Jn4
} j-i>Jd7
}=gD,]2x8
I;?PDhDb
mL#$8wUdt{
九、常见错误9#:在catch 块中作清除工作 5~[][VV^
B=r+
m;(
qv:DpK
i3L2N~:V
一段在catch块中作清除工作的代码如下所示: 9`H4"H>yG
K0I.3|6C
~(Q#G"t
v/v PU
OutputStream os = null; G~_D'o<r
try{ f"AT@Ga]
os = new OutputStream (); 8y[Rwa
// Do something with os here. m'
|wlI[lq
os.close(); <4zSh3
}catch (Exception e){ 4OAR ["f
if (os != null) @1bl<27
os.close(); W|sU[dxZ
} ~GJ;;v1b2
U ){4W0
![I|hB
m9DTz$S.
尽管这段代码在几个方面都是有问题的,但是在测试中很容易漏掉这个错误。下面列出了这段代码所存在的三个问题: 6p&uifY}tR
#IDLfQ5g
3'0Jn6(
g2M1zRm;
1.语句os.close()在两处出现,多此一举,而且会带来维护方面的麻烦。 23'{{@30
]!jfrj
;"RyHow
:hWG:`
2.上面的代码仅仅处理了Exception,而没有涉及到Error。但是当try块运行出现了Error,流也应该被关闭。 *xj2Z,u
\O,yWyU4
bTAY5\wB
-H|!KnR
3.close()可能会抛出异常。 4v{Ye,2
2FO<Z %Y
J*6B~)Sp@
$
)2zz>4
上面代码的一个更优版本为: !tx.2m*5
x'6i9]+r
]n:R#55A
WYcZD_
OutputStream os = null; m0^~VK |
try{ J{XRltI+
os = new OutputStream (); Zz'g&ew