前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、命名风格

1、类名 命名

类名使用 UpperCamelCase 风格,必须遵从驼峰形式,但以下情形例外:(领 域模型的相关命名) DO / BO / DTO / VO / DAO

影响: 代码可读性差

 正例 ForceCode / UserDO / HtmlDTO /TcpUdpDeal / TaPromotion
 反例 forcecode / UserDo / HTMLDto /TCPUDPDeal / TAPromotion

2、方法名、参数名、成员/局部变量 命名

方法名、参数名、成员/局部变量统一使用 lowerCamelCase,必须遵从驼峰形式

影响:反序列化或者 Lombok@Data赋值失败

正例: localValue / testMethod()

3、抽象类 命名

抽象类命名使用 AbstractBase开头

影响: 代码可读性差

正例 AbstractActionDemo

4、单元测试类 命名

单元测试类命名以它要测试的类的名称开始,以 Test结尾

影响: 代码可读性差

 正例 DemoTest

5、下划线和美元符号 使用

代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束

影响: 代码可读性差

反例:  _name / __name / $name / name_ / name$ / name__

6、异常类 命名

异常类命名使用 Exception 结尾

影响: 代码可读性差

CacheDemoException

7、常量 命名

常量命名应该全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长

影响: 代码可读性差

正例:MAX_STOCK_COUNT / CACHE_EXPIRED_TIME 

反例:MAX_COUNT / EXPIRED_TIME

8、 ServiceDAO类 使用

对于 ServiceDAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用 Impl 的后缀与接口区别

影响: 代码可读性差

正例public class DemoServiceImpl implements DemoService

9、包名 命名

包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式

影响: 代码可读性差

正例 com.jd.mpp.util/com.jd.tddl.domain.dto

10、POJO 类使用

1)布尔类型 命名

POJO 类中的任何布尔类型的变量,都不要加is,否则部分框架解析会引起序列化错误

影响: 部分框架解析会引起序列化错误

正例Boolean success
  
反例定义为基本数据类型Boolean isDeleted的属性当它的方法也isDeleted()框架在反向解析的时候,“误以为对应的属性名称是deleted导致属性获取不到进而抛出异常

2)禁止在 POJO 类中,同时存在对应属性 xxxisXxx()getXxx() 方法

禁止在 POJO 类中,同时存在对应属性 xxxisXxx()getXxx() 方法

影响:框架在调用属性 xxx 的提取方法时,并不能确定哪一个方法一定是被优先调用到的

3)类型与中括号紧挨相连来表示数组

类型与中括号紧挨相连来表示数组

影响: 代码可读性差

正例定义整形数组int[] arrayDemo;

反例在main参数中使用String args[]来定义

11、如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式

将设计模式体现在名字中,有利于阅读者快速理解架构设计理念。

正例:

public class OrderFactory; 

public class LoginProxy; 

public class ResourceObserver;

12、枚举命名

枚举类名带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开

13、方法命名

场景 命名
saveBean
deleteBean
udpateBean
查-单记录查询 getBeane
查-列表记录查询 listBean
查-分页记录查询 pageBean

二、注释

1、字段和方法 注释

所有的字段和方法必须要用 javadoc 注释

影响:严重影响代码可读性和编码效率

1)@see

@see 标签通常用于在注释的末尾添加一个单独的引用部分,用于引导读者查看其他相关的类、方法、文档等。@link@see 的基本用法和区别大体一致。

@see 是用来列出其他相关的参考信息,引导读者查看更多关联内容。

/**
 * 此方法用于计算两个整数的和。
 *
 * @param a 第一个整数
 * @param b 第二个整数
 * @return 两个整数的和
 * @see Subtract#subtract(int, int) 查看减法操作的方法
 */
public int add(int a, int b) {
    return a + b;
}
/**
 * 类目类型,DA、ZHONG、XIAO
 * @see InsuranceCateTypeEnum
 */
private String insuranceCateType;

@link 标签一般用于在注释内部创建一个指向其他类、方法、字段等的链接。当使用工具生成文档时,这个链接可以直接跳转至对应的文档部分。@link@see 的基本用法和区别大体一致。

@link 主要是在注释中嵌入链接,方便在注释文本里快速跳转

