從命名風(fēng)格等方面解讀阿里巴巴 Java 代碼規(guī)范

前言

2017 年阿里云棲大會(huì),阿里發(fā)布了針對(duì) Java 程序員的《阿里巴巴 Java 開發(fā)手冊(cè)(終極版)》,這篇文檔作為阿里數(shù)千位 Java 程序員的經(jīng)驗(yàn)積累呈現(xiàn)給公眾,并隨之發(fā)布了適用于 Eclipse 和 Intellim 的代碼檢查插件。為了能夠深入了解 Java 程序員編碼規(guī)范,也為了深入理解為什么阿里這樣規(guī)定,是否規(guī)定有誤,本文以阿里發(fā)布的這篇文檔作為分析起源,擴(kuò)大范圍至業(yè)界其他公司的規(guī)范,例如谷歌、FaceBook、微軟、百度、華為,并搜索網(wǎng)絡(luò)上技術(shù)大牛發(fā)表的技術(shù)文章,深入理解每一條規(guī)范的設(shè)計(jì)背景和目標(biāo)。

由于解讀文章僅有兩篇,所以按照阿里的篇幅權(quán)重分為上篇僅針對(duì) Java 語(yǔ)言本身的編碼規(guī)約,下篇包含日志管理、異常處理、單元測(cè)試、MySQL 規(guī)范、工程規(guī)范等方面內(nèi)容進(jìn)行解讀。本文是上篇,主要針對(duì)編碼規(guī)約部分進(jìn)行解讀,由于篇幅限制,僅挑選一小部分進(jìn)行解讀,如果需要全篇,請(qǐng)聯(lián)系本文作者。

編碼規(guī)約

命名風(fēng)格

下劃線或美元符號(hào)

阿里強(qiáng)制規(guī)定代碼中的命名均不能以下劃線或美元符號(hào)開始,也不能以下劃線或美元符號(hào)結(jié)束。

例如以下為錯(cuò)誤,如清單 1 所示:

清單 1 錯(cuò)誤示例

_name/__name/$Object/name_/name$/Object$。

我的理解

Oracle 官網(wǎng)建議不要使用$或者_(dá)開始變量命名,并且建議在命名中完全不要使用"$"字符,原文是"The convention,however,is to always begin your variable names with a letter,not '$' or '_'"。對(duì)于這一條,騰訊的看法是一樣的,百度認(rèn)為雖然類名可以支持使用"$"符號(hào),但只在系統(tǒng)生成中使用(如匿名類、代理類),編碼不能使用。

這類問(wèn)題在 StackOverFlow 上有很多人提出,主流意見為人不需要過(guò)多關(guān)注,只需要關(guān)注原先的代碼是否存在"_",如果存在就繼續(xù)保留,如果不存在則盡量避免使用。也有一位提出盡量不適用"_"的原因是低分辨率的顯示器,肉眼很難區(qū)分"_"(一個(gè)下劃線)和"__"(兩個(gè)下劃線)。

我個(gè)人覺(jué)得可能是由于受 C 語(yǔ)言的編碼規(guī)范所影響。因?yàn)樵?C 語(yǔ)言里面,系統(tǒng)頭文件里將宏名、變量名、內(nèi)部函數(shù)名用_開頭,因?yàn)楫?dāng)你#include 系統(tǒng)頭文件時(shí),這些文件里的名字都有了定義,如果與你用的名字沖突,就可能引起各種奇怪的現(xiàn)象。綜合各種信息,建議不要使用"_"、"$"、空格作為命名開始,以免不利于閱讀或者產(chǎn)生奇怪的問(wèn)題。

類命名

阿里強(qiáng)制規(guī)定類名使用 UpperCamelCase 風(fēng)格,必須遵從駝峰形式,但以下情形例外:DO/BO/DTO/VO/AO。

清單 2 類命名例子

正例:MarcoPolo/UserDO/XmlService/TcpUdpDeal/TarPromotion

反例:macroPolo/UserDo/XMLService/TCPUDPD/TAPromotion

我的理解

百度除了支持阿里的規(guī)范以外,規(guī)定雖然類型支持"$"符號(hào),但只在系統(tǒng)生成中使用(如匿名類、代理類),編碼中不能使用。

