貢獻者的 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 標準註釋

每個文件的頂部都應該有一個版權聲明,然後是包和導入聲明(每個塊由一個空行分隔),最後是類或接口聲明。在 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”,則無需為諸如setFoo()之類的瑣碎的 get 和 set 方法編寫 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註釋時,重構代碼以隔離應用註釋的軟件元素。

將首字母縮略詞視為單詞

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

好的壞的
XmlHttp請求XMLHTTP請求
獲取客戶編號獲取客戶ID
類 Html類 HTML
字符串網址字符串網址
長號長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級別或更高級別觸發日誌記錄的情況是模塊或應用程序在其自身級別或來自較低級別檢測到錯誤時。
  • 當通常證明某些日誌記錄合理的情況可能會多次發生時,實施某種速率限制機制以防止日誌溢出相同(或非常相似)信息的許多重複副本可能是個好主意。
  • 網絡連接丟失被認為是常見的並且是完全可以預料的,不應無緣無故地記錄下來。應在DEBUGVERBOSE級別記錄在應用程序中產生後果的網絡連接丟失(取決於後果是否足夠嚴重和意外到足以記錄在發布版本中)。
  • 在可供第三方應用程序訪問或代表第三方應用程序訪問的文件系統上擁有完整的文件系統不應記錄在高於信息性的級別。
  • 來自任何不受信任來源的無效數據(包括共享存儲上的任何文件,或來自網絡連接的數據)被認為是預期的,並且當檢測到無效數據時不應觸發高於DEBUG級別的任何日誌記錄(甚至然後記錄應盡可能有限)。
  • 當用於String對象時, +運算符會隱式創建一個具有默認緩衝區大小(16 個字符)和可能的其他臨時String對象的StringBuilder實例。因此,顯式創建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))
}