/**
 * queryPolicyInfoSingle
 *
 * @param userPin userPin
 * @param policyNo policyNo
 * @return {@link PolicyInfoPlusDto}
 */

2、枚举 注释

所有的枚举类型字段必须要有注释,说明每个数据项的用途

影响: 代码可读性差

public enum TestEnum {
      /**
      * 月份
      */
      MONTH;
}

3、单行 注释

方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使用/**/注释,注意与代码对齐

影响:影响代码可读性和编码效率

4、类注释

类必须有 @author(创建者)、@since(创建日期)、类功能描述等注释信息

5、中英文注释

与其“半吊子”英文来注释,不如用中文注释把问题说清楚。专有名词与关键字保持英文原文即可

6、代码修改记得改注释

代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。

说明:代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义

7、对于注释的要求

第一、能够准确反映设计思想和代码逻辑;

第二、能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息。完全没有注释的大段代码对于阅读者形同天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路; 注释也是给继任者看的,使其能够快速接替自己的工作

三、集合

1、集合转数组

使用集合转数组的方法,必须使用 toArray(T[] array),传入类型完全一样的数组,大小 list.size()

说明:使用 toArray 带参方法,数组空间大小的 length

1) 等于 0,动态创建与 size 相同的数组,性能最好。

2) 大于 0 但小于size,重新创建大小等于 size的数组,增加 GC 负担。

3) 等于 size,在高并发情况下,数组创建完成之后,size 正在变大的情况下,负面影响与2相同。

4) 大于 size,空间浪费,且在 size 处插入 null 值,存在 NPE 隐患。

 正例Integer[] b = (Integer [])c.toArray(new Integer[0]);

 反例直接使用toArray无参方法存在问题返回值只能是Object[]若强转其它类型数组将出现ClassCastException错误

Integer[] a =(Integer[])c.toArray();

2、集合初始化时,指定集合初始值大小

集合初始化时,指定集合初始值大小

说明:HashMap 使用 HashMap(int initialCapacity) 初始化,如果暂时无法确定集合大小,那么指定默认值(16)即可。

影响:浪费内存,降低性能


正例initialCapacity = (需要存储的元素个数 / 负载因子) + 1注意负载因子即loader factor默认为0.75如果暂时无法确定初始值大小请设置为16即默认值)。

反例HashMap需要放置1024个元素由于没有设置容量初始大小随着元素不断增加容量7次被迫扩大resize需要重建hash表当放置的集合元素个数达千万级别时不断扩容会严重影响性能

四、OOP

1、比较相等

1)Object的equals

Objectequals 方法容易抛出空指针异常,应使用常量或确定有值的对象来调用 equals

影响:导致业务逻辑错误

正例"test".equals(object); Objects.equals(a, b);

反例object.equals("test");

2)包装类对象之间值的比较,全部使用 equals 方法比较

所有的包装类对象之间值的比较,全部使用 equals 方法比较

影响:导致业务逻辑错误 (对于 Integer 在-128至127之间的值会在缓存里对象复用,区间外数据会产生新对象)

 正例:Objects.equals(a, b);

 反例:Integer.valueOf(a) == Integer.valueOf(b);

3、 DO / DTO / VOPOJO 类时,不要加任何属性默认值

说明:ORM 框架根据列修改时,会将默认值覆盖数据库的存储值

反例 `POJO` 类的 createTime 默认值为 new Date()但是这个属性在数据提取时并没有置入具体值在更新其它字段时又附带更新了此字段导致创建时间被修改成当前时间

4) POJO 类必须写 toString方法

说明:使用工具类 source > generate toString 时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString。打印对象信息日志,性能优于 JSON

影响: 降低编码效率,影响日志打印性能

public class ToStringDemo extends Super {

    private String secondName;

    @Override
    public String toString() {
        return (
            super.toString() +
            "ToStringDemo{" +
            "secondName=" +
            secondName +
            "}"
        );
    }
}

class Super {

    private String firstName;

    @Override
    public String toString() {
        return "Super{" + "firstName=" + firstName + "}";
    }
}

5)BigDecimal

1)精度问题

禁止使用构造方法 BigDecimal(double) 的方式把 double值转化为 BigDecimal 对象

影响: 数据精度丢失

说明: BigDecimal(double) 存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。