對(duì)于類名,俄羅斯 Java 專家 Yegor Bugayenko 給出的建議是盡量采用現(xiàn)實(shí)生活中實(shí)體的抽象,如果類的名字以"-er"結(jié)尾,這是不建議的命名方式。他指出針對(duì)這一條有一個(gè)例外,那就是工具類,例如 StringUtils、FileUtils、IOUtils。對(duì)于接口名稱,不要使用 IRecord、IfaceEmployee、RedcordInterface,而是使用現(xiàn)實(shí)世界的實(shí)體命名。如清單 3 所示。

清單 3 示例

Class SimpleUser implements User{};

Class DefaultRecord implements Record{};

Class Suffixed implements Name{};

Class Validated implements Content{};

抽象類的命名

阿里強(qiáng)制規(guī)定抽象類命名使用 Abstratc 或 Base 開頭。

我的理解

Oracle 的抽象類和方法規(guī)范并沒(méi)有要求必須采用 Abstract 或者 Base 開頭命名,事實(shí)上官網(wǎng)上的示例沒(méi)有這種命名規(guī)范要求,如清單 4 所示。

清單 4 示例

public abstract class GraphicObject{

//declare fields

//declare nonabstract methods

abstract void draw();

}

我也查了一下 JDK,確實(shí)源碼里很多類都是以這樣的方式命名的,例如抽象類 java.util.AbstractList。

Stackoverflow 上對(duì)于這個(gè)問(wèn)題的解釋是,由于這些類不會(huì)被使用,一定會(huì)由其他的類繼承并實(shí)現(xiàn)內(nèi)部細(xì)節(jié),所以需要明白地告訴讀者這是一個(gè)抽象類,那以 Abstract 開頭比較合適。

Joshua Bloch的理解是支持以 Abstract 開頭。我的理解是不要以 Base 開頭命名,因?yàn)閷?shí)際的基類也以 Base 開頭居多,這樣意義有多樣性,不夠直觀。

常量定義

避免魔法值的使用

阿里強(qiáng)制規(guī)定不允許任何魔法值(未經(jīng)定義的常量)直接出現(xiàn)在代碼中,反例如清單 5 所示。

清單 5 反例

String key = "Id#taobao_" + tradeId;

cache.put(key,value);

我的理解

魔法值確實(shí)讓你很疑惑,比如你看下面這個(gè)例子:

int priceTable[] = new int[16];//這樣定義錯(cuò)誤;這個(gè) 16 究竟代表什么?

正確的定義方式是這樣的:

static final int PRICE_TABLE_MAX = 16; //這樣定義正確,通過(guò)使用完整英語(yǔ)單詞的常量名明確定義

int price Table[] = new int[PRICE_TABLE_MAX];

魔法值會(huì)讓代碼的可讀性大大降低,而且如果同樣的數(shù)值多次出現(xiàn)時(shí),容易出現(xiàn)不清楚這些數(shù)值是否代表同樣的含義。另一方面,如果本來(lái)應(yīng)該使用相同的數(shù)值,一旦用錯(cuò),也難以發(fā)現(xiàn)。因此可以采用以下兩點(diǎn),極力避免使用魔法數(shù)值。

1. 不適用魔法數(shù)值,使用帶名字的 Static final 或者 enum 值;

2. 原則上 0 不用于魔法值,這是因?yàn)?0 經(jīng)常被用作數(shù)組的最小下標(biāo)或者變量初始化的缺省值。

變量值范圍

阿里推薦如果變量值僅在一個(gè)范圍內(nèi)變化,且?guī)в忻Q之外的延伸屬性,定義為枚舉類。下面這個(gè)正例中的數(shù)字就是延伸信息,表示星期幾。正例如清單 6 所示。

清單 6 正例

public Enum {MONDAY(1),TUESDAY(2),WEDNESDAY(3),THURSDAY(4),FRIDAY(5),SATURDAY(6),SUNDAY(7);}

我的理解

