AOSP 贡献者 Java 代码风格

此页面上的代码风格是为 Android 开源项目 (AOSP) 贡献 Java 代码的严格规则。通常不接受不遵守这些规则对 Android 平台的贡献。我们认识到并非所有现有代码都遵循这些规则,但我们希望所有新代码都符合要求。请参阅尊重地编写代码,了解要使用和避免使用的术语示例,以构建更具包容性的生态系统。

保持一致

最简单的规则之一是保持一致。如果您正在编辑代码,请花几分钟查看周围的代码并确定其风格。如果该代码在 if 子句周围使用空格,您也应该这样做。如果代码注释周围有小星星框,请使您的注释周围也有小星星框。

制定风格指南的目的是为了拥有通用的编码词汇表,以便读者可以专注于您所表达的内容,而不是您表达的方式。我们在此处介绍全局风格规则,以便您了解词汇表,但局部风格也很重要。如果您添加到文件中的代码与周围的现有代码看起来截然不同,则读者在阅读时会感到节奏被打乱。尽量避免这种情况。

Java 语言规则

Android 遵循标准的 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);
        }
      }
  • 作为最后的手段,如果您确信忽略异常是合适的,那么您可以忽略它,但您还必须评论原因,并给出充分的理由。
    /** 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 语句的数量。

  • import foo.Bar;

    使使用的类一目了然,并且代码对于维护人员来说更具可读性。

对于导入所有 Android 代码,请使用 import foo.Bar;。Java 标准库(java.util.*java.io.* 等)和单元测试代码(junit.framework.*)是显式例外。

Java 库规则

使用 Android 的 Java 库和工具存在一些约定。在某些情况下,约定已发生重大变化,较旧的代码可能使用已弃用的模式或库。当处理此类代码时,可以继续使用现有的风格。但是,在创建新组件时,永远不要使用已弃用的库。

Java 风格规则

使用 Javadoc 标准注释

每个文件的顶部都应包含版权声明,后跟 package 和 import 语句(每个块之间用空行分隔),最后是类或接口声明。在 Javadoc 注释中,描述类或接口的作用。

/*
 * Copyright (C) yyyy 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) {
    ...
}

对于简单的 get 和 set 方法(例如 setFoo()),如果您的所有 Javadoc 都只是说“sets Foo”,则无需编写 Javadoc。如果该方法执行了更复杂的操作(例如强制执行约束或具有重要的副作用),则必须对其进行文档记录。如果不清楚属性“Foo”的含义,则应对其进行文档记录。

您编写的每个方法(公共或其他方法)都将从 Javadoc 中受益。公共方法是 API 的一部分,因此需要 Javadoc。Android 不强制执行编写 Javadoc 注释的特定风格,但您应遵循如何为 Javadoc 工具编写文档注释中的说明。

编写简短的方法

在可行的情况下,保持方法小而专注。我们认识到,有时较长的方法是合适的,因此对方法长度没有硬性限制。如果一个方法超过 40 行左右,请考虑是否可以在不损害程序结构的情况下将其分解。

在标准位置定义字段

在文件顶部或紧接在使用它们的Methods之前定义字段。

限制变量作用域

将局部变量的作用域保持在最低限度。这可以提高代码的可读性和可维护性,并降低出错的可能性。在包含变量所有用法的最内层代码块中声明每个变量。

在首次使用局部变量的位置声明它们。几乎每个局部变量声明都应包含一个初始值设定项。如果您还没有足够的信息来合理地初始化变量,请推迟声明,直到您有足够的信息为止。

例外是 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());
}

对 import 语句排序

import 语句的顺序为:

  1. Android imports
  2. 来自第三方的 imports(comjunitnetorg
  3. javajavax

为了与 IDE 设置完全匹配,imports 应为:

  • 在每个分组内按字母顺序排列,大写字母在小写字母之前(例如,Z 在 a 之前)
  • 每个主要分组之间用空行分隔(androidcomjunitnetorgjavajavax

最初,对排序没有风格要求,这意味着 IDE 要么总是更改排序,要么 IDE 开发者必须禁用自动 import 管理功能并手动维护 imports。这被认为是不好的。当询问 Java 风格时,首选风格差异很大,最终 Android 需要简单地“选择一个排序并保持一致”。因此,我们选择了一种风格,更新了风格指南,并使 IDE 遵守它。我们希望随着 IDE 用户处理代码,所有包中的 imports 都将匹配此模式,而无需额外的工程工作。

我们选择这种风格,以便:

  • 人们希望首先查看的 imports 往往位于顶部(android)。
  • 人们希望最后查看的 imports 往往位于底部(java)。
  • 人类可以轻松地遵循这种风格。
  • IDE 可以遵循这种风格。

将静态 imports 放在所有其他 imports 之上,并以与常规 imports 相同的方式排序。

使用空格进行缩进

我们对代码块使用四个 (4) 空格缩进,从不使用制表符。如有疑问,请与周围的代码保持一致。

我们对换行符使用八个 (8) 空格缩进,包括函数调用和赋值。

推荐

Instrument i =
        someLongExpression(that, wouldNotFit, on, one, line);

不推荐

Instrument i =
    someLongExpression(that, wouldNotFit, on, one, line);

遵循字段命名约定

  • 非公共、非静态字段名称以 m 开头。
  • 静态字段名称以 s 开头。
  • 其他字段以小写字母开头。
  • 静态 final 字段(常量,深度不可变)为 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 个字符。
  • Import 行可以超出限制,因为人们很少看到它们(这也简化了工具编写)。

使用标准 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 XMLHTTPRequest
getCustomerId getCustomerID
class Html class HTML
String url String URL
long id long 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 级别记录(取决于后果是否足够严重且意外,足以在发布版本中记录)。
  • 在第三方应用可访问或代表第三方应用访问的文件系统上拥有完整的文件系统不应以高于 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))
}