Java编码约定

代码约定提高了软件的可读性,使工程师能够更快、更彻底地理解彼此的代码。 本文参考Google Java 代码规范 和 Oracle 的 Code Conventions 结合自己项目中的思考合并而来。

1 源文件基础

1.1 编码格式:UTF-8

源文件采用UTF-8编码

1.2 特殊字符

1.2.1 转义字符

对于任何有特殊意义的转义序列应当用(\b,\t,\n,\f,\r,",'和),而不是用八进制转义符(e.g.\012)或Unicode转义符(e.g.\u000a)

1.2.2 非ASCII字符

对于非ASCII字符,采用Unicode字符(e.g.∞)或Unicode转义符(e.g.\u221e)。这取决于那种方式更易于理解,建议不要才有Unicode转义符,即使有注释。

示例

Example    Discussion
String unitAbbrev = "μs";    最优解:清晰明了甚至不需要注释
String unitAbbrev = "\u03bcs"; // "μs"    允许:但是没必要这么做
String unitAbbrev = "\u03bcs"; // 希腊字母mu,"s"    允许:但是笨拙且易出错
String unitAbbrev = "\u03bcs";    不建议:不易理解
return '\ufeff' + content; // 字节顺序标记    良好:对于不会打印出的字符在需要的时候提供了注释

不要担心一些程序不能处理非ASCII字符而使得你的程序不可读,如果发生这种情况,程序将会被破坏,会有人修复的。

2 源文件结构

2.1 头部内容

Java源文件头部按顺序包括:

  • 许可或版权信息(如果有的话)
  • 包声明 (不换行)
  • 导入声明 (不换行,禁用通配符的方式导入,不导入内部类)
  • 类说明

2.2 类中内容的顺序

  1. 禁止添加方法的时候直接添加在类的最后。
  2. 相似方法、类似业务处理代码放在一起。
  3. 重载:永远放在一起。 当一个类有多个构造器或多个同名方法时,它们应该依次放在一起,中间没有其他代码,甚至是私有成员。

3 代码格式

术语小贴士:块状构造是指类,方法或构造函数的主体。注意,根据第4.8.3.1节关于数组初始化程序的介绍,可以选择将任何数组初始化程序视为类似于块的构造。

格式尽量沿用主流IDE IntelliJ的默认格式,避免在这上面花费过多精力。

3.1 大括号

3.1.1 大括号用于可选的地方

大括号和if,else,for,do还有while语句一起使用,即使代码块儿为空或只有一行。

3.1.2 非空块儿:K & R风格

在非空块儿和块状构造中,大括号准守Kernighan和Ritchie风格(“埃及风格”):

  • 左括号前不换行
  • 左括号后换行
  • 右括号前换行
  • 仅在大括号终止语句或终止方法,构造函数或命名类的主体时,才在右括号后换行。例如,如果大括号后跟着else或逗号,则没有换行符。

采用这种方式也是因为这个是大部分IDE (如: IntelliJ)默认格式。

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();
        }
        {
            int x = foo();
            frob(x);
        }
    }
};

3.1.3 空块儿:可以简介一些

一个空的块或构造块儿可以按K&R风格,也可以把左右括号紧挨着,中间没有字符或空行,除非这时多个块中的一部分(一个直观的例子就是代码块儿:if/else或try/catch/finally)。

例如:

// 这时被允许的
void doNothing() {}

// 这也是被允许的
void doNothingElse() {
}

// 这是不允许的:在多块儿代码中不是简洁代码
try {
    doSomething();
} catch (Exception e) {}

3.2 块儿缩进:四个空格

每个新块儿或块状构造中,都要缩进四个空格。当块结束时,缩进返回到原来的状态。缩进适用于代码和注释。(参见3.1.2,非空代码块儿:K&R风格)

3.3 每句代码占一行

每句代码后面跟一个换行符。

3.4 每行字数限制:100

Java代码限制每行最多100个字符。一个字符就是任意的一个Unicode单元,除非另有说明,任何超过限制的代码都应该换行,就像3.5换行解释的那样。每个Unicode单位算一个字符,不管其展示如何。例如,在全角模式下,你可能比半角模式更早的换行。

例外情况:

  • 不是每一行都必须遵守字数限制(例如,Javadoc中的长URL或长JSNI方法引用)
  • 包声明和导入声明(详见,3.2包声明和3.3导入声明)
  • 换行会导致不在注释区的注释

3.5 换行原则

  1. 在非运算符处换行,具体细节可以参考Google的说明
  2. 换行比原行缩进至少4个空格;IntelliJ默认8个
  3. 当有多行换行时,通常,两条连续在当它们在句法上平行时使用相同的缩进级别。

3.6 空行

空行根据逻辑来添加空行, 禁止连续两个空行

3.7 横向空格

