代码审查是消灭Bug最重要的方法之一,这些审查在大多数时候都特别奏效。由于代码审查本身所针对的对象,就是俯瞰整个代码在测试过程中的问题和Bug。并且,代码审查对消除一些特别细节的错误大有裨益,尤其是那些能够容易在阅读代码的时候发现的错误,这些错误往往不容易通过机器上的测试识别出来。本文就常见的Java代码中容易出现的问题提出一些建设性建议,以便您在审查代码的过程中注意到这些常见的细节性错误。 YH'$_,8peM
TDAWI_83-
uOG-IHuF
通常给别人的工作挑错要比找自己的错容易些。别样视角的存在也解释了为什么作者需要编辑,而运动员需要教练的原因。不仅不应当拒绝别人的批评,我们应该欢迎别人来发现并指出我们的编程工作中的不足之处,我们会受益匪浅的。 43J\8WBn@
$c@w$2
83
i1
Z@uTkqG)
正规的代码审查(code inspection)是提高代码质量的最强大的技术之一,代码审查?由同事们寻找代码中的错误?所发现的错误与在测试中所发现的错误不同,因此两者的关系是互补的,而非竞争的。 %qS]NC
tIGVB+g{F
w\o)bn
+
%MO7vL
如果审查者能够有意识地寻找特定的错误,而不是靠漫无目的的浏览代码来发现错误,那么代码审查的效果会事半功倍。在这篇文章中,我列出了11个Java编程中常见的错误。你可以把这些错误添加到你的代码审查的检查列表(checklist)中,这样在经过代码审查后,你可以确信你的代码中不再存在这类错误了。 (Pk"NEP
aJ5H3X}Y
c7+Djqs
aE7u5PM
一、常见错误1# :多次拷贝字符串 %ezb^O_6v
VDByj "%
atLV`U&t
uq !;
测试所不能发现的一个错误是生成不可变(immutable)对象的多份拷贝。不可变对象是不可改变的,因此不需要拷贝它。最常用的不可变对象是String。 <$i"zb
zd*3R+>U'>
$N}/1R^?r
tjZ \h=
如果你必须改变一个String对象的内容,你应该使用StringBuffer。下面的代码会正常工作: i<4>\nc
pKt-R07*
)YzH k ;(
fJ)N:q`
String s = new String ("Text here"); fg9?3x
Z
JJ/1daj
,&.W6sW
Z0[)u_<
但是,这段代码性能差,而且没有必要这么复杂。你还可以用以下的方式来重写上面的代码: )%iRZ\`f
F>~ xzc
<`R|a *
\!+-4,CbZY
String temp = "Text here"; [ME}Cv`?<E
String s = new String (temp); u\{qH!?t
]Q6+e(:~ZH
.e`,{G(5q7
w=f0*$ue+w
但是这段代码包含额外的String,并非完全必要。更好的代码为: La"o)L +m_
gd337jw
Sao>P[#x
*:=];1O
String s = "Text here"; UGhW0X3k
(;;J,*NP
pOqGAD{D$
.MDYGWKt
二、常见错误2#: 没有克隆(clone)返回的对象 nE/=:{~Ws
uy/y wm/?=
.A3DFm3 t
gw_|C|!P
封装(encapsulation)是面向对象编程的重要概念。不幸的是,Java为不小心打破封装提供了方便??Java允许返回私有数据的引用(reference)。下面的代码揭示了这一点: p=!#],[
`9.dgV
t*{,Gk
![^EsgEB*
import java.awt.Dimension; _9D|u<D
/***Example class.The x and y values should never*be negative.*/ #|qm!aGs
public class Example{ FE/$(7rM
private Dimension d = new Dimension (0, 0); M[ x_#m|
public Example (){ } BJ~ivT<
{5T0RL{\N
/*** Set height and width. Both height and width must be nonnegative * or an exception is thrown.*/ 9*#$0Y=
public synchronized void setValues (int height,int width) throws IllegalArgumentException{ m)s
xotgXf
if (height < 0 || width < 0) <"*"1(wN
throw new IllegalArgumentException(); ZhH+D`9
d.height = height; hVMYB_<~
d.width = width; X?tj$
} o_iEkn
+"'F Be
public synchronized Dimension getValues(){ ]]>nbgGn#
// Ooops! Breaks encapsulation H76E+AY
return d; ecn}iN
} :/+>e
IE
} B;VH `*+X
>&bv\R/
Rr%tbt.sE
82lr4
Example类保证了它所存储的height和width值永远非负数,试图使用setValues()方法来设置负值会触发异常。不幸的是,由于getValues()返回d的引用,而不是d的拷贝,你可以编写如下的破坏性代码: \X&]FZ(*
@u,+F0Yd
x+4vss
iJ}2"i7M
Example ex = new Example(); m&Lt6_vi
Dimension d = ex.getValues();
F[5S(7M
7
d.height = -5; HtxLMzgz<<
d.width = -10; brb[})}
ya:sW5fk
j5kA^MTG
^w>&?A'!
现在,Example对象拥有负值了!如果getValues() 的调用者永远也不设置返回的Dimension对象的width 和height值,那么仅凭测试是不可能检测到这类的错误。 f2NA=%\
'<TD6jBs
9o EpPL5
|Eb&}m:E$
不幸的是,随着时间的推移,客户代码可能会改变返回的Dimension对象的值,这个时候,追寻错误的根源是件枯燥且费时的事情,尤其是在多线程环境中。 xJ-*%'(KZ
~%`EeJwT
|VK:2p^ u
|V lMmaz
更好的方式是让getValues()返回拷贝: 8=:A/47=J
'f 3HKn<L
\I;cZ>{u"}
h-7A9:
public synchronized Dimension getValues(){ 't7Z] G
return new Dimension (d.x, d.y); 9qEOgJ
} [6H}/_nD
b7bSTFZxC
bZ/
hgqS
oew|23Ytb
现在,Example对象的内部状态就安全了。调用者可以根据需要改变它所得到的拷贝的状态,但是要修改Example对象的内部状态,必须通过setValues()才可以。 qmEoqU
z
OtkC3hY
f3!n$lj
_74UdD{^o
三、常见错误3#:不必要的克隆 m=H_?W;
>)LAjwhBp
u*hH}
>rKhlUD
我们现在知道了get方法应该返回内部数据对象的拷贝,而不是引用。但是,事情没有绝对: zhX;6= X2
7{-@}j`
X<Z(]`i
_
\l
HI
/*** Example class.The value should never * be negative.*/ K5{{:NR$
public class Example{ GA\2i0ow
private Integer i = new Integer (0); Rb#/qkk/
public Example (){ } H<,bq*@
Uj,g]e8e
/*** Set x. x must be nonnegative* or an exception will be thrown*/ *6XRjq^#
public synchronized void setValues (int x) throws IllegalArgumentException{ EY~7oNfc`R
if (x < 0) !
tGiTzzp
throw new IllegalArgumentException(); 8
}-7{
i = new Integer (x); ABcBEv3
} [m\,+lG?)j
k{a)gFH
O
public synchronized Integer getValue(){ k d+l k:
// We can’t clone Integers so we makea copy this way. fWj@e"G
return new Integer (i.intValue()); e8{^f]5
} G]-%AO{K
} 7%4.b7Q
7,h3V=^)Q
Qwv '<
9\AS@SH{^T
这段代码是安全的,但是就象在错误1#那样,又作了多余的工作。Integer对象,就象String对象那样,一旦被创建就是不可变的。因此,返回内部Integer对象,而不是它的拷贝,也是安全的。 SiV*WxQe
VG)="g[%)
uJY.5w
\n_3Bwd~
方法getValue()应该被写为: #&V5H{
d@ZoV
9<l-NU9 _
088C|
public synchronized Integer getValue(){ ^>^\CP]
// ’i’ is immutable, so it is safe to return it instead of a copy. B7!;]'&d
return i; frc{>u~t
} VHW`NP 5Jl
,E?4f
@|X
.fEwk
Ukc'?p,*
Java程序比C++程序包含更多的不可变对象。JDK 所提供的若干不可变类包括: jn$j^51`C
FZ p<|t
n'?4.tb
"U{,U`@?
?Boolean pDOM:lGya
?Byte oIb)
Rq!m
?Character Y
9i][
?Class 0wFh%/:
?Double -L8YJ8J6
?Float D#jX6
?Integer y"-{$ N
?Long b
=b:
?Short VhvTBo<cw
?String TT7PQf >
?大部分的Exception的子类 /PqUXF
BC({ EE~R)
DWrbp
]_u`EvEx6
四、常见错误4# :自编代码来拷贝数组 Fg=v6j4W
sKd)BA0`
bnr|Y!T}Bi
s@~/x5jwCs
Java允许你克隆数组,但是开发者通常会错误地编写如下的代码,问题在于如下的循环用三行做的事情,如果采用Object的clone方法用一行就可以完成: hJ[UB
N@()F&e
o,FUfO}F
G3dhM#!
public class Example{ mgVML&^
private int[] copy; ?E7=:h(@t
/*** Save a copy of ’data’. ’data’ cannot be null.*/ u!Bk,}CE`
public void saveCopy (int[] data){ &$#99\/
copy = new int[data.length]; .S!-e$EJ
for (int i = 0; i < copy.length; ++i) O>AFF@=
copy = data; Pq?*C;D
} v9rVpYc"
} F\LsI;G
h<% U["
~<,Sh~Ana.
l.oBcg[
这段代码是正确的,但却不必要地复杂。saveCopy()的一个更好的实现是: -B9S}NPo
q-
:4=vkn
yW("G-Nm
d}-'<Z#G
void saveCopy (int[] data){ xNX'~B^4d
try{ A(+:S"|@
copy = (int[])data.clone(); ;SY.WfVA7
}catch (CloneNotSupportedException e){ e+@xsn3
// Can’t get here. QNArZ6UQ
} :l"dYfl
} v`B4(P1Z
jdM=SBy7q
S}cF0B1E*
?Y3@" rdR
如果你经常克隆数组,编写如下的一个工具方法会是个好主意: m}5q]N";x
\_VmY!I5\
.zSD`v@[
nxQ}&n
static int[] cloneArray (int[] data){ T3z(k
la
try{ yM ,VrUh
return(int[])data.clone(); <%K UdkzEP
}catch(CloneNotSupportedException e){ ? )_7U
// Can’t get here. ^ ulps**e
} K-(;D4/sQE
} d>!p=O`>{q
H$tb;:
5v9uHxy
S}7>RHe
这样的话,我们的saveCopy看起来就更简洁了: &{W^W8,%
WZ?!!
bulboyA
x?L hq2
void saveCopy (int[] data){ V]c5
Z$Bd
copy = cloneArray ( data); }V]eg,.BJ
} z-@-O
J+Bdz6lt
IN^_BKQt
V@Wcb$mgk
五、常见错误5#:拷贝错误的数据 uV~e|X
"9s
uTGcQs}
[H,u)8)
}q'WC4.
有时候程序员知道必须返回一个拷贝,但是却不小心拷贝了错误的数据。由于仅仅做了部分的数据拷贝工作,下面的代码与程序员的意图有偏差: GuO`jz F
wiE]z
yd>}wHt
?/d!R]3
import java.awt.Dimension; T"!EK&
/*** Example class. The height and width values should never * be l!IGc:
negative. */ ``9 GY
public class Example{ O&'/J8
static final public int TOTAL_VALUES = 10; Q4wc-s4RN
private Dimension[] d = new Dimension[TOTAL_VALUES]; q#vlBL
public Example (){ } ,%hj cGX11
};sMU6e
/*** Set height and width. Both height and width must be nonnegative * or an exception will be thrown. */ <*Y'lV
public synchronized void setValues (int index, int height, int width) throws IllegalArgumentException{ GBbh ar},g
if (height < 0 || width < 0) 5@P-g
throw new IllegalArgumentException(); ]0/p 7N14
if (d[index] == null) ]MAT2$"le
d[index] = new Dimension(); IKcKRw/O$
d[index].height = height; ;fGx;D
d[index].width = width; U)[ty@zyF
} y $V[_TN
public synchronized Dimension[] getValues() sX:lE^)-z
throws CloneNotSupportedException{ XnXb&@Y
return (Dimension[])d.clone(); !Iq{ 5:
} &1GUi{I
} bGv4.:)
p4>,Fwy2
Qb`C)Nh:
%S#WPD'Y
这儿的问题在于getValues()方法仅仅克隆了数组,而没有克隆数组中包含的Dimension对象,因此,虽然调用者无法改变内部的数组使其元素指向不同的Dimension对象,但是调用者却可以改变内部的数组元素(也就是Dimension对象)的内容。方法getValues()的更好版本为: Hr
}k5'
ow.6!tl0=h
x~/+RF XF
<4mQ*6
public synchronized Dimension[] getValues() throws CloneNotSupportedException{ g:gB`8w?
Dimension[] copy = (Dimension[])d.clone(); ^\wl2
for (int i = 0; i < copy.length; ++i){ inF6M8
A1
// NOTE: Dimension isn’t cloneable. A/ 0qk
if (d != null) J_ J+cRwq
copy = new Dimension (d.height, d.width); >;nS8{2o
} Coa -8j*R7
return copy; @J vZ[T/
} >V!LitdJ
~L4eZ
D;js.ZF
Y\?j0X;
在克隆原子类型数据的多维数组的时候,也会犯类似的错误。原子类型包括int,float等。简单的克隆int型的一维数组是正确的,如下所示: 0ar=cuDm
|F!F{d^p
E
_iO@
CV^c",b_
public void store (int[] data) throws CloneNotSupportedException{ `="v>qN2\
this.data = (int[])data.clone(); 7GZq|M_:y
// OK G|9B)`S
} z{?4*Bq
J_xG}d
T:!MBWYe |
509Q0 [k
拷贝int型的二维数组更复杂些。Java没有int型的二维数组,因此一个int型的二维数组实际上是一个这样的一维数组:它的类型为int[]。简单的克隆int[][]型的数组会犯与上面例子中getValues()方法第一版本同样的错误,因此应该避免这么做。下面的例子演示了在克隆int型二维数组时错误的和正确的做法: z[&s5"
_Bk
U+=|J
)saR0{e0N
tWD|qg_
public void wrongStore (int[][] data) throws CloneNotSupportedException{ 9?`RR/w
this.data = (int[][])data.clone(); // Not OK! O9]\Q@M.
} LSkk;)'2K
public void rightStore (int[][] data){ 97!5Q~I
// OK! km\%BD~
this.data = (int[][])data.clone(); =B(mIx;m
for (int i = 0; i < data.length; ++i){ G6O/(8
if (data != null) PZM42"[&
this.data = (int[])data.clone(); MF.[8Zb
} ixw(c&gL
} % vS8?nG
8tQ|-l*
F2>%KuM
d6.}.*7Whc
?R6`qe_F
六、常见错误6#:检查new 操作的结果是否为null 0BTLcEqgZ
<_:zI r,
@ajM^L!O
9]$`)wZ
Java编程新手有时候会检查new操作的结果是否为null。可能的检查代码为: Y}.Ystem
PXEKV0y
V5MO}
6Rz[?-mkLO
Integer i = new Integer (400); GGE[{Gb9
if (i == null) _ #'9kx|)
throw new NullPointerException(); =A n`D
QIA R
jA`a/vWu
|.w;r
检查当然没什么错误,但却不必要,if和throw这两行代码完全是浪费,他们的唯一功用是让整个程序更臃肿,运行更慢。 arj$dAW
Q}P-$X+/ n
j Z'&0x"U
- L~Uu^o
C/C++程序员在开始写java程序的时候常常会这么做,这是由于检查C中malloc()的返回结果是必要的,不这样做就可能产生错误。检查C++中new操作的结果可能是一个好的编程行为,这依赖于异常是否被使能(许多编译器允许异常被禁止,在这种情况下new操作失败就会返回null)。在java 中,new 操作不允许返回null,如果真的返回null,很可能是虚拟机崩溃了,这时候即便检查返回结果也无济于事。 0HbJKix!
<abKiXA"
七、常见错误7#:用== 替代.equals -p8e
~A >oO-0K
在Java中,有两种方式检查两个数据是否相等:通过使用==操作符,或者使用所有对象都实现的.equals方法。原子类型(int, flosat, char 等)不是对象,因此他们只能使用==操作符,如下所示: )H+kB<n
dAxp ,):&J
XxOn3i
dDlG!F_=
int x = 4; 6P+DnS[]
int y = 5; XO
wiHW{
if (x == y) S< x:t(
System.out.println ("Hi"); 4/MNqit+
// This ’if’ test won’t compile. u~'OcO
if (x.equals (y)) T]71lRY5
System.out.println ("Hi"); )zJ=PF
gaeOgP.0
Sdc*rpH"(
Yx1 D)
对象更复杂些,==操作符检查两个引用是否指向同一个对象,而equals方法则实现更专门的相等性检查。 RvW.@#EH0
aZgNPw
)w"0w(
9}
*$n&B
更显得混乱的是由java.lang.Object 所提供的缺省的equals方法的实现使用==来简单的判断被比较的两个对象是否为同一个。 ~3=2=Uf
/DU*M,
kxo.v |)8
;|30QUYh
许多类覆盖了缺省的equals方法以便更有用些,比如String类,它的equals方法检查两个String对象是否包含同样的字符串,而Integer的equals方法检查所包含的int值是否相等。 KO,_6>8]U
treXOC9^B8
cyMs(21
2
sSwDF
大部分时候,在检查两个对象是否相等的时候你应该使用equals方法,而对于原子类型的数据,你用该使用==操作符。 oh\1>3,Ns
]^@0+!
e@j8T
gI)
I,j3bC
八、常见错误8#: 混淆原子操作和非原子操作 hTw}X.<4
%dmfBf Ev
d@g2k> >
#F4X}
Java保证读和写32位数或者更小的值是原子操作,也就是说可以在一步完成,因而不可能被打断,因此这样的读和写不需要同步。以下的代码是线程安全(thread safe)的: |s|/]aD}o
e2Jp'93o'
}ywi"k4>
%F5 =n"
public class Example{ ,so4Lb(vG
private int value; // More code here... !}q."%%J_%
public void set (int x){ NI\H
\#bJ
// NOTE: No synchronized keyword h{/ve`F>@
this.value = x; x,1=D~L}
} A&l7d0Z^j5
} RVP 18ub.S
z!CD6W1n
-N z}DW>
AbZ:(+@cP
不过,这个保证仅限于读和写,下面的代码不是线程安全的: XV5`QmB9
U;gp)=JNT
4$Pr|gx
Nza; O[
public void increment (){ 0yTQ{'Cc
// This is effectively two or three instructions: JS7dsO0;
// 1) Read current setting of ’value’. (C\r&N