對(duì)于固定并且編譯時(shí)對(duì)象,如 Status、Type 等,應(yīng)該采用 enum 而非自定義常量實(shí)現(xiàn),enum 的好處是類型更清楚,不會(huì)再編譯時(shí)混淆。這是一個(gè)建議性的試用推薦,枚舉可以讓開發(fā)者在 IDE 下使用更方便,也更安全。另外就是枚舉類型是一種具有特殊約束的類類型,這些約束的存在使得枚舉類本身更加簡(jiǎn)潔、安全、便捷。

代碼格式

大括號(hào)的使用約定

阿里強(qiáng)制規(guī)定如果是大括號(hào)為空,則簡(jiǎn)潔地寫成{}即可,不需要換行;如果是非空代碼塊則:

1. 左大括號(hào)前不換行

2. 左大括號(hào)后換行

3. 右大括號(hào)前換行

4. 右大括號(hào)后還有 else 等代碼則不換行表示終止的右大括號(hào)后必須換行

我的理解

阿里的這條規(guī)定應(yīng)該是參照了 SUN 公司 1997 年發(fā)布的代碼規(guī)范(SUN 公司是 JAVA 的創(chuàng)始者),Google 也有類似的規(guī)定,大家都是遵循 K&R 風(fēng)格(Kernighan 和 Ritchie),Kernighan 和 Ritchie 在《The C Programming Language》一書中推薦這種風(fēng)格,JAVA 語(yǔ)言的大括號(hào)風(fēng)格就是受到了 C 語(yǔ)言的編碼風(fēng)格影響。

注意,SUN 公司認(rèn)為方法名和大括號(hào)之間不應(yīng)該有空格。

單行字符數(shù)限制

阿里強(qiáng)制規(guī)定單行字符數(shù)限制不超過(guò) 120 個(gè),超出需要換行,換行時(shí)遵循如下原則:

1. 第二行相對(duì)第一行縮進(jìn) 4 個(gè)空格,從第三行開始,不再繼續(xù)縮進(jìn),參考示例。

2. 運(yùn)算符與下文一起換行。

3. 方法調(diào)用的點(diǎn)符號(hào)與下文一起換行。

4. 方法調(diào)用時(shí),多個(gè)參數(shù),需要換行時(shí),在逗號(hào)后進(jìn)行。

5. 在括號(hào)前不要換行,見反例。

如清單 7 所示。

清單 7 示例

StringBuffer sb = new StringBuffer();

//超過(guò) 120 個(gè)字符的情況下,換行縮進(jìn) 4 個(gè)空格,點(diǎn)號(hào)和方法名稱一起換行

sb.append("zi").append("xin")…

.append("huang")…

.append("huang")…

.append("huang")…

反例:

StringBuffer sb = new StringBuffer();

//超過(guò) 120 個(gè)字符的情況下,不要在括號(hào)前換行

sb.append("zi").append("xin").append

("huang");

//參數(shù)很多的方法調(diào)用可能超過(guò) 120 個(gè)字符,不要在逗號(hào)前換行

method(args1,args2,args3,….,argsX);

我的理解

SUN 公司 1997 年的規(guī)范中指出單行不要超過(guò) 80 個(gè)字符,對(duì)于文檔里面的代碼行,規(guī)定不要超過(guò) 70 個(gè)字符單行。當(dāng)表達(dá)式不能在一行內(nèi)顯示的時(shí)候,genuine 以下原則進(jìn)行切分:

1. 在逗號(hào)后換行;

2. 在操作符號(hào)前換行;

3. 傾向于高級(jí)別的分割;

4. 盡量以描述完整作為換行標(biāo)準(zhǔn);

5. 如果以下標(biāo)準(zhǔn)造成代碼閱讀困難,直接采用 8 個(gè)空格方式對(duì)第二行代碼留出空白。

示例代碼如清單 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;//做法錯(cuò)誤

