项目经验_之_开发规范
前言
Github:https://github.com/HealerJean
一、命名风格
1、类名 命名
类名使用
UpperCamelCase风格,必须遵从驼峰形式,但以下情形例外:(领 域模型的相关命名)DO/BO/DTO/VO/DAO影响: 代码可读性差
正例: ForceCode / UserDO / HtmlDTO /TcpUdpDeal / TaPromotion
反例: forcecode / UserDo / HTMLDto /TCPUDPDeal / TAPromotion
2、方法名、参数名、成员/局部变量 命名
方法名、参数名、成员/局部变量统一使用
lowerCamelCase,必须遵从驼峰形式影响:反序列化或者
Lombok中@Data赋值失败
正例: localValue / testMethod()
3、抽象类 命名
抽象类命名使用
Abstract或Base开头影响: 代码可读性差
正例: 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、 Service 和 DAO类 使用
对于
Service和DAO类,基于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 类中,同时存在对应属性 xxx 的 isXxx() 和 getXxx() 方法
禁止在
POJO类中,同时存在对应属性xxx的isXxx()和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;
2)@link
@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
Object的equals方法容易抛出空指针异常,应使用常量或确定有值的对象来调用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 / VO 等 POJO 类时,不要加任何属性默认值
说明:
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)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。如:
BigDecimalg=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) FixedThreadPool 和 SingleThreadPool :允许的请求队列长度为 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 是直接存在栈上的原始值,而 Long、String 都是堆上的对象,创建和访问它们需要内存分配、方法调用和垃圾回收——这些操作远比直接操作一个 long 数值昂贵得多。
- 内存极度敏感:每个对象多 4~16 字节,乘以千万 = 百 MB 级别浪费
- 无数据库/
RPC语义负担:这是纯内存中间状态,不是对外接口 - 你可以完全控制取值范围:用
-1表示“无值”是安全的 - 避免
GC压力:减少千万个Long对象 = 减少 GC Roots 和 Young GC 频率
| 场景 | 推荐做法 |
|---|---|
| 数值计算、累加、统计 | 使用 long、int 等基础类型,避免 Long、Integer |
| 集合存储大量数值 | 优先用 long[]、int[] 或 ArrayList<Long>(不得已时) |
| 核心循环中 | 避免 toString() / parseLong() / valueOf() 等转换 |
| 确实需要字符串 | 将转换移到入口/出口,如:List<String> → 入口解析为 long[],处理完再转回 |
a、绝大多数场景用基本数据类型
比如对象中定义id、count等字段时,直接用long:基本类型存储在栈或对象的内存布局中,不会产生额外的对象引用,也不存在装箱 / 拆箱开销,能减少内存占用和 GC 压力。
public class User {
private long id; // 优先用基本类型
private int age;
private double score;
// ...
}
b、仅在必要时用包装类以下场景不得不使用Long等包装类:
-
需要表示
null值时(比如数据库字段允许null,ORM框架映射时需用包装类); -
用于泛型集合(如
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,也占引用空间- 在
64位JVM(启用压缩指针时):每个引用字段占 4 字节 - 数千万对象 × 4 字节 = 巨大开销
5000万个对象 → 50,000,000 × 4B = 200 MB 内存仅用于存储null引用!
- 在
- 非
null的 30 万Integer还要额外堆内存-
每个
Integer对象本身在堆上至少占 16 字节(对象头12B+int值4B,对齐到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.UNKNOWN、Order.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,对于生产环境的故障回滚业非常关键,能够提供一些有价值的信息。