如:BigDecimal g = new BigDecimal(0.1f); 实际的存储值为:0. 100000000000000005551115123125782702118

正例:优先推荐入参为 `String` 的构造方法:

BigDecimal recommend1 = new BigDecimal("0.1"); 

BigDecimal recommen d2 = BigDecimal.valueOf(0.1);
或使用BigDecimal的valueOf方法,此方法内部其实执行了Double的toString,而 Double的toString按double的实际能表达的精度对尾数进行了截断。 

6、方法的参数个数建议不超过 5 个,方便代码阅读与理解

方法的参数个数建议不超过5个,方便代码阅读与理解

影响:降低编码效率,不方便阅读和理解

7、体的行数不能多于70行

影响: 降低编码效率,不方便阅读和理解

8、强转对象操作需要判断对象类型是否匹配

说明:强转之前必须使用 instanceof 进行类型判断

影响: 类型转换失败,抛出ClassCastException

理解:个人认为大可不必

9、字符串循环拼接

字符串循环拼接,必须用 StringBuilder,否则会严重浪费内存、拖慢性能。

影响:浪费内存,影响性能

  • 原因:在 Java 中,String 类是 不可变类(immutable),也就是说,一旦一个字符串对象被创建,它的值就不能被修改。

  • 影响:内存问题,产生大量临时对象

10、switch 控制语句

在一个 switch 块内,每个 case 要么通过 break / return 等来终止,要么注释说明程序将继续执行到哪一个case 为止;在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有

说明:注意break 是退出switch 语句块,而 return 是退出方法体。 没有 break 的情况下,增加 case 场景容易出现 bug

11、避免采用取反逻辑运算符

’!’运算符不利于快速理解

说明:取反逻辑不利于快速理解,并且取反逻辑写法必然存在对应的正向逻辑

12、并发规约

1)线程资源必须通过线程池提供,不允许在应用中自行显式创建线程

使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。

如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题

2)线程池不允许使用 Executors 去创建

线程池不允许使用Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的人员更加明确线程池的运行规则,规避资源耗尽的风险

说明:Executors返回的线程池对象的弊端如下:

1) FixedThreadPoolSingleThreadPool :允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM

2) CachedThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM

3)创建线程或线程池时请指定有意义的线程名称,方便出错时回溯

说明:创建线程池的时候请使用带ThreadFactory 的构造函数,并且提供自定义 ThreadFactory 实现或者使用第三方实现

4)避免Random实例被多线程使用

虽然共享该实例是线程安全的,但会因竞争同一 seed导致的性能下降

正例:可以使用 ThreadLocalRandom