if ((condition1 && condition2)

|| (condition3 && condition4)

|| !(condition5 && condition6) {

doSomethingAboutIt();

}//這種做法錯(cuò)誤

if ((condition1 && condition2)

|| (condition3 && condition4)

|| !(condition5 && condition6) {

doSomethingAboutIt();

}//這種做法正確

if ((condition1 && condition2) || (condition3 && condition4)

|| !(condition5 && condition6) {

doSomethingAboutIt();

}//這種做法正確

OOP 規(guī)約

靜態(tài)變量及方法調(diào)用

阿里強(qiáng)制規(guī)定代碼中避免通過(guò)一個(gè)類的對(duì)象引用訪問(wèn)此類的靜態(tài)變量或靜態(tài)方法,暫時(shí)無(wú)謂增加編譯器解析成本,直接用類名來(lái)訪問(wèn)即可。

我的理解

谷歌公司在代碼規(guī)范中指出必須直接使用類名對(duì)靜態(tài)成員進(jìn)行引用,并同時(shí)舉例說(shuō)明,如清單 9 所示。

清單 9 示例

Foo aFoo = …;

Foo.aStaticMethod();//good

aFoo.aStaticMethod();//bad

somethingThatYieldsAFoo().aStaticMethod();//very bad

SUN 公司 1997 年發(fā)布的代碼規(guī)范也做了類似的要求。

為什么需要這樣做呢?因?yàn)楸?static 修飾過(guò)的變量或者方法都是隨著類的初始化產(chǎn)生的,在堆內(nèi)存中有一塊專門的區(qū)域用來(lái)存放,后續(xù)直接用類名訪問(wèn)即可,避免編譯成本的增加和實(shí)例對(duì)象存放空間的浪費(fèi)。

StackOverflow 上也有人提出了相同的疑問(wèn),網(wǎng)友較為精辟的回復(fù)是"這是由于生命周期決定的,靜態(tài)方法或者靜態(tài)變量不是以實(shí)例為基準(zhǔn)的,而是以類為基準(zhǔn),所以直接用類訪問(wèn),否則違背了設(shè)計(jì)初衷"。那為什么還保留了實(shí)例的訪問(wèn)方式呢?可能是因?yàn)樵试S應(yīng)用方無(wú)污染修改吧。

可變參數(shù)編程

阿里強(qiáng)制規(guī)定相同參數(shù)類型、相同業(yè)務(wù)類型,才可以使用 Java 的可變參數(shù),避免使用 Object,并且要求可變參數(shù)必須放置在參數(shù)列表的最后(提倡同學(xué)們盡量不用可變參數(shù)編程)。

我的理解

我們先來(lái)了解可變參數(shù)的使用方式:

1. 在方法中定義可變參數(shù)后,我們可以像操作數(shù)組一樣操作該參數(shù)。

2. 如果該方法除了可變參數(shù)還有其他的參數(shù),可變參數(shù)必須放到最后。

3. 擁有可變參數(shù)的方法可以被重載,在被調(diào)用時(shí),如果能匹配到參數(shù)定長(zhǎng)的方法則優(yōu)先調(diào)用參數(shù)定長(zhǎng)的方法。

4. 可變參數(shù)可以兼容數(shù)組參數(shù),但數(shù)組參數(shù)暫時(shí)無(wú)法兼容可變參數(shù)。

至于為什么可變參數(shù)需要被放在最后一個(gè),這是因?yàn)閰?shù)個(gè)數(shù)不定,所以當(dāng)其后還有相同類型參數(shù)時(shí),編譯器無(wú)法區(qū)分傳入的參數(shù)屬于前一個(gè)可變參數(shù)還是后邊的參數(shù),所以只能讓可變參數(shù)位于最后一項(xiàng)。

可變參數(shù)編程有一些好處,例如反射、過(guò)程建設(shè)、格式化等。對(duì)于阿里同學(xué)提出的盡量不使用可變參數(shù)編程,我猜測(cè)的原因是不太可控,比如 Java8 推出 Lambda 表達(dá)式之后,可變參數(shù)編程遇到了實(shí)際的實(shí)現(xiàn)困難。

我們來(lái)看一個(gè)例子。假設(shè)我們想要實(shí)現(xiàn)以下功能,如清單 10 所示。

清單 10 實(shí)現(xiàn)功能

test((arg0,arg1) -> me.call(arg0,arg1));

>test((arg0,arg1,arg2)->me.call(arg0,arg1,arg2));

對(duì)應(yīng)的實(shí)現(xiàn)定義接口的繼承關(guān)系,并且使用默認(rèn)方法避免失敗,如清單 11 所示。

