2017 年阿里雲棲大會,阿里發佈了針對 Java 程序員的《阿里巴巴 Java 開發手冊(終極版)》,這篇文檔做爲阿里數千位 Java 程序員的經驗積累呈現給公衆,並隨之發佈了適用於 Eclipse 和 Intellim 的代碼檢查插件。爲了可以深刻了解 Java 程序員編碼規範,也爲了深刻理解爲何阿里這樣規定,是否規定有誤,本文以阿里發佈的這篇文檔做爲分析起源,擴大範圍至業界其餘公司的規範,例如谷歌、FaceBook、微軟、百度、華爲,並搜索網絡上技術大牛發表的技術文章,深刻理解每一條規範的設計背景和目標。html
因爲解讀文章僅有兩篇,因此按照阿里的篇幅權重分爲上篇僅針對 Java 語言自己的編碼規約,下篇包含日誌管理、異常處理、單元測試、MySQL 規範、工程規範等方面內容進行解讀。本文是上篇,主要針對編碼規約部分進行解讀,因爲篇幅限制,僅挑選一小部分進行解讀,若是須要全篇,請聯繫本文做者。java
阿里強制規定代碼中的命名均不能如下劃線或美圓符號開始,也不能如下劃線或美圓符號結束。程序員
例如如下爲錯誤,如清單 1 所示:數據庫
1_name/__name/$Object/name_/name$/Object$。
Oracle 官網建議不要使用$或者_開始變量命名,而且建議在命名中徹底不要使用"$"字符,原文是"The convention,however,is to always begin your variable names with a letter,not '$' or '_'"。對於這一條,騰訊的見解是同樣的,百度認爲雖然類名能夠支持使用"$"符號,但只在系統生成中使用(如匿名類、代理類),編碼不能使用。編程
這類問題在 StackOverFlow 上有不少人提出,主流意見爲人不須要過多關注,只須要關注原先的代碼是否存在"_",若是存在就繼續保留,若是不存在則儘可能避免使用。也有一位提出儘可能不適用"_"的緣由是低分辨率的顯示器,肉眼很難區分"_"(一個下劃線)和"__"(兩個下劃線)。數組
我我的以爲多是因爲受 C 語言的編碼規範所影響。由於在 C 語言裏面,系統頭文件裏將宏名、變量名、內部函數名用_開頭,由於當你#include 系統頭文件時,這些文件裏的名字都有了定義,若是與你用的名字衝突,就可能引發各類奇怪的現象。綜合各類信息,建議不要使用"_"、"$"、空格做爲命名開始,以避免不利於閱讀或者產生奇怪的問題。安全
阿里強制規定類名使用 UpperCamelCase 風格,必須聽從駝峯形式,但如下情形例外:DO/BO/DTO/VO/AO。網絡
正例:MarcoPolo/UserDO/XmlService/TcpUdpDeal/TarPromotion反例:macroPolo/UserDo/XMLService/TCPUDPD/TAPromotion
百度除了支持阿里的規範之外,規定雖然類型支持"$"符號,但只在系統生成中使用(如匿名類、代理類),編碼中不能使用。數據結構
對於類名,俄羅斯 Java 專家 Yegor Bugayenko 給出的建議是儘可能採用現實生活中實體的抽象,若是類的名字以"-er"結尾,這是不建議的命名方式。他指出針對這一條有一個例外,那就是工具類,例如 StringUtils、FileUtils、IOUtils。對於接口名稱,不要使用 IRecord、IfaceEmployee、RedcordInterface,而是使用現實世界的實體命名。如清單 3 所示。多線程
Class SimpleUser implements User{};Class DefaultRecord implements Record{};Class Suffixed implements Name{};Class Validated implements Content{};
阿里強制規定抽象類命名使用 Abstratc 或 Base 開頭。
Oracle 的抽象類和方法規範並無要求必須採用 Abstract 或者 Base 開頭命名,事實上官網上的示例沒有這種命名規範要求,如清單 4 所示。
public abstract class GraphicObject{//declare fields//declare nonabstract methods abstract void draw();}
我也查了一下 JDK,確實源碼裏不少類都是以這樣的方式命名的,例如抽象類 java.util.AbstractList。
Stackoverflow 上對於這個問題的解釋是,因爲這些類不會被使用,必定會由其餘的類繼承並實現內部細節,因此須要明白地告訴讀者這是一個抽象類,那以 Abstract 開頭比較合適。
Joshua Bloch的理解是支持以 Abstract 開頭。個人理解是不要以 Base 開頭命名,由於實際的基類也以 Base 開頭居多,這樣意義有多樣性,不夠直觀。
阿里強制規定不容許任何魔法值(未經定義的常量)直接出如今代碼中,反例如清單 5 所示。
String key = "Id#taobao_" + tradeId;cache.put(key,value);
魔法值確實讓你很疑惑,好比你看下面這個例子:
int priceTable[] = new int[16];//這樣定義錯誤;這個 16 究竟表明什麼?
正確的定義方式是這樣的:
static final int PRICE_TABLE_MAX = 16; //這樣定義正確,經過使用完整英語單詞的常量名明肯定義
int price Table[] = new int[PRICE_TABLE_MAX];
魔法值會讓代碼的可讀性大大下降,並且若是一樣的數值屢次出現時,容易出現不清楚這些數值是否表明一樣的含義。另外一方面,若是原本應該使用相同的數值,一旦用錯,也難以發現。所以能夠採用如下兩點,極力避免使用魔法數值。
1. 不適用魔法數值,使用帶名字的 Static final 或者 enum 值;
2. 原則上 0 不用於魔法值,這是由於 0 常常被用做數組的最小下標或者變量初始化的缺省值。
阿里推薦若是變量值僅在一個範圍內變化,且帶有名稱以外的延伸屬性,定義爲枚舉類。下面這個正例中的數字就是延伸信息,表示星期幾。正例如清單 6 所示。
public Enum {MONDAY(1),TUESDAY(2),WEDNESDAY(3),THURSDAY(4),FRIDAY(5),SATURDAY(6),SUNDAY(7);}
對於固定而且編譯時對象,如 Status、Type 等,應該採用 enum 而非自定義常量實現,enum 的好處是類型更清楚,不會再編譯時混淆。這是一個建議性的試用推薦,枚舉可讓開發者在 IDE 下使用更方便,也更安全。另外就是枚舉類型是一種具備特殊約束的類類型,這些約束的存在使得枚舉類自己更加簡潔、安全、便捷。
阿里強制規定若是是大括號爲空,則簡潔地寫成{}便可,不須要換行;若是是非空代碼塊則:
1. 左大括號前不換行
2. 左大括號後換行
3. 右大括號前換行
4. 右大括號後還有 else 等代碼則不換行表示終止的右大括號後必須換行
阿里的這條規定應該是參照了 SUN 公司 1997 年發佈的代碼規範(SUN 公司是 JAVA 的創始者),Google 也有相似的規定,你們都是遵循 K&R 風格(Kernighan 和 Ritchie),Kernighan 和 Ritchie 在《The C Programming Language》一書中推薦這種風格,JAVA 語言的大括號風格就是受到了 C 語言的編碼風格影響。
注意,SUN 公司認爲方法名和大括號之間不該該有空格。
阿里強制規定單行字符數限制不超過 120 個,超出須要換行,換行時遵循以下原則:
1. 第二行相對第一行縮進 4 個空格,從第三行開始,再也不繼續縮進,參考示例。
2. 運算符與下文一塊兒換行。
3. 方法調用的點符號與下文一塊兒換行。
4. 方法調用時,多個參數,須要換行時,在逗號後進行。
5. 在括號前不要換行,見反例。
如清單 7 所示。
StringBuffer sb = new StringBuffer();//超過 120 個字符的狀況下,換行縮進 4 個空格,點號和方法名稱一塊兒換行sb.append("zi").append("xin")….append("huang")….append("huang")….append("huang")…反例:StringBuffer sb = new StringBuffer();//超過 120 個字符的狀況下,不要在括號前換行sb.append("zi").append("xin").append("huang");//參數不少的方法調用可能超過 120 個字符,不要在逗號前換行method(args1,args2,args3,….,argsX);
SUN 公司 1997 年的規範中指出單行不要超過 80 個字符,對於文檔裏面的代碼行,規定不要超過 70 個字符單行。當表達式不能在一行內顯示的時候,genuine 如下原則進行切分:
1. 在逗號後換行;
2. 在操做符號前換行;
3. 傾向於高級別的分割;
4. 儘可能以描述完整做爲換行標準;
5. 若是如下標準形成代碼閱讀困難,直接採用 8 個空格方式對第二行代碼留出空白。
示例代碼如清單 8 所示。
function(longExpression1, longExpression2, longExpression3,longExpression4, longExpression5);var = function(longExpression1,function2(longExpression2,longExpression3));longName1 = longName2 * (longName3 + longName4 – longName5)+ 4 * longName6;//作法正確longName1 = longName2 * (longName3 + longName4– longName5) + 4 * longName6;//作法錯誤if ((condition1 && condition2) || (condition3 && condition4) || !(condition5 && condition6) {doSomethingAboutIt();}//這種作法錯誤if ((condition1 && condition2)|| (condition3 && condition4) || !(condition5 && condition6) { doSomethingAboutIt(); }//這種作法正確if ((condition1 && condition2) || (condition3 && condition4)|| !(condition5 && condition6) { doSomethingAboutIt();}//這種作法正確
阿里強制規定代碼中避免經過一個類的對象引用訪問此類的靜態變量或靜態方法,暫時無謂增長編譯器解析成本,直接用類名來訪問便可。
谷歌公司在代碼規範中指出必須直接使用類名對靜態成員進行引用,並同時舉例說明,如清單 9 所示。
Foo aFoo = …;Foo.aStaticMethod();//goodaFoo.aStaticMethod();//badsomethingThatYieldsAFoo().aStaticMethod();//very bad
SUN 公司 1997 年發佈的代碼規範也作了相似的要求。
爲何須要這樣作呢?由於被 static 修飾過的變量或者方法都是隨着類的初始化產生的,在堆內存中有一塊專門的區域用來存放,後續直接用類名訪問便可,避免編譯成本的增長和實例對象存放空間的浪費。
StackOverflow 上也有人提出了相同的疑問,網友較爲精闢的回覆是"這是因爲生命週期決定的,靜態方法或者靜態變量不是以實例爲基準的,而是以類爲基準,因此直接用類訪問,不然違背了設計初衷"。那爲何還保留了實例的訪問方式呢?多是由於容許應用方無污染修改吧。
阿里強制規定相同參數類型、相同業務類型,纔可使用 Java 的可變參數,避免使用 Object,而且要求可變參數必須放置在參數列表的最後(提倡同窗們儘可能不用可變參數編程)。
咱們先來了解可變參數的使用方式:
1. 在方法中定義可變參數後,咱們能夠像操做數組同樣操做該參數。
2. 若是該方法除了可變參數還有其餘的參數,可變參數必須放到最後。
3. 擁有可變參數的方法能夠被重載,在被調用時,若是能匹配到參數定長的方法則優先調用參數定長的方法。
4. 可變參數能夠兼容數組參數,但數組參數暫時沒法兼容可變參數。
至於爲何可變參數須要被放在最後一個,這是由於參數個數不定,因此當其後還有相同類型參數時,編譯器沒法區分傳入的參數屬於前一個可變參數仍是後邊的參數,因此只能讓可變參數位於最後一項。
可變參數編程有一些好處,例如反射、過程建設、格式化等。對於阿里同窗提出的儘可能不使用可變參數編程,我猜想的緣由是不太可控,好比 Java8 推出 Lambda 表達式以後,可變參數編程遇到了實際的實現困難。
咱們來看一個例子。假設咱們想要實現如下功能,如清單 10 所示。
test((arg0,arg1) -> me.call(arg0,arg1));>test((arg0,arg1,arg2)->me.call(arg0,arg1,arg2));…
對應的實現定義接口的繼承關係,而且使用默認方法避免失敗,如清單 11 所示。
interface VarArgsRunnable{ default void run(Object…arguments){ throw new UnsupportedOperationException("not possible"); } default int getNumberOfArguments(){ throw new UnsupportedOperationException("unknown"); }}@FunctionalInterfaceInterface VarArgsRunnable4 extends VarArgsRnnable { @Override default void run(Object…arguments){ assert(arguments.length == 4); run(arguments[0], arguments[1], arguments[2], arguments[3]);} void run(Object arg0, Object arg1, Object arg2, Object arg3, Object arg4); @Override default int getNumberOfArguments(){ return 4;}}
這樣咱們就能夠定義 11 個接口,從 VarArgsRnnable0 到 VarArgsRnnable10,而且覆蓋方法,調用方式如清單 12 所示。
public void myMethod(VarArgsRnnable runnable,Object…arguments){ runnable.run(arguments);}
針對上述需求,咱們也能夠編寫代碼如清單 13 所示。
public class Java8VariableArgumentsDemo{ interface Invoker{ void invoke(Object…args);} public static void invokeInvoker(Invoker invoker,Object…args){ invoker.invoke(args); } public static void applyWithStillAndPrinting(Invoker invoker){ invoker.invoke("Still","Printing"); } Public static void main(String[] args){ Invoker printer = new Invoker(){ Public void invoke(Object…args){ for(Object arg:args){ System.out.println(arg); } } }; printer.invoke("I","am","printing"); invokeInvoker(printer, "Also","printing"); applyWithStillAndPrinting(printer); applyWithStillAndPrinting((Object…args)->System.out.println("Not done")); applyWithStillAndPrinting(printer::invoke); }}
運行後輸出如清單 14 所示。
IamprintingAlsoprintingStillPrintingNot doneStillPrinting
阿里強制要求獲取單例對象須要保證線程安全,其中的方法也要保證線程安全,並進一步說明資源驅動類、工具類、單例工廠類都須要注意。
對於這一條規範是通識化規定,我這裏進一步講講如何作好針對單例對象的線程安全,主要有如下幾種方式:
1. 方法中申明 synchronized 關鍵字
出現非線程安全問題,是因爲多個線程能夠同時進入 getInstance()方法,那麼只須要對該方法進行 synchronized 鎖同步便可,如清單 15 所示。
public class MySingleton{ private static MySingleton instance = null; private MySingleton(){} public synchronized static MySingleton getInstance(){ try{ if(instance != null){//懶漢式 }else{ //建立實例以前可能會有一些準備性的耗時工做 Thread.sleep(500); Instance = new MySingleton(); } }catch(InterruptedException e){ e.printStackTrace(); } return instance; }}
執行結果如清單 16 所示。
174342932174342932174342932174342932174342932174342932
從執行結果上來看,多線程訪問的問題已經解決了,返回的是一個實例。可是這種實現方式的運行效率很低。咱們接下來採用同步方法塊實現。
2. 同步方法塊實現
public class MySingleton { private static MySingleton instance = null; private MySingleton(){} //public synchronized static MySingleton getInstance() { public static MySingleton getInstance() { try { synchronized (MySingleton.class) { if(instance != null){//懶漢式 }else{ //建立實例以前可能會有一些準備性的耗時工做 Thread.sleep(300); instance = new MySingleton(); } } } catch (InterruptedException e) { e.printStackTrace(); } return instance; } }
這裏的實現可以保證多線程併發下的線程安全性,可是這樣的實現將所有的代碼都被鎖上了,一樣的效率很低下。
3. 針對某些重要的代碼來進行單獨的同步
針對某些重要的代碼進行單獨的同步,而不是所有進行同步,能夠極大的提升執行效率,代碼如清單 18 所示。
public class MySingleton { private static MySingleton instance = null; private MySingleton(){} public static MySingleton getInstance() { try { if(instance != null){//懶漢式 }else{ //建立實例以前可能會有一些準備性的耗時工做 Thread.sleep(300); synchronized (MySingleton.class) { instance = new MySingleton(); } } }catch (InterruptedException e) { e.printStackTrace(); } return instance; } }
從運行結果來看,這樣的方法進行代碼塊同步,代碼的運行效率是可以獲得提高,可是卻沒能保住線程的安全性。看來還得進一步考慮如何解決此問題。
4.雙檢查鎖機制(Double Check Locking)
爲了達到線程安全,又能提升代碼執行效率,咱們這裏能夠採用 DCL 的雙檢查鎖機制來完成,代碼實現如清單 19 所示。
public class MySingleton { //使用 volatile 關鍵字保其可見性 volatile private static MySingleton instance = null; private MySingleton(){} public static MySingleton getInstance() { try { if(instance != null){//懶漢式 }else{ //建立實例以前可能會有一些準備性的耗時工做 Thread.sleep(300); synchronized (MySingleton.class) { if(instance == null){//二次檢查 instance = new MySingleton(); } } } } catch (InterruptedException e) { e.printStackTrace(); } return instance; } }
這裏在聲明變量時使用了 volatile 關鍵字來保證其線程間的可見性;在同步代碼塊中使用二次檢查,以保證其不被重複實例化。集合其兩者,這種實現方式既保證了其高效性,也保證了其線程安全性。
5. 靜態內置類方式
DCL 解決了多線程併發下的線程安全問題,其實使用其餘方式也能夠達到一樣的效果,代碼實現如清單 20 所示。
public class MySingleton { //內部類 private static class MySingletonHandler{ private static MySingleton instance = new MySingleton(); } private MySingleton(){} public static MySingleton getInstance() { return MySingletonHandler.instance; } }
6. 序列化與反序列化方式
靜態內部類雖然保證了單例在多線程併發下的線程安全性,可是在遇到序列化對象時,默認的方式運行獲得的結果就是多例的。
import java.io.Serializable; public class MySingleton implements Serializable { private static final long serialVersionUID = 1L; //內部類 private static class MySingletonHandler{ private static MySingleton instance = new MySingleton(); } private MySingleton(){} public static MySingleton getInstance() { return MySingletonHandler.instance; } }
7. 使用枚舉數據類型方式
枚舉 enum 和靜態代碼塊的特性類似,在使用枚舉時,構造方法會被自動調用,利用這一特性也能夠實現單例。
public enum EnumFactory{ singletonFactory; private MySingleton instance; private EnumFactory(){//枚舉類的構造方法在類加載是被實例化 instance = new MySingleton(); } public MySingleton getInstance(){ return instance; } } class MySingleton{//須要獲實現單例的類,好比數據庫鏈接 Connection public MySingleton(){} }
這樣寫枚舉類被徹底暴露了,聽說違反了"職責單一原則",咱們能夠按照下面的代碼改造。
public class ClassFactory{ private enum MyEnumSingleton{ singletonFactory; private MySingleton instance; private MyEnumSingleton(){//枚舉類的構造方法在類加載是被實例化 instance = new MySingleton(); } public MySingleton getInstance(){ return instance; } } public static MySingleton getInstance(){ return MyEnumSingleton.singletonFactory.getInstance(); } } class MySingleton{//須要獲實現單例的類,好比數據庫鏈接 Connection public MySingleton(){} }
阿里強制規定在一個 switch 塊內,每一個 case 要麼經過 break/return 等來終止,要麼註釋說明程序將繼續執行到哪個 case 爲止;在一個 switch 塊內,都必須包含一個 default 語句而且放在最後,即便它什麼代碼也沒有。
首先理解前半部分,"每一個 case 要麼經過 break/return 等來終止,要麼註釋說明程序將繼續執行到哪個 case 爲止"。由於這樣能夠比較清楚地表達程序員的意圖,有效防止無端遺漏的 break 語句。咱們來看一個示例,如清單 24 所示。
switch(condition){case ABC:statements;/*程序繼續執行直到 DEF 分支*/case DEF:statements;break;case XYZ:statements;break;default:statements;break;}
上述示例中,每當一個 case 順着往下執行時(由於沒有 break 語句),一般應在 break 語句的位置添加註釋。上面的示例代碼中就包含了註釋"/*程序繼續執行直到 DEF 分支*/"(這一條也是 SUN 公司 1997 年代碼規範的要求)。
語法上來講,default 語句中的 break 是多餘的,可是若是後續添加額外的 case,能夠避免找不到匹配 case 項的錯誤。
阿里強制規定使用集合轉數組的方法,必須使用集合的 toArray(T[] arrays),傳入的是類型徹底同樣的數組,大小就是 list.size()。使用 toArray 帶參方法,入參分配的數組空間不夠大時,toArray 方法內部將從新分配內存空間,並返回新數組地址;若是數組元素大於實際所需,下標爲[list.size()]的數組元素將被置爲 null,其它數組元素保持原值,所以最好將方法入參數組大小定義與集合元素個數一致。正例如清單 25 所示。
List<String> list = new ArrayList<String>(2);list.add("guan");list.add("bao");String[] array = new String[list.size()];array = list.toArray(array);
反例:直接使用 toArray 暫時無參方法存在問題,此方法返回值只能是 Object[]類,若強轉其餘類型數組將出現 ClassCastException 錯誤。
ArrayList 類的 toArray()源碼如清單所示,toArray()方法暫時無需傳入參數,能夠直接將集合轉成 Object 數組進行返回,並且也只能返回 Object 類型。
Public Object[] toArray(){ Object aobj[] = new Object[size]; System.arraycopy(((Object)(elementData)),0,((Object)(aobj)),0,size); return aobj;}public <T> T[] toArray(T[] a){ if(a.length < size) // Make a new array of a's runtime type, but my contents: return (T[]) Arrays.copyOf(elementData,size, a.getClass()); System.arraycopy(elementData,0,a,0,size); if(a.length> a[size] = null; return a;}
由源碼可知,不帶參數的 toArray()構造一個 Object 數組,而後進行數據拷貝,此時進行轉型就會產生 ClassCastException。緣由是不能將 Object[]轉化爲 Strng[]。Java 中的強制類型轉換隻是針對單個對象,想要將一種類型數組轉化爲另外一種類型數組是不可行的。
針對傳入參數的數組大小,測試大於 list、等於 list 和小於 list 三種狀況,測試代碼如清單 27 所示。
public static void main(String[] args){ List<String> list = new ArrayList<String>(); for(int i=0;i<20;i++){ list.add("test"); } long start = System.currentTimeMills(); for(int i=0;i<10000000;i++){ String[] array = new String[list.size()]; Array = list.toArray(array); } System.out.println("數組長度等於 list 耗時:"+(System.currentTimeMills()-start)+"ms"); start = System.currentTimeMills(); for(int i=0;i<10000000;i++){ String[] array = new String[list.size()*2]; Array = list.toArray(array); } System.out.println("數組長度等於 list 耗時:"+(System.currentTimeMills()-start)+"ms"); start = System.currentTimeMills(); for(int i=0;i<10000000;i++){ String[] array = new String[0]; Array = list.toArray(array); } System.out.println("數組長度等於 list 耗時:"+(System.currentTimeMills()-start)+"ms");}
清單運行後輸出結果如清單 28 所示。
數組長度等於 list 耗時:431ms數組長度等於 list 耗時:509ms數組長度等於 list 耗時:1943ms
經過測試可知不管數據大小如何,數組轉換均可以成功,只是耗時不一樣,數組長度等於 list 時性能最優,所以強制方法入參數組大小與集合元素個數一致。
阿里強制要求方法內部單行註釋,在被註釋語句上方另起一行,使用//註釋。方法內部多行註釋使用/**/註釋,注意與代碼對照。
百度規定方法註釋採用標準的 Javadoc 註釋規範,註釋中必須提供方法說明、參數說明及返回值和異常說明。騰訊規定採用 JavaDoc 文檔註釋,在方法定義以前應該對其進行註釋,包括方法的描述、輸入、輸出以及返回值說明、拋出異常說明、參考連接等。
阿里推薦任何數據結構的構造或初始化,都應指定大小,避免數據結構暫時無限增加吃光內存。
首先明確一點,阿里這裏指的大小具體是指數據結構的最大長度。大部分 Java 集合類在構造時指定的大小都是初始尺寸(initial Capacity),而不是尺寸上限(Capacity),只有幾種隊列除外,例如 ArrayBlockingQueue、LinkedBlockingQueue,它們在構造時能夠指定隊列的最大長度。阿里推薦的目的是爲了合理規劃內存,避免出現 OOM(Out of Memory)異常。
本文主要介紹了阿里巴巴針對命名風格、常量定義、代碼格式、OOP 規約、併發處理、控制語句、集合處理、註釋規約、其餘這些關於編碼規約的要求。本文僅覆蓋了阿里代碼規範的少數內容,更多內容請諮詢本文做者。
參考文檔《阿里巴巴 Java 開發手冊(又名阿里巴巴 Java 代碼規約)》。
參考 developerWorks 上的 Java 文章,瞭解更多 Java 知識。
參考書籍 《Effective Java Second Edition》Joshua Bloch。
原做者:周明耀
原文連接: 從命名風格等方面解讀阿里巴巴 Java 代碼規範
原出處:IBM Developer