此页面上的代码样式是向 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
)并适当地处理错误。不过,在这样做之前请仔细考虑,并在评论中解释为什么它在这种情况下是安全的。
捕获一般异常的替代方法:
- 作为多捕获块的一部分分别捕获每个异常,例如:
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()); }
订单进口报表
导入语句的顺序是:
- 安卓导入
- 从第三方(
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
)可以与语言元素列在同一行。如果有多个注释或参数化注释,请按字母顺序每行列出一个。
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
级别或更高级别触发日志记录的情况是模块或应用程序在其自身级别或来自较低级别检测到错误时。 - 当通常证明某些日志记录合理的情况可能会多次发生时,实施某种速率限制机制以防止日志溢出相同(或非常相似)信息的许多重复副本可能是个好主意。
- 网络连接丢失被认为是常见的并且是完全可以预料的,不应无缘无故地记录下来。应在
DEBUG
或VERBOSE
级别记录在应用程序中产生后果的网络连接丢失(取决于后果是否足够严重和意外到足以记录在发布版本中)。 - 在可供第三方应用程序访问或代表第三方应用程序访问的文件系统上拥有完整的文件系统不应记录在高于信息性的级别。
- 来自任何不受信任来源的无效数据(包括共享存储上的任何文件,或来自网络连接的数据)被认为是预期的,并且当检测到无效数据时不应触发高于
DEBUG
级别的任何日志记录(甚至然后记录应尽可能有限)。 - 当用于
String
对象时,+
运算符会隐式创建一个具有默认缓冲区大小(16 个字符)和可能的其他临时String
对象的StringBuilder
实例。因此,显式创建StringBuilder
对象并不比依赖默认的+
运算符更昂贵(而且效率更高)。请记住,调用Log.v()
的代码是在发布版本上编译和执行的,包括构建字符串,即使没有读取日志也是如此。 - 任何旨在供其他人阅读并在发布版本中可用的日志记录都应该简洁而不晦涩,并且应该易于理解。这包括所有直到
DEBUG
级别的日志记录。 - 如果可能,请保持单行记录。可接受最长 80 或 100 个字符的行。尽可能避免长度超过 130 或 160 个字符(包括标签的长度)。
- 如果日志记录报告成功,切勿在高于
VERBOSE
的级别使用它。 - 如果您使用临时日志记录来诊断难以重现的问题,请将其保持在
DEBUG
或VERBOSE
级别并将其包含在允许在编译时禁用它的 if 块中。 - 小心日志中的安全漏洞。避免记录私人信息。尤其要避免记录有关受保护内容的信息。这在编写框架代码时尤为重要,因为事先很难知道哪些是私人信息或受保护的内容,哪些不是。
- 切勿使用
System.out.println()
(或本机代码的printf()
)。System.out
和System.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)) }