代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 2 z7}+lH
;1`!wG-DD
1HbFtU`y~
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 u]M\3V.
99u/fk L
WK==j1
&yU>2=/T
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 5/?P|T
@7W?8
,?(ciO)
`\N]wlB2/b
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 Jf_%<\ O
j;\[pg MR/
d>|;f
!n<o)DsZR
一、常见错误1# :多次拷贝字符串 E(4w5=8TI
uv]{1S{tb
?#BV+#(
\|%E%Yc
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 :Fe_,[FR
=K(JqSw+M
Yw)Fbt^
-bS)=L
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: _TUm$#@Y`
s bnjy"Z%
BpH%STEN
VEs5;]#<2D
String s = new String ("Text here"); G\=_e8(
,lm=M5b
Z\ )C_p\-
+sf .PSz$
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: !^WHZv4
h5GU9M
zvO:"w}
W5SN I>|E
String temp = "Text here"; &= eYr{
String s = new String (temp); `PlOwj@u0`
{^m Kvc
ER^QV(IvP8
xu\eX x6H
但是这段代码包含额外的String,并非完全必要。更好的代码为: n]y EdL/1
x2W#ROfg
$1Z6\G O
U>F{?PReA?
String s = "Text here"; cyQBqG
"9XfQ"P
Ew$I\j*
aG{$Ic
二、常见错误2#: 没有克隆(clone)返回的对象 u9Y3?j,oC
a]B[`^`z
U| 5-0 u5
"2{%JFE
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: I ~$1Lu`~
4W;S=#1
(Rd$VYuf
`A)"%~
import java.awt.Dimension; obo&1Uv,/
/***Example class.The x and y values should never*be negative.*/ 80;n|nNB
public class Example{ FTf<c0
private Dimension d = new Dimension (0, 0); 2@khSWV
public Example (){ } 4kl Ao$
X`JVR"=4
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ [4Q"#[V&9
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ :O-1rD
if (height < 0 || width < 0) $yu?.b
9H#
throw new IllegalArgumentException(); ub K7B |p
d.height = height; Eu,`7iQ?(
d.width = width; pqR\>d0
} NM#-Af*pg
nxo+?:**
public synchronized Dimension getValues(){ ?LP9iY${
// Ooops! Breaks encapsulation gfg n68k
return d; cWLqU
} BVpO#c~I
} ~*.-
'@=PGpRF
T!|=El>
#07!-)Gv
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: xDLG=A%]z
eu#'SXSC
F
_ZY\,_
"r'ozf2\
Example ex = new Example(); s?C&s|'.
Dimension d = ex.getValues(); @xAfZb2 E
d.height = -5; l"+Jc1\ X
d.width = -10; T+IF}4ed
/)L
0`:I#
;zH
HIdQ>-
_NZ@4+aW
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 (k?7:h
oBQm05x"
L.'}e{ldW
h2Bz F
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 6iA( o*'Yn
"Cz<d w]D
k G0Yh2;#
c&nh>oN
更好的方式是让getValues()返回拷贝: p&b5% 4P
PnYBy| yl
</`yd2 >
7'lZg<z{~j
public synchronized Dimension getValues(){ 2kh"8oQ
return new Dimension (d.x, d.y); yxWO[ Z
} ec3<%+0f
;2xO`[#
9jir*UI
Af(WV>'
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 ipE]}0q
<wd]D@l7r
, X{>
Z u*K-ep"
三、常见错误3#:不必要的克隆 sW@krBxMv
s>n(`?@L
T^.Cc--c
jeUUa-zR3
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: Wr?'$:
b;cMl'
E%N2k|%8d_
<%?#AVU[
/*** Example class.The value should never * be negative.*/ o4y']JSN
public class Example{ p
*w$:L
private Integer i = new Integer (0); eD?3"!c!
public Example (){ } @OpNHQat9
dt\jGD
/*** Set x. x must be nonnegative* or an exception will be thrown*/ G4 _,
public synchronized void setValues (int x) throws IllegalArgumentException{ ?Bi*1V<R
if (x < 0) KKe8
ly,
throw new IllegalArgumentException(); "tk-w{>
i = new Integer (x); ;3eKqr0
} }f}}A=
KvFMs\o6p
public synchronized Integer getValue(){ ~a9W3b4j
// We can’t clone Integers so we makea copy this way. SGL|Ck
return new Integer (i.intValue()); [{u(C!7L`
} hsRvr`#m|
} LPd\-S_rsP
,M5}4E7L%s
w f.T3
!^c@shLN4
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 dEa<g99[?
2BXy<BM @
m"eteA,"k_
)RgGcHT@
方法getValue()应该被写为: ,5
j"ruZ
Q,T"Zd Q
a?h*eAAc.
Hh;:`;}
public synchronized Integer getValue(){ q'[}9e`Q
// ’i’ is immutable, so it is safe to return it instead of a copy. w*9br SK
return i; |OO in]5
} WiL2
"_UdBG
}n:?7
KL,/2(
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: _*M42<wcO
g`^X#-!(
l\0w;:N3
n"Veem[_4g
?Boolean jhgX{xc
?Byte *A 'FC|\
?Character SymwAS+
?Class R7jmv n
?Double Ga>uFb}W~
?Float K BE Ax3
?Integer ym,H@~
?Long iRo.RU8>
?Short 9# 4Y1L S)
?String #FOqP!p.E
?大部分的Exception的子类 BimjQ;jtI
a3SlxsWW
URgk^nt2p
DB526O*
[
四、常见错误4# :自编代码来拷贝数组 6Q&r0>^{
2|iV,uJ&
\2-@' ^i
Yj|eji7y
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: RprKm'b8x`
%q;jVj[
g:l.MJT
[&[^G25
public class Example{ A5:qKaAq
private int[] copy; BaF!O5M
/*** Save a copy of ’data’. ’data’ cannot be null.*/ f"u*D,/sS
public void saveCopy (int[] data){ <:>SGSE9
copy = new int[data.length]; >I
for (int i = 0; i < copy.length; ++i) }TQ{`a@
copy = data; Am0{8
'
} Qhi '')Q
} KOq;jH{$
moj]j`P5a
/
O/`<
7M_U2cd|TD
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: RgdysyB
YpAg
^mWybPqx
8b.u'r174
void saveCopy (int[] data){ h}_~y'^!
try{ ?<&O0'Q
copy = (int[])data.clone(); G0 J4O!3
}catch (CloneNotSupportedException e){ c
!ZM
// Can’t get here. yq-=],h
} HW4.zw
} >Iewx
Gb>
6Tw#^;q-
=\#%j|9N9
X=JmF97
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: sbkQ71T:
4D%9Rc0 G
'3]p29v{
#PDf,^
static int[] cloneArray (int[] data){ HjqB^|z
try{ )0vU
k
return(int[])data.clone(); _\PNr.D8
}catch(CloneNotSupportedException e){ W!blAkM%i
// Can’t get here. mME4 l
} jr7C}B-Fb^
} B_U{ s\VY
YIt& >
Md6]R-l@
$8USyGi3J
这样的话,我们的saveCopy看起来就更简洁了: m=AqV:%|
X{n- N5*
do-ahl,
aSuM2
void saveCopy (int[] data){ H.<a`mm8
copy = cloneArray ( data); e~ aqaY~}
} [3l*F
n%R;-?*v
FlfI9mm
8(.mt/MR
五、常见错误5#:拷贝错误的数据 !UOCJj.cA
V}d9f2
IKtB;
s]T""-He
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: lkyzNy9R
Mypc3
&R|/t:DN
M<SdPC(+
import java.awt.Dimension; &1l=X]%
/*** Example class. The height and width values should never * be IKMeJ(:S
negative. */ #j#_cImE
public class Example{ |py6pek|
static final public int TOTAL_VALUES = 10; uPYmHA}_/
private Dimension[] d = new Dimension[TOTAL_VALUES]; gj\)CBOv
public Example (){ } +_v$!@L8
W"{v2x i
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ QB:i/9
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ 4k/VBZB
if (height < 0 || width < 0) lf>*Y.!@me
throw new IllegalArgumentException(); =.]l*6WV
if (d[index] == null) [S.ZJUns
d[index] = new Dimension(); RT93Mt%P
d[index].height = height; < v]3g
d[index].width = width; )&era` e[
} P o jmC
public synchronized Dimension[] getValues() E^GHVt/.
throws CloneNotSupportedException{ 6{[pou&
return (Dimension[])d.clone(); Am8x74?
} [s9O0i"
Y
} @prG%vb"
9_\'LJ
6.5T/D*TT
{X2`&<i6
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: N=:5eAza
0JgL2ayIVI
VIP7OHJh
G*S|KH
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ @)kO=E d
Dimension[] copy = (Dimension[])d.clone(); DjU9
uZT
for (int i = 0; i < copy.length; ++i){ hlu:=<B
// NOTE: Dimension isn’t cloneable. ,+qVu,
if (d != null) < B_Vc:Q
copy = new Dimension (d.height, d.width); 2([2Pb3<"
} 7iHK_\t n
return copy; 2L AYDaS
} k5kdCC0FCk
-(`OcGM'L
_3]][a,
{_(\`>
在克隆原子类型数据的多维数组的时候,也会犯类似的错误。原子类型包括int,float等。简单的克隆int型的一维数组是正确的,如下所示: DC1'Kyk
=0@&GOq
&t5{J53
tvXW
public void store (int[] data) throws CloneNotSupportedException{ #j@71]GI
this.data = (int[])data.clone(); 'Dvv?>=&
// OK mh<=[J,%p
} Ladsw
Xtwun
}SI GPVM
oG$)UTzGc
拷贝int型的二维数组更复杂些。Java没有int型的二维数组,因此一个int型的二维数组实际上是一个这样的一维数组:它的类型为int[]。简单的克隆int[][]型的数组会犯与上面例子中getValues()方法第一版本同样的错误,因此应该避免这么做。下面的例子演示了在克隆int型二维数组时错误的和正确的做法: LlBN-9p
0-LpqX
e*+FpW@
R*|LI
public void wrongStore (int[][] data) throws CloneNotSupportedException{ Z~A@o""F
this.data = (int[][])data.clone(); // Not OK! {bO|409>W
} `@i5i((
public void rightStore (int[][] data){ Z%GTnG|rG
// OK! A2}Rl%+X]6
this.data = (int[][])data.clone(); MNH1D!}
for (int i = 0; i < data.length; ++i){ |QV!-LK
if (data != null) jjJ2>3avY
this.data = (int[])data.clone(); qQ!1t>j+H
} 0O k,oW{
} Qb8KPpd
Mv c`)_Md
pfx3C*
;['[?wk
0&ByEN99
六、常见错误6#:检查new 操作的结果是否为null I@Xn3oN
AxxJk"v'y
.^$YfTabq
3] 1-M
Java编程新手有时候会检查new操作的结果是否为null。可能的检查代码为: OB~X/
"O8gJ0e
IVlf=k
E7Cy(LO
Integer i = new Integer (400); +UJuB
if (i == null) $A3<G-4O
throw new NullPointerException(); i{D=l7j|w
+GsWTEz
XC7%vDIt
B2Xn?i3 l
检查当然没什么错误,但却不必要,if和throw这两行代码完全是浪费,他们的唯一功用是让整个程序更臃肿,运行更慢。 @"T"7c?Cv
i(?,6)9
{cpEaOyOM
+n}$pM|NKU
C/C++程序员在开始写java程序的时候常常会这么做,这是由于检查C中malloc()的返回结果是必要的,不这样做就可能产生错误。检查C++中new操作的结果可能是一个好的编程行为,这依赖于异常是否被使能(许多编译器允许异常被禁止,在这种情况下new操作失败就会返回null)。在java 中,new 操作不允许返回null,如果真的返回null,很可能是虚拟机崩溃了,这时候即便检查返回结果也无济于事。 PSawMPw
tNVV)C
七、常见错误7#:用== 替代.equals %gnM(pxl
gX{loG
在Java中,有两种方式检查两个数据是否相等:通过使用==操作符,或者使用所有对象都实现的.equals方法。原子类型(int, flosat, char 等)不是对象,因此他们只能使用==操作符,如下所示: TpA\9N#$
T0)"1D<l
_LwOOZj
vIvVq:6_3
int x = 4; l"n{.aL
int y = 5; >;z<j$;F<
if (x == y) iCP/P%
System.out.println ("Hi"); CE15pNss
// This ’if’ test won’t compile. VF&Z%O3n
if (x.equals (y)) ]pEV}@7
System.out.println ("Hi"); ^\B:R,
Kb =@ =Xta
yT{8d.Rh
2iu_pjj
对象更复杂些,==操作符检查两个引用是否指向同一个对象,而equals方法则实现更专门的相等性检查。 p0.|<
-T6(hT\
CIjZG ?A
'WHHc 9rG,
更显得混乱的是由java.lang.Object 所提供的缺省的equals方法的实现使用==来简单的判断被比较的两个对象是否为同一个。 `>DP,D)w(
g+-;J+X8
iut`7
5>J=YLq
许多类覆盖了缺省的equals方法以便更有用些,比如String类,它的equals方法检查两个String对象是否包含同样的字符串,而Integer的equals方法检查所包含的int值是否相等。 qH"Gm
]]}tdn _
Lp5U"6y
PX|=(:(k
大部分时候,在检查两个对象是否相等的时候你应该使用equals方法,而对于原子类型的数据,你用该使用==操作符。 XWJwJ
q P ;A}C
H"2uxhdLK3
F_xbwa*=
八、常见错误8#: 混淆原子操作和非原子操作 #S%Q*k<hw
8+mH:O
S'dV>m`
6.t',LTB
Java保证读和写32位数或者更小的值是原子操作,也就是说可以在一步完成,因而不可能被打断,因此这样的读和写不需要同步。以下的代码是线程安全(thread safe)的: I2(zxq&2M\
:a:[.
_WX#a|4h{
569}Xbc/
public class Example{ $4jell
private int value; // More code here... +7Kyyu)y@
public void set (int x){ )pw&c_x
// NOTE: No synchronized keyword *%Qn{x
this.value = x; s08u @
} rzp +:
} ,mPnQ?
Oo?,fw
4E44Hzs
D[O{(<9
不过,这个保证仅限于读和写,下面的代码不是线程安全的: ?}Z1(it0
E2GGEKrW
iAY!oZR(WT
\yrisp#`
public void increment (){ :hGPTf
// This is effectively two or three instructions: <lr*ZSNY
// 1) Read current setting of ’value’. P)dL?vkK
// 2) Increment that setting. MJj4Hd
// 3) Write the new setting back. 3p?KU-
++this.value; T+LJ*I4
} 7z_;t9Y
`"vZ);i<
pIWI
Es 5
在测试的时候,你可能不会捕获到这个错误。首先,测试与线程有关的错误是很难的,而且很耗时间。其次,在有些机器上,这些代码可能会被翻译成一条指令,因此工作正常,只有当在其它的虚拟机上测试的时候这个错误才可能显现。因此最好在开始的时候就正确地同步代码: KCe13!
|L_wX:d`9
uGdp@]z&8Q
W;?(,xx
public synchronized void increment (){ :5GZ \Z8F
++this.value; '2hbJk
} JT[*3h
uhN%Aj\iu(
NGYyn`Lx
h5
Vv:C
九、常见错误9#:在catch 块中作清除工作 !#wd Ve_(
IB.yU,v
S\y%4}j
Z,N$A7SBE
一段在catch块中作清除工作的代码如下所示: Uadr>#C*
- ~O'vLG
Q5S,{ ZeT
6VD1cb\lF
OutputStream os = null; ryO$6L
try{ S)He$B$pp
os = new OutputStream (); y0v]N
// Do something with os here. Oc9#e+_&
os.close(); Ct$82J
}catch (Exception e){ -6Tk<W
if (os != null) /E wGW
os.close(); {>0V[c[~
} "Clz'J]{
5p?!ni9
e2CV6F@a
%u?HF4S'
尽管这段代码在几个方面都是有问题的,但是在测试中很容易漏掉这个错误。下面列出了这段代码所存在的三个问题: QGiAW7b5
4^c-D
SEKN|YQV/t
U7&x rif
1.语句os.close()在两处出现,多此一举,而且会带来维护方面的麻烦。 "rXOsX\;
;??ohA"{5
ps1YQ3Ep&
;D ~L|
2.上面的代码仅仅处理了Exception,而没有涉及到Error。但是当try块运行出现了Error,流也应该被关闭。 lfk9+)
rl:KJ\*D
b syq*
T+"f]v
3.close()可能会抛出异常。 8F;>5i
zIQzmvf
K0+;bu
9T2xU3UyY
上面代码的一个更优版本为: F-n"^.7
j{#Wn
!,
'p)Q68;&
'(@YK4_M
OutputStream os = null; 5/ecaAB2
try{ ;mm!0]V
os = new OutputStream (); &!7+Yb(1
// Do something with os here. <*'cf2Q$Av
}finally{ @%tXFizh
if (os != null) [nN7qG
os.close(); PW}OU9is
} p5c8YfM
+ R$?2
pLoy
"5DJu~
这个版本消除了上面所提到的两个问题:代码不再重复,Error也可以被正确处理了。但是没有好的方法来处理第三个问题,也许最好的方法是把close()语句单独放在一个try/catch块中。 V7CoZnz
vTr34n
?s}
%
t> Q{yw
十、常见错误10#: 增加不必要的catch 块 x49!{}
k/&]KYwu
P1 +"v*
XOrfs sj
一些开发者听到try/catch块这个名字后,就会想当然的以为所有的try块必须要有与之匹配的catch块。 90 {tI X
7u11&(Lz
vg%QXaM
lhn8^hOJ/
C++程序员尤其是会这样想,因为在C++中不存在finally块的概念,而且try块存在的唯一理由只不过是为了与catch块相配对。 :,]S}R
+KK$0pL
>POO-8Q
ayp b
增加不必要的catch块的代码就象下面的样子,捕获到的异常又立即被抛出: 5P^ U_
_&{%Wc5W~F
D\L!F6taS
|:iEfi]j
try{ ~P1_BD(
// Nifty code here !oSLl.fQd
}catch(Exception e){ ='Oj4T
throw e; H;vZm[\0N-
}finally{ QrjDF>
// Cleanup code here i3V/`)iz
} Vk<k +=7
\&|CM8A
?_4^le[;
tFU;SBt8Ki
不必要的catch块被删除后,上面的代码就缩短为: M$#sc`4*
=DgCC|p
&W_th\%
E1q%gi4 Q%
try{ MZm'npRf
// Nifty code here k0K A ~
}finally{ -Q[g/%
// Cleanup code here 9{J?HFw*;
} mVf.sA8
mX_)b>iW
xe:' 8J6L
FUTn
常见错误11#;没有正确实现equals,hashCode,或者clone 等方法 #qL9{P<}
n
E:'Zxj
(9.yOc4
cK}Pf+r>
方法equals,hashCode,和clone 由java.lang.Object提供的缺省实现是正确的。不幸地是,这些缺省实现在大部分时候毫无用处,因此许多类覆盖其中的若干个方法以提供更有用的功能。但是,问题又来了,当继承一个覆盖了若干个这些方法的父类的时候,子类通常也需要覆盖这些方法。在进行代码审查时,应该确保如果父类实现了equals,hashCode,或者clone等方法,那么子类也必须正确。正确的实现equals,hashCode,和clone需要一些技巧。 ,7/
_T\d<
O8 RzUg&
xEoip?O?7F
r#h {$iW
小结 -ut=8(6&
=:K@zlO:
.P/xs4
Lo3-X
我在代码审查的时候至少遇到过一次这些错误,我自己也犯过其中的几个错误。好消息是只要你知道你在找什么错误,那么代码审查就很容易管理,错误也很容易被发现和修改。即便你找不到时间来进行正规的代码审查,以自审的方式把这些错误从你的代码中根除会大大节省你的调试时间。花时间在代码审查上是值得的。 qe?Ggz3p.
mUwUs~PjA