清單 11 實(shí)現(xiàn)方式代碼段 1

interface VarArgsRunnable{

default void run(Object…arguments){

throw new UnsupportedOperationException("not possible");

}

default int getNumberOfArguments(){

throw new UnsupportedOperationException("unknown");

}

}

@FunctionalInterface

Interface 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 個(gè)接口,從 VarArgsRnnable0 到 VarArgsRnnable10,并且覆蓋方法,調(diào)用方式如清單 12 所示。

清單 12 實(shí)現(xiàn)方式代碼段 2

public void myMethod(VarArgsRnnable runnable,Object…arguments){

runnable.run(arguments);

}

針對(duì)上述需求,我們也可以編寫代碼如清單 13 所示。

清單 13 實(shí)現(xiàn)方式 2 代碼段

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);

}

}

運(yùn)行后輸出如清單 14 所示。

清單 14 實(shí)現(xiàn)方式 2 代碼段運(yùn)行結(jié)果

I

am

printing

Also

printing

Still

Printing

Not done

Still

Printing

并發(fā)處理

單例模式需要保證線程安全

阿里強(qiáng)制要求獲取單例對(duì)象需要保證線程安全,其中的方法也要保證線程安全,并進(jìn)一步說(shuō)明資源驅(qū)動(dòng)類、工具類、單例工廠類都需要注意。

我的理解

對(duì)于這一條規(guī)范是通識(shí)化規(guī)定,我這里進(jìn)一步講講如何做好針對(duì)單例對(duì)象的線程安全,主要有以下幾種方式:

1. 方法中申明 synchronized 關(guān)鍵字

出現(xiàn)非線程安全問(wèn)題,是由于多個(gè)線程可以同時(shí)進(jìn)入 getInstance()方法,那么只需要對(duì)該方法進(jìn)行 synchronized 鎖同步即可,如清單 15 所示。

清單 15 synchronized 關(guān)鍵字方式

public class MySingleton{

private static MySingleton instance = null;

private MySingleton(){}

public synchronized static MySingleton getInstance(){

try{

if(instance != null){//懶漢式

}else{

//創(chuàng)建實(shí)例之前可能會(huì)有一些準(zhǔn)備性的耗時(shí)工作

Thread.sleep(500);

Instance = new MySingleton();

}

}catch(InterruptedException e){

e.printStackTrace();

}

return instance;

}

}

執(zhí)行結(jié)果如清單 16 所示。

清單 16 synchronized 關(guān)鍵字方式運(yùn)行結(jié)果

174342932

174342932

174342932

174342932

174342932

174342932

從執(zhí)行結(jié)果上來(lái)看,多線程訪問(wèn)的問(wèn)題已經(jīng)解決了,返回的是一個(gè)實(shí)例。但是這種實(shí)現(xiàn)方式的運(yùn)行效率很低。我們接下來(lái)采用同步方法塊實(shí)現(xiàn)。

2. 同步方法塊實(shí)現(xiàn)

清單 17 同步方法塊方式

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{

//創(chuàng)建實(shí)例之前可能會(huì)有一些準(zhǔn)備性的耗時(shí)工作

Thread.sleep(300);

instance = new MySingleton();

}

}

} catch (InterruptedException e) {

e.printStackTrace();

}

return instance;

}

}

這里的實(shí)現(xiàn)能夠保證多線程并發(fā)下的線程安全性,但是這樣的實(shí)現(xiàn)將全部的代碼都被鎖上了,同樣的效率很低下。

3. 針對(duì)某些重要的代碼來(lái)進(jìn)行單獨(dú)的同步

針對(duì)某些重要的代碼進(jìn)行單獨(dú)的同步,而不是全部進(jìn)行同步,可以極大的提高執(zhí)行效率,代碼如清單 18 所示。

清單 18 單獨(dú)同步方式

public class MySingleton {

private static MySingleton instance = null;

private MySingleton(){}

public static MySingleton getInstance() {

try {

if(instance != null){//懶漢式

}else{

//創(chuàng)建實(shí)例之前可能會(huì)有一些準(zhǔn)備性的耗時(shí)工作

Thread.sleep(300);

synchronized (MySingleton.class) {

instance = new MySingleton();

}

}

}catch (InterruptedException e) {

e.printStackTrace();

}

return instance;

}

}

