開發(fā)團(tuán)隊(duì)在開發(fā)過程中,由于每個(gè)人的開發(fā)習(xí)慣钦睡,以及對于技術(shù)的理解深
淺程度不一,往往一個(gè)項(xiàng)目在開發(fā)過程中,代碼的質(zhì)量佑女,代碼的風(fēng)格都不
盡相似,所以有一份適合團(tuán)隊(duì)的代碼規(guī)范是非常有必要的谈竿,而一個(gè)團(tuán)隊(duì)的
代碼規(guī)范团驱,包含了開發(fā)常見的風(fēng)格習(xí)慣以及一些常見代碼細(xì)節(jié)的寫法規(guī)范
等,本篇就來淺談一些代碼規(guī)范涉及的技術(shù)細(xì)節(jié)和對這些部分的思考
命名規(guī)范
相信經(jīng)歷過項(xiàng)目開發(fā)的人都知道空凸,在開發(fā)過程中會涉及無數(shù)次的申明操作嚎花,這個(gè)過程中最讓人頭疼的就是給申明的文件(實(shí)例)起個(gè)名字了。名字需要準(zhǔn)確的表達(dá)出背后代表的含義呀洲,并且還要通俗易懂紊选,使得代碼干凈漂亮啼止,否則不好的命名反而成為開發(fā)的阻礙,干擾維護(hù)者和開發(fā)者的思路兵罢。那么一個(gè)好的命名會給我們開發(fā)帶來什么好處呢献烦?
- 為標(biāo)識符提供附加的信息,賦予標(biāo)識符現(xiàn)實(shí)意義卖词。幫助我們理順編碼的邏輯巩那,減少閱讀和理解代碼的工作量;
- 使代碼審核變得更有效率此蜈,專注于更重要的問題即横,而不是爭論語法和命名規(guī)范這類小細(xì)節(jié),提高開發(fā)效率裆赵;
- 提高代碼的清晰度东囚、可讀性以及美觀程度;
- 避免不同產(chǎn)品之間的命名沖突战授。
那么常見的命名方式有哪些呢页藻?根據(jù)各大規(guī)范和Java框架主流的方案來說,一般分為四種:
駝峰命名法
駝峰命名法基本上是各大企業(yè)使用最多植兰,也是各大規(guī)范首推的命名方式惕橙。其使用大小寫混合的格式,單詞之間不使用空格隔開或者連接字符連接的命名方式钉跷,因此發(fā)展處兩種格式:大駝峰命名法(UpperCamelCase)和小駝峰命名法(lowerCamelCase)
這兩種命名方式的區(qū)別主要在第一個(gè)單詞的首字母上弥鹦,大駝峰命名法則是首字母大寫命名,而小駝峰則是首個(gè)單詞的首字母小寫爷辙,比如:firstName, toString等彬坏。在jdk中參照了谷歌制定的駝峰命名轉(zhuǎn)換規(guī)則,用來細(xì)分不同情況下的駝峰轉(zhuǎn)換:
1.從正常的表達(dá)形式開始膝晾,把短語轉(zhuǎn)換成 ASCII 碼栓始,并且移除單引號,如“Müller’s algorithm”轉(zhuǎn)換為“Muellers algorithm”
2.如果存在連接符號,就將連接符開始分割為兩個(gè)單詞血当,如果某個(gè)分割前某個(gè)單詞已經(jīng)是駝峰命名幻赚,也拆分為小寫的兩個(gè)單詞,如:AdWords會轉(zhuǎn)換為ad words臊旭,而non-current則轉(zhuǎn)換為non current 等
3.將所有的字母轉(zhuǎn)為小寫字母落恼,每個(gè)單詞的首字母大寫,就轉(zhuǎn)換為了大駝峰命名/首字母小寫則轉(zhuǎn)為小駝峰
4.將所有的單詞連接在一起离熏,即為標(biāo)示符命名
例如下面的轉(zhuǎn)換案例:
蛇形命名法
蛇形命名法在Java中極少見到佳谦,一般為每個(gè)單詞之間都通過‘_’進(jìn)行連接,例如‘out_of’
串式命名法
串式命名法和蛇形規(guī)則一樣滋戳,唯一區(qū)別是钻蔑,每個(gè)單詞之間通過'-'連接啥刻,例如'out-of'
匈牙利命名法
匈牙利命名法在Java早期的框架中開始出現(xiàn),由一個(gè)或者多個(gè)小寫字母開始咪笑,使用這些字母作為標(biāo)示符可帽,用來標(biāo)記當(dāng)前命名的變量的用途,例如:usName(表示是用戶的名稱),lAccountNum(表示是Long類型的長整數(shù))等
而在jdk中窗怒,針對每一種類型的命名有特定的規(guī)范蘑拯,針對每一種編碼規(guī)范來組合使用在不同場景的命名中,如下:
總結(jié)下來兜粘,jdk命名遵循了三點(diǎn):
1.命名有準(zhǔn)確的意義,絕不使用單詞縮寫或者單詞的部分弯蚜,例如GoodsItem孔轴,絕不會命名為GdItem
2.嚴(yán)格遵守命名規(guī)范,決不允許一個(gè)規(guī)則內(nèi)出現(xiàn)多個(gè)規(guī)范混用的情況碎捺,例如在一個(gè)命名中同時(shí)出現(xiàn)駝峰命名與蛇形命名等
3.盡量將可讀性的命名放在前面路鹰,開發(fā)者的習(xí)慣一般都是從左到右開始閱讀和編碼,所以將能體現(xiàn)出想要的信息的內(nèi)容優(yōu)先放在前面收厨,例如BeijingTime和TimeBeijing的區(qū)別
變量申明的時(shí)機(jī)
前面我們說過命名的規(guī)范晋柱,那么申明變量是否也需要規(guī)范呢?其實(shí)也需要诵叁,例如現(xiàn)在申明一個(gè)類型的變量的時(shí)候雁竞,往往有人喜歡一個(gè)類型的變量在一行內(nèi)申明完畢,例如:
int size, length;
甚至于出現(xiàn)了一行申明了七八個(gè)屬性的情況拧额,或者是在一行內(nèi)申明了好幾個(gè)類型的變量碑诉,例如:
int size,entity[];//一行申明多個(gè)不同類型變量
看起來代碼似乎節(jié)省了,但是對于開發(fā)和維護(hù)來說侥锦,其實(shí)反而更容易忽略錯(cuò)誤进栽,更重要的是申明類型是數(shù)組的時(shí)候不要把基本類型和[]分開,因?yàn)閕nt[] 才是代表了一個(gè)類型的整體恭垦,分開申明容易被忽略快毛,或者埋下隱患的錯(cuò)誤,所以往往建議每一行僅申明一個(gè)變量番挺,如下:
int size;
int[] entity;
在開發(fā)中往往還存在另外一個(gè)情況唠帝,就是方法內(nèi)申明局部變量的時(shí)候,往往喜歡在方法開始的時(shí)候就創(chuàng)建或者申明該變量玄柏,但是使用的時(shí)機(jī)往往在n行代碼以后没隘,甚至于到后面這個(gè)申明的變量并沒有使用到,由于間隔太遠(yuǎn)禁荸,也沒有關(guān)注右蒲,后面就成了一個(gè)死變量阀湿,這種情況是很多見的,而反觀jdk的規(guī)范中瑰妄,可以看到都是在需要使用變量的時(shí)候創(chuàng)建陷嘴,或者在需要使用的前幾行代碼申明再去創(chuàng)建,例如:
public void test(String userName){
Account userAccount;
String groceryStoreName;
//中間一堆業(yè)務(wù)代碼和操作
/*****
****
***/
//通過用戶名獲取userAccount
userAccount = AccountManager.getUserAccount(userName);
if(userAccount == null){
//為null的操作间坐,拋異常
}
//再去獲取名稱
groceryStoreName = userAccount.getGroceryStoreName();
if(groceryStoreName == null){
//為null灾挨,拋異常
}
//后續(xù)一堆業(yè)務(wù)代碼
}
但是我們看下規(guī)范后的寫法:
public void test(String userName){
//中間一堆業(yè)務(wù)代碼和操作
/*****
****
***/
//通過用戶名獲取userAccount
Account userAccount = AccountManager.getUserAccount(userName);
if(userAccount == null){
//為null的操作,拋異常
}
//再去獲取名稱
String groceryStoreName = userAccount.getGroceryStoreName();
if(groceryStoreName == null){
//為null竹宋,拋異常
}
//后續(xù)一堆業(yè)務(wù)代碼
}
很明顯的可以看出來劳澄,代碼更清晰明了,也更有邏輯性蜈七。另外在申明類屬性變量的時(shí)候秒拔,我們建議將變量申明在一起,分塊存放飒硅,不建議在類中變量和方法混合在一起使用砂缩,例如:
另外在申明類變量的時(shí)候,切記不要忘記類變量如果是基礎(chǔ)類型三娩,會有默認(rèn)值庵芭,如非必要,在類屬性創(chuàng)建中建議使用包裝類型雀监,防止因默認(rèn)值帶來的數(shù)據(jù)不一致等問題双吆,而在方法內(nèi)創(chuàng)建局部變量的時(shí)候,由于基本類型變量沒有默認(rèn)值会前,需要手動申明值伊诵,反而建議使用基本類型,而不是使用包裝類回官,這樣同樣也可以盡量避免無意的拆箱曹宴、裝箱行為,在數(shù)十萬次百萬次的情況下歉提,對于程序也會造成一定的影響笛坦。
if與大括號
if語句是我們開發(fā)中最常見的邏輯分支語句之一,同樣的在java中if也會有一些簡潔寫法苔巨,例如邏輯業(yè)務(wù)僅有一行代碼的時(shí)候版扩,我們可以省去大括號,直接在if下一行編寫業(yè)務(wù)代碼侄泽,如下:
if(flag)
count ++;
//if以外的邏輯
user.setAge(10);
......
但是熟悉規(guī)范的都知道礁芦,無論是阿里規(guī)范還是jdk的規(guī)范,都不推薦使用簡化代碼,這是為什么呢柿扣?這讓我想起了2014年蘋果的ios系統(tǒng)爆出來的一個(gè)嚴(yán)重安全漏洞(“GoTo Fail 漏洞”)肖方,而這個(gè)漏洞就和大括號有關(guān)系,而對應(yīng)漏洞的代碼大概可以理解為這樣:
if ((error = doSomething()) != 0)
goto fail;
//無論如何都是走到這里未状,下面再也觸發(fā)不了了
goto fail;
if ((error= doMore()) != 0)
goto fail;
fail:
return error;
是不是看出來什么了俯画?沒錯(cuò),如果前面的條件生效司草,就會跳轉(zhuǎn)到fail的操作艰垂,返回error,但是如果不滿足也會跳轉(zhuǎn)到fail埋虹,那么也就是說后續(xù)的業(yè)務(wù)代碼無論如何也觸發(fā)不了了猜憎,其實(shí)了解這個(gè)問題的人其實(shí)大概可以猜出來,這里就是多寫了一個(gè)goto fail;導(dǎo)致編譯器認(rèn)為了別的業(yè)務(wù)代碼搔课,但是假設(shè)我們加了大括號胰柑,這個(gè)問題就會迎刃而解,例如:
if ((error = doSomething()) != 0)
{
goto fail;
//無論如何都是走到這里辣辫,下面再也觸發(fā)不了了
goto fail;
}
if ((error= doMore()) != 0)
goto fail;
fail:
return error;
其實(shí)這個(gè)時(shí)候就會發(fā)現(xiàn)即使是多寫了一行代碼,也不會影響整個(gè)業(yè)務(wù)的邏輯魁巩,減少了bug產(chǎn)生急灭。看到這里我們似乎明白了谷遂,為什么各大規(guī)范都建議不省略大括號的寫法了
包裝類與基本類型
做Java開發(fā)的都知道葬馋,Java中默認(rèn)有八種基本類型,但是同樣的也有八種對應(yīng)的包裝類型肾扰,很多時(shí)候企業(yè)開發(fā)和使用的時(shí)候?qū)τ诎b類型和基本類型的使用并不規(guī)范畴嘶,往往會導(dǎo)致一部分小的隱患的發(fā)生。前面我們有介紹建議在類屬性申明的時(shí)候使用包裝類型集晚,而在方法內(nèi)建議使用基本類型窗悯,這里我們可以再去思考兩個(gè)開發(fā)的時(shí)候常用的使用場景:
1.判斷兩個(gè)數(shù)值類型的值是否相等
2.創(chuàng)建數(shù)值類型
看過阿里手冊和JDK規(guī)范的應(yīng)該知道,里面都有一條規(guī)范偷拔,明確指出基本數(shù)值類型的包裝類型在比較的時(shí)候不允許使用==的方式蒋院,而是使用equals,這是為什么呢莲绰?我們來看看一個(gè)例子:
Integer a = 100, b = 100, c = 150, d = 150;
System.out.println(a == b);//true
System.out.println(c == d);//false
可以看到兩個(gè)Integer類型的變量欺旧,值一樣的情況下,==比較的結(jié)果居然是false蛤签?我們通過斷點(diǎn)的方式知道 Integer var = ? 形式聲明變量辞友,會通過 java.lang.Integer#valueOf(int) 來構(gòu)造 Inte
ger 對象,我們來看看valueOf方法的源碼:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
可以看到,會去判斷value的值是否在IntegerCache的范圍內(nèi)称龙,如果在留拾,就會使用IntegerCache中緩存的實(shí)例,不存在才會創(chuàng)建新的Integer實(shí)例茵瀑,這個(gè)緩存的值间驮,默認(rèn)是-128到127之間,并且是可以通過配置環(huán)境變量的方式動態(tài)改變的马昨,這點(diǎn)可以從IntegerCache源碼中看到:
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
// 省略其它代碼
}
// 省略其它代碼
}
從這我們也可以看出問題2的答案竞帽,為什么很多規(guī)范都推薦構(gòu)建實(shí)例的時(shí)候是Integer a = 5;的形式,而不是new Integer(5)鸿捧;的原因屹篓,可以減少實(shí)例的創(chuàng)建,復(fù)用緩存對象匙奴。接著我們再來看第一個(gè)問題堆巧,==比較和equals比較的區(qū)別在哪?我們知道==比較的是兩個(gè)實(shí)例對象的內(nèi)存地址泼菌,而equals則是比較的具體的實(shí)現(xiàn)谍肤,而基本類型的包裝類實(shí)現(xiàn)實(shí)例如果不在緩存范圍內(nèi),肯定不是同一個(gè)對象哗伯,邏輯上內(nèi)存地址肯定是不一樣的荒揣,所以==在超過緩存范圍后,比較的結(jié)果并不準(zhǔn)確焊刹,那么我們該如何比較呢系任?事實(shí)上,基本類型的包裝類中都有獲取具體value的方法虐块,例如Integer中就有intValue的方法俩滥,獲取具體的值,類型為基本類型贺奠,這樣我們再去==比較就可以了霜旧,那么equlas方法為什么可以比較呢?我們就拿Long類型的equals方法的源碼來看一下具體實(shí)現(xiàn):
public boolean equals(Object obj) {
if (obj instanceof Long) {
return value == ((Long)obj).longValue();
}
return false;
}
可以看到這類包裝類型的比較其實(shí)也就是我們上述說的獲取具體value值以后再去==比較的操作
空指針
空指針基本是每個(gè)Java開發(fā)人員最惡心的異常也是見過最多的異常之一儡率,可能出現(xiàn)在各種業(yè)務(wù)代碼和場景中颁糟,在阿里規(guī)范手冊中,有很多針對空指針的規(guī)范和處理喉悴,如下:
【強(qiáng)制】Object 的 equals 方法容易拋空指針異常棱貌,應(yīng)使用常量或確定有值的對象來調(diào)用 equals。
【推薦】防止 NPE箕肃,是程序員的基本修養(yǎng)婚脱,注意 NPE 產(chǎn)生的場景:
1. 返回類型為基本數(shù)據(jù)類型,return 包裝數(shù)據(jù)類型的對象時(shí),自動拆箱有可能產(chǎn)生
NPE障贸。
反例:public int f () { return Integer 對象}错森, 如果為 null,自動解箱拋 NPE篮洁。
2. 數(shù)據(jù)庫的查詢結(jié)果可能為 null涩维。
3. 集合里的元素即使 isNotEmpty,取出的數(shù)據(jù)元素也可能為 null袁波。
4. 遠(yuǎn)程調(diào)用返回對象時(shí)瓦阐,一律要求進(jìn)行空指針判斷,防止 NPE篷牌。
5. 對于 Session 中獲取的數(shù)據(jù)睡蟋,建議進(jìn)行 NPE 檢查,避免空指針枷颊。
6. 級聯(lián)調(diào)用 obj.getA ().getB ().getC (); 一連串調(diào)用戳杀,易產(chǎn)生 NPE。
可見空指針出現(xiàn)的場景可能會有很多夭苗,而在開發(fā)中一些必要的檢查信卡,減少空指針是每個(gè)程序員都應(yīng)該有的素質(zhì),但是有些不規(guī)范的操作或者疏忽可能會導(dǎo)致空指針的誕生题造,例如:
1.服務(wù)交互信息不規(guī)范的坑:一個(gè)經(jīng)典的接口服務(wù)交互的場景下傍菇,往往有時(shí)候會將服務(wù)中的異常進(jìn)行try-catch處理,返回的是一個(gè)固定的result封裝實(shí)例晌梨,這種情況下桥嗤,如果內(nèi)部屬性設(shè)置不規(guī)范很容易在調(diào)用方使用返回實(shí)例進(jìn)行操作的時(shí)候因?yàn)槭韬鰧?dǎo)致空指針異常须妻。
2.返回實(shí)例的坑:還有一些服務(wù)的代碼編寫過程中仔蝌,部分開發(fā)人員有自己的個(gè)性寫法,例如數(shù)據(jù)庫查詢某個(gè)數(shù)據(jù)的時(shí)候荒吏,如果查詢不出結(jié)果集敛惊,并不是返回null,而是創(chuàng)建一個(gè)空的實(shí)例绰更,進(jìn)行返回瞧挤,這一下可好,調(diào)用方無論怎么校驗(yàn)空指針都會在使用getxxx方法獲取到的屬性進(jìn)行操作的時(shí)候報(bào)空指針異常儡湾,除非調(diào)用方將內(nèi)部所有的get返回的結(jié)果都去進(jìn)行一次空指針判斷特恬,或者根據(jù)某幾個(gè)唯一屬性確認(rèn)實(shí)例是否為空等,但無論如何操作徐钠,都無法避免可能存在的大量的空指針
3.自動拆箱裝箱的坑:在企業(yè)開發(fā)的過程中癌刽,往往存在大量的實(shí)例轉(zhuǎn)換操作,這個(gè)時(shí)候我們往往是通過工具類進(jìn)行轉(zhuǎn)換,但是有時(shí)候我們的實(shí)例是存在于兩個(gè)工程內(nèi)的显拜,往往有時(shí)候因?yàn)槭莾蓚€(gè)人定義的衡奥,同樣名稱的類變量,但是類型一個(gè)是基礎(chǔ)類型远荠,一個(gè)是包裝類型矮固,這個(gè)時(shí)候往往我們下意識會覺得java會自動拆箱裝箱,所以沒關(guān)系的譬淳,肯定會轉(zhuǎn)換過去的档址,再或者基本類型有默認(rèn)值的,肯定不會出現(xiàn)空指針瘦赫,想法很美好辰晕,但是事實(shí)真的如此嗎?我們看一個(gè)例子:
@Data
public class GoodCreateDTO {
private String title;
private Long price;
private Long count;
}
@Data
public class GoodCreateParam implements Serializable {
private static final long serialVersionUID = -560222124628416274L;
private String title;
private long price;
private long count;
}
這個(gè)時(shí)候我們潛意識中會認(rèn)為外部接口的變量都是包裝類型或者引用類型确虱,所以我們在實(shí)現(xiàn)了類似如下的轉(zhuǎn)換代碼的時(shí)候就容易出現(xiàn)空指針操作:
public class GoodCreateConverter {
public static GoodCreateParam convertToParam(GoodCreateDTO goodCreateDTO) {
if (goodCreateDTO == null) {
return null;
}
GoodCreateParam goodCreateParam = new GoodCreateParam();
//賦值操作
goodCreateParam.setTitle(goodCreateDTO.getTitle());
goodCreateParam.setPrice(goodCreateDTO.getPrice());
goodCreateParam.setCount(goodCreateDTO.getCount());
return goodCreateParam;
}
}
但是如果在傳遞來的實(shí)例中含友,count不是必傳參數(shù),可能存在null的時(shí)候校辩,這個(gè)時(shí)候我們使用getCount操作窘问,由于獲取的類型是包裝類型,而我們需要賦值的是基本類型宜咒,這個(gè)時(shí)候就會觸發(fā)自動拆箱裝箱惠赫,null的拆箱就會報(bào)空指針異常!