public class RandomInThread extends Thread { 
private Random random = ThreadLocalRandom.current(); 
	@Override 
	public void run() { 
		long t = random.nextLong(); 
} 

反例:

public class RandomInThread extends Thread {
private Random random = new Random();
  @Override
  public void run() {
  	long t = random.nextLong();
  }
}

13、圈复杂度

圈复杂度 方法圈复杂度
圈复杂度-高: 方法圈复杂度大于50(等级:·BLOCKER
圈复杂度-中高: 方法圈复杂度大于40,小于等于50(等级:CRITICAL
圈复杂度-中: 方法圈复杂度大于30,小于等于40(等级:MAJOR
圈复杂度-低: 方法圈复杂度大于20,小于等于30(等级:WARNING

14、避免出现重复的代码

随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。

15、避免多次调用 get 方法(尤其是复杂逻辑的 get

get** 方法的结果如果被多次使用,务必提取为局部变量

  • 性能问题
    • 如果 userInfo.get("name") 背后不是简单的内存访问(例如:是重写了 get 方法、访问数据库、远程调用、复杂计算等),重复调用会带来不必要的开销
    • 即使是普通 Map,多次 get 也会重复计算 hash、查找桶等(虽轻量但不必要)。
  • 维护性差:如果将来要修改 key(如 "name""userName"),需要修改 多处,容易遗漏,引发bug
  • 可读性差:重复的 userInfo.get("name") 让代码显得冗余、啰嗦。

正例:

public void processUser(Map<String, String> userInfo) {
    String name = userInfo.get("name"); // 只调用一次 get

    if (name != null) {
        System.out.println("User name: " + name);
    }
    if ("admin".equals(name)) {
        System.out.println("Welcome, admin!");
    }
}

反例:

public void processUser(Map<String, String> userInfo) {
    if (userInfo.get("name") != null) {
        System.out.println("User name: " + userInfo.get("name"));
    }
    if ("admin".equals(userInfo.get("name"))) {
        System.out.println("Welcome, admin!");
    }
}

16、避免多层嵌套判断,应采用“快速失败”原则

避免层层嵌套,优先使用“快速失败”提前校验异常情况,让主逻辑清晰聚焦,提升代码可读性与维护性。

  • 代码深度嵌套(金字塔式代码),可读性差。
  • 正常业务逻辑被深埋,难以定位核心逻辑。
  • 错误处理不明确,没有提示用户具体哪里出错。
  • 维护困难,新增校验条件会进一步增加嵌套层级。

正例:

public void processOrder(Order order) {
    if (order != null) {
        if (order.getUser() != null) {
            if (order.getItems() != null && !order.getItems().isEmpty()) {
                // 业务逻辑
                System.out.println("Processing order for user: " + order.getUser().getName());
            }
        }
    }
}

反例:

public void processOrder(Order order) {
    // 快速失败:提前校验,抛出明确异常
    if (order == null) {
        throw new IllegalArgumentException("Order cannot be null");
    }
    if (order.getUser() == null) {
        throw new IllegalArgumentException("User cannot be null");
    }
    if (order.getItems() == null || order.getItems().isEmpty()) {
        throw new IllegalArgumentException("Order items cannot be empty");
    }

    // 主流程:到这里说明所有前置条件都满足
    System.out.println("Processing order for user: " + order.getUser().getName());
}

17、高并发下能用for就别使用forEach+Lambda

高频用 for,低频用 forEach;性能优先选传统,可读优先选 Lambda

高并发、高频调用场景下:优先使用传统 for 循环而非 forEach + Lambda,以避免额外对象开销和 GC 压力;

一般业务代码中:可优先选择 forEach 以提升可读性和开发效率。

方式 是否创建对象 性能 可读性 适用场景
for (int i=0; i<list.size(); i++) 不创建 最高 一般 高并发、性能敏感
for (String name : names)(增强for) 创建 Iterator对象,但非常轻量 一般推荐
names.forEach(name -> ...) 创建 Lambda 对象 很好 一般业务代码
names.stream().forEach(...) 创建 Stream + Lambda 较低 复杂处理,非高频

优化前:

  • 增强 for 底层会隐式创建一个 Iterator 对象,iterator() 会显式创建 Iterator 对象。

  • 捕获型lambda的使用会导致每次执行循环的时候都会隐式创建一个lambda对象。

  • 在高并发、频繁遍历的场景下,会产生大量临时垃圾对象(Iterator),增加 GC 压力。

public void processList(List<String> list) {
    for (String item : list) {
        // 业务逻辑
        ...
    }
}
或者
public void processList(List<String> list) {
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
        // 业务逻辑
        ...
    }
}
或者
String prefix = "Name: ";
list.forEach(s -> System.out.println(prefix + s));

优化后:

  • 高并发下,大量 遍历 ArrayList时,优先考虑下标 for 循环,减少临时对象产生。

  • 对于 LinkedList 等不支持高效随机访问的集合,仍应使用迭代器。

public void processList(List<String> list) {
    int size = list.size();
    for (int i = 0; i < size; i++) {
        // 业务逻辑
        ...
    }
}

18、基本数据类型与包装数据类型的(装箱/拆箱)

规范 主张 适用场景
Java 开发手册》类规范(如阿里规约) POJO 属性必须用包装类(Long, Integer 通用业务系统、数据库映射、RPC 接口
高性能/内存敏感系统最佳实践 能用基本类型就用基本类型(long, int 缓存、高频对象、大数据量内存结构(如你的场景)

1)通用业务

说明:POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证

1、 所有的 POJO 类属性必须使用包装数据类型;

2、RPC 方法的返回值和参数必须使用包装数据类型

3、所有的局部变量推荐使用基本数据类型

正例:数据库查询结果可能是null,因为自动拆箱,用基本数据类型接收有NPE风险。

反例: RPC方法的返回值使用基本类型,无法区分null值和0的区别

2)高性能业务

能用 long 就不用 Long,能不用 toString()/parseLong() 就别用,尤其在循环中——避免无谓的装箱、拆箱和字符串转换,减少临时对象,减轻 GC 压力,提升系统性能。在高并发、大数据量场景下,要警惕“自动但昂贵”的操作,这些操作看似简单,实则会创建大量临时对象,加重 GC 压力,拖慢系统性能

原因:因为 long 是直接存在栈上的原始值,而 LongString 都是堆上的对象,创建和访问它们需要内存分配、方法调用和垃圾回收——这些操作远比直接操作一个 long 数值昂贵得多。

  • 内存极度敏感:每个对象多 4~16 字节,乘以千万 = 百 MB 级别浪费
  • 无数据库/RPC 语义负担:这是纯内存中间状态,不是对外接口
  • 你可以完全控制取值范围:用 -1 表示“无值”是安全的
  • 避免 GC 压力:减少千万个 Long 对象 = 减少 GC Roots 和 Young GC 频率
场景 推荐做法
数值计算、累加、统计 使用 longint 等基础类型,避免 LongInteger
集合存储大量数值 优先用 long[]int[]ArrayList<Long>(不得已时)
核心循环中 避免 toString() / parseLong() / valueOf() 等转换
确实需要字符串 将转换移到入口/出口,如:List<String> → 入口解析为 long[],处理完再转回

a、绝大多数场景用基本数据类型

比如对象中定义idcount等字段时,直接用long:基本类型存储在栈或对象的内存布局中,不会产生额外的对象引用,也不存在装箱 / 拆箱开销,能减少内存占用和 GC 压力。

public class User {
    private long id; // 优先用基本类型
    private int age;
    private double score;
    // ...
}

b、仅在必要时用包装类以下场景不得不使用Long等包装类:

  • 需要表示 null值时(比如数据库字段允许 nullORM 框架映射时需用包装类);

  • 用于泛型集合(如List<Long>,泛型不支持基本类型);

c、避免 NULL

方案 内存估算(5000 万对象) 优点 缺点
Integer v = null ~200 MB(引用) + ~4.8 MB(30 万对象) 语义清晰 内存浪费严重
int v = -1 200 MB(固定) 简单、快速、无 GC 压力 浪费 99.4% 的空间存无效值
外部 Map 主对象 0 字节 + Map ~5–15 MB 内存最优,只存有效数据 需要额外查找,代码稍复杂

场景:对象数量:数千万(比如 5000 万)其中只有约 30 万有实际数值,其余都是 null

解决:优先使用 int(基本类型) + 一个“特殊默认值”(如 -1、0 或其他业务无效值)来代替 Integer = null

  • 每个 Integer 字段即使为 null,也占引用空间
    • 64JVM(启用压缩指针时):每个引用字段占 4 字节
    • 数千万对象 × 4 字节 = 巨大开销
    • 5000 万个对象 → 50,000,000 × 4B = 200 MB 内存仅用于存储 null 引用!
  • null 的 30 万 Integer 还要额外堆内存
    • 每个 Integer 对象本身在堆上至少占 16 字节(对象头 12B + int4B,对齐到 16B

    • 30 万 × 16B ≈ 4.8 MB

    • 虽然这部分不大,但加上引用的 200MB,总开销远高于必要

  • 改用 int + 特殊值的优势
    • 改用 int 就没有指针(引用)了
    • 每个 int 字段固定占 4 字节(和引用一样大小,但无需额外堆对象)
    • 无论是否有数据,内存占用恒定且最小

19、循环Map 优先用 entrySet

循环 Map 优先用 entrySet ,避免 keySet 二次 hash 开销,提升性能

virtualSkuMap.keySet() 遍历先获取所有键的集合,然后需要通过 map.get(key) 二次查询值,get() 方法在 HashMap 等实现中虽然是 O (1) 复杂度,但仍涉及哈希计算、桶位查找等操作,相当于每次循环多执行了一次哈希表查询,当 map 中元素量很大(超过 10万)时二次查询性能开销会被显著放大。

for (String mainSkuFlag : virtualSkuMap.keySet()) {
    LazyRoaringBitmap lazyRoaringBitmap = virtualSkuMap.get(mainSkuFlag);
}

entrySet() 遍历一次迭代即可同时获取键(key)和值(value),通过entry.getKey()entry.getValue() 直接获取,本质上是对已存在的 Entry 对象的字段访问,时间复杂度O(1),几乎没有额外开销。

for (Map.Entry<String, LazyRoaringBitmap> entry : virtualSameActMap.entrySet()) {
    String key = entry.getKey();
    LazyRoaringBitmap lazyRoaringBitmap = entry.getValue();
}
数据量 keySet() entrySet() forEach() Stream 并行流
1k 基准 快 ~1.2x ≈ entrySet 略慢(线程开销)
10k 慢 ~30% 基准 ≈ entrySet 开始占优
100k+(百万级) 最慢,比 entrySet 慢 2~3 倍 高效稳定 ≈ entrySet 最快,提速达 300%

20、避免void 出参

当一个方法不通过返回值传递结果,而是依赖修改传入的参数来输出数据时,会导致代码可读性差、调用方难以预知行为,并容易引入副作用。副作用指的是方法除了返回值外还修改了外部状态,这会使程序逻辑变得复杂且不易测试。

反例:

public class OrderProcessor {

    public void splitOrdersByPriority(List<Order> allOrders, 
                                      List<Order> highPriority, 
                                      List<Order> lowPriority) {
        for (Order order : allOrders) {
            if (order.getUrgencyLevel() >= 5) {
                highPriority.add(order);
            } else {
                lowPriority.add(order);
            }
        }
    }
}

正例:

   public PrioritizedOrders classifyByPriority(List<Order> allOrders) {
        List<Order> urgent = new ArrayList<>();
        List<Order> normal = new ArrayList<>();

        if (allOrders != null) {
            for (Order order : allOrders) {
                if (order.getUrgencyLevel() >= 5) {
                    urgent.add(order);
                } else {
                    normal.add(order);
                }
            }
        }

        return new PrioritizedOrders(urgent, normal);
    }

21、避免多重否定

在编写条件判断时,应尽量使用肯定式表达,避免使用双重或多重否定。多重否定会增加认知负担,使逻辑难以快速理解,容易引发误读或错误判断。

  • 方法命名应使用正向语义,如:
    • isEnabled() 而不是 isNotDisabled()
    • canExecute() 而不是 cannotBlock()
    • hasPermission() 而不是 doesNotLackAccess()
  • 布尔变量也应避免否定前缀嵌套:
    • 推荐:isValid, isReady, shouldRetry
    • 避免:isNotInvalid, shouldNotNotRetry

反例:

if (!buffer.shouldNotCompact() )

正例:

if (buffer.shouldCompact())  

22、避免对象链式穿透(避免“火车失事”式调用)

维度 传递浏览(a.getB().getC()) 封装 + 中介类
可读性 差,需理解对象链 好,调用意图清晰
耦合度 高,依赖多层结构 低,只依赖直接对象
可维护性 差,修改链路需改多处 好,只需改封装类
扩展性 差,插入新层级困难 好,可在 Department 内部调整

反面示例:对象链式穿透(“火车失事”代码),这段代码看似简洁,实则存在严重设计问题:

  • ExpenseReport 不仅知道 Employee(提交人),还知道其 Department,甚至知道 Approver
  • 调用方必须了解整个对象关系链:报销 → 提交人 → 部门 → 审批人;
  • 这种结构暴露了系统内部细节,破坏了封装性。
// 用户想提交一份报销申请
expenseReport.getSubmitter().getDepartment().getApprover().approve(expenseReport);

正例:

public class ExpenseReport {
    public void submitTo(Department department) {
        department.processExpense(this);
    }

public class Department {
    public void processExpense(ExpenseReport report) {
        Approver approver = determineApprover(report.getAmount());
        approver.approve(report);
    }

    private Approver determineApprover(BigDecimal amount) {
        // 根据金额、类型等策略选择审批人
        return amount.compareTo(new BigDecimal("5000")) > 0 ? seniorApprover : regularApprover;
    }
}
  
  
ExpenseReport report = new ExpenseReport(employee, new BigDecimal("3000"));
report.submitTo(salesDepartment);  // 调用清晰,不暴露内部结构

21、避免返回 null

返回 null 值,基本上是在给自己增加工作量,也是在给调用者添乱。只要有一处没检查 null 值,应用程序就会失控。

  • 集合类方法永远不要返回 null,应返回 Collections.emptyList()Set.of()Map.of() 等空不可变集合。
  • 对象查询方法优先返回“特例对象”而非 null,如 User.UNKNOWNOrder.VOID 等。
  • 使用 Optional<T> 谨慎:它适合作为返回类型提示调用方“可能无值”,但不应在类字段或集合中滥用。
  • 在方法契约中明确声明:“本方法永不返回 null”,增强调用方信心。
特性 使用 null 使用“空对象”或“默认返回”
可读性 差,需层层判空 好,逻辑清晰
安全性 低,易出 NPE 高,无空指针风险
可维护性 差,修改时需检查所有判空 好,调用方无需关心是否为空
团队协作 易引发“防御性编程” 鼓励信任与清晰契约

反面示例:层层嵌套的null 检查

更糟糕的是,在团队协作中,由于对他人代码缺乏信任,开发者会“处处判空”,形成“if (x != null) 瘟疫”,严重污染代码主干。

  • 包含 三层嵌套判空,逻辑分支复杂,难以阅读;
  • 每一层都可能返回 null,调用方必须“替上游擦屁股”;
  • 实际业务逻辑(“验证并创建订单”、“发送确认通知”)被淹没在防御性代码中;
  • 一旦某处遗漏判空,运行时异常就会发生。
public void processOrder(OrderSubmission submission) {
    if (submission != null) {
        OrderProcessor processor = systemConfig.getOrderProcessor();
        if (processor != null) {
            ProcessingResult result = processor.validateAndCreateOrder(submission);
            if (result != null && result.isSuccessful()) {
                notificationService.sendConfirmation(result.getOrder());
            }
        }
    }
}

正确做法:用“特例对象”或“空对象”替代 null

应通过设计确保方法总是返回一个有效对象,即使在“无数据”或“未命中”的情况下,也返回一个有意义的默认实现,如空集合、空结果对象等。

public class TaskService {

    /**
     * 获取指定用户的所有待办事项
     * @param userId 用户ID
     * @return 永不返回null,无任务时返回空列表
     */
    public List<TaskItem> getPendingTasks(String userId) {
        if (userId == null || !userExists(userId)) {
            return Collections.emptyList(); // 返回不可变空列表
        }

        List<TaskItem> tasks = database.findPendingByUser(userId);
        return tasks != null ? tasks : Collections.emptyList();
    }
}

进阶:使用“特例对象”(Special Case Pattern)

对于非集合类型的返回值,也可以定义“空对象”或“默认对象”来替代 null

public User findUser(String id) {
    User user = database.lookup(id);
    return user != null ? user : User.ANONYMOUS; // 返回一个预定义的“匿名用户”
}
public class User {
    private final String id;
    private final String name;
    private final boolean isActive;

    // 静态常量:表示“不存在的用户”
    public static final User ANONYMOUS = new User("ANON", "Unknown", false);

    public User(String id, String name, boolean active) {
        this.id = id;
        this.name = name;
        this.isActive = active;
    }

    // getters...
}
// 特例对象
User user = userService.findUser("U9999");
if (user.isActive()) {
    sendWelcomeMessage(user);
} else {
    log.info("用户不可用或未找到: " + user.getName());
}


// 或者
public Optional<User> findUserById(String id) {
    User user = db.query("SELECT * FROM users WHERE id = ?", id);
    return Optional.ofNullable(user);
}

五、异常处理

1、异常不要用来做流程控制,条件控制

异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多

2、不随意 catch

catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。

说明:对大段代码进行 try- catch ,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。

正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。

3、捕获异常是为了处理它

捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。

4、定义时区分 unchecked / checked异常

避免直接抛出 new RuntimeException(),更不允许抛出 Exception 或者Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException 等。

5、错误码还是抛异常

对于公司外的 http / api 开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用 Result方式,封装 isSuccess() 方法、“错误码”、“错误简短信息”。

问题:关于 RPC 方法返回方式使用 Result 方式的理由:

1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。

2)如果不加栈信息,只是 new 自定义异常,加入自己的理解的error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。

六、Git

1、commit

代码的 commit 的规范对团队非常重要,清晰的 commit 信息生成的 release tag,对于生产环境的故障回滚业非常关键,能够提供一些有价值的信息。

ContactAuthor