除了语言或其他样式规则的要求之外,除了文字,注释和Javadoc外,单个ASCII空间也仅出现在以下位置:

  1. 将保留字例如if,for或catch与后面的左括号(()分开。如: if (condition)
  2. 将保留字例如else或catch与前面的右括号(})分开
  3. 在所有左大括号前({),有两个例外: SomeAnnotation({a, b})String[][] x = {{"foo"}};
  4. 在二元或三元操作符两边: if (userInfoStr != null)

一般保持IntelliJ默认即可

3.8. 变量声明

  • 每次声明一个变量
  • 在需要的地方声明变量;尽量在靠近它第一次被使用的地方(也可能一组业务相关的变量,可提前放在一起生命)。变量声明要么附带赋值,要么马上赋值。

3.9 Switch语句

  • 在switch块内,,每个语句必须通过break,continue,return或抛出异常)
  • 提供default条件; 每个switch语句要包含一个default语句。即使后面没有代码。例外:如果包含了所有的枚举情况,枚举的switch语句可以省略default。IDE或者静态检查工具可以分析出是否漏掉了某些情况。

3.10 注释

本节主要讲普通注释。Javadoc在后面单独讲。 任何换行符前都可以加任意的空格然后添加普通注释,这中注释使这行成为非空白行。

3.10.1 块儿注释风格

块儿注释和它所注释的代码保持相同的缩进。他们可能是/ ... /或// ...。对于多行/ ... /注释后续行必须以开头并且与上一行的对齐。

/*
 * This is          
 * okay.            
 */

 // And so          
 // is this.          

  /* Or you can
   * even do this. */

注释不包含在有型号或其他字符的框中。

当写多行注释时,如果你想在注释自动换行时。使用/ ... /。大多数格式化工具对// ...没有处理。

3.11 修饰符

如果有类和成员的修饰符,按Java语言的推荐顺序来写:

public protected private abstract default static final transient volatile synchronized native strictfp

3.12 数字

long类型的值用大写字母L,不要用小写l(避免和数字1混淆),例如,使用3000000000L而不是3000000000l

4 命名

4.1 所有标识的通用规则

标识使用ASCII字母或数字并且仅在少量的情况下使用下划线。

4.2 标识类型的规则

4.2.1 包名

包名采用全小写,通过连续的简单单词组合在一起(没有下划线),例如采用com.example.deepspace,而不是com.example.deepSpace或者com.exaple.deep_space。

4.2.2 类名

  • 类名采用大写驼峰UpperCamelCase 。
  • 类名通常是名称或名词短语。例如,Charater或ImmutabelList。
  • 接口名字也可能是名词或名词短语(例如,List),但有时也可能是形容词或形容词短语。
  • 注解的命名没有特殊的或者已经完备的规则。
  • 测试类命名以被测试类名开头,以Test结尾。例如,HashTest或者HashIntegrationTast。

4.2.3 方法名

  • 方法名采用小写小写驼峰lowerCamelCase 。
  • 方法名通常是动词或动词短语。例如,sendMessage或stop 。
  • 下划线在JUnit测试方法中划分不同的逻辑部分,每个部分用小写的驼峰来写。通常采用_,例如pop_emptyStack。对于测试方法没唯一的正确名命方法。

4.2.4 常量名

常量名使用CONSTANT_CASE:全大写字母,单词之间使用下划线分割。什么是常量呢? 常量是静态的final字段,其值确定并且方法不能改变。包括,基本类型,字符串,不可变类型,以及不可变类型的集合。如果有一个实例可以改变,那这个实例不是常量。一般的变量即使不打算改变其值也不是常量。例如:

// 常量
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 }
// 非常量
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"};

他们的名字通常都是名词或名词短语

//全局常量即:跨类使用的常量,采用接口的方式分组定义

/**
 * @author : Joe
 * @date : 2021/1/14
 */
public interface Constants {

    Long DISTRIBUTOR_ID_WX = 931L;//小程序
    interface Log{
        String TRACE_ID = "X-TraceId";
        String BIZTYPE = "X-BizType";
        String KEYID = "X-KeyId";
    }

    interface UserName{
        String ADMIN_USER = "adminUsername";
        String ANNON_USER = "annon";
    }
}

注意考虑面向对象的封装性,单个类使用的常量直接定义在其类中即可。

4.2.5 非常量名 参数名 局部变量

采用小驼峰。

4.2.6 泛型名

泛型遵守以下规则之一:

  • 单个大写字母,后面可以跟单个数字(例如E, T, X, T2)
  • 以类的形式命名,后面跟上大写字母T(例如:RequestT,FooBarT)。

4.3 驼峰:定义

CamelCase.png

4.3 上下文考虑

Java是面向对象的, 在命名的时候要考虑上线文,避免累赘的添加领域名称的前缀。参考以下示例来理解:

类名
错误 正确 说明
com.tyymt.sota.controller.admin.AdminController com.tyymt.sota.controller.admin.Controller 上级 (包)已经确定了是Admin相关,无需在Class名上多此一举
com.tyymt.sota.controller.admin.AdminController com.tyymt.sota.controller.AdminController 上级 (包)未确定,需在Class名上添加前缀

包名是否需要加上业务前缀,可根据实际规模来,如果业务小,可以采取第二种方式。

方法名

如果我们在用户的Service类(假定:UserService)中, 以下CURD方法

错误 正确 说明
getUser(Long id) get(Long id) 对象类已经明确了是User业务,方法名无需赘述
updateUser(UserEntity userEntity) update(UserEntity userEntity) 对象类已经明确了是User业务,方法名无需赘述
deleteUser(Long id) delete(Long id) 对象类已经明确了是User业务,方法名无需赘述
createUser(UserEntity userEntity) create(UserEntity userEntity) 对象类已经明确了是User业务,方法名无需赘述

错误 正确 说明
List<UserEntity> findUsers(Role role) List<UserEntity> find(Role role) 对象类已经明确了是User业务,方法名无需赘述
List<UserEntity> findByRole(Role role) List<UserEntity> find(Role role) 通过方法的签名已经很明确是ByRole, 无需赘述。 这类条件过滤查询列表的方法可能会存在很多重载的方法, 名字保持一致,放在一起

一般尽量保持用 get来获取单个返回一个对象; find 查询多个返回集合。

5 编码实践

5.1 @Override:必须添加

方法在可以添加@Override的时候添加上他。这包括重写父类方法,实现接口方法。一个接口方法重新指定父接口方法。 例外:当父方法被标记为@Deprecated时@Override可以被省略。

5.2 捕获异常:不可忽略

除以下情况外,不处理异常是不正确的。(典型的是打印日志,如果认为这种情况不可能出现,重新以AssertionError的形式抛出。) 当在捕获块中确实应当什么都不做的话,原因应该写在注释里面。在业务代码,考虑增加相关日志打印。

try {
  int i = Integer.parseInt(response);
  return handleNumericResponse(i);
} catch (NumberFormatException ok) {
  // 这不是数字,正常,继续执行
    log.info();
}
return handleTextResponse(response);

5.3 静态成员:通过类引用

当使用一个静态的类成员,应当使用该类的名词而不是该类对象的引用。

5.4 回收器:不要用

极少重写Object.finalize。

6 Javadoc

6.1 格式化

6.1.1 一般格式

在此示例中可以看到一般的JavaDoc块的格式:

/**
 * Multiple lines of Javadoc text are written here,
 * wrapped normally...
 */
public int method(String p1) { ... }

或者单行的例子

/** An especially short bit of Javadoc. */

基本格式总是可以接受的。当JavaDoc可以在一行内放下时可以采用单行格式代替。注意,这仅适用于没有@return之类的标签的时候。

6.1.2 段落

段落之间以一个空行--仅在这行开头有一个星号*,隔开。除第一个单词外,每个段落的第一个单词前面加上

后面没有空格。

6.1.3 块标签

所有的“块标签”按顺序出现@param,@return,@throws,@desprecate。这四种标签都不会出现空描述,当一行放不下的时候,下一行缩进到@后面四个或者更多空格的地方。

6.2 摘要碎片

每个JavaDoc都会以一个简短的摘要碎片开始。这个碎片非常重要:它是上下文的一部分文本,例如类和方法的索引。 这是一个碎片,是一个名词或动词,而不是完整的句子,它不以A {@code Foo} is a...或者This method returns...开头,也不会形成完整的命令式句子Save the record.,但是该碎片被大写并且被标点,就像他是完整句子一样。

提示:一个常见的错误是简单JavaDoc被写成了这种格式/ @return the customer ID */。这是不正确的,应当写成/ Returns the customer ID. */。

6.3 哪里需要Javadoc

至少,在public的类中,每个public或protect成员需要JavaDoc。当然, 规范的命名可以考虑省掉JavaDoc。 因为 “Code is the best document!”

以下,如果逻辑中没有特殊情况,可以考虑不写JavaDoc

不言自明的方法

Javadoc在一个简单明显的方法像Foo类的 get(Long Id)。如果真的没有其他话,除了“返回foo”之外,没有什么好说的。

下面是订单OrderService的方法

public OrderEntity update(OrderEntity entity) // 一看就明白
public Page<OrderEntity> find(LocalDateTime startOrderTime, LocalDateTime endOrderTime, Pageable pageable); //如果有个好的命名规范,这个也可以不写, 这个明显就是根据传入的过滤条件过滤并分页查询
重写

对于重写的方法,不用写Javadoc

最后更新于 28th Feb 2024