Android代码贡献者编码风格指南

原文链接: https://source.android.com/source/code-style.html


Java语言规则

不要忽略异常

有时你会写出完全忽视异常的代码:

void setServerPort(String value) {
    try {
        serverPort = Integer.parseInt(value);
    } catch (NumberFormatException e) { }
}

一定不要这样做。尽管你可能认为你的代码不会遇到这个错误,或者认为处理错误是不重要的,向上面那样忽略异常会会以后的代码留下隐患,其他人很可能会掉入这个陷阱中。你必须处理代码中的每一个异常。特定的处理方式要按具体情况而定。

扫描二维码关注公众号,回复: 3741157 查看本文章

以下是可以接受的做法:

将异常抛向当前方法的调用者。

void setServerPort(String value) throws NumberFormatException {
    serverPort = Integer.parseInt(value);
}

2 抛出一个适合当前层级的新的异常。

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

4 捕获一个异常并且抛出一个新的运行时异常。这很危险,只有在一种情况下可以这样做:当这个异常发生时让程序崩溃是恰当的。

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

要把原始异常的信息传到运行时异常的构造函数中。如果你的代码在Java 1.3之下编译,你需要忽略原始异常的信息。

5 如果你确定忽略异常是合理的,你可以忽略它,但是一定要写上注释来说明为什么。