從運(yùn)行結(jié)果來(lái)看,這樣的方法進(jìn)行代碼塊同步,代碼的運(yùn)行效率是能夠得到提升,但是卻沒(méi)能保住線程的安全性??磥?lái)還得進(jìn)一步考慮如何解決此問(wèn)題。

4.雙檢查鎖機(jī)制(Double Check Locking)

為了達(dá)到線程安全,又能提高代碼執(zhí)行效率,我們這里可以采用 DCL 的雙檢查鎖機(jī)制來(lái)完成,代碼實(shí)現(xiàn)如清單 19 所示。

清單 19 雙檢查鎖機(jī)制

public class MySingleton {

//使用 volatile 關(guān)鍵字保其可見性

volatile private static MySingleton instance = null;

private MySingleton(){}

public static MySingleton getInstance() {

try {

if(instance != null){//懶漢式

}else{

//創(chuàng)建實(shí)例之前可能會(huì)有一些準(zhǔn)備性的耗時(shí)工作

Thread.sleep(300);

synchronized (MySingleton.class) {

if(instance == null){//二次檢查

instance = new MySingleton();

}

}

}

} catch (InterruptedException e) {

e.printStackTrace();

}

return instance;

}

}

這里在聲明變量時(shí)使用了 volatile 關(guān)鍵字來(lái)保證其線程間的可見性;在同步代碼塊中使用二次檢查,以保證其不被重復(fù)實(shí)例化。集合其二者,這種實(shí)現(xiàn)方式既保證了其高效性,也保證了其線程安全性。

5. 靜態(tài)內(nèi)置類方式

DCL 解決了多線程并發(fā)下的線程安全問(wèn)題,其實(shí)使用其他方式也可以達(dá)到同樣的效果,代碼實(shí)現(xiàn)如清單 20 所示。

清單 20 靜態(tài)內(nèi)置類方式

public class MySingleton {

//內(nèi)部類

private static class MySingletonHandler{

private static MySingleton instance = new MySingleton();

}

private MySingleton(){}

public static MySingleton getInstance() {

return MySingletonHandler.instance;

}

}

6. 序列化與反序列化方式

靜態(tài)內(nèi)部類雖然保證了單例在多線程并發(fā)下的線程安全性,但是在遇到序列化對(duì)象時(shí),默認(rèn)的方式運(yùn)行得到的結(jié)果就是多例的。

清單 21 序列化與反序列化

import java.io.Serializable;

public class MySingleton implements Serializable {

private static final long serialVersionUID = 1L;

//內(nèi)部類

private static class MySingletonHandler{

private static MySingleton instance = new MySingleton();

}

private MySingleton(){}

public static MySingleton getInstance() {

return MySingletonHandler.instance;

}

}

7. 使用枚舉數(shù)據(jù)類型方式

枚舉 enum 和靜態(tài)代碼塊的特性相似,在使用枚舉時(shí),構(gòu)造方法會(huì)被自動(dòng)調(diào)用,利用這一特性也可以實(shí)現(xiàn)單例。

清單 22 枚舉數(shù)據(jù)方式 1

public enum EnumFactory{

singletonFactory;

private MySingleton instance;

private EnumFactory(){//枚舉類的構(gòu)造方法在類加載是被實(shí)例化

instance = new MySingleton();

}

public MySingleton getInstance(){

return instance;

}

}

class MySingleton{//需要獲實(shí)現(xiàn)單例的類,比如數(shù)據(jù)庫(kù)連接 Connection

public MySingleton(){}

}

這樣寫枚舉類被完全暴露了,據(jù)說(shuō)違反了"職責(zé)單一原則",我們可以按照下面的代碼改造。

清單 23 枚舉數(shù)據(jù)方式 2

public class ClassFactory{

private enum MyEnumSingleton{

singletonFactory;

private MySingleton instance;

private MyEnumSingleton(){//枚舉類的構(gòu)造方法在類加載是被實(shí)例化

instance = new MySingleton();

}

public MySingleton getInstance(){

return instance;

}

}

public static MySingleton getInstance(){

return MyEnumSingleton.singletonFactory.getInstance();

}

}

