此頁面上的程式碼樣式是向 Android 開源專案 (AOSP) 貢獻 Java 程式碼的嚴格規則。不遵守這些規則的 Android 平台貢獻通常不會被接受。我們認識到並非所有現有代碼都遵循這些規則,但我們希望所有新代碼都符合這些規則。有關更具包容性的生態系統使用和避免的術語範例,請參閱編碼。
始終如一
最簡單的規則之一就是保持一致。如果您正在編輯程式碼,請花幾分鐘查看周圍的程式碼並確定其樣式。如果程式碼在if
子句周圍使用空格,您也應該這樣做。如果程式碼註解周圍有星星小框,那麼您的註解周圍也有星星小框。
制定風格指南的目的是擁有通用的編碼詞彙,讓讀者可以專注於你所說的內容,而不是你所說的方式。我們在這裡介紹全局樣式規則,以便您了解詞彙,但本地樣式也很重要。如果添加到文件中的程式碼看起來與周圍的現有程式碼截然不同,那麼讀者在閱讀該文件時就會失去節奏。盡量避免這種情況。
Java語言規則
Android 遵循標準 Java 程式碼約定以及下述附加規則。
不要忽視異常
編寫忽略異常的程式碼可能很誘人,例如:
void setServerPort(String value) { try { serverPort = Integer.parseInt(value); } catch (NumberFormatException e) { } }
不要這樣做。雖然您可能認為您的程式碼永遠不會遇到這種錯誤情況,或者處理它並不重要,但忽略這種類型的異常會在您的程式碼中埋下地雷,讓其他人有一天觸發。您必須以有原則的方式處理程式碼中的每個異常;具體處理視情況而定。
「任何時候有人有一個空的catch 子句,他們都會有一種令人毛骨悚然的感覺。確實有時這實際上是正確的事情,但至少你必須考慮一下。在Java 中,你無法逃避這種令人毛骨悚然的感覺。 ——詹姆斯·高斯林
可接受的替代方案(按優先順序)是:
- 將異常拋出給方法的呼叫者。
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); } }
- 作為最後的手段,如果您確信忽略異常是適當的,那麼您可以忽略它,但您也必須用充分的理由來評論原因。
/** 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. } }
不要捕獲通用異常
在捕獲異常時,很容易偷懶並執行以下操作:
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
異常)。它很危險,因為這意味著您從未預料到的異常(包括運行時異常,如ClassCastException
)會在應用程式層級錯誤處理中被捕獲。它掩蓋了程式碼的失敗處理屬性,這意味著如果有人在您呼叫的程式碼中新增類型的異常,編譯器不會指出您需要以不同的方式處理錯誤。在大多數情況下,您不應該以相同的方式處理不同類型的異常。
此規則的罕見例外是測試程式碼和頂級程式碼,您希望在其中捕獲各種錯誤(以防止它們顯示在 UI 中,或保持批次作業運行)。在這些情況下,您可以捕獲通用Exception
(或Throwable
)並適當地處理錯誤。不過,在執行此操作之前請仔細考慮,並添加註釋來解釋為什麼它在這種情況下是安全的。
捕獲通用異常的替代方法:
- 作為多 catch 區塊的一部分單獨捕獲每個異常,例如:
try { ... } catch (ClassNotFoundException | NoSuchMethodException e) { ... }
- 重構程式碼以透過多個 try 區塊進行更細微的錯誤處理。將 IO 從解析中分離出來,並在每種情況下單獨處理錯誤。
- 重新拋出異常。很多時候你根本不需要在這個層級捕獲異常,只需讓方法拋出它即可。
請記住,例外是你的朋友!當編譯器抱怨你沒有捕獲異常時,不要皺眉。微笑!編譯器只是讓您更容易捕獲程式碼中的執行時間問題。
不要使用終結器
終結器是一種在物件被垃圾收集時執行一段程式碼的方法。雖然終結器可以方便地進行清理(尤其是外部資源),但無法保證何時調用終結器(甚至根本不會呼叫它)。
Android 不使用終結器。在大多數情況下,您可以使用良好的異常處理來代替。如果您絕對需要終結器,請定義close()
方法(或類似方法)並準確記錄何時需要呼叫該方法(有關範例,請參閱InputStream )。在這種情況下,從終結器列印一條短日誌訊息是合適的,但不是必需的,只要預計不會淹沒日誌即可。
完全合格的進口
當您想要使用foo
套件中的Bar
類別時,有兩種可能的方法來匯入它:
-
import foo.*;
可能會減少導入語句的數量。
-
import foo.Bar;
使使用的類別變得顯而易見,並且程式碼對於維護人員來說更具可讀性。
使用import foo.Bar;
用於導入所有 Android 程式碼。 Java 標準函式庫( java.util.*
、 java.io.*
等)和單元測試程式碼( junit.framework.*
)有一個明確的例外。
Java 函式庫規則
使用 Android 的 Java 函式庫和工具有一些約定。在某些情況下,約定在重要方面發生了變化,舊程式碼可能使用已棄用的模式或函式庫。使用此類程式碼時,可以繼續現有的樣式。但是,在建立新元件時,切勿使用已棄用的程式庫。
Java 風格規則
使用Javadoc標準註釋
每個文件的頂部都應該有一個版權聲明,後面是套件和導入聲明(每個區塊都以空行分隔),最後是類別或介面聲明。在 Javadoc 註解中,描述類別或介面的作用。
/* * Copyright 2023 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 { ... }
您編寫的每個類別和重要的公共方法都必須包含 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”,那麼您不需要為簡單的 get 和 set 方法(例如setFoo()
編寫 Javadoc。如果該方法執行更複雜的操作(例如強制執行約束或具有重要的副作用),那麼您必須記錄它。如果屬性“Foo”的含義不明顯,您應該記錄它。
您編寫的每個方法,無論是公共方法還是其他方法,都將從 Javadoc 中受益。公共方法是 API 的一部分,因此需要 Javadoc。 Android 不強制規定編寫 Javadoc 註解的特定樣式,但您應該遵循如何為 Javadoc 工具撰寫文件註解中的說明。
寫簡短的方法
在可行的情況下,保持方法小而集中。我們認識到長方法有時是合適的,因此對方法長度沒有硬性限制。如果一個方法超過 40 行左右,請考慮是否可以在不損害程式結構的情況下將其分解。
在標準位置定義字段
在文件頂部或緊鄰使用它們的方法之前定義欄位。
限制變數範圍
將局部變數的範圍保持在最小範圍。這提高了程式碼的可讀性和可維護性,並減少了出錯的可能性。在最裡面的區塊中聲明每個變量,該區塊包含變數的所有使用。
在首次使用局部變數時聲明它們。幾乎每個局部變數宣告都應該包含一個初始值設定項。如果您還沒有足夠的資訊來明智地初始化變量,請推遲聲明,直到您獲得為止。
try-catch 語句是個例外。如果使用引發已檢查異常的方法的回傳值來初始化變量,則必須在 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()); }
訂單進口報表
導入語句的順序是:
- 安卓導入
- 從第三方匯入(
com
、junit
、net
、org
) -
java
和javax
為了完全匹配 IDE 設置,導入應該是:
- 每個分組內按字母順序排列,大寫字母在小寫字母之前(例如,Z 在 a 之前)
- 每個主要分組之間以空白行分隔(
android
、com
、junit
、net
、org
、java
、javax
)
最初,排序沒有樣式要求,這意味著 IDE 要么總是更改排序,要么 IDE 開發人員必須禁用自動導入管理功能並手動維護導入。這被認為是不好的。當詢問 Java 風格時,首選的風格差異很大,最終 Android 只需要「選擇一個順序並保持一致」。因此,我們選擇了一種風格,更新了風格指南,並使 IDE 遵守它。我們希望 IDE 使用者處理程式碼時,所有套件中的匯入都會符合此模式,而無需額外的工程工作。
我們選擇這種風格是為了:
- 人們首先想要查看的導入往往位於頂部(
android
)。 - 人們至少想要查看的導入往往位於底部(
java
)。 - 人類可以很容易地遵循這種風格。
- IDE 可以遵循這種風格。
將靜態導入放在所有其他導入之上,其排序方式與常規導入相同。
使用空格進行縮排
我們對區塊使用四 (4) 個空格縮進,而不是製表符。如有疑問,請與周圍的代碼保持一致。
我們使用八 (8) 個空格縮排進行換行,包括函數呼叫和賦值。
受到推崇的
Instrument i = someLongExpression(that, wouldNotFit, on, one, line);
不建議
Instrument i = someLongExpression(that, wouldNotFit, on, one, line);
遵循字段命名約定
- 非公共、非靜態欄位名稱以
m
開頭。 - 靜態欄位名稱以
s
開頭。 - 其他欄位以小寫字母開頭。
- 靜態最終欄位(常數,深度不可變)是
ALL_CAPS_WITH_UNDERSCORES
。
例如:
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 個字符,以便於剪下和貼上。
- 導入行可能會超出限制,因為人們很少看到它們(這也簡化了工具編寫)。
使用標準Java註釋
註釋應位於同一語言元素的其他修飾符之前。簡單的標記註釋(例如, @Override
)可以與語言元素列在同一行。如果有多個註釋或參數化註釋,請按字母順序將它們逐行列出。
Android 對於 Java 中預先定義的三個註解的標準做法是:
- 每當不鼓勵使用帶有註釋的元素時,請使用
@Deprecated
註釋。如果您使用@Deprecated
註釋,則還必須有一個@deprecated
Javadoc 標記,並且它應該命名一個替代實作。此外,請記住@Deprecated
方法仍然應該有效。如果您看到舊程式碼具有@deprecated
Javadoc 標記,請新增@Deprecated
註解。 - 每當方法重寫超類別的宣告或實作時,請使用
@Override
註解。例如,如果您使用@inheritdocs
Javadoc 標記,並從類別(而不是介面)派生,則您也必須註解該方法覆寫父類別的方法。 - 僅在無法消除警告的情況下才使用
@SuppressWarnings
註解。如果警告通過了這個「不可能消除」的測試,則必須使用@SuppressWarnings
註釋,以確保所有警告反映程式碼中的實際問題。當需要
@SuppressWarnings
註解時,必須以TODO
註解作為前綴,解釋「無法消除」的情況。這通常會識別出具有尷尬介面的違規類別。例如:// TODO: The third-party class com.third.useful.Utility.rotate() needs generics @SuppressWarnings("generic-cast") List<String> blix = Utility.rotate(blax);
當需要
@SuppressWarnings
註解時,重構程式碼以隔離註解應用的軟體元素。
將縮寫詞視為單字
在命名變數、方法和類別時將首字母縮寫詞和縮寫詞視為單詞,以使名稱更具可讀性:
好的 | 壞的 |
---|---|
XmlHttp請求 | XMLHTTP請求 |
取得客戶ID | 取得客戶ID |
類別 HTML | HTML 類別 |
字串網址 | 字串網址 |
長ID | 長ID |
由於 JDK 和 Android 程式碼庫在縮寫詞方面不一致,因此幾乎不可能與周圍的程式碼保持一致。因此,始終將首字母縮略詞視為單字。
使用 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後刪除此代碼」)。 )。
謹慎記錄
雖然日誌記錄是必要的,但如果不保持合理簡潔,它會對效能產生負面影響並失去其用處。日誌記錄設施提供五種不同等級的日誌記錄:
-
ERROR
:當發生致命事件時使用,也就是說,某些事件將產生用戶可見的後果,並且如果不刪除某些資料、卸載應用程式、擦除資料分區或重新刷新整個設備(或更糟),就無法恢復。該級別始終被記錄。證明某些ERROR
等級的日誌記錄合理的問題很適合報告給統計資料收集伺服器。 -
WARNING
:當發生嚴重且意外的事情時使用,即會產生用戶可見的後果,但透過執行某些明確的操作(從等待或重新啟動應用程式一直到重新下載)可能可以恢復而不會丟失資料應用程式的新版本或重新啟動設備。該級別始終被記錄。向統計資料收集伺服器報告時,也可能會考慮在「WARNING
層級記錄日誌的問題。 -
INFORMATIVE
:用於指出發生了一些有趣的事情,即,當檢測到可能產生廣泛影響的情況時,儘管不一定是錯誤。這種情況只能由認為自己在該域中最權威的模組來記錄(以避免非權威組件的重複記錄)。該級別始終被記錄。 -
DEBUG
:用於進一步記錄設備上發生的情況,這些情況可能與調查和調試意外行為有關。僅記錄收集有關組件運作情況的足夠資訊所需的內容。如果您的偵錯日誌在日誌中占主導地位,那麼您應該使用詳細日誌記錄。即使在發布版本上也會記錄此級別,並且需要被
if (LOCAL_LOG)
或if LOCAL_LOGD)
區塊包圍,其中LOCAL_LOG[D]
在您的類別或子元件中定義,以便可以停用所有此類日誌記錄。因此,if (LOCAL_LOG)
區塊中不能有活動邏輯。日誌的所有字串建置也需要放置在if (LOCAL_LOG)
區塊內。如果日誌記錄呼叫會導致字串建置發生在if (LOCAL_LOG)
區塊之外,則不要將日誌記錄呼叫重構為方法呼叫。有一些程式碼仍然顯示
if (localLOGV)
。儘管名稱不標準,但這也被認為是可以接受的。 -
VERBOSE
:用於其他一切。此等級僅記錄偵錯版本,並且應該被if (LOCAL_LOGV)
區塊(或等效區塊)包圍,以便預設可以編譯出來。任何字串建置都會從發布版本中刪除,並且需要出現在if (LOCAL_LOGV)
區塊內。
筆記
- 在給定模組內,除了
VERBOSE
等級之外,如果可能的話,錯誤應該只報告一次。在模組內的單一函數呼叫鏈中,只有最裡面的函數應該會傳回錯誤,並且同一模組中的呼叫者僅應添加一些日誌記錄(如果這有助於隔離問題)。 - 在模組鏈中,除了
VERBOSE
等級之外,當較低等級的模組偵測到來自較高等級模組的無效資料時,較低等級的模組應該只將此情況記錄到DEBUG
日誌中,並且僅當日誌記錄提供時呼叫者無法以其他方式取得的資訊。具體來說,無需記錄引發異常的情況(異常應包含所有相關資訊),或記錄的唯一資訊包含在錯誤代碼中。這對於框架和應用程式之間的互動尤其重要,並且由框架正確處理的第三方應用程式引起的情況不應觸發高於DEBUG
等級的日誌記錄。唯一應該觸發INFORMATIVE
等級或更高等級的日誌記錄的情況是當模組或應用程式偵測到其自身等級的錯誤或來自較低等級的錯誤時。 - 當通常證明某些日誌記錄合理的情況可能會多次發生時,最好實施某種速率限制機制,以防止相同(或非常相似)資訊的許多重複副本導致日誌溢出。
- 網路連線遺失被認為是常見的並且完全是預料之中的,不應該無緣無故地記錄下來。在應用程式內產生後果的網路連線遺失應在
DEBUG
或VERBOSE
層級記錄(取決於後果是否足夠嚴重且意外到足以記錄在發布版本中)。 - 在可供第三方應用程式存取或代表第三方應用程式的檔案系統上擁有完整的檔案系統,不應以高於資訊的層級進行記錄。
- 來自任何不受信任來源的無效數據(包括共享儲存上的任何文件,或透過網路連接傳入的數據)均被視為預期數據,並且當檢測到無效數據時,不應觸發高於
DEBUG
級別的任何日誌記錄(甚至隨後進行日誌記錄)應盡可能受到限制)。 - 當用於
String
物件時,+
運算子會隱式建立一個具有預設緩衝區大小(16 個字元)的StringBuilder
實例以及可能的其他臨時String
物件。因此,明確建立StringBuilder
物件並不比依賴預設的+
運算子更昂貴(且效率更高)。請記住,呼叫Log.v()
的程式碼是在發布版本上編譯和執行的,包括建置字串,即使未讀取日誌也是如此。 - 任何供其他人閱讀並在發布版本中可用的日誌記錄都應該簡潔而不神秘,並且應該易於理解。這包括所有達到
DEBUG
等級的日誌記錄。 - 如果可能,請保持在一行上記錄。行長度最多為 80 或 100 個字元是可接受的。如果可能,請避免長度超過 130 或 160 個字元(包括標籤的長度)。
- 如果日誌記錄報告成功,切勿在高於
VERBOSE
層級使用它。 - 如果您使用臨時日誌記錄來診斷難以重現的問題,請將其保留在
DEBUG
或VERBOSE
級別,並使用允許在編譯時停用它的 if 區塊將其括起來。 - 小心日誌中的安全漏洞。避免記錄私人資訊。特別是,避免記錄有關受保護內容的資訊。這在編寫框架程式碼時尤其重要,因為事先知道什麼是私人資訊或受保護內容並不容易。
- 切勿使用
System.out.println()
(或本機程式碼的printf()
)。System.out
和System.err
被重新導向到/dev/null
,因此您的 print 語句沒有可見的效果。但是,這些呼叫發生的所有字串建置仍然會被執行。 - 日誌記錄的黃金法則是,您的日誌可能不會不必要地將其他日誌推出緩衝區,就像其他人可能不會推出您的日誌一樣。
Javatests 風格規則
遵循測試方法命名約定,並使用下劃線將正在測試的內容與正在測試的特定案例分開。這種風格可以更輕鬆地查看正在測試哪些案例。例如:
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)) }