/** 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!
}

不要这样做。在几乎所有情况中,捕获一般的异常或Throwable都是不恰当的。最好不要捕获Throwable,因为它里面包括错误异常(Error)。这很危险。这意味着不期望捕获的异常也会在应用层程序的错误处理层面被捕获到(包括像 ClassCastException这样的运行时异常)。这掩盖了你代码中处理代码属性的错误。这意味着如果其他人向你调用的代码中加入了其他类型的异常,编译器不能让你意识到你需要对这个异常进行特别处理。在大多数情况中,你不应该使用相同的错误处理方式来处理不同类型的异常。

以下方式是捕获一般异常的替代方案:

在一个单独的try块之后使用多个catch块捕获多个类型的异常。这虽然显得很拙劣, 但是比捕获一般异常更好。注意不要在多个catch块中写过多的重复代码。

将你的代码重构成多个try块,在语法上分离出IO操作,在每个try块上分别处理异常。

重新抛出异常。 在很多时候,在当前代码层级上,你不必捕获异常,只让当前代码抛出异常即可。

记住:异常是你的朋友,当编译器提示你未捕获某个异常,不要感到生气。要保持微笑, 因为编译器能够很容易的让你捕获运行时的问题。

不要使用finalize方法

finalize方法是对象在被垃圾收集的时候执行的代码块。

好处:可以被用来做清理工作,尤其是外部资源。

弊端:该方法被调用的时机不确定。

决定:我们不使用finalize方法。在多数情况下,你可以从finalize方法中做你需要做的事情,并且很好的处理了意外情况。如果你确实需要这样做,定义一个类似close()的方法,并且在文档中写明什么时候应该调用它。以InputStream来举例说明。在这种情况下,这是合理的,但是只要它不是用来打印日志的,并不要求它在finalize方法中打印一个简短的消息。

规范导入

如果你想使用foo包中的类Bar,有两种方式导入它:

1.  import foo.*;

好处:减少了导入语句的数量。

2.  import foo.Bar;

好处:可以明显的看到使用了什么类,对于维护人员来说代码更具可读性。

决定:在Android中使用后一种方式来导入。Java标准库和测试类库可以使用第一种方式导入,如:java.util.*, java.io.* 和 junit.framework.* 

Java库使用规则

对于Android中的Java类库和工具, 也有一些使用惯例。在一些情况下,这些惯例已经发生了重大改变,所以老代码可能用了过时的模式和类库。当使用这些代码的时候,遵循已有的风格也是可以的。当写一些新的组件时,不要再使用过时的类库。

Java风格规范

使用Javadoc标准注释

所有的文件都应该在顶部有一个版权声明。紧接着是包和导入, 每部分以空白行分割。 接下来是类或接口声明,使用Javadoc注释描述类或者接口的用途。

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

你不必为不重要的getset方法写Javadoc注释。如果这个方法做了一些更复杂的事情(比如强加了一个约束或者有一个重要的副作用), 那么就必须为这个方法写注释。并且如果setget方法的属性的意义不是显而易见的, 那么也要写注释。

你写的每一个方法,不管是否是公有的, 都会从Javadoc中受益。公有方法是API的一部分,所以他们需要Javadoc

目前安卓并没有强制Javadoc注释的具体风格,但是你应该遵循Java语言的编码规范。

写短的方法

在可行的程度上, 方法应该保持短小并且有针对性。 然而有时长方法也是合理的,所以并没有严格限制方法的长度。如果一个方法超过了40行,在不破坏程序结构的情况下, 试着将方法拆分。

在标准的地方定义属性

所有属性应定义在文件的开始处, 或者定义在使用它的方法的上面。

限制局部变量的作用域

局部变量的作用于应该限制到最小。这样可以增加代码的可读性和可维护性, 降低出错的可能性。每个变量都要声明在使用这个变量的最内层的块中。

局部变量应该在第一次使用的时候声明。每个变量在声明的时候尽量初始化。如果没有足够的信息来初始化这个变量, 那么将这个变量的声明推迟到有足够的信息对它初始化的时候。

这个规则的一个特例是try-catch块。 如果一个变量被一个声明抛出受检查异常的方法的返回值初始化, 那么它必须被放在try-catch块中初始化。 如果这个变量必须在try-catch块之外被使用, 那么必须在try-catch块之前声明, 这是它不能被显式的初始化。

// 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  Android中的导入

2  第三方库的导入(com, junit, net, org

3  javajavax

如果要完全符合IDE的设置,应该这样做:

每个导入分组都按字母顺序排序,并且大写字母排在小写字母之前

每个分组之间都应该有一个空行

起初并没有要求导入的具体风格。 IDE有时会改变导入的顺序,或者IDE的开发者必须禁用自动导入的特性,手动导入。 这被认为是不好的做法。当要求编码符合java风格时,遵循的风格五花八门。归根结底, 指定具体的顺序更能保持一致性。所以我们选择了一个风格,更新了这个风格指南,并且让IDE遵循这一风格。 我们期望当IDE的使用者在编码时,所有包中的导入都遵循这一模式,而没有任何额外的工程错误。

这个被选定的风格如下:

人们最想首先看到的导入排在最前面(android

人们最不想先看到的导入排在最后面(java

人可以容易的遵循这一风格

4 IDE可以遵循这一风格

静态导入的使用和放置的位置稍微存在争议。 有人希望静态导入分散在已存在的导入之中, 有人希望静态导入放置在其他导入的上面或下面。 我们还没有提出一个方案可以让所有IDE使用相同的顺序。

因为大部分人将这个问题视为低优先级的问题,所以你需要自己做判断, 并且保持一致性。

使用空格进行缩进

我们使用四个空格对代码块进行缩进。 我们不使用Tab键。 如果你不能确定,请让你的代码和周围的代码保持一致。 

我们用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,便于复制或剪切。

例外:导入行可以打破这一限制,因为人们很少看导入语句。 这也简化了工具的编写。

10 使用标准的Java注解

对于同一个语言元素,注解应该位于其他访问修饰符之前。简单的标志性的注解可以和一个语言元素位于同一行, 如(Override)。如果存在多个注解,或者带参数的注解,他们必须按字母顺序放置在语言元素之前的行上。

对于Java中三个内置注解,Android的标注实践如下:

1 @Deprecated: Deprecated注解用于过时的元素上。如果你使用了Deprecated注解,那么必须有一个@deprecated文档标签,并且指定一个可替换的实现。此外,记住被Deprecated注解标示的方法同样是可以工作的。

如果你看到旧代码中有一个@deprecated文档标签,请为他加上Deprecated注解。

2 @Override: Override注解必须用在实现了接口中的方法或者覆盖了父类中的方法的时候。

举例来说, 如果你使用了@inheritdocs文档标签,并且是从一个类派生而来,那么你必须在该方法上表上Override注解。

3 @SuppressWarnings: 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注解的时候,代码必须被重构来隔离使用该注解的代码。

11 将缩略词当成一个单词

在给字段,方法或者类命名的时候, 将缩略词当做一个独立的单词。这样的命名会更加易读:


所有JDKAndroid中的代码都在缩略词方法非常不一致。所以让你的代码和周围代码保持一致几乎是不可能的。克服困难,将缩略词当做一个单词。

12 使用TODO注释

为临时代码,简短的解决方法或者已经足够好但还不完美的代码使用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注释说明的是将来要做什么事,确保包含一个确切的日期,或者一个特定的事件。

13 简化日志

尽管输出日志十分有必要,但是却对系统的性能有负面作用。如果不保持日志的简洁,日志会失去应有的作用。日志工具提供5种不同级别的输出。下面是不同的日志级别和什么时候使用它们,怎样使用它们。

1 ERROR: 当发生一些致命性的错误的时候应该使用这个级别的日志。这些错误导致用户可见的结果,如果不明确的删除一些数据,卸载应用,擦除数据分区或者刷新系统就无法恢复。这个级别的日志总是会输出。该级别的日志导致的问题可以被报告给统计服务器。

2 WARNING: 当发生一些严重的出乎意料的事情时输出该级别的日志。这些事情会导致用户可见的结果,但是通过明确的做一些动作,等待或者重启应用程序来降低应用程序的版本或者重启设备可以不丢失数据的恢复。该级别的日志总会被输出。该级别的日志引起的问题也可以报告给统计服务器。

3 INFORMATIVE: 该级别的日志用来通知大多数人感兴趣的事情发生了。 也就是当一些有着广泛影响的情况发生时输出该级别的日志,尽管这不是一个错误。这种情况只会被一个在特定领域中具有权威性的模块输出。这个级别的日志也经常被输出。

4 DEBUG: 这个级别的日志应该被用来通知一些事情的发生, 这些事情对于调查和调试意料之外的行为有重大意义。你只应该输出能反映你的组件的运行情况的信息。

该日志在正式发布的版本中也可以输出。该日志要求放在if (LOCAL_LOG) o或者if (LOCAL_LOGD) 代码块中使用。 LOCAL_LOG[D]被定义在你的类或者子组件中,所以可以禁用所有这些输出。在 if (LOCAL_LOG)中不能有其他逻辑代码。所有创建输出信息字符串的逻辑要全部放在if (LOCAL_LOG)块中。输出日志的代码不能被重构到一个方法中。

仍然存在if (localLOGV)代码,这被认为是可以接受的,尽管命名不标准。

5 VERBOSE: 该级别的日志用于所有其他情况。这种日志只应该出现在调试版本中,并且应该位于 if (LOCAL_LOGV)块中,以便于在编译时不被编译到代码中。所有创建日志字符串的代码都必须不包含在发布版本中,并且位于if (LOCAL_LOGV)块中。

注意:

在一个给定的模块中,如果不是在 VERBOSE 级别, 如果可能的话,一个错误应该只被报告一次。在一个模块中的一个单独的方法调用链中,只有最内层的方法才应该返回错误,如果可以明显的帮助隔离一些问题,该方法的调用者应该加上一些日志 。

在一些相互关联的模块中,如果不是 VERBOSE 级别的日志,如果低层的模块检测到了来自于高层模块的非法数据,低层模块只应该打印DEBUG级别的日志来说明这种情况。只有日志提供了调用者不能获得的信息时,才应该传递到调用者。在抛出异常的地方, 一般不需要打印日志(异常应该包含所有相关的信息)。只使用一个错误码就可以说明的信息, 也不必使用日志。这在框架和应用程序的交互过程中十分重要。由第三方应用程序引起的,应该由框架处理的状况不能触发高于DEBUG级别的日志。只有当一个模块检测到来自它所在的当前层级或者更低层级的错误时,才应该输出INFORMATIVE或更高级别的日志。

当一些日志输出会多次发生,实现一些限制机制来阻止相同或相似的日志输出,是一个好的主意。

4失去网络连接是很普遍的,不必输出日志。在一个应用中,会导致一些后果的网络断开连接,应该以DEBUGVERBOSE级别的日志输出(这取决于导致的后果是否足够严重, 或者是否应该存在以发布版本中)。

可被第三方应用程序访问的整个文件系统不应该输出高于INFORMATIVE级别的日志。

来自不受信任的源的无效数据被认为是可以预期的,不应该触发高于DEBUG级别的日志。

要留意操作符+, 当使用它操作字符串对象时,会隐式创建一个包含16个字符的缓冲区的StringBuilder 对象,还会创建一些临时的String对象。显式的创建StringBuilder 对象并不比使用+操作符更加昂贵。同样要牢记,调用 Log.v()的方法会被编译进发布版本中, 并且会执行,会创建String对象,即使这些日志不会被读到。

需要被其他人读取的日志或者需要在发布版本中存在的日志都不应该被加密, 并且应该容易让人读懂。所有日志都应该这样。

如果可能的话, 尽量将日志保持在一行中。80100的行长度是可以接受的,要避免超过130个字符或160个字符的行。

10 提示操作成功的日志不应该高于VERBOSE级别。

11 用于诊断难以重现的问题的临时日志必须使用DEBUG VERBOSE级别, 并且应该被封装在if块中,以便在编译时整个禁用该日志。

12 要小心日志带来的安全漏洞。避免输出私有信息。避免输出受保护内容的相关信息。这在写框架代码时至关重要,因为无法提前得知什么是私有信息或者保护内容。

13 不要使用System.out.println() System.out System.err 被重定向到 /dev/null ,所以你不会看到任何输出,但是创建日志字符串的操作同样会执行。

14 日志输出的黄金法则是,你的日志不要将其他日志刷出缓冲区,就像其他日志也不会将你的日志刷出缓冲区。

14 保持一致性

我们的部分想法:要保持一致性。如果你正在编码,花几分钟的时间看看你周围的代码并判断他的风格。如果他们在if子句周围使用空格,你也应该这样做。如果他们的注释被星号包围,也这样写你自己的注释。

拥有风格指南的关键点是拥有一个通用词汇表,这样人们就能明确你在说什么,而不是你是怎样说的。我们在这里给出全局的风格来让人们知道这个词汇表。但是局部的风格同样重要。如果你添加进一个文件的代码和周围的代码风格很不一样,会打乱读代码的人的节奏。要避免这样做。

Java测试风格规范

1  遵循测试方法命名惯例

当为测试方法命名时,你可以使用下划线来分割要测试的部分和要测试的特定案例。这种风格可以清晰的看出正在测试什么用例。

举例:

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



猜你喜欢

转载自blog.csdn.net/brave2211/article/details/25187933