class MySingleton{//需要獲實(shí)現(xiàn)單例的類,比如數(shù)據(jù)庫(kù)連接 Connection

public MySingleton(){}

}

控制語(yǔ)句

Switch 語(yǔ)句的使用

阿里強(qiáng)制規(guī)定在一個(gè) switch 塊內(nèi),每個(gè) case 要么通過(guò) break/return 等來(lái)終止,要么注釋說(shuō)明程序?qū)⒗^續(xù)執(zhí)行到哪一個(gè) case 為止;在一個(gè) switch 塊內(nèi),都必須包含一個(gè) default 語(yǔ)句并且放在最后,即使它什么代碼也沒(méi)有。

我的理解

首先理解前半部分,"每個(gè) case 要么通過(guò) break/return 等來(lái)終止,要么注釋說(shuō)明程序?qū)⒗^續(xù)執(zhí)行到哪一個(gè) case 為止"。因?yàn)檫@樣可以比較清楚地表達(dá)程序員的意圖,有效防止無(wú)故遺漏的 break 語(yǔ)句。我們來(lái)看一個(gè)示例,如清單 24 所示。

清單 24 synchronized 關(guān)鍵字方式運(yùn)行結(jié)果

switch(condition){

case ABC:

statements;

/*程序繼續(xù)執(zhí)行直到 DEF 分支*/

case DEF:

statements;

break;

case XYZ:

statements;

break;

default:

statements;

break;

}

上述示例中,每當(dāng)一個(gè) case 順著往下執(zhí)行時(shí)(因?yàn)闆](méi)有 break 語(yǔ)句),通常應(yīng)在 break 語(yǔ)句的位置添加注釋。上面的示例代碼中就包含了注釋"/*程序繼續(xù)執(zhí)行直到 DEF 分支*/"(這一條也是 SUN 公司 1997 年代碼規(guī)范的要求)。

語(yǔ)法上來(lái)說(shuō),default 語(yǔ)句中的 break 是多余的,但是如果后續(xù)添加額外的 case,可以避免找不到匹配 case 項(xiàng)的錯(cuò)誤。

集合處理

集合轉(zhuǎn)數(shù)組處理

阿里強(qiáng)制規(guī)定使用集合轉(zhuǎn)數(shù)組的方法,必須使用集合的 toArray(T[] arrays),傳入的是類型完全一樣的數(shù)組,大小就是 list.size()。使用 toArray 帶參方法,入?yún)⒎峙涞臄?shù)組空間不夠大時(shí),toArray 方法內(nèi)部將重新分配內(nèi)存空間,并返回新數(shù)組地址;如果數(shù)組元素大于實(shí)際所需,下標(biāo)為[list.size()]的數(shù)組元素將被置為 null,其它數(shù)組元素保持原值,因此最好將方法入?yún)?shù)組大小定義與集合元素個(gè)數(shù)一致。正例如清單 25 所示。

清單 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 暫時(shí)無(wú)參方法存在問(wèn)題,此方法返回值只能是 Object[]類,若強(qiáng)轉(zhuǎn)其他類型數(shù)組將出現(xiàn) ClassCastException 錯(cuò)誤。

我的理解

ArrayList 類的 toArray()源碼如清單所示,toArray()方法暫時(shí)無(wú)需傳入?yún)?shù),可以直接將集合轉(zhuǎn)成 Object 數(shù)組進(jìn)行返回,而且也只能返回 Object 類型。

清單 26 toArray()源碼

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;

}

由源碼可知,不帶參數(shù)的 toArray()構(gòu)造一個(gè) Object 數(shù)組,然后進(jìn)行數(shù)據(jù)拷貝,此時(shí)進(jìn)行轉(zhuǎn)型就會(huì)產(chǎn)生 ClassCastException。原因是不能將 Object[]轉(zhuǎn)化為 Strng[]。Java 中的強(qiáng)制類型轉(zhuǎn)換只是針對(duì)單個(gè)對(duì)象,想要將一種類型數(shù)組轉(zhuǎn)化為另一種類型數(shù)組是不可行的。

