以下規則并非指導或推薦的性質,而是必須遵守的規定。如果不遵守這些規定,Android通常不會接受投稿。已有的代碼未必全部遵守了這些規定,但是新的代碼全部都應該遵守。
Java語言規范
我們遵循標準的Java編碼規范,并加入了新的規則:
不要忽略異常
有時,完全忽略異常是非常誘人的,比如:
void?setServerPort(String value)?{
try?{
serverPort = Integer.parseInt(value);
}?catch?(NumberFormatException e) { }
}
絕對不要這么做。也許你會認為:你的代碼永遠不會碰到這種出錯的情況,或者處理異常并不重要,可類似上述忽略異常的代碼將會在代碼中埋下一顆地雷,說不定哪天它就會炸到某個人了。你必須在代碼中以某種規矩來處理所有的異常。根據情況的不同,處理的方式也會不一樣。
無論何時,空的catch語句都會讓人感到不寒而栗。雖然很多情況下確實是一切正常,但至少你不得不去憂慮它。在Java中你無法逃離這種恐懼感。 -James Gosling
可接受的替代方案包括(按照推薦順序):
· 向方法的調用者拋出異常。
void?setServerPort(String value)?throws?NumberFormatException?{
serverPort = Integer.parseInt(value);
}
· 根據抽象級別拋出新的異常。
void?setServerPort(String value)?throws?ConfigurationException?{
try?{
serverPort = Integer.parseInt(value);
}?catch?(NumberFormatException e) {
throw?new?ConfigurationException("Port "?+ value +?" is not valid.");
}
}
· 默默地處理錯誤并在catch {}語句塊中替換為合適的值。
/** Set port. If value is not a valid number, 80 is substituted. */
void?setServerPort(String value)?{
try?{
serverPort = Integer.parseInt(value);
}?catch?(NumberFormatException e) {
serverPort =?80;?// default port for server
}
}
· 捕獲異常并拋出一個新的RuntimeException。這種做法比較危險:只有確信發生該錯誤時最合適的做法就是崩潰,才會這么做。
/** Set port. If value is not a valid number, die. */
void?setServerPort(String value)?{
try?{
serverPort = Integer.parseInt(value);
}?catch?(NumberFormatException e) {
throw?new?RuntimeException("port "?+ value?" is invalid, ", e);
}
}
請記住,最初的異常是傳遞給構造方法的RuntimeException。如果代碼必須在Java 1.3版本下編譯,需要忽略該異常。
· 最后一招:如果確信忽略異常比較合適,那就忽略吧,但必須把理想的原因注釋出來:
/** If value is not a valid number, original port number is used. */
void?setServerPort(String value)?{
try?{
serverPort = Integer.parseInt(value);
}?catch?(NumberFormatException e) {
// Method is documented to just ignore invalid user input.
// serverPort will just be unchanged.
}
}
不要捕獲頂級的Exception
有時在捕獲Exception時偷懶也是很吸引人的,類似如下的處理方式:
try?{
someComplicatedIOFunction();?// may throw IOException
someComplicatedParsingFunction();?// may throw ParsingException
someComplicatedSecurityFunction();?// may throw SecurityException
// phew, made it all the way
}?catch?(Exception e) {?// I'll just catch all exceptions
handleError();?// with one generic handler!
}
不要這么做。絕大部分情況下,捕獲頂級的Exception或Throwable都是不合適的,Throwable更不合適,因為它還包含了Error異常。這種捕獲非常危險。這意味著本來不必考慮的Exception(包括類似ClassCastException的RuntimeException)被卷入到應用程序級的錯誤處理中來。這會讓代碼運行的錯誤變得模糊不清。這意味著,假如別人在你調用的代碼中加入了新的異常,編譯器將無法幫助你識別出各種不同的錯誤類型。絕大部分情況下,無論如何你都不應該用同一種方式來處理各種不同類型的異常。
本規則也有極少數例外情況:期望捕獲所有類型錯誤的特定的測試代碼和頂層代碼(為了阻止這些錯誤在用戶界面上顯示出來,或者保持批量工作的運行)。這種情況下可以捕獲頂級的Exception(或Throwable)并進行相應的錯誤處理。在開始之前,你應該非常仔細地考慮一下,并在注釋中解釋清楚為什么這么做是安全的。
比捕獲頂級Exception更好的方案:
· 分開捕獲每一種異常,在一條try語句后面跟隨多個catch 語句塊。這樣可能會有點別扭,但總比捕獲所有Exception要好些。請小心別在catch語句塊中重復執行大量的代碼。
· 重新組織一下代碼,使用多個try塊,使錯誤處理的粒度更細一些。把IO從解析內容的代碼中分離出來,根據各自的情況進行單獨的錯誤處理。
· 再次拋出異常。很多時候在你這個級別根本就沒必要捕獲這個異常,只要讓方法拋出該異常即可。
請記住:異常是你的朋友!當編譯器指出你沒有捕獲某個異常時,請不要皺眉頭。而應該微笑:編譯器幫助你找到了代碼中的運行時(runtime)問題。
不要使用Finalizer
Finalizer提供了一個機會,可以讓對象被垃圾回收器回收時執行一些代碼。
優點:便于執行清理工作,特別是針對外部資源。
缺點:調用finalizer的時機并不確定,甚至根本就不會調用。
結論:我們不要使用finalizers。大多數情況下,可以用優秀的異常處理代碼來執行那些要放入finalizer的工作。如果確實是需要使用finalizer,那就定義一個close()方法(或類似的方法),并且在文檔中準確地記錄下需要調用該方法的時機。相關例程可以參見InputStream。這種情況下還是適合使用finalizer的,但不需要在finalizer中輸出日志信息,因為日志不能因為這個而被撐爆。
使用完全限定Import
當需要使用foo包中的Bar類時,存在兩種可能的import方式:
1.?? import foo.*;
優點:可能會減少import語句。
1.?? import foo.Bar;
優點:實際用到的類一清二楚。代碼的可讀性更好,便于維護。
結論:用后一種寫法來import所有的Android代碼。不過導入java標準庫(java.util.*、java.io.*等) 和單元測試代碼 (junit.framework.*)時可以例外。
Java類庫規范
使用Android Java類庫和工具存在一些慣例。有時這些慣例會作出重大變化,可之前的代碼也許會用到過時的模板或類庫。如果用到這部分過時的代碼,沿用已有的風格就是了(參閱Consistency)。創建新的組件時就不要再使用過時的類庫了。
Java編程規范
使用Javadoc標準注釋
每個文件的開頭都應該有一句版權說明。然后下面應該是package包語句和import語句,每個語句塊之間用空行分隔。然后是類或接口的定義。在Javadoc注釋中,應描述類或接口的用途。
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package?com.android.internal.foo;
import?android.os.Blah;
import?android.view.Yada;
import?java.sql.ResultSet;
import?java.sql.SQLException;
/**
* Does X and Y and provides an abstraction for Z.
*/
public?class?Foo?{
...
}
每個類和自建的public方法必須包含Javadoc注釋,注釋至少要包含描述該類或方法用途的語句。并且該語句應該用第三人稱的動詞形式來開頭。
例如:
/** Returns the correctly rounded positive square root of a double value. */
static?double?sqrt(double?a)?{
...
}
或
/**
* Constructs a new String by converting the specified array of
* bytes using the platform's default character encoding.
*/
public?String(byte[] bytes)?{
...
}
如果所有的Javadoc都會寫成“sets Foo”,對于那些無關緊要的類似setFoo()的get和set語句是不必撰寫Javadoc的。如果方法執行了比較復雜的操作(比如執行強制約束或者產生很重要的副作用),那就必須進行注釋。如果“Foo”屬性的意義不容易理解,也應該進行注釋。
無論是public的還是其它類型的,所有自建的方法都將受益于Javadoc。public的方法是API的組成部分,因此更需要Javadoc。
Android目前還沒有規定自己的Javadoc注釋撰寫規范,但是應該遵守Sun Javadoc約定。
編寫簡短的方法
為了把規模控制在合理范圍內,方法應該保持簡短和重點突出。不過,有時較長的方法也是合適的,所以對方法的代碼長度并沒有硬性的限制。如果方法代碼超過了40行,就該考慮是否可以在不損害程序結構的前提下進行分拆。
在標準的位置定義字段
字段應該定義在文件開頭,或者緊挨著使用這些字段的方法之前。
限制變量的作用范圍
局部變量的作用范圍應該是限制為最小的(Effective Java第29條)。使用局部變量,可以增加代碼的可讀性和可維護性,并且降低發生錯誤的可能性。每個變量都應該在最小范圍的代碼塊中進行聲明,該代碼塊的大小只要能夠包含所有對該變量的使用即可。
應該在第一次用到局部變量的地方對其進行聲明。幾乎所有局部變量聲明都應該進行初始化。如果還缺少足夠的信息來正確地初始化變量,那就應該推遲聲明,直至可以初始化為止。
本規則存在一個例外,就是涉及try-catch語句的情況。如果變量是用方法的返回值來初始化的,而該方法可能會拋出一個checked異常,那么必須在try塊中進行變量聲明。如果需在try塊之外使用該變量,那它就必須在try塊之前就進行聲明了,這時它是不可能進行正確的初始化的。
// Instantiate class cl, which represents some sort of Set
Set s =?null;
try?{
s = (Set) cl.newInstance();
}?catch(IllegalAccessException e) {
throw?new?IllegalArgumentException(cl +?" not accessible");
}?catch(InstantiationException e) {
throw?new?IllegalArgumentException(cl +?" not instantiable");
}
// Exercise the set
s.addAll(Arrays.asList(args));
但即便是這種情況也是可以避免的,把try-catch 塊封裝在一個方法內即可:
Set?createSet(Class cl)?{
// Instantiate class cl, which represents some sort of Set
try?{
return?(Set) cl.newInstance();
}?catch(IllegalAccessException e) {
throw?new?IllegalArgumentException(cl +?" not accessible");
}?catch(InstantiationException e) {
throw?new?IllegalArgumentException(cl +?" not instantiable");
}
}
...
// Exercise the set
Set s = createSet(cl);
s.addAll(Arrays.asList(args));
除非理由十分充分,否則循環變量都應該在for語句內進行聲明,:
for?(int?i =?0; i n; i++) {
doSomething(i);
}
和
for?(Iterator i = c.iterator(); i.hasNext(); ) {
doSomethingElse(i.next());
}
對Import語句排序
import語句的次序應該如下:
1.?? Android imports
2.?? 第三方庫(com、junit、net、org)
3.?? java和javax
為了精確匹配IDE的配置,import順序應該是:
· 在每組內部按字母排序,大寫字母排在小寫字母的前面。
· 每個大組之間應該空一行(android、com、junit、net、org、java、javax)。
原先次序是不作為規范性要求的。這意味著要么允許IDE改變順序,要么使用IDE的開發者不得不禁用import自動管理功能并且人工維護import。這看起來比較糟糕。每當說起java規范,推薦的規范到處都是。符合我們要求的差不多就是“選擇一個次序并堅持下去。”于是,我們就選擇一個規范,更新規范手冊,并讓IDE去遵守它。我們期望:不必耗費更多的精力,用IDE編碼的用戶就按照這種規則去import所有的package。
基于以下原因,選定了本項規則:
· 導入人員期望最先看到的放在最開始位置(android)
· 導入人員期望最后才看到的放在最后(java)
· 風格讓人容易遵守
· IDE可以遵守
靜態import的使用和位置已經成為略帶爭議的話題。有些人愿意讓靜態import和其它import混在一起,另一些人則期望讓它們位于其它import之上或者之下。另外,我們還未提到讓所有IDE都遵守同一個次序的方法。
因為大多數人都認為這部分內容并不要緊,只要遵守你的決定并堅持下去即可。
使用空格進行縮進
我們的代碼塊縮進使用4個空格。我們從不使用制表符tab。如果存在疑惑,與前后的其它代碼保持一致即可。
我們用8個空格作為換行后的縮進,包括函數調用和賦值。例如這是正確的:
Instrument i =
someLongExpression(that, wouldNotFit, on, one, line);
而這是錯誤的:
Instrument i =
someLongExpression(that, wouldNotFit, on, one, line);
遵守字段命名慣例
· 非public的、非static的字段名稱以m開頭。
· static字段名稱以s開頭。
· 其它字段以小寫字母開頭。
· public static final字段(常量)全部字母大寫并用下劃線分隔。
例如:
public?class?MyClass?{
public?static?final?int?SOME_CONSTANT =?42;
public?int?publicField;
private?static?MyClass sSingleton;
int?mPackagePrivate;
private?int?mPrivate;
protected?int?mProtected;
}
使用標準的大括號風格
大括號不單獨占用一行;它們緊接著上一行書寫。就像這樣:
class?MyClass?{
int?func()?{
if?(something) {
// ...
}?else?if?(somethingElse) {
// ...
}?else?{
// ...
}
}
}
我們需要用大括號來包裹條件語句塊。不過也有例外,如果整個條件語句塊(條件和語句本身)都能容納在一行內,也可以(但不是必須)把它們放入同一行中。也就是說,這是合法的:
if?(condition) {
body();
}
這也是合法的:
if(condition) body();
但這是非法的:
if?(condition)
body();?// bad!
限制代碼行的長度
每行代碼的長度應該不超過100個字符。
有關本規則的討論有很多,最后的結論還是最多不超過100個字符。
例外:如果注釋行包含了超過100個字符的命令示例或者URL文字,為了便于剪切和復制,其長度可以超過100個字符。
例外:import行可以超過限制,因為很少有人會去閱讀它。這也簡化了編程工具的寫入操作。
使用標準的Java Annotation
Annotation應該位于Java語言元素的其它修飾符之前。 簡單的marker annotation(@Override等)可以和語言元素放在同一行。 如果存在多個annotation,或者annotation是參數化的,則應按字母順序各占一行來列出。
對于Java 內建的三種annotation,Android標準的實現如下:
· @Deprecated:只要某個語言元素已不再建議使用了,就必須使用@Deprecated annotation。如果使用了@Deprecated annotation,則必須同時進行@deprecated Javadoc標記,并且給出一個替代的實現方式。此外請記住,被@Deprecated的方法仍然是能正常執行的。
如果看到以前的代碼帶有@deprecated Javadoc標記,也請加上@Deprecated annotation。
· @Override:只要某個方法覆蓋了已過時的或繼承自超類的方法,就必須使用@Override annotation。
例如,如果方法使用了@inheritdocs Javadoc標記,且繼承自超類(而不是interface),則必須同時用@Override標明覆蓋了父類方法。
· @SuppressWarnings:@SuppressWarnings annotation僅用于無法消除編譯警告的場合。 如果警告確實經過測試“不可能消除”,則必須使用@SuppressWarnings annotation,以確保所有的警告都能真實反映代碼中的問題。
當需要使用@SuppressWarnings annotation時,必須在前面加上TODO注釋行,用于解釋“不可能消除”警告的條件。通常是標明某個令人討厭的類用到了某個拙劣的接口。比如:
//?TODO:?The third-party class com.third.useful.Utility.rotate() needs generics
@SuppressWarnings("generic-cast")
List<String> blix = Utility.rotate(blax);
如果需要使用@SuppressWarnings annotation,應該重新組織一下代碼,把需要應用annotation的語言元素獨立出來。
簡稱等同于單詞
簡稱和縮寫都視為變量名、方法名和類名。以下名稱可讀性更強:
好?? ??????????????????????? 差
XmlHttpRequest?? XMLHTTPRequest
getCustomerId? ? ? getCustomerID
class Html?? ? ? ? ? ? class HTML
String url? ? ? ? ? ? ?? String URL
long id? ? ? ? ? ? ? ? ?? long ID
如何對待簡稱,JDK和Android底層代碼存在很大的差異。因此,你幾乎不大可能與其它代碼取得一致。別無選擇,把簡稱當作完整的單詞看待吧。
關于本條規則的進一步解釋,請參閱Effective Java第38條和Java Puzzlers第68條。
使用TODO注釋
對那些臨時性的、短期的、夠棒但不完美的代碼,請使用TODO注釋。
TODO注釋應該包含全部大寫的TODO,后跟一個冒號:
//TODO:Remove this code after the UrlTable2 has been checked in.
和
//TODO:Change this to use a flag instead of a constant.
如果TODO注釋是“將來要做某事”的格式,則請確保包含一個很明確的日期(“在2005年11月會修正”),或是一個很明確的事件(“在所有代碼整合人員理解了V7協議之后刪除本段代碼”)。
慎用Log
記錄日志會對性能產生顯著的負面影響。如果日志內容不夠簡煉的話,很快會喪失可用性。日志功能支持五種不同的級別。以下列出了各個級別及其使用場合和方式。
· ERROR: 該級別日志應該在致命錯誤發生時使用,也就是說,錯誤的后果能被用戶看到,但是不明確刪除部分數據、卸裝程序、清除數據區或重新刷機(或更糟糕)就無法恢復。該級別總是記錄日志。需要記錄ERROR級別日志的事件一般都應該向統計信息收集(statistics-gathering )服務器報告。
· WARNING: 該級別日志應該用于那些重大的、意外的事件,也就是說,錯誤的后果能被用戶看到,但是不采取明確的動作可能就無法無損恢復,從等待或重啟應用開始,直至重新下載新版程序或重啟設備。該級別總是記錄日志。需記錄WARNING級別日志的事件也可以考慮向統計信息收集服務器報告。
· INFORMATIVE: 該級別的日志應該用于記錄大部分人都會感興趣的事件,也就是說,如果檢測到事件的影響面可能很廣,但不一定是錯誤。應該只有那些擁有本區域內最高級別身份認證的模塊才能記錄這些日志(為了避免級別不足的模塊重復記錄日志)。該級別總是記錄日志。
· DEBUG: 該級別的日志應該用于進一步記錄有關調查、調試意外現象的設備事件。應該只記錄那些有關控件運行所必需的信息。如果debug日志占用了太多的日志空間,那就應該使用詳細級別日志(verbose)才更為合適。
即使是發行版本(release build),該級別也會被記錄,并且需用if (LOCAL_LOG)或if (LOCAL_LOGD)語句塊包裹,這里的LOCAL_LOG[D]在你的類或子控件中定義。這樣就能夠一次性關閉所有的調試日志。因此在if (LOCAL_LOG)語句塊中不允許存在邏輯判斷語句。所有日志所需的文字組織工作也應在if (LOCAL_LOG)語句塊內完成。如果對記錄日志的調用會導致在if (LOCAL_LOG)語句塊之外完成文字組織工作,那該調用就必須控制在一個方法內完成。
還存在一些代碼仍然在使用if (localLOGV)。這也是可以接受的,雖然名稱不是標準的。
· VERBOSE: 該級別日志應用于所有其余的事件。該級別僅會在調試版本(debug build)下記錄日志,并且需用if (LOCAL_LOGV)語句塊(或等效語句)包裹,這樣該部分代碼默認就不會編譯進發行版本中去了。所有構建日志文字的代碼將會在發行版本中剝離出去,并且需包含在if (LOCAL_LOGV)語句塊中。
注意:
· 除了VERBOSE級別外,在同一個模塊中同一個錯誤應該盡可能只報告一次:在同一個模塊內的一系列層層嵌套的函數調用中,只有最內層的函數才返回錯誤;并且只有能為解決問題提供明顯幫助的時候,同一模塊中的調用方才寫入一些日志。
· 除了VERBOSE級別外,在一系列嵌套的模塊中,當較低級別的模塊對來自較高級別模塊的非法數據進行檢測時,應該只把檢測情況記錄在DEBUG日志中,并且只記錄那些調用者無法獲取的信息。特別是不需要記錄已經拋出異常的情況(異常中應該包含了全部有價值的信息),也不必記錄那些只包含錯誤代碼的信息。當應用程序與系統框架間進行交互時,這一點尤為重要。系統框架已能正確處理的第三方應用程序,也不應該記錄大于DEBUG級別的日志。僅當一個模塊或應用程序檢測到自身或來自更低級別模塊的錯誤時,才應該記錄INFORMATIVE及以上級別的日志。
· 如果一個通常要記錄日志的事件可能會多次發生,則采取一些頻次限制措施或許是個好主意,以防日志被很多重復(或類似)的信息給撐爆了。
· 網絡連接的丟失可被視為常見現象,也是完全可以預見的,不應該無緣無故就記錄進日志。影響范圍限于應用程序內部的網絡中斷應該記錄在DEBUG或VERBOSE級別的日志中(根據影響的嚴重程度及意外程度,再來確定是否在發行版本中也記錄日志)。
· 有權訪問的文件系統或第三方應用程序發起的系統空間滿,應該記錄大于INFORMATIVE級別的日志。
· 來自任何未授信源的非法數據(包括共享存儲上的任何文件,或來自任何網絡連接的數據)可被視為可預見的,如果檢測到非法數據也不應該記錄大于DEBUG級別的日志(即使記錄也應盡可能少)。
· 請記住,對字符串使用+操作符時,會在后臺以默認大小(16個字符)緩沖區創建一個StringBuilder對象,并且可能還會創建一些其它的臨時String對象。換句話說,顯式創建StringBuilders對象的代價并不會比用'+'操作符更高(事實上效率還將會提高很多)。還要記住,即使不會再去讀取這些日志,調用Log.v()的代碼也將編譯進發行版中并獲得執行,包括創建字符串的代碼。
· 所有要被人閱讀并存在于發行版本中的日志,都應該簡潔明了、沒有秘密、容易理解。這里包括所有DEBUG以上級別的日志。
· 只要有可能,日志就應該一句一行。行長最好不超過80或100個字符,盡可能避免超過130或160個字符(包括標識符)的行。
· 報告成功的日志記錄絕不應該出現在大于VERBOSE級別的日志中。
· 用于診斷難以重現事件的臨時日志應該限于DEBUG或VERBOSE級別,并且應該用if語句塊包裹,以便在編譯時能夠一次全部關閉。
· 小心日志會泄漏隱私。應該避免將私人信息記入日志,受保護的內容肯定也不允許記錄。這在編寫系統框架級代碼時尤為重要,因為很難預知哪些是私人信息和受保護信息。
· 絕對不要使用System.out.println() (或本地代碼中的printf())。System.out 和 System.err會重定向到/dev/null,因此print語句不會產生任何可見的效果。可是,這些調用中的所有字符串創建工作都仍然會執行。
· 日志的黃金法則是:你的日志記錄不會導致其它日志的緩沖區溢出,正如其他人的日志也不會讓你的溢出一樣。
保持一致
我們的最終想法是:保持一致。如果你正在編寫代碼,請花幾分鐘瀏覽一下前后的其它代碼,以確定它們的風格。如果它們在if語句前后使用了空格,那你也應該遵循。如果它們的注釋是用星號組成的框框圍起來的,那也請你照辦。
保持風格規范的重點是有一個公共的代碼詞匯表,這樣大家就可以把注意力集中于你要說什么,而不是你如何說。我們在這里列出了全部的風格規范,于是大家也知道了這些詞匯。不過本地化的風格也很重要。如果你要加入的代碼和已存在的代碼風格迥異,那就會突然打破閱讀的節奏。請努力避免這種情況的發生。
Javatest風格規范
遵守測試方法命名規范
命名測試方法時,可以用下劃線來分隔測試的條件。這種風格可以讓測試的條件一目了然。
比如:
testMethod_specificCase1 testMethod_specificCase2
void?testIsDistinguishable_protanopia()?{
ColorMatcher colorMatcher =?new?ColorMatcher(PROTANOPIA)
assertFalse(colorMatcher.isDistinguishable(Color.RED, Color.BLACK))
assertTrue(colorMatcher.isDistinguishable(Color.X, Color.Y))
}