參考
編寫高質(zhì)量代碼:改善Java程序的151個建議
持續(xù)更新
成洗、Java
、筆記
1.使用 long
類型時注意使用 大寫L
// 錯誤的
long i = 1l;
// 正確的
long i = 1L;
2.三元表達(dá)式類型務(wù)必一致
??三元操作符是if-else的簡化寫法藏杖,在項目中使用它的地方很多,也非常好用泞遗,但是好用 又簡單的東西并不表示就可以隨便用看幼,我們來看看下面這段代碼:
public static void main(String[] args) {
int i = 80;
String s = String.valueOf(i<100 ? 90 : 100);
String si = String.valueOf(i<100 ? 90 : 100.0);
System.out.println("兩者是否相等:"+s.equals(si));
}
??分析一下這段程序:i是80,那它當(dāng)然小于100销睁,兩者的返回值肯定都是90供璧,再轉(zhuǎn)成 String類型,其值也絕對相等榄攀,毋庸置疑的嗜傅。恩,分析得有點道理檩赢,但是變量s中三元操作符的第二個操作數(shù)是100,而si的第二個操作數(shù)是100.0违寞,難道沒有影響嗎贞瞒?不可能有影響吧,三元操作符的條件都為真了趁曼,只返回第一個值嘛军浆,與第二個值有一毛錢的關(guān)系嗎?貌似有道理挡闰。
??果真如此嗎乒融?我們通過結(jié)果來驗證一下掰盘,運行結(jié)果是:“兩者是否相等:false”,什么赞季? 不相等愧捕,Why?
??問題就出在了 100和100.0這兩個數(shù)字上,在變量s中申钩,三元操作符中的第一個操作數(shù) (90)和第二個操作數(shù)(100)都是int類型次绘,類型相同,返回的結(jié)果也就是int類型的90撒遣,而變量si的情況就有點不同了邮偎,第一個操作數(shù)是90 (int類型),第二個操作數(shù)卻是100.0义黎,而這是個浮點數(shù)禾进,也就是說兩個操作數(shù)的類型不一致,可三元操作符必須要返回一個數(shù)據(jù)廉涕,而且類型要確定泻云,不可能條件為真時返回int類型,條件為假時返回float類型火的,編譯器是不允許如此的壶愤,所以它就會進(jìn)行類型轉(zhuǎn)換了,int型轉(zhuǎn)換為浮點數(shù)90.0馏鹤,也就是說三元操作符的返回值是浮點數(shù)90.0征椒,那這當(dāng)然與整型的90不相等了。這里可能會有疑惑了:為什么是整型轉(zhuǎn)為浮點湃累,而不是浮點轉(zhuǎn)為整型呢勃救?這就涉及三元操作符類型的轉(zhuǎn)換規(guī)則:
??若兩個操作數(shù)不可轉(zhuǎn)換,則不做轉(zhuǎn)換治力,返回值為Object類型蒙秒。
??若兩個操作數(shù)是明確類型的表達(dá)式(比如變量),則按照正常的二進(jìn)制數(shù)字來轉(zhuǎn)換宵统,int類型轉(zhuǎn)換為long類型晕讲,long類型轉(zhuǎn)換為float類型等。
??若兩個操作數(shù)中有一個是數(shù)字S马澈,另外一個是表達(dá)式瓢省,且其類型標(biāo)示為T,那么痊班,若數(shù)字S在T的范圍內(nèi)勤婚,則轉(zhuǎn)換為T類型;若S超出了T類型的范圍涤伐,則T轉(zhuǎn)換為S類型馒胆。
??若兩個操作數(shù)都是直接量數(shù)字(Literal缨称、字面量),則返回值類型為范圍較大者祝迂。
??知道是什么原因了睦尽,相應(yīng)的解決辦法也就有了 :保證三元操作符中的兩個操作數(shù)類型一致,即可減少可能錯誤的發(fā)生液兽。
3.避免 instanceof 非預(yù)期結(jié)果
??instanceof是一個簡單的二元操作符骂删,它是用來判斷一個對象是否是一個類實例的,其 操作類似于 >=四啰、==宁玫,非常簡單,我們來看段程序柑晒,代碼如下:
public static void main(String[] args) {
// String對象是否是Object的實例
boolean b1 = "Sting" instanceof Object;
// String對象是否是String的實例
boolean b2 = new String() instanceof String;
// Object對象是否是String的實例
boolean b3 = new Object() instanceof String;
// 拆箱類型是否是裝箱矣型的實例
boolean b4 = 'A' instanceof Character;
// 空對象是否是String的實例
boolean b5 = null instanceof String;
// 類負(fù)轉(zhuǎn)換后的空對象是否是String的實例
boolean b6 = (String) null instanceof String;
//Date對象是否是String的實例
boolean b7 = new Date() instanceof String;
boolean b8 = new GenericClass<String>().isDateInstance("");
}
class GenericClass<T> {
// 判斷是否是Date類型
public boolean isDateInstance(T t) {
return t instanceof Date;
}
}
??就這么一段程序欧瘪,instanceof的所有應(yīng)用場景都出現(xiàn)了,同時問題也產(chǎn)生了:這段程序中哪些語句會編譯通不過匙赞?我們一個一個地來解說佛掖。
??"Sting" instanceof Object
??返回值是true,這很正常涌庭,"String"是一個字符串芥被,字符串又繼承了 Object,那當(dāng)然是返回true了坐榆。
??new String() instanceof String
??返回值是true,沒有任何問題拴魄,一個類的對象當(dāng)然是它的實例了。
??new Object() instanceof String
??返回值是false席镀,Object是父類匹中,其對象當(dāng)然不是String類的實例了。要注意的是豪诲,這句話其實完全可以編譯通過顶捷,只要instanceof關(guān)鍵字的左右兩個操作數(shù)有繼承或?qū)崿F(xiàn)關(guān)系,就可以編譯通過屎篱。
??'A' instanceof Character
??這句話可能有讀者會猜錯服赎,事實上它編譯不通過,為什么呢交播?因為是一個char類型专肪,也就是一個基本類型,不是一個對象堪侯,instanceof只能用于對象的判斷,不能用于基本類型的判斷荔仁。
??null instanceof String
??返回值是false伍宦,這是instanceof特有的規(guī)則:若左操作數(shù)是null芽死,結(jié)果就直接返回 false,不再運算右操作數(shù)是什么類次洼。這對我們的程序非常有利关贵,在使用instanceof操作符時, 不用關(guān)心被判斷的類(也就是左操作數(shù))是否為null,這與我們經(jīng)常用到的equals卖毁、toString方法不同揖曾。
??(String)null instanceof String
??返回值是false,不要看這里有個強(qiáng)制類型轉(zhuǎn)換就認(rèn)為結(jié)果是true亥啦,不是的炭剪,null是一個萬用類型,也可以說它沒類型翔脱,即使做類型轉(zhuǎn)換還是個null奴拦。
??new Date() instanceof String
??編譯通不過,因為Date類和String沒有繼承或?qū)崿F(xiàn)關(guān)系届吁,所以在編譯時直接就報錯了错妖,instanceof操作符的左右操作數(shù)必須有繼承或?qū)崿F(xiàn)關(guān)系,否則編譯會失敗疚沐。
??new GenericClass<String>().isDateInstance("")
??編譯通不過暂氯?非也,編譯通過了亮蛔,返回值是false, T是個String類型痴施,與Date之間沒有繼承或?qū)崿F(xiàn)關(guān)系,為什么"t instanceof Date"會編譯通過呢尔邓?那是因為Java的泛型是為編碼服務(wù)的晾剖,在編譯成字節(jié)碼時,T已經(jīng)是Object類型了梯嗽,傳遞的實參是String類型齿尽,也就是說T的表面類型是Object,實際類型是String灯节,那t instanceof Date
這句話就等價于 Object instance of Date
了循头,所以返回 false 就很正常了。
4.奇偶判斷炎疆,用偶判斷卡骂,不要用奇判斷
??判斷一個數(shù)是奇數(shù)還是偶數(shù)是小學(xué)里學(xué)的基本知識,能夠被2整除的整數(shù)是偶數(shù)形入,不能被2整除的是奇數(shù)全跨,這規(guī)則簡單又明了,還有什么好考慮的亿遂?好浓若,我們來看一個例f,代碼 如下:
public static void main(String[] args) {
//接收鍵盤輸入?yún)?shù)
Scanner input = new Scanner(System.in);
System.out.print("請輸入多個數(shù)字判斷奇偶:");
while(input.hasNextlnt()) {
int i = input.nextInt();
String str = i + "->" + (i % 2 == 1 ? "奇數(shù)" : "偶數(shù)");
System.out.println(str);
}
}
??輸入多個數(shù)字渺杉,然后判斷每個數(shù)字的奇偶性,不能被2整除就是奇數(shù)挪钓,其他的都是偶數(shù)是越,完全是根據(jù)奇偶數(shù)的定義編寫的程序,我們來看看打印的結(jié)果:
請輸入多個數(shù)字利斷奇偶:1 2 0 -1 -2
1 -> 奇數(shù)
2 -> 偶數(shù)
0 -> 偶數(shù)
-1 -> 偶數(shù)
-2 -> 偶數(shù)
??前三個還很靠譜碌上,第四個參數(shù)怎么可能會是偶數(shù)呢倚评,這Java也太差勁了,如此簡單的計算也會錯馏予!別忙著下結(jié)論天梧,我們先來了解一下Java中的取余(%標(biāo)示符)算法,模擬代碼如下:
// 橫擬取余計算吗蚌,dividend被除數(shù)腿倚,divisor除數(shù)
public static int remainder(int dividend, int divisor) {
return dividend - dividend / divisor * divisor;
}
??看到這段程序,相信大家都會心地笑了蚯妇,原來Java是這么處理取余計算的呀敷燎。根據(jù)上面的模擬取余可知,當(dāng)輸入-1
的時候箩言,運算結(jié)果是-1
硬贯,當(dāng)然不等于1 了,所以它就被判定為偶數(shù)了陨收,也就是說是我們的判斷失誤了饭豹。問題明白了,修正也很簡單务漩,改為判斷是否是偶數(shù)即可拄衰,代碼如下:
i % 2 == 0 ? "偶數(shù)" : "奇數(shù)";
5.不要讓類型默默轉(zhuǎn)換
看以下代碼
public static final int LIGHT_SPEED = 30 * 10000 * 1000;
public static void main(String[] args) {
long dis = LIGHT_SPEED * 60 * 8;
System.out.println(dis);
}
結(jié)果:
-2028888064
??這是應(yīng)為Java是先運算然后再進(jìn)行類型轉(zhuǎn)換的,具體地說就是因為disc的三個運算參數(shù)都是int類型饵骨,三者相乘的結(jié)果雖然也是int類型翘悉,但是已經(jīng)超過了int的最大值,所以其值就是負(fù)值了(為什么是負(fù)值居触?因為過界了就會從頭開始)妖混,再轉(zhuǎn)換成long型,結(jié)果還是負(fù)值轮洋。
- 問題知道了制市,解決問題也很簡單,只需一個小小的
L
long dis = LIGHT_SPEED * 60L * 8;
在實際開發(fā)中弊予,更通用的做法祥楣,是主動聲明類型轉(zhuǎn)化
long dis = 1L * LIGHT_SPEED * 60 * 8;
意思是:嗨,我已經(jīng)是長整型了,你們都跟著我一起轉(zhuǎn)為長整型吧
6.邊界荣堰,邊界床未,還是邊界
??某商家生產(chǎn)的電子產(chǎn)品非常暢銷,需要提前30天預(yù)訂才能搶到手振坚,同時它還規(guī)定了一 個會員可擁有的最多產(chǎn)品數(shù)量,目的是防止囤積壓貨肆意加價斋扰。會員的預(yù)定過程是這樣的: 先登錄官方網(wǎng)站渡八,選擇產(chǎn)品型號,然后設(shè)置需要預(yù)訂的數(shù)量传货,提交屎鳍,符合規(guī)則即提示下單成功,不符合規(guī)則提示下單失敗问裕。后臺的處理邏輯模擬如下:
// 一個會員擁有產(chǎn)品的最多數(shù)量
public final static int LIMIT = 2000;
public static void main(String[] args) {
// 會員當(dāng)前擁有的產(chǎn)品數(shù)量
int cur = 1000;
Scanner input = new Scanner(System.in);
System.out.print("請輸入需要預(yù)定的數(shù)量:");
while (input.hasNextInt()) {
int order = input.nextInt()逮壁;
// 當(dāng)前擁有的與準(zhǔn)備訂購的產(chǎn)品數(shù)量之和
if (order > 0 && order + cur <= LIMIT) {
System.out .println ("你已經(jīng)成功定的" + order + "個產(chǎn)品!");
} else {
System.out.print("超出限額粮宛,預(yù)定失斂!");
}
}
}
??這是一個簡易的訂單處理程序巍杈,其中cur代表的是會員已經(jīng)擁有的產(chǎn)品數(shù)LIMIT是一個會員最多擁有的產(chǎn)品數(shù)置(現(xiàn)實中這兩個參數(shù)當(dāng)然是從數(shù)據(jù)庫中獲得的忧饭,不過這里是一 個模擬程序),如果當(dāng)前預(yù)訂數(shù)置與擁有數(shù)之和超過了最大數(shù)量筷畦,則預(yù)訂失敗词裤,否則下單成功。業(yè)務(wù)邏輯很簡單鳖宾,同時在Web界面上對訂單數(shù)量做了嚴(yán)格的校驗吼砂,比如不能是負(fù)值、 不能超過最大數(shù)量等鼎文,但是人算不如天算渔肩,運行不到兩小時數(shù)據(jù)庫中就出現(xiàn)了異常數(shù)據(jù):某會員擁有產(chǎn)品的數(shù)址與預(yù)訂數(shù)量之和遠(yuǎn)遠(yuǎn)大于限額。怎么會這樣漂问?程序邏輯上不可能有問題呀赖瞒,這是如何產(chǎn)生的呢?我們來模擬一下蚤假,第一次輸入:
??請褕入需要預(yù)定的致量:800
??你己經(jīng)成功預(yù)定的800
個產(chǎn)品栏饮!
??這完全滿足條件,沒有任何問題磷仰,繼續(xù)輸入:
??請輸入需要預(yù)定的數(shù)量:2147483647
??你已徑成功預(yù)定的2147483647
個產(chǎn)品袍嬉!
??看到?jīng)],這個數(shù)字遠(yuǎn)遠(yuǎn)超過了 2000的限額,但是競?cè)活A(yù)訂成功了伺通,真是神奇箍土!
??看著2147483647
這個數(shù)字很眼熟?那就對了罐监,它是int類型的最大值吴藻,沒錯,有人輸入 了一個最大值弓柱,使校驗條件失效了沟堡,Why?我們來看程序矢空,order的值是2147483647
航罗,那再加上1000就超出int的范圍了,其結(jié)果是-21W482649
屁药,那當(dāng)然是小于正數(shù)2000
了粥血!一句話可歸結(jié)其原因:數(shù)字越界使檢驗條件失效
。
??在單元測試中酿箭,有一項測試叫做邊界測試(也有叫做臨界測試)复亏,如果一個方法接收的是int類型的參數(shù),那以下三個值是必測的:0七问、正最大蜓耻、負(fù)最小
,其中正最大和負(fù)最小是邊界值械巡,如果這三個值都沒有問題刹淌,方法才是比較安全可靠的。我們的例子就是因為缺少邊界測試讥耗,致使生產(chǎn)系統(tǒng)產(chǎn)生了嚴(yán)重的偏差有勾。
??也許你要疑惑了,Web界面既然已經(jīng)做了嚴(yán)格的校驗古程,為什么還能輸入2147483647
這么大的數(shù)字呢蔼卡?是否說明Web校驗不嚴(yán)格?錯了挣磨,不是這樣的雇逞,Web校驗都是在頁面上通過JavaScript實現(xiàn)的,只能限制普通用戶(這里的普通用戶是指不懂HTML茁裙、不懂HTTP塘砸、不懂Java的簡單使用者),而對干高手晤锥,這些校驗基本上就是擺設(shè)掉蔬,HTTP是明文傳輸?shù)睦认埽瑢⑵鋽r截幾次,分析一下數(shù)據(jù)結(jié)構(gòu)女轿,然后再寫一個模擬器箭启,一切前端校驗就都成了浮云!想往后臺提交個什么數(shù)據(jù)那還不是信手拈來蛉迹?
7.不要讓四舍五入虧了一方
??本建議還是來重溫一個小學(xué)數(shù)學(xué)問題:四舍五入傅寡。四舍五入是一種近似精確的計算方法,在Java 5之前婿禽,我們一般是通過使用Math.round來獲得指定精度的整數(shù)或小數(shù)的赏僧,這種方法使用非常廣泛,代碼如下:
public static void main(String[] args) {
System.out.println("10.5近似值:" + Math.round(10.5));
System.out.println("-10.5近似值:"+ Math.round(-10.5));
}
??輸出結(jié)果為:
10.5近似值:11
-10.5近似值:-10
??這是四舍五入的經(jīng)典案例扭倾,也是初級面試官很樂意選擇的考題,絕對值相同的兩個數(shù)字挽绩,近似值為什么就不同了呢膛壹?這是由Math.round
采用的舍入規(guī)則所決定的(采用的是正無窮方向舍入規(guī)則,后面會講解)唉堪。我們知道四舍五入是有誤差的:其誤差值是舍入位的一半模聋。我們以舍入運用最頻繁的銀行利息計算為例來闡述該問題。
??我們知道銀行的盈利渠道主要是利息差唠亚,從儲戶手里收攏資金链方,然后放貸出去,其間的利息差額便是所獲得的利潤蒜茴。對一個銀行來說蝇完,對付給儲戶的利息的計算非常頻繁水援,人民銀行規(guī)定每個季度末月的20日為銀行結(jié)息日,一年有4次的結(jié)息日前酿。
??場景介紹完畢,我們回過頭來看四舍五入鹏溯,小于5的數(shù)字被舍去罢维,大于等于5的數(shù)字進(jìn)位后舍去,由于所有位上的數(shù)字都是自然計算出來的丙挽,按照概率計算可知肺孵,被舍入的數(shù)字均勻分布在0到9之間,下面以10筆存款利息計算作為模型颜阐,以銀行家的身份來思考這個算法平窘。
??四舍。舍棄的數(shù)值:0.000瞬浓、0.001初婆、0.002、0.003、0.004磅叛,因為是舍棄的屑咳,對銀行家來說,就不用付款給儲戶了弊琴,那每舍棄一個數(shù)字就會賺取相應(yīng)的金額:0.000兆龙、0.001、0.002敲董、0.003紫皇、0.004。
??五入腋寨。進(jìn)位的數(shù)值:0.005聪铺、0.006、0.007萄窜、0.008铃剔、0.009,因為是進(jìn)位查刻,對銀行家來說键兜,每進(jìn)一位就會多付款給儲戶,也就是虧損了穗泵,那虧損部分就是其對應(yīng)的10進(jìn)制補(bǔ)數(shù):0.005普气、0.004、0.003佃延、0.002现诀、0.001。
??因為舍棄和進(jìn)位的數(shù)字是在0到9之間均勻分布的苇侵,所以對于銀行家來說赶盔,每10筆存款的利息因采用四舍五入而獲得的盈利是:
0.000+0.001+0.002+0.003+0.004-0.005-0.004-0.003-0.002-0.001 = -0.005
??也就是說,每10筆的利息計算中就損失0.005元榆浓,即每筆利息計算損失0.0005元于未,這對一家有5千萬儲戶的銀行來說(對國內(nèi)的銀行來說,5千萬是個很小的數(shù)字)陡鹃,每年僅僅因為四舍五入的誤差而損失的金額是:
public static void main(String[] args) {
// 銀行賬戶數(shù)量烘浦,5千萬
int accountNum = 5000 * 10000;
//按照人行的規(guī)定,每個季度末月的20日為銀行結(jié)息日
double cost = 0.0005 * accountNum * 4 ;
System.out.println("銀行每年損失的金額:" + cost);
}
??輸出的結(jié)果是:“銀行每年損失的金額:100000.0”萍鲸。即闷叉,每年因為一個算法誤差就損失了10萬元,事實上以上的假設(shè)條件都是非常保守的脊阴,實際情況可能損失得更多握侧。那各位可能要說了蚯瞧,銀行還要放貸呀,放出去這筆計算誤差不就抵消掉了嗎品擎?不會抵銷埋合,銀行的貸款數(shù)量是非常有限的,其數(shù)量級根本沒有辦法和存款相比萄传。
??這個算法誤差是由美國銀行家發(fā)現(xiàn)的(那可是私人銀行甚颂,錢是自己的,白白損失了可不行)秀菱,并且對此提出了一個修正算法振诬,叫做銀行家舍入(Banker's Round)的近似算法,其規(guī)則如下:
??舍去位的數(shù)值小于5時衍菱,直接舍去赶么;
??舍去位的數(shù)值大于等于6時,進(jìn)位后舍去脊串;
??當(dāng)舍去位的數(shù)值等于5時禽绪,分兩種情況:5后面還有其他數(shù)字(非0),則進(jìn)位后舍去洪规;若5后面是0(即5是最后一個數(shù)字),則根據(jù)5前一位數(shù)的奇偶性來判斷是否需要進(jìn)位循捺,奇數(shù)進(jìn)位斩例,偶數(shù)舍去。
以上規(guī)則匯總成一句話:四舍六入五考慮从橘,五后非零就進(jìn)一念赶,五后為零看奇偶,五前為偶應(yīng)舍去恰力,五前為奇要進(jìn)一叉谜。
- 我們舉例說明,取2位精度:
round(10.5551) = 10.56
round(10.555) = 10.56
round(10.545) = 10.54
??要在Java 5以上的版本中使用銀行家的舍入法則非常簡單踩萎,直接使用RoundingMode類提供的Round模式即可停局,示例代碼如下:
public static void main(String[] args) {
// 存款
BigDecimal d = new BigDecimal(888888);
// 月利率,乘3計算季利率
BigDecimal r = new BigDecimal(0.001875 * 3);
//計算利息
BigDecimal i = d.multiply(r).setScale(2, RoundingMode.HALF_EVEN);
System.out.println("季利息是:" + i);
}
??在上面的例子中香府,我們使用了BigDecimal類董栽,并且采用setScale方法設(shè)置了精度,同時傳遞了一個RoundingMode.HALF_EVEN
參數(shù)表示使用銀行家舍入法則進(jìn)行近似計算企孩,BigDecimal和RoundingMode
是一個絕配锭碳,想要采用什么舍入模式使用RoundingMode設(shè)置即可。目前Java支持以下七種舍入方式:
??ROUND_UP:遠(yuǎn)離零方向舍入勿璃。 向遠(yuǎn)離0的方向舍入擒抛,也就是說推汽,向絕對值最大的方向舍入,只要舍棄位非0即進(jìn)位歧沪。
??ROUND_DOWN:趨向零方向舍入歹撒。 向0方向靠攏,也就是說槽畔,向絕對值最小的方向輸入栈妆,注意:所有的位都舍棄,不存在進(jìn)位情況厢钧。
??ROUND_CEILING:向正無窮方向舍入鳞尔。 向正最大方向靠攏,如果是正數(shù)早直,舍入行為類似于ROUND_UP寥假;如果為負(fù)數(shù),則舍入行為類似于ROUND_DOWN霞扬。注意:Math.round方法使用的即為此模式糕韧。
??ROUND_FLOOR:向負(fù)無窮方向舍入。 向負(fù)無窮方向靠攏喻圃,如果是正數(shù)萤彩,則舍入行為類似于ROUND_DOWN;如果是負(fù)數(shù)斧拍,則舍入行為類似于ROUND_UP雀扶。
??HALF_UP:最近數(shù)字舍入(5進(jìn))。這就是我們最最經(jīng)典的四舍五入模式肆汹。
??HALF_DOWN:最近數(shù)字舍入(5舍)愚墓。 在四舍五入中,5是進(jìn)位的昂勉,而在HALF_DOWN中卻是舍棄不進(jìn)位浪册。
??HALF_EVEN:銀行家算法。
??在普通的項目中舍入模式不會有太多影響岗照,可以直接使用Math.round方法村象,但在大量與貨幣數(shù)字交互的項目中,一定要選擇好近似的計算模式谴返,盡量減少因算法不同而造成的損失煞肾。
??注意 根據(jù)不同的場景,慎重選擇不同的舍入模式嗓袱,以提高項目的精準(zhǔn)度籍救,減少算法損失。
8.在接口中不要存在實現(xiàn)代碼
??看到這樣的標(biāo)題讀者可能會納悶:接口中有實現(xiàn)代碼渠抹?這怎么可能呢蝙昙?確實闪萄,接口中可以聲明常量,聲明抽象方法奇颠,也可以繼承父接口败去,但就是不能有具體實現(xiàn),因為接口是一種契約(Contract
)烈拒,是一種框架性協(xié)議圆裕,這表明它的實現(xiàn)類都是同一種類型,或者是具備相似特征的一個集合體荆几。對于一般程序吓妆,接口確實沒有任何實現(xiàn),但是在那些特殊的程序中就例外了吨铸,閱讀如下代碼:
public static void main(String[] args) {
// 調(diào)用接口的實現(xiàn)
B.s.doSomething();
}
// 在接口中存在實現(xiàn)代碼
interface B {
public static final S s = new S() {
public void doSomething() {
System.out.println("我在接口中實現(xiàn)了");
}
};
}
// 被實現(xiàn)的接口
interface S {
public void doSomething();
}
??仔細(xì)看main方法行拢,注意那個B接口。它調(diào)用了接口常量诞吱,在沒有任何顯式實現(xiàn)類的情況下舟奠,它竟然打印出了結(jié)果,那B接口中的s常量(接口是S)是在什么地方被實現(xiàn)的呢房维?答案是在B接口中沼瘫。
??在B接口中聲明了一個靜態(tài)常量s,其值是一個匿名內(nèi)部類(Anonymous Inner Class
)的實例對象咙俩,就是該匿名內(nèi)部類(當(dāng)然晕鹊,可以不用匿名,直接在接口中實現(xiàn)內(nèi)部類也是允許的)實現(xiàn)了S接口暴浦。你看,在接口中存在著實現(xiàn)代碼吧晓锻!
??這確實很好歌焦,很強(qiáng)大,但是在一般的項目中砚哆,此類代碼是嚴(yán)禁出現(xiàn)的独撇,原因很簡單:這是一種不好的編碼習(xí)慣,接口是用來干什么的躁锁?接口是一個契約纷铣,不僅僅約束著實現(xiàn)者,同時也是一個保證战转,保證提供的服務(wù)(常量搜立、方法)是穩(wěn)定、可靠的槐秧,如果把實現(xiàn)代碼寫到接口中啄踊,那接口就綁定了可能變化的因素忧设,這就會導(dǎo)致實現(xiàn)不再穩(wěn)定和可靠,是隨時都可能被拋棄颠通、被更改址晕、被重構(gòu)的。所以顿锰,接口中雖然可以有實現(xiàn)谨垃,但應(yīng)避免使用。
??注意 接口中不能存在實現(xiàn)代碼
硼控。
9.避免在構(gòu)造函數(shù)中初始化其他類
??構(gòu)造函數(shù)是一個類初始化必須執(zhí)行的代碼刘陶,它決定著類的初始化效率,如果構(gòu)造函數(shù)比較復(fù)雜淀歇,而且還關(guān)聯(lián)了其他類易核,則可能產(chǎn)生意想不到的問題,我們來看如下代碼:
public class Client {
public static void main(String[] args) {
Son s = new Son(); s.doSomething();
}
}
// 父類
class Father {
Father() {
new Other();
}
}
// 子類
class Son extends Father {
public void doSomething() {
System.out.println("Hi,show me something");
}
}
// 相關(guān)類
class Other {
public Other() {
new Son();
}
}
??這段代碼并不復(fù)雜浪默,只是在構(gòu)造函數(shù)中初始化了其他類牡直,想想看這段代碼的運行結(jié)果是什么?是打印"Hi, show me something
"嗎纳决?
??答案是這段代碼不能運行碰逸,報StackOverflowError
異常,棧(Stack)內(nèi)存溢出
阔加。這是因為聲明s變量時饵史,調(diào)用了Son的無參構(gòu)造函數(shù),JVM又默認(rèn)調(diào)用了父類Father的無參構(gòu)造函數(shù)胜榔,接著Father類又初始化了Other類胳喷,而Other類又調(diào)用了Son類,于是一個死循環(huán)就誕生了夭织,直到棧內(nèi)存被消耗完畢為止吭露。
??可能有讀者會覺得這樣的場景不可能在開發(fā)中出現(xiàn),那我們來思考這樣的場景:Father是由框架提供的尊惰,Son類是我們自己編寫的擴(kuò)展代碼讲竿,而Other類則是框架要求的攔截類(Interceptor類或者Handle類或者Hook方法
),再來看看該問題弄屡,這種場景不可能出現(xiàn)嗎题禀?
??那有人可能要說了,這種問題只要系統(tǒng)一運行就會發(fā)現(xiàn)膀捷,不可能對項目產(chǎn)生影響迈嘹。
??那是因為我們在這里展示的代碼比較簡單,很容易一眼洞穿全庸,一個項目中的構(gòu)造函數(shù)可不止一兩個江锨,類之間的關(guān)系也不會這么簡單的吃警,要想瞥一眼就能明白是否有缺陷這對所有人員來說都是不可能完成的任務(wù),解決此類問題的最好辦法就是:不要在構(gòu)造函數(shù)中聲明初始化其他類啄育,養(yǎng)成良好的習(xí)慣酌心。
10.讓工具類不可實例化
??Java項目中使用的工具類非常多,比如JDK自己的工具類java.lang.Math挑豌、java.util.Collections
等都是我們經(jīng)常用到的安券。工具類的方法和屬性都是靜態(tài)的,不需要生成實例即可訪問氓英,而且JDK也做了很好的處理侯勉,由于不希望被初始化,于是就設(shè)置構(gòu)造函數(shù)為private訪問權(quán)限铝阐,表示除了類本身外址貌,誰都不能產(chǎn)生一個實例,我們來看一下java.lang.Math代碼:
public final class Math {
/**
* Don't let anyone instantiate this class.
*/
private Math() {}
}
??之所以要將"Don't let anyone instantiate this class.
"留下來徘键,是因為Math的構(gòu)造函數(shù)設(shè)置為private了:我就是一個工具類练对,我只想要其他類通過類名來訪問,我不想你通過實例對象訪問吹害。這在平臺型或框架型項目中已經(jīng)足夠了螟凭。但是如果已經(jīng)告訴你不能這么做了,你還要生成一個Math實例來訪問靜態(tài)方法和屬性(Java的反射是如此的發(fā)達(dá)它呀,修改個構(gòu)造函數(shù)的訪問權(quán)限易如反掌)螺男,那我就不保證正確性了,隱藏問題隨時都有可能爆發(fā)纵穿!那我們在項目開發(fā)中有沒有更好的限制辦法呢下隧?有,即不僅僅設(shè)置成private訪問權(quán)限谓媒,還拋異常汪拥,代碼如下:
public class UtilsClass {
private UtilsClass() {
throw new Error("不要實例化我!");
}
}
??如此做才能保證一個工具類不會實例化篙耗,并且保證所有的訪問都是通過類名來進(jìn)行的。需要注意一點的是宪赶,此工具類最好不要做繼承的打算宗弯,因為如果子類可以實例化的話,那就要調(diào)用父類的構(gòu)造函數(shù)搂妻,可是父類沒有可以被訪問的構(gòu)造函數(shù)蒙保,于是問題就會出現(xiàn)。
??注意 如果一個類不允許實例化欲主,就要保證“平车瞬蓿”渠道都不能實例化它逝嚎。