1.5 最佳实践

优质
小牛编辑
134浏览
2023-12-01
  • 1.介绍
  • 2.源文件基础
  • 3.源文件结构
  • 4.格式
  • 5.命名规范
  • 6.编程实践
  • 7.Javadoc

本文档关于JAVA编码规范,旨在统一编码风格,减少bug处理,降低维护成本,有助于代码审查,促进团队合作,同时养成代码规范的习惯,有助于程序员自身的成长。

2.1 文件名

源文件的命名由文件里的顶级类的名称(区分大小写)以及.java扩展组成


2.2 文件编码

源文件用UTF-8进行编码


2.3 特殊字符

2.3.1 空白字符

除了换行符,源文件中只能用ASCII中的空格字符(0x20)来产生空白,这意味着:

  • 所有字符串中的空格都被转义

  • 制表符不用于缩进

2.3.2 特殊转义字符

对于有特殊意义的转义字符(\b\t\n\f\"\'\\),要使用转义序列的这种书写形式,不要用八进制或者Unicode转义。

2.3.3 非ASCII字符

对于其他的非ASCII字符,是使用实际的Unicode字符,还是使用对应的Unicode码转义,要取决于哪个让代码更加容易阅读和理解。如果不好理解时,最好加上一些注释让别人能够容易看懂。

源文件中的内容主要包括如下几个内容(按照先后顺序):

  • 许可或者版权信息(不是必须有)
  • 包申明
  • Imoprt 语句
  • 顶级类申明(只能有一个)

一般来说,每个内容之间最好要有一行空行进行分隔。


3.1 许可或者版权信息

如果有许可或者版权信息,一般放在文件首部。


3.2 包申明

包申明并没有单行字符个数限制,所以不需要换行。


3.3 Import语句

3.3.1 不要使用通配符

就算是静态导入,也不要用通配符进行导入。


//This is not permitted
import static com.xiaomi.example.*

3.3.2 不要换行

import语句就算很长,也不要换行,它不受单行字符个数的限制。

3.3.3 顺序

import语句按如下顺序排列:

  • 所有的静态导入放在同一个块里
  • 所有的非静态导入放在另外一个块里
import static com.xiaomi.example.Common.AGE;
import static com.xiaomi.example.Common.output;

import com.xiaomi.example.hello;
import com.xiaomi.example.world;

如果同时有静态导入和非静态导入,用空行隔开。同一组的import语句之间不能有空行,同时语句之间按照导入名称进行ASCII顺序排列。

3.3.4 静态内部类导入

静态内部类不能用静态导入,它们使用非静态导入。


3.4 类申明

3.4.1 顶级类只有一个

每个文件最多只有一个顶级类,并且文件名就是该类名。 HelloWorld.java文件内容:

class HelloWorld {
    private int a;
}
//This is Forbidden.
class HelloWorld {
    private int a;
}
class Test{
    private int b;
}

3.4.2 类成员顺序

类成员推荐按如下顺序排列:

  • 公开静态变量
  • 包私有的静态变量
  • 私有静态变量
  • 公开实例变量
  • 包私有的实例变量
  • 私有实例变量
  • 静态构造块
  • 构造函数和构造块
  • 抽象方法(如果这个类是抽象类)
  • 公开实例方法
  • 公开静态方法
  • 私有方法
  • 私有静态方法
  • 非公开内部类
  • 非公开静态内部类
  • 公开内部类
  • 公开静态内部类

可以按照finalregularvolatile进行分组,增加可读性。如果有多个重载函数,应该按顺序放在一起。

class HelloWorld {
    public static int cat;
    static int duck;
    private static int dog;

    static {
        int a;
    }

    public int book;
    int pen;
    private int paper;

    {
        int b;
    }

    public HelloWolrd() {
        //TODO
    }

    public static void doJob() {
        //TODO
    }

    private static void methodB() {
        //TODO
    }

    public void getCat() {
        //TODO
    }

    private void methodA() {
        //TODO
    }
}

4.1 大括号

  1. ifelsefordowhile通常情况下需要和大括号一起使用,就算后面跟着空语句或者只有一条语句,也需要用大括号。

     if (true) {
         //...
     }
    
  2. 非空语句块采用K&R风格,具体如下:

    • 左大括号前不需要换行
    • 左大括号后面需要换行
    • 右大括号前需要换行
    • 右大括号后面如果还有else等代码则不换行;表示终止的右括号必须换行
     return () -> {
         while (condition()) {
             method();
         }
     };
     return new MyClass() {
         @Override
         public void method() {
             if (condition()) {
                 try {
                     something();
                 } catch (ProblemException e) {
                     recover();
                 }
             } else if (otherCondition()) {
                 somethingElse();
             } else {
                 lastThing();
             }
         }
     };
    
  3. 空语句块可以有两种书写风格

    • 可以采用上述的K&R风格
    • 也可以简洁地写成{},不要换行。但是如果这个空语句块是多块语句的一部分(比如if/else 或者 try/catch/finnaly),就只能用K&R风格。
    // This is acceptable
    void doNothing() {}
    // This is equally acceptable
    void doNothingElse() {
    }
// This is not acceptable: No concise empty blocks in a multi-block statement
    try {
        doSomething();
    } catch (Exception e) {}

4.2 块缩进

块缩进采用4个空格

    if (true) {
        //此处缩进4个空格
    }

4.3 每条语句单起一行

每条语句单独一行,不要将多条语句放在一起

    //This is not acceptable
    int a;char b;
    //This is acceptable
    int a;
    char b;

4.4 列限制

  1. 单行限制不超过100个字符,超过需要换行
  2. 但是下面3种情况可以不用遵守这个规则:
    • 无法满足这个规则的地方(比如在Javadoc中很长的URl或者JSNI的长方法引用)
    • 包和导入语句
    • 注释中可以用于剪切粘贴到shell中的命令行

4.5 换行

  1. 通常情况下,当一条语句需要换行时,我们对断行的地方做出如下规定:
    • 在非赋值操作符处断行时,在该符号之前断行
    • 在赋值操作符处断行,那么在赋值操作符后面进行断行
    • 方法名和构造函数名要与紧随其后的左括号放在同一行
    • ,要和前面的符号放在同一行,即在逗号会后换行
    • lambda表达式中的箭头符号处不能断行,除非这个lambda表达式的主体部分没有用大括号进行包裹。
    MyLambda<String, Long, Object> lambda =
        (String label, Long value, Object obj) -> {
            //...
        };
    Predicate<String> predicate = str ->
        longExpressionInvolving(str);
  1. 语句断行时,第二行相对第一行要至少4个空格缩进。从第三行不再继续缩进。

4.6 空白(空行和空格)

4.6.1 空行

如下几个情况需要一个空行:

  • 不同类型的类成员之间用空格进行分隔:成员变量、构造函数、成员函数、内部类、静态初始化块、实例初始化语句块。
  • 方法体内的执行语句组、变量的定义语句组、不同的业务逻辑组之间或者不同的语义之间插入一个空行。相同业务逻辑和语义之间不需要插入空行。
  • 类的第一个成员之前和最后一个成员之后可以都加一个空行。【可选】
  • 本文档中其它章节推荐需要空行的情况。
  • 可以同时连续有多个空行,但是不鼓励。

4.6.2 空格

如下几个情况需要用到空格:

  • 保留关键词和紧跟其后的左括号(需要用空格分开(比如:if,for,catch)。
  • 保留关键词和其前面的右大括号}需要用空格分开(比如:else,catch).
  • 在任何左大括号{之前都是要用空格,但是有两个例外: @SomeAnnotation({a, b}) String[][] x=foo(不需要空格)
  • 所有的二元运算符和三元运算符的两边,都需要空格隔开。还有operator-like的符号也适用这条规则,这些符号包括: 类型界限中的&(<T extends Foo & Bar>) catch语句块中的管道符号(catch (FooException | BarException e)) foreach中的: lambda表达式中的箭头符号((String str) -> str.length())
  • ,,:,;,)之后
  • 如果在一行语句后面进行注释,那么//两边都需要空格
  • 在申明一个变量时,类型和变量之间需要用空格分开
  • 数组初始化时,大括号内侧的空格是可选的(new int[] {5,6}new int[] { 5,6 }都是可以的)

4.6.3 对水平对齐不做要求

没有必要增加如干个空格来使某一行的字符与上一行对齐

    private int x; // this is fine
    private Color color; // this too
    private int   x;      // permitted, but future edits
    private Color color;  // may leave it unaligned

4.7 一些特别的情况

4.7.1 Enum类

  1. 每个逗号后面接一个枚举变量,不需要换行。枚举量之间可以有空行。
  2. 没有方法和doc的枚举类,可以直接携程数组初始化的格式。

     private enum Suit { CLUBS, HEARTS, SPADES, DIAMONDS }
    

4.7.2 变量申明

  1. 每次只申明一个变量,不要使用组合申明,如int a, b
  2. 需要时才申明,并且尽快初始化,最好申明时就初始化

4.7.3 数组

  1. 数组初始化可以用块式的方式初始化,让它看起来就像是块状构造函数。下面的例子都是可以的:

     new int[] {           new int[] {
       0, 1, 2, 3            0,
     }                       1,
                             2,
     new int[] {             3,
       0, 1,               }
       2, 3
     }                     new int[]
                               {0, 1, 2, 3}
    
  2. 不要采用C风格的数组申明方式,也就是说要用String[] args而不是String args[]

4.7.4 Switch语句

  1. 和其他语句块一样,采用2个空格缩进。每个标签之后的非标签行也要比标签行多缩进2个空格。
  2. switch语句中,每个标签的执行代码要么通过breakcontinuereturn或者抛出异常等方式结束,要么就要通过显眼的注释来表明代码是否会继续执行到下一个标签中。任何可以清晰表明这个意思的注释都是可以的,一般用fall through.最后一个标签不需要注释。

     switch (input) {
         case 1:
         case 2:
             prepareOneOrTwo();
             // fall through
         case 3:
             handleOneTwoOrThree();
             break;
         default:
             handleLargeNumber(input);
     }
    
  3. 需要有default标签,即使它没有代码。

4.7.5 注解

  1. 当注解用来注解类、函数或者构造函数时,应该紧随javadoc之后。每一行只有一个注解,这个换行不属于断行,所以不需要缩进。

     @Override
     @Nullable
     public String getNameIfPresent() { ... }
    
  2. 单个注解可以直接和方法签名放在同一行。

     @Override public int hashCode() { ... }
    
  3. 当对成员变量进行注解时,也是紧随javadoc之后,但是允许多个注解和成员变量放在同一行。

     @Partial @Mock DataLoader loader;
    

4.7.6 注释

注释的缩进方式和它所注释的代码的缩进方式相同,可以用/*··· */注释,也可以用//···注释。使用/*···*/进行多行注释时,每一行都应该以*开始,并且上下对齐。

    /*
     * This is          // And so           /* Or you can
     * okay.            // is this.          * even do this. */
     */

4.7.7 修饰符

修饰符的顺序推荐按如下顺序排列: public protected private abstract default static final transient volatile synchronized native strictfp

4.7.8 数字字面量

long类型的字面量不要用小写的l,要用大写的L,避免产生混淆。

4.7.9 方法链

连续调用多个方法时,建议分行写:

    object.method1()
      .method2()
      .method3();

5.1 通用规则

  1. 标识符只能用ASCII字符、数字还有下划线组成。同时,不能以下划线作为开头和结尾(比如_name,name_
  2. 不要用一些没有明确意义的字符作为前缀和后缀 反例:mNames_namekName
  3. 不要使用拼音和英文混合方式,当然也不能用中文
  4. 最好最多只使用4-5个单词

5.2 具体规则

5.2.1 包名

  1. 包名都是小写的,由连续的单词组成
  2. 不需要用特别的符号隔开单词

     //This is accpetable
     com.example.deepspace
     //This is not accpetable
     com.example.deepSpace
     come.example.deep_space
    

5.2.2 类和接口

  1. 使用UpperCamelCase风格
  2. 抽象类以Abstract或者Base开头
  3. 测试类以Test结尾
  4. 异常类以要测试的类的名称开头并以Exception结尾

     class HelloWord {
         //TODO
     }
     abstract class AbstractHelloWord {
         //TODO
     }
     class HelloWordTest {
         //TODO
     }
     class HelloWoldException {
         //TODO
     }
    

5.2.3 方法名

  1. 使用UpperCamelCase风格
  2. 由动词或者动词词组成
  3. 对于ServiceDAO类,根据SOA理念,暴露出的服务一定是接口,内部实现类用Impl的后缀与接口区别 正例:CacheServiceImpl实现CacheService接口
  4. 枚举类建议带上Enum后缀,成员名称全部大写,单词间用下划线分隔开 正例:枚举名字:DealStatusEnum。成员名称:SUCCESS、UNKNOWN_REASON

5.2.4 常量字段

  1. 使用CONSTANT_CASE风格,即字母全部是大写的,单词之间用下划线分开
  2. 常量用static final修饰,是不可变的,包括元数据、String、不可变类型和不可变类型的集合。如果任何一个实例的状态可以发生改变,那它就不是常量。下面举一些例子:
// Constants
    static final int NUMBER = 5;
    static final ImmutableList<String> NAMES = ImmutableList.of("Ed", "Ann");
    static final ImmutableMap<String, Integer> AGES = ImmutableMap.of("Ed", 35, "Ann", 32);
    static final Joiner COMMA_JOINER = Joiner.on(','); // because Joiner is immutable
    static final SomeMutableType[] EMPTY_ARRAY = {};
    enum SomeEnum { ENUM_CONSTANT }
    // Not constants
    static String nonFinal = "non-final";
    final String nonStatic = "non-static";
    static final Set<String> mutableCollection = new HashSet<String>();
    static final ImmutableSet<SomeMutableType> mutableElements = ImmutableSet.of(mutable);
    static final ImmutableMap<String, SomeMutableType> mutableValues =
        ImmutableMap.of("Ed", mutableInstance, "Ann", mutableInstance2);
    static final Logger logger = Logger.getLogger(MyClass.getName());
    static final String[] nonEmptyArray = {"these", "can", "change"};

5.2.5 非常量成员变量

  1. 使用UpperCamelCase风格
  2. 使用名词和名词词组

5.2.6 参数

  1. 使用UpperCamelCase风格
  2. 不能只用单个字符命名,比如int method(int a)是不被允许的

5.2.7 局部变量

  1. 使用UpperCamelCase风格
  2. 就算局部变量是final不可变的,它们也不能被当成常量
  1. @override能用就尽量用,覆盖的方法必须加这个注解
  2. 不能忽视捕获的异常。要么要对其打印异常日志,要么把它重新抛出。如果确实不需要再catch语句块中进行处理,那么也应该通过注释说明。

     try {
         int i = Integer.parseInt(response);
         return handleNumericResponse(i);
     } catch (NumberFormatException ok) {
         // it's not numeric; that's fine, just continue
     }
     return handleTextResponse(response);
    

    例外:在测试中,如果异常的名字就是expected或者以此开头,那么就可以直接忽视,也不用注释

     try {
         emptyStack.pop();
         fail();
     } catch ( NoSuchElementException expected ) {
         //...
     }
    
  3. 静态成员应该通过类访问而不是对象

  4. 不要使用finalize方法。很少情况下会去重载Object.finalize
  5. 不要将类修饰成final
  6. 避免重新定义局部变量和参数,变量只能被分配一次。但是有两点特例:

    • forwhile这类的循环体中的变量,可以看下面的具体代码例子

      for (int i = 0; i < max; i++) {
         /* do stuff */
      }
      while ((line = reader.readLine()) != null) {
         /* do stuff */
      }
      
    • 需要条件初始化的变量

      public void foo(String bar) {
         if (bar == null) {
             bar = DEFAULT_BAR;
         }
      }
      public void goo(int bar) {
         if (bar == -1) {
             bar = DEFAULT_BAR;
         }
      }
      public void hoo(Config config) {
         int bar = config.getBar();
         if (bar == null) {
             bar = DEFAULT_BAR;
         }
      }
      
  7. 有多个构造器的情况下,一般推荐按照参数最多到最少的顺序进行定义。

  8. 不能用过时的类和方法
  9. 当比较相等时,应该使用字面量来调用equals方法,这样能够防止抛出空指针异常

     //Noncompliant
     String myString = null;
     System.out.println("Equal? " + myString.equals("foo")); // Noncompliant; will raise a NPE
     System.out.println("Equal? " + (myString != null && myString.equals("foo")));  // Noncompliant
     //Compliant
     System.out.println("Equal?" + "foo".equals(myString));   // properly deals with the null case
    
  10. 构造方法里禁止加入任何业务逻辑。

  11. 实现Cloneable接口的方法应该实现clone方法

    //Noncompliant
    class Team implements Cloneable {
       private Person coach;
       private List<Person> players;
       public void addPlayer(Person p) {...}
       public Person getCoach() {...}
    }
    //Compliant
    class Team implements Cloneable {
       private Person coach;
       private List<Person> players;
       public void addPlayer(Person p) { ... }
       public Person getCoach() { ... }
       @Override
       public Object clone() {
           Team clone = (Team) super.clone();
           //...
       }
    }
    
  12. public static字段最好设成不变的

  13. POJO类必须重写toString方法,如果继承了另外一个POJO类,注意在前面加上super.toString
  14. 所有相同类型的包装类对象之间值的比较,应使用equals方法
  15. 循环体中的字符串连接,不要用+,可以用StringBulider对象的append方法

    //Noncompliant Code Example
    String str = "";
    for (int i = 0; i < arrayOfStrings.length; ++i) {
       str = str + arrayOfStrings[i];
    }
    //Compliant Solution
    StringBuilder bld = new StringBuilder();
    for (int i = 0; i < arrayOfStrings.length; ++i) {
       bld.append(arrayOfStrings[i]);
    }
    String str = bld.toString();
    
  16. finally块中不应该有诸如returnbreakthrow的跳转语句

    //Noncompliant Code Example
    public static void main(String[] args) {
       try {
          doSomethingWhichThrowsException();
          System.out.println("OK");   // incorrect "OK" message is printed
       } catch (RuntimeException e) {
           System.out.println("ERROR");  // this message is not shown
       }
    }
    public static void doSomethingWhichThrowsException() {
       try {
          throw new RuntimeException();
       } finally {
           for (int i = 0; i < 10; i ++) {
               if (q == i) {
                   break; // ignored
               }
           }
           /* ... */
           return;      // Noncompliant - prevents the RuntimeException from being propagated
       }
    }
    //Compliant Solution
    public static void main(String[] args) {
       try {
           doSomethingWhichThrowsException();
           System.out.println("OK");
       } catch (RuntimeException e) {
           System.out.println("ERROR"); // "ERROR" is printed as expected
       }
    }
    public static void doSomethingWhichThrowsException() {
       try {
           throw new RuntimeException();
       } finally {
           for (int i = 0; i < 10; i++) {
               //...
               if (q == i) {
                   break; // ignored
               }
           }
           /* ... */
       }
    }
    
  17. 添加新字段的子类需要覆盖equals方法

  18. 块同步时应该使用private final的字段或者参数的锁

    //Nocompliant
    private String color = "red";
    private void doSomething() {
       synchronized(color) {  // Noncompliant; lock is actually on object instance "red" referred to by the color variable
           //...
           color = "green"; // other threads now allowed into this block
           // ...
       }
    }
    //Compliant
    private String color = "red";
    private final Object lockObj = new Object();
    private void doSomething() {
       synchronized(lockObj) {
           //...
           color = "green";
           // ...
       }
    }
    
  19. 不能捕获ThrowableError

    //Noncompliant
    try { /* ... */ } catch (Throwable t) { /* ... */ }
    try { /* ... */ } catch (Error e) { /* ... */ }
    //Compliant
    try { /* ... */ } catch (RuntimeException e) { /* ... */ }
    try { /* ... */ } catch (MyException e) { /* ... */ }
    
  20. 不能直接抛出诸如ErrorRuntimeExceptionThrowableException这样的通用异常,一般要自定义一个异常类

    // Noncompliant
    public void foo(String bar) throws Throwable {
       throw new RuntimeException("My Message");
    }
    //Compliant Solution
    public void foo(String bar) {
       throw new MyOwnRuntimeException("My Message");
    }
    

7.1 格式

  1. 注释的通用格式区分单行和多行,一般格式如下:
  2. 空白行是以*作为对齐,后面没有内容的一行,通常在段落之间以及段落标签之前会用到它。每个段落之间通常在*后面加一个<p>标签。
  3. 使用的块标签一般按照@param,@return,@throws,@deprecated顺序排列,这些标签后面不能不带内容,没有内容就不需要写这个标签。如果标签的内容单行无法容纳,那么从@的地方缩进四个空格。

7.2 摘要片段

每个类或成员的Javadoc以一个简短的摘要片段开始。可以看下面的一个例子:

/**
 * Returns an Image object that can then be painted on the screen.
 * The url argument must specify an absolute {@link URL}. The name
 * argument is a specifier that is relative to the url argument.
 * <p>
 * This method always returns immediately, whether or not the
 * image exists. When this applet attempts to draw the image on
 * the screen, the data will be loaded. The graphics primitives
 * that draw the image will incrementally paint on the screen.
 *
 * @param  url  an absolute URL giving the base location of the image
 * @param  name the location of the image, relative to the url argument
 * @return      the image at the specified URL
 * @see         Image
 */
public Image getImage(URL url, String name) {
   try {
       return getImage(new URL(url, name));
   } catch (MalformedURLException e) {
       return null;
   }
}

7.3 哪些地方需要Javadoc

所有的public类、publicprotected的成员变量和方法都应该注释。不过有两个例外:

  • 方法本身就能显而易见的说明自身的用处,比如getFoo
  • 覆盖的方法也不一定需要javadoc