Home / cs-notes / Books / Java / 阿里Java开发手册 / v2022.02.03 / Note / 注意 / 01. 编程规约
编程规约
命名风格
- 抽象类 以 Abstract 或 Base 开头
- 异常类以 Exception 结尾
- 测试类以 Test 结尾
-
boolean 变量不加 is 前缀
- 包名小写
- 每一级只允许一个单词
- 单数形式
-
避免不规范的英文缩写
- 枚举名带 Enum 后缀
- 枚举成员名 大写, 单词间下划线分隔
- Service 等接口层方法命名
- 获取多个对象
- list 前缀
- 复数结尾
- 举例: listObjects
- 获取对象数量
- count 前缀
- 举例: countObject
- 获取多个对象
OOP 规约
- Integer 判等也应使用 equals 方法
- IntegerCache 外的 Integer 判等相当于 引用判等
- Objects.equals
- 货币类型均存储
- 以最小单位存储
- 整形
- 浮点数判等
- 方式1: Math.abs
- 方式2: BigDecimal.compareTo
- BigDecimal 判等
- 使用 compareTo 而非 equals
- compareTo 忽略精度 (1.0 等价于 1.00)
- equals 不忽略精度 (1.0 不等价于 1.00)
- 使用 compareTo 而非 equals
- BigDecimal 不能直接由 float 或 double 构造
- 方式1: String 构造
- 方式2: BigDecimal.valueOf 创建
- POJO 属性字段要使用 包装类型, 由使用方进行 空引用 校验
- 避免 空引用 与 默认值 含义冲突
- POJO 的 默认属性值 设置
- 应该在逻辑方设置, 而不是在 POJO 类定义中设置
- 避免逻辑中对默认值的要求不一致
- 应该在逻辑方设置, 而不是在 POJO 类定义中设置
- 类变更是, 需要序列化向下兼容时, 不要修改 serialVersionUID, 否则需要修改 serialVersionUID
- @see java.io.Serializable
由于目前项目中通常不使用 ObjectStream 等原生序列化手段, 所以 serialVersionUID 应用较少
-
构造方法中不应有初始化逻辑, 初始化逻辑应该在单独的初始化方法中
- POJO 必须实现 toString()
- 如果有基类, 必须先调用 super.toString()
-
同一属性, 禁止同时存在 isA() 与 getA() 方法
- String.split 字符串分隔多元素时, 校验元素数量
- 如果有相邻的分隔符, 结果数组长度将小于
分隔符数量-1
- 如果有相邻的分隔符, 结果数组长度将小于
-
多个构造, 或多个同名方法, 放在一起, 便于阅读
- 类内方法顺序
- public
- protected
- private
- getter / setter
- setter 方法
- 参数名与字段名一致
- 方法内只赋值, 没逻辑
- 在循环中拼接字符串, 应显示提前创建 StringBuilder
- 避免在循环中 隐式 多次 创建 StringBuilder
- 能用 final 时尽量用 final
- 包括局部变量
- final 类
- final 方法
时期时间
- pattern 中 应使用 y 而非 Y
- yyyy 与 YYYY 含义不同
- Y 的含义指当前周属于哪一年, 误用会存在跨年问题
- 月 M
- 分 m
- 时
- 24: H
- 12: h
-
时刻及时间差处理 使用 Instant 类
- 禁用的 时间 API
- java.sql.Date
- API 需要自己处理异常
- java.sql.Time
- 继承自 java.sql.Date
- 容易误用, 不易做异常处理
- java.sql.Timestamp
- 继承自 java.sql.Date
- 误用构造可能抛异常
- time 与 nanos 容易误解误用
- java.sql.Date
-
闰年等时间问题 使用 LocalDate 等新的时间API处理, 而不是 Calendar
-
时间逻辑不要绑定具体的日期数值, 避免
后一年没有2月29日
等问题 - 月份使用枚举
- Date 和 Calendar 等API获取月份数值为 0~11, 容易误用
集合处理
- hashcode 和 equals 必须同时重写
- 需要用 Set, Map 存储的类, 必须实现 hashcode 和 equals
-
使用 isEmpty 判空, 而不是 size == 0
使用 Collectors.toMap 需要指定 mergeFunction, 避免 key冲突时抛异常- 甚至可指定用什么类型的 map
- Collectors.toMap 可能导致 NPE
- why
- Collectors.toMap 会调用 Map.merge
- Map.merge 不支持 null value, 否则 NPE
- 为什么不支持 null value
- null value 在 merge 中的语义为 remove
- 如果允许 null value 存在, 将于 remove 逻辑冲突
- 为什么不支持 null value
- how to solve
-
使用
Stream.<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);
-
或者在 Collectors.toMap 中传入 value getter 时进行 null value 处理
不推荐
-
- why
相关问题 - 如何判断 Map 是否支持 null value
- HashMap 等非线程安全的 Map 支持 null value
- HashTable, ConcurrentHashMap 等线程安全的 Map 不支持 null value
- 因为 null value 与 not contains Key 存在二义性
- 对 null key 的支持情况 同理
- ArrayList.subList 返回值类型是 SubList, 而不是 ArrayList
- 返回的 SubList 为原数据视图, 对视图的操作对原数据有效
- 而且尽可能避免针对集合具体类型编程
- 不可同时对 SubList 及其父集合 进行 遍历操作或增删操作
- 因为 SubList 是镜像, 不是副本, 所以可能引起并发修改异常
- ArrayList toArray(new T[0])
// class ArrayList
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
- 泛型 PECS 原则 (Producer Extends Consumer Super)
- 生产具体的
- 消费抽象的
- 实现 Comparator 一定要处理相等的情况, 以保证比较相关的数学运算性质
- 互反性
- 传递性
- 等价替换性
-
集合初始化时, 尽可能指定容量大小, 避免频繁扩容 (数据越多影响越大)
- 先遍历 Map.keySet 再 Map.get 相当于 1次遍历 + N次查找
- 可以替换成遍历 entrySet, 或 Map.forEach
- 对 Map 的 key 与 value 可否为 null 的总结
class | key nullable | value nullable | super | thread-safe |
---|---|---|---|---|
HashTable | No | No | Dictionary | synchronized |
ConcurrentHashMap | No | No | AbstractMap | sync on table element |
HashMap | Yes | Yes | AbstractMap | not thread-safe |
TreeMap | No | Yes | AbstractMap | not thread-safe |
- 集合的 有序性 sort 和 稳定性 order
- 稳定性: 每次遍历, 次序相同
- ArrayList: unsort, order
- HashSet: unsort, unorder
- TreeSet: sort, order
并发处理
- 创建线程使用 ThreadPoolExecutor, 不用 Executors
- 更明确指定线程运行规则
- 明确限制线程数量, 避免 OOM
newCachedThreadPool
- 明确限制请求队列长度, 避免 OOM
newCachedThreadPoolnewSingleThreadExecutornewFixedThreadPoolnewScheduledThreadPool
- SimpleDateFormat 线程不安全, 不能作为静态单例使用
- solve
- Instant 代替 Date
- LocalDateTime 代替 Calendar
- DateTimeFormatter 代替 SimpleDateFormat
- why
- simple
- beautiful
- strong
- immutable
- thread-safe
- solve
- ThreadLocal 变量必须 及时 回收
- 避免内存泄漏
- 避免脏数据影响后续业务
- 使用 try 块, 在 finally 中回收, 避免中途异常导致回收失败
- 尽量不用锁, 少用锁, 尽量缩小锁定范围
- 范围: 代码块锁 < 方法锁 < 对象锁 < 类锁
- 锁定范围内不要存在异步调用
- 避免锁定时间过长, 甚至死锁
-
锁定多表, 必须保证一致的锁定顺序, 避免死锁
- lock 用法
Lock lock = new XxxLock();
// lock 须在 try 外调动, 如果 lock 未调用, 直接调用 unlock 会抛异常
lock.lock();
// lock 与 try 之间不可再有额外代码, 避免未捕获的异常导致 unlock 未执行
try {
// do something
} finally {
// 确保解锁, 未解的锁, 重复锁定会抛异常
lock.unlock();
}
- 如果是尝试获取锁, 需要判断是否锁定成功
Lock lock = new XxxLock();
boolean isLocked = lock.tryLock();
if (isLocked) {
try {
// do something
} finally {
lock.unlock();
}
}
- 冲突概率小于 20% 使用 乐观锁, 否则使用悲观锁
- 乐观锁的重试次数 不小于 3次
- 乐观锁: 基于数据版本号
- Timer 运行多个 TimerTask, 如果一个task抛异常, 所有 task 都会中止
- 使用 ScheduledExecutorService 来避免此问题
- 金融等敏感信息使用悲观锁
- 悲观锁使用原则: 一锁二判三更四放 (详见第10条)
-
CountDownLatch, 在 finally 中 countDown, 确保解锁成功 (各种锁的通用解锁规则)
- Random 实例避免多线程共享, 虽然线程安全, 但是会影响性能
- 使用 ThreadLocalRandom
- 双检锁需要声明 volatile
public class LazyInitDemo {
// 声明 volatile
private volatile Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null) {
helper = new Helper();
}
}
}
return helper;
}
// ...
}
- LongAdder 相比于 AtomicLong
- 优点:
- 多线程分散热点, 高并发的写操作性能好
- 缺点:
- 需要更多内存
- 总数读取基于多线程状态累计, 不具原子性, 无法做到数据的精确控制
- 优点:
LongAccumulator
- 与 LongAdder 类似, 基于 Striped64 实现
- LongAdder 初值为0, 累加规则为数值相加, 有单独的减操作
- LongAccumulator 可自定义初值, 自定义累加规则, 没有直接的减操作
- 减操作需要自定义的累加规则间接实现
- 如果误将 HashMap 用于多线程场景, resize 可能引发死链
- JDK 1.7 采用头插法, 扩容后链表倒置
- 扩容较慢的线程会发现, next 已经变成 previous, 成环而死锁
- JDK 1.8 已将头插法改为尾插法
- 相比头插法, 不仅需要头指针, 还需要尾指针
- 但是保证了和原链表一致的相对顺序
- JDK 1.7 采用头插法, 扩容后链表倒置
- 用 static 修饰 ThreadLocal 变量
- 步骤
- get
- do something
- 在 finally 中 remove
- 为什么 static
- 因为在 Thread.threadLocalMap 中会以该 ThreadLocal 实例为 key
- 避免 key 的错误创建
- 避免浪费不必要的内存
- 因为在 Thread.threadLocalMap 中会以该 ThreadLocal 实例为 key
- 步骤
可以将 ThreadLocal 可以封装在 AutoCloseable 中, 通过 try-with-resources 使用
控制语句
-
switch(String) 前 需要 null 判定
-
三目运算法可能隐含拆箱, 注意避免 NPE
- 并发场景的中止条件, 不要使用相等判断, 因为相等条件可能被击穿
- 比如将
== 0
换成<= 0
之类
- 比如将
-
方法超过 10 行时, return throw 等中断语句后面空一行
-
异常情况, 尽量将 if-else 改成 if-return
- 条件判断语句中不要执行复杂语句
- 可以将条件结果赋值给 boolean 变量, 使语义更加清晰明确
-
赋值语句单独成行, 不要嵌套在其他赋值语句中
-
开销大的代码尽可能放在循环体外, 比如 IO, try-catch 等
-
尽量用正向逻辑, 少用取反, 使判断条件更易读
-
public 接口需要入参维护
- 高频调用的内部方法, 确保安全时可以不做参数校验
- 需要注释说明, 须在调用时确保外部已做过校验
注释规约
- Javadoc 使用
/** ... */
而不是// ...
: 类, 属性, 方法- IDE 对
/** ... */
Javadoc 提供预览支持, 提供代码阅读效率, 不必每次查看方法定义 - 支持生成 Javadoc 文档
- IDE 对
- 抽象方法 必写 Javadoc
- 作用
- 参数
- 返回值
- 异常
-
类头必写作者日期
-
枚举字段必注释
- 半吊子英文不如中文
- 专有名词可以英文
-
代码与注释要同步
-
注掉代码需要注释说明原因, 如果永久弃用就直接删掉
- 注释要点
- 设计思想
- 业务含义
- 注释精简准确, 表达到位
- 好的命名和代码结构, 是自解释的
- 特殊注释标记
- TODO
- FIXME
前后端规约
-
key: lowerCamelCase
- 超大整数用 String 代替 Long
- JS Number 类型相当于 double, 64位浮点数
- 尾数只有 53 位精度
- JS Number 类型相当于 double, 64位浮点数
- 翻页
- <1: first page
- >size: last page
其他
-
正则需要预编译
- 属性拷贝
ApacheBeanUtils性能差- SpringBeanUtils 浅拷贝
- CglibBeanCopier 浅拷贝
-
enum 的属性必须私有 且 不可变
-
任何数据结构都应指定大小, 避免 OOM
- 及时清理弃用代码及配置
- 临时弃用, 使用
/// ...
三斜线注释说明注释原因
- 临时弃用, 使用