貢獻者的 AOSP Java 代碼風格

此頁面上的代碼樣式是向 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!
  }

不要這樣做。在幾乎所有情況下,捕獲泛型ExceptionThrowable都是不合適的(最好不是Throwable ,因為它包含Error異常)。這很危險,因為這意味著您從未預料到的異常(包括ClassCastException之類的運行時異常)會在應用程序級錯誤處理中被捕獲。它掩蓋了代碼的故障處理屬性,這意味著如果有人在你調用的代碼中添加了一種新類型的異常,編譯器不會指出你需要以不同的方式處理錯誤。在大多數情況下,您不應該以相同的方式處理不同類型的異常。

此規則的罕見例外是測試代碼和頂級代碼,您希望在其中捕獲各種錯誤(以防止它們出現在 UI 中,或保持批處理作業運行)。在這些情況下,您可能會捕獲通用Exception (或Throwable )並適當地處理錯誤。不過,在這樣做之前請仔細考慮,並在評論中解釋為什麼在這種情況下它是安全的。

捕獲通用異常的替代方法:

  • 作為多捕獲塊的一部分分別捕獲每個異常,例如:
    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 標準註釋

每個文件的頂部都應該有一個版權聲明,然後是 package 和 import 聲明(每個塊用空行分隔),最後是類或接口聲明。在 Javadoc 註釋中,描述類或接口的作用。

/*
 * Copyright 2022 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());
}

訂單導入語句

導入語句的順序是:

  1. 安卓導入
  2. 從第三方進口( comjunitnetorg
  3. javajavax

為了完全匹配 IDE 設置,導入應該是:

  • 每個分組內按字母順序排列,大寫字母在小寫字母之前(例如,Z 在 a 之前)
  • 每個主要分組( androidcomjunitnetorgjavajavax )之間用空行分隔

最初,排序沒有樣式要求,這意味著 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 )可以與語言元素列在同一行。如果有多個註釋或參數化註釋,請按字母順序逐行列出它們。

Java 中三個預定義註解的 Android 標準做法是:

  • 每當不鼓勵使用帶註釋的元素時,請使用@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註釋時,重構代碼以隔離註釋應用的軟件元素。

將首字母縮略詞視為單詞

在命名變量、方法和類時將首字母縮寫詞和縮寫詞視為單詞,以使名稱更具可讀性:

好的壞的
XmlHttpRequest XMLHTTP請求
獲取客戶 ID獲取客戶 ID
類 HTML類 HTML
字符串網址字符串網址
長身份證長身份證

由於 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級別或更高級別觸發日誌記錄的唯一情況是當模塊或應用程序檢測到其自身級別或來自較低級別的錯誤時。
  • 當通常證明某些日誌記錄合理的情況可能多次發生時,最好實施一些速率限制機制,以防止相同(或非常相似)信息的許多重複副本溢出日誌。
  • 網絡連接的丟失被認為是常見的並且是完全可以預料的,不應無故記錄下來。應在DEBUGVERBOSE級別記錄在應用程序內產生後果的網絡連接丟失(取決於後果是否足夠嚴重和意外到足以在發布版本中記錄)。
  • 在第三方應用程序可訪問或代表第三方應用程序訪問的文件系統上擁有完整的文件系統不應記錄在高於 INFORMATIVE 的級別。
  • 來自任何不受信任來源的無效數據(包括共享存儲上的任何文件,或來自網絡連接的數據)被認為是預期的,並且在檢測到無效時不應觸發高於DEBUG級別的任何日誌記錄(甚至隨後記錄應盡可能有限)。
  • 當在String對像上使用時, +運算符隱式地創建一個具有默認緩衝區大小(16 個字符)的StringBuilder實例和可能的其他臨時String對象。因此,顯式創建StringBuilder對象並不比依賴默認的+運算符更昂貴(並且效率更高)。請記住,調用Log.v()的代碼會在發布版本上編譯和執行,包括構建字符串,即使沒有讀取日誌也是如此。
  • 任何打算被其他人閱讀並在發布版本中可用的日誌記錄應該簡潔而不神秘,並且應該是可以理解的。這包括所有到DEBUG級別的日誌記錄。
  • 如果可能,請保持單行記錄。最多 80 或 100 個字符的行長是可以接受的。盡可能避免超過 130 或 160 個字符(包括標籤長度)的長度。
  • 如果日誌記錄報告成功,請不要在高於VERBOSE的級別使用它。
  • 如果您使用臨時日誌記錄來診斷難以重現的問題,請將其保持在DEBUGVERBOSE級別,並使用允許在編譯時禁用它的 if 塊將其括起來。
  • 小心通過日誌的安全漏洞。避免記錄私人信息。特別是,避免記錄有關受保護內容的信息。這在編寫框架代碼時尤其重要,因為提前知道哪些是私人信息或受保護的內容並不容易。
  • 切勿使用System.out.println() (或printf()用於本機代碼)。 System.outSystem.err被重定向到/dev/null ,因此您的打印語句沒有明顯的效果。但是,為這些調用發生的所有字符串構建仍然會執行。
  • 日誌記錄的黃金法則是,您的日誌可能不會不必要地將其他日誌推出緩衝區,就像其他人可能不會推出您的一樣。

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