代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 <,@%*G1-
|`rJJFA
j]4,<ppWSH
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 vDj;>VE2b
m.Lij!0
B;#J"6w
>4i>C
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 1}m3;
IVvtX}
-yH,5vD
3c'#6virz
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 8;gXg
8F5|EpB9M
'xK.UI
Q(7ob}+jQ
一、常见错误1# :多次拷贝字符串 @E9" Zv-$
PO-"M)M
Tbbz'b;{
v({N:ya
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 dbdM"z4
'o4p#`R:8
XFwLz
{<$bAj
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: f'En#-?O
<E,%@
r|<DqTc6l
Ww3wsy x
String s = new String ("Text here"); 2B1xUj ]
yJx?M
VU.@R,
>7Jr^o#|_x
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: EM j;2!
BzJ;%ywS
A&5:ATQ/|
.)XP\m\
String temp = "Text here"; @I3eK^#|P
String s = new String (temp); GRqT-/n"
77 r(*.O|
C|-pD
T3%C%BcX
但是这段代码包含额外的String,并非完全必要。更好的代码为: 5r,r%{@K
.10y0FL4
8AFczeg[[
3)Ac"nuyqH
String s = "Text here"; O~Wt600{E
i&Fiq&V)[
m}j:nk
dR^"X3$
二、常见错误2#: 没有克隆(clone)返回的对象 I~*
? d
(<*e
R=j% S!
BHFY%6J!
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: f2I6!_C!+
myFAKRc
TX8<J>x
cQj-+Tmu
import java.awt.Dimension; njPPztv/@
/***Example class.The x and y values should never*be negative.*/ hcCp,b
public class Example{ !BIOY!M
private Dimension d = new Dimension (0, 0); "B7`'jz
public Example (){ } 9SQ4cv*2
@p=AWi}\
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ ShOX<Fb&
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ R,2P3lv1v@
if (height < 0 || width < 0) nR;D#"p%
throw new IllegalArgumentException(); CO+/.^s7}S
d.height = height; dP2irC%f8
d.width = width; LtgXShp_!
} ,,L2(N
Y k7-`
public synchronized Dimension getValues(){ tB7}|jC
// Ooops! Breaks encapsulation &BE
g
return d; vV?rpe|%
} arK_oh0B
} {No L
uGN^!NG-0
XM1`x
0IkM
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: RJeDEYXeg
F/d7q%I
p>=[-(mt
0U/,aHvhP
Example ex = new Example(); B@YyQ'
Dimension d = ex.getValues(); PCrU<J 7
d.height = -5; }G <T :(a
d.width = -10; 58xnB!h\}
P(k(m<0
z&8un%Jt
yL4 T
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 |R/.r_x,V?
V%0I%\0Y
IeX^4rc(
*u6Y8IL1
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 (h-*_a}F4
a&{X!:X
i+3fhV
mog[pu:!,
更好的方式是让getValues()返回拷贝: 2S3lsp5!
>O9o,o/6R
d5 Edu44
3uu~p!2
public synchronized Dimension getValues(){ <bck~E
return new Dimension (d.x, d.y); fU3`v\X
} 7}O.wUKw%
BKa-
k!
&)F*@C-
ikB Yd
}5
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 G$zL)R8GE|
HL3XyP7
rZPT89M6
N/QiI.V6
三、常见错误3#:不必要的克隆 9i5,2~
rX7QbAB
o_M.EZO
_Us*+
2(4L
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: A=zPLq{Sb
2L_6x<u'
<Peebv&v
_96~rel_P
/*** Example class.The value should never * be negative.*/ \vfBrN
public class Example{ gwd (N
private Integer i = new Integer (0); G.'+-v=\]
public Example (){ }
6 Si-u
5v\!]?(O;
/*** Set x. x must be nonnegative* or an exception will be thrown*/ w9RS)l2FQ
public synchronized void setValues (int x) throws IllegalArgumentException{ 5qUTMT['T
if (x < 0) vR6Bn
throw new IllegalArgumentException(); k^ F@X
i = new Integer (x); 2f`nMW
} 8N%Bn&
_/* U2.xS
public synchronized Integer getValue(){ h_d +$W5
// We can’t clone Integers so we makea copy this way. ]'~vI/p
return new Integer (i.intValue());
'uDjFQX
} J~B
7PW
} _lKZmhi
)\;Z4x;]U
q*![AzFh
i|)Su4Dw
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 6&Juv
5m:i6,4
L(>=BK*
+z9@:L
方法getValue()应该被写为: 1=7jz]t
H y"x
;< )~Y-
oY~ Dg
public synchronized Integer getValue(){ Q zZ;Ob]'
// ’i’ is immutable, so it is safe to return it instead of a copy. Z4$cyL'$P
return i; pCpb;<JG
} 4F>Urh+
IPSF]"}~
Wjh/M&,
E@05e
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: Xb
!MaNm)
P #F=c34u
vzel#
Xd E`d.
?Boolean Rd7_~.Bo
?Byte d%I"/8-J
?Character C9DJO:f.2y
?Class m@`8A
?Double ,B&fFis
?Float 0n ~ Zz
?Integer K-<^$VWh
?Long kc'pN&]r:
?Short H`8``#-|@S
?String qa(>wR"mT
?大部分的Exception的子类
B<8N96fx
I-]>d;4.
+bK.NcS
^ 5VK>
四、常见错误4# :自编代码来拷贝数组 GhY1k";
`u!l3VZ/4
,
$Qo =
MC((M,3L
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: K'iIJA*Sn
b?4/#&z]
M}_i52
Kz<@x`0
public class Example{ 8By,#T".
private int[] copy; ]u-]'P
/*** Save a copy of ’data’. ’data’ cannot be null.*/ I]Tsz'T!9
public void saveCopy (int[] data){ 5 )2:stT73
copy = new int[data.length]; 3lLMu B+
for (int i = 0; i < copy.length; ++i) BYW^/B Y)
copy = data; @ ''GPL@
} ]Fvm 7V
} H_!4>G@
O?8Ni=]
Nfe>3uQK
YI-O{U
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: b 6t}{_7
Iq+>qX
D47R
#zrTY9m7
void saveCopy (int[] data){ e}@)z3Q<l
try{ cw&Hgjj2
copy = (int[])data.clone(); =z{JgD/
}catch (CloneNotSupportedException e){ IvpcSam'
// Can’t get here. ;Z j]~|
} 3'c\;1lhT
} M@P1, Y
gx03xPeu
{:c]|^w6
zJM S=r
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: Sx*oo{Kk%
?6c-7QV
j7FN\
cz
G5dO 3lwq
static int[] cloneArray (int[] data){ q(5j(G ;
try{ 2M)]!lYy
return(int[])data.clone(); b,P ]9$Ut
}catch(CloneNotSupportedException e){ ~`>e5OgOJ
// Can’t get here. qj01]
} H4OhIxK
} ky>wOaTmN6
#QvMVy
,U *)2`[
L.xZ_ 6
这样的话,我们的saveCopy看起来就更简洁了: C^t(^9
=S[yE]v^
Z'^U ad6
7z\m;
1
void saveCopy (int[] data){ PCd0 ?c
copy = cloneArray ( data); KucV3-I
} VHOfaCE
c[}(OH
C
]Si|D
.%'(9E
五、常见错误5#:拷贝错误的数据 ES <1tG
VhT=
l
in<Rq"L
"+KJop
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: 5ep/h5*/
gu)=wu0
Lf:uNl*D
` b !5^W
import java.awt.Dimension; O 2{)WWOT
/*** Example class. The height and width values should never * be :ztr)
negative. */ n}A\2bO
public class Example{ l5Y/Ok0,
static final public int TOTAL_VALUES = 10; nfb]VN~(
private Dimension[] d = new Dimension[TOTAL_VALUES]; It_M@
public Example (){ } DPrBFmHF
>}~#>Ru
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ /wQL
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ ]DFXPV
if (height < 0 || width < 0) rI5Foh6
throw new IllegalArgumentException(); <
`qRA]
if (d[index] == null) UX`]k{Mz
d[index] = new Dimension(); EG'[`<*h
d[index].height = height; -]Cc
d[index].width = width; gw+9x<e
} e73^#O&Xt
public synchronized Dimension[] getValues() d{et8N
throws CloneNotSupportedException{ nmlPX7!{$
return (Dimension[])d.clone(); E{=2\Wkcp
} _2fkb=2@
} 0,*%vG?Q
qP!eJ6[Nh"
8 9{HJ9}
=U
OLT>!
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: <VjJAu
3>zN/f
Fhq9D{TeY,
I4rPHZ|
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ 8pM>Co!
Dimension[] copy = (Dimension[])d.clone(); L+B?~_*
for (int i = 0; i < copy.length; ++i){ OYM@szM
// NOTE: Dimension isn’t cloneable. =9L$L|W
if (d != null) {-9jm%N
copy = new Dimension (d.height, d.width); ^\ ?O4,L
} 1{pmKPu
return copy; M_B:{%4
} z2ms^Y=j
PYB+FcR6?n
Uts"aQ
"wH) mQnd
在克隆原子类型数据的多维数组的时候,也会犯类似的错误。原子类型包括int,float等。简单的克隆int型的一维数组是正确的,如下所示: HDM<w+ZxX
L~{_!Q
jD){I
e"-X U@`k1
public void store (int[] data) throws CloneNotSupportedException{ W[[oSqp
this.data = (int[])data.clone(); gOT+%Ab{_
// OK hf!|\f
} qv
3^5d
G
DSfT{kK\
,F+B Wot4
N;F)jO
xsl
拷贝int型的二维数组更复杂些。Java没有int型的二维数组,因此一个int型的二维数组实际上是一个这样的一维数组:它的类型为int[]。简单的克隆int[][]型的数组会犯与上面例子中getValues()方法第一版本同样的错误,因此应该避免这么做。下面的例子演示了在克隆int型二维数组时错误的和正确的做法: iMF<5fLH&
'f8(#n=6qP
>YW\~T
Auy".br'
public void wrongStore (int[][] data) throws CloneNotSupportedException{ '2J0>Bla
this.data = (int[][])data.clone(); // Not OK! /4=-b_2Y~
} C`oa3B,z
public void rightStore (int[][] data){ si1*Wt<3Bc
// OK! _\5~>g_
this.data = (int[][])data.clone(); 71FeDpe
for (int i = 0; i < data.length; ++i){ 6XEZ4QP}
if (data != null) fi PIAT}
this.data = (int[])data.clone(); G"
b60RQ
} (A k\Lm
} S6nhvU:
Eu@5L9A
\`'KlF2
Qx|H1_6
`znB7VQ0
六、常见错误6#:检查new 操作的结果是否为null q)u2Y]
@b&84Gn2
r
78#!Q.##
;'T{li2
Java编程新手有时候会检查new操作的结果是否为null。可能的检查代码为: v|Jlf$>
hSqY$P
&Y|Xd4:
x!S;SU
Integer i = new Integer (400); n_[i0x7#
if (i == null) .W\ve>;
throw new NullPointerException(); z,;;=V6j
8$P>wCK\l
.r|*Ch#;P
+,'T=Ic{
检查当然没什么错误,但却不必要,if和throw这两行代码完全是浪费,他们的唯一功用是让整个程序更臃肿,运行更慢。 zbw7U'jk
! U0z"
qcB){p+UQ
,a|@d}U
C/C++程序员在开始写java程序的时候常常会这么做,这是由于检查C中malloc()的返回结果是必要的,不这样做就可能产生错误。检查C++中new操作的结果可能是一个好的编程行为,这依赖于异常是否被使能(许多编译器允许异常被禁止,在这种情况下new操作失败就会返回null)。在java 中,new 操作不允许返回null,如果真的返回null,很可能是虚拟机崩溃了,这时候即便检查返回结果也无济于事。 _68BP)nz>.
4Wel[]
七、常见错误7#:用== 替代.equals U SOKDDm
yFIy`9R
在Java中,有两种方式检查两个数据是否相等:通过使用==操作符,或者使用所有对象都实现的.equals方法。原子类型(int, flosat, char 等)不是对象,因此他们只能使用==操作符,如下所示: 6y+b5-{'
wjU.W5IR
UP1?5Q=H]Q
cleOsj;S
int x = 4; .,2V5D-${
int y = 5; HP2wtN{Zs
if (x == y) F:FMeg
System.out.println ("Hi"); j28 _HhT
// This ’if’ test won’t compile. N?r>%4
if (x.equals (y)) my^ak*N
System.out.println ("Hi"); f*((;*n;
hAR?
t5c
8 ,}ikOZ?
#~Q=h`9
对象更复杂些,==操作符检查两个引用是否指向同一个对象,而equals方法则实现更专门的相等性检查。 #m. AN
JV"NZvjN7d
IFNWS,:
%Tcf6cK"
更显得混乱的是由java.lang.Object 所提供的缺省的equals方法的实现使用==来简单的判断被比较的两个对象是否为同一个。 -<f/\U
0Vv9BL{
*DeTqO65
HB&
&
许多类覆盖了缺省的equals方法以便更有用些,比如String类,它的equals方法检查两个String对象是否包含同样的字符串,而Integer的equals方法检查所包含的int值是否相等。 <)m%*9{
:{g7lTM
$`Nd?\$
'8`T|2
大部分时候,在检查两个对象是否相等的时候你应该使用equals方法,而对于原子类型的数据,你用该使用==操作符。 S0w> hr
MOz}Q1`a
Y)HbxFF`/
B+VuUt{S
八、常见错误8#: 混淆原子操作和非原子操作 tiQ;#p7%
Fxd{ Zk`
zok D:c
))#'4
Java保证读和写32位数或者更小的值是原子操作,也就是说可以在一步完成,因而不可能被打断,因此这样的读和写不需要同步。以下的代码是线程安全(thread safe)的: TYS\95<
W^g'}}]T
_g|acBF
9w^zY;Y
public class Example{ W? ,$!]0
private int value; // More code here... D5]{2z}k
public void set (int x){ |"k&fkS$
// NOTE: No synchronized keyword ~uaP$*B[
this.value = x; (i`(>I.(/
} +cg
{[f,J;
} aO1IVESr$
<)#kq1b?
%]4-{%v
\ElX~$fS
不过,这个保证仅限于读和写,下面的代码不是线程安全的: O]=C#E{
?C;JJ#Ho
bkQ3c-C<
mN1Ssq"B
public void increment (){ +uQB
rG
// This is effectively two or three instructions: &sOM>^SAD
// 1) Read current setting of ’value’. E20&hc5 8
// 2) Increment that setting. ia{kab|_5
// 3) Write the new setting back. T!^Mvat
++this.value; }=GM?,7b
} %xg"Q|
?ApRJm:T
mvTb~)
F,}s$v
在测试的时候,你可能不会捕获到这个错误。首先,测试与线程有关的错误是很难的,而且很耗时间。其次,在有些机器上,这些代码可能会被翻译成一条指令,因此工作正常,只有当在其它的虚拟机上测试的时候这个错误才可能显现。因此最好在开始的时候就正确地同步代码: [%8@DC'
'V!kL,
9ES
zXre~b03ZS
=HE
m)
public synchronized void increment (){ &4kM8Qh
++this.value; R2^iSl%pj
} k/`i6%F#m
<MZi<Z`
'U)8rR
:m`/Q_y"
九、常见错误9#:在catch 块中作清除工作 gue(C(~.k_
1L[S*X
Jp]T9W\
1D1b"o
一段在catch块中作清除工作的代码如下所示: N/{?7sG&
-<oZ)OfU
7:o+iP4 6
_Y-$}KwY!
OutputStream os = null; rx:lKoOnB
try{ TETsg5#
os = new OutputStream (); .hN3`>*V
// Do something with os here. h~ha
os.close(); rSyaZ6#
}catch (Exception e){ 0j@Ix EPs
if (os != null) |=3 *;}
os.close(); ;nk@XFJ
} |~NeB"l{
X<xqT
878tI3-
h)o]TV
尽管这段代码在几个方面都是有问题的,但是在测试中很容易漏掉这个错误。下面列出了这段代码所存在的三个问题: u2lmwE
*Q/E~4AW|t
.BL:h&h|y
raQYn?[
1.语句os.close()在两处出现,多此一举,而且会带来维护方面的麻烦。 (3fPt;U
v*DFiCQD
TN ci.']
*/U$sZQ)
2.上面的代码仅仅处理了Exception,而没有涉及到Error。但是当try块运行出现了Error,流也应该被关闭。 6y@<?08Q
iEhDaC[e(b
+"=~o5k3Q
>B~?dT m
3.close()可能会抛出异常。 s1=u{ET
'3%*U*I
Oxn'bh6R0
4TJ!jDkox
上面代码的一个更优版本为: r,nn~
m|dF30~A
rk|a'&
CjZ6NAHc
OutputStream os = null; '#f?#(
try{ ~~dfpW _"
os = new OutputStream (); IMR$x(g=
F
// Do something with os here. nO
[QcOf
}finally{ nDn{zea7
if (os != null) KgU[
os.close(); YPQCOG
} ~%G Ssm\J
* D3
w{ m#Yt
4H9xO[iM
这个版本消除了上面所提到的两个问题:代码不再重复,Error也可以被正确处理了。但是没有好的方法来处理第三个问题,也许最好的方法是把close()语句单独放在一个try/catch块中。 Kz^ hQd
Ib(,P3
D4\(:kF\Hg
nK:`e9ES
十、常见错误10#: 增加不必要的catch 块 nP)-Y#`~7
m2MPWy5s
<^'{ G
V9]uFL
一些开发者听到try/catch块这个名字后,就会想当然的以为所有的try块必须要有与之匹配的catch块。 {q2<KRU2+#
Px#4pmz
Sh47c4{
m[#%/
C++程序员尤其是会这样想,因为在C++中不存在finally块的概念,而且try块存在的唯一理由只不过是为了与catch块相配对。 )XZ,bz*jn
iy9VruT<