針對(duì)傳入?yún)?shù)的數(shù)組大小,測(cè)試大于 list、等于 list 和小于 list 三種情況,測(cè)試代碼如清單 27 所示。

清單 27 toArray()測(cè)試

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("數(shù)組長(zhǎng)度等于 list 耗時(shí):"+(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("數(shù)組長(zhǎng)度等于 list 耗時(shí):"+(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("數(shù)組長(zhǎng)度等于 list 耗時(shí):"+(System.currentTimeMills()-start)+"ms");

}

清單運(yùn)行后輸出結(jié)果如清單 28 所示。

清單 28 清單運(yùn)行輸出

數(shù)組長(zhǎng)度等于 list 耗時(shí):431ms

數(shù)組長(zhǎng)度等于 list 耗時(shí):509ms

數(shù)組長(zhǎng)度等于 list 耗時(shí):1943ms

通過(guò)測(cè)試可知無(wú)論數(shù)據(jù)大小如何,數(shù)組轉(zhuǎn)換都可以成功,只是耗時(shí)不同,數(shù)組長(zhǎng)度等于 list 時(shí)性能最優(yōu),因此強(qiáng)制方法入?yún)?shù)組大小與集合元素個(gè)數(shù)一致。

注釋規(guī)約

方法注釋要求

阿里強(qiáng)制要求方法內(nèi)部單行注釋,在被注釋語(yǔ)句上方另起一行,使用//注釋。方法內(nèi)部多行注釋使用/**/注釋,注意與代碼對(duì)照。

我的理解

百度規(guī)定方法注釋采用標(biāo)準(zhǔn)的 Javadoc 注釋規(guī)范,注釋中必須提供方法說(shuō)明、參數(shù)說(shuō)明及返回值和異常說(shuō)明。騰訊規(guī)定采用 JavaDoc 文檔注釋,在方法定義之前應(yīng)該對(duì)其進(jìn)行注釋,包括方法的描述、輸入、輸出以及返回值說(shuō)明、拋出異常說(shuō)明、參考鏈接等。

其他

數(shù)據(jù)結(jié)構(gòu)初始化大小

阿里推薦任何數(shù)據(jù)結(jié)構(gòu)的構(gòu)造或初始化,都應(yīng)指定大小,避免數(shù)據(jù)結(jié)構(gòu)暫時(shí)無(wú)限增長(zhǎng)吃光內(nèi)存。

我的理解

首先明確一點(diǎn),阿里這里指的大小具體是指數(shù)據(jù)結(jié)構(gòu)的最大長(zhǎng)度。大部分 Java 集合類在構(gòu)造時(shí)指定的大小都是初始尺寸(initial Capacity),而不是尺寸上限(Capacity),只有幾種隊(duì)列除外,例如 ArrayBlockingQueue、LinkedBlockingQueue,它們?cè)跇?gòu)造時(shí)可以指定隊(duì)列的最大長(zhǎng)度。阿里推薦的目的是為了合理規(guī)劃內(nèi)存,避免出現(xiàn) OOM(Out of Memory)異常。

結(jié)束語(yǔ)

本文主要介紹了阿里巴巴針對(duì)命名風(fēng)格、常量定義、代碼格式、OOP 規(guī)約、并發(fā)處理、控制語(yǔ)句、集合處理、注釋規(guī)約、其他這些關(guān)于編碼規(guī)約的要求。本文僅覆蓋了阿里代碼規(guī)范的少數(shù)內(nèi)容,更多內(nèi)容請(qǐng)咨詢本文作者。


參考資源

參考文檔《阿里巴巴 Java 開發(fā)手冊(cè)(又名阿里巴巴 Java 代碼規(guī)約)》。

參考 developerWorks 上的 Java 文章,了解更多 Java 知識(shí)。

參考書籍 《Effective Java Second Edition》Joshua Bloch。

在最后面分享一個(gè)我自己的后端技術(shù)群,群里自己收集了一些Java架構(gòu)資料,大家可以進(jìn)群領(lǐng)取,qun號(hào):680075317,也可以進(jìn)群一起交流,比如遇到技術(shù)瓶頸、面試不過(guò)的,大家一些交流學(xué)習(xí)!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容