Skip to the content.

Home / cs-notes / Books / Java / 阿里Java开发手册 / v2022.02.03 / Note / 注意 / 01. 编程规约

编程规约

命名风格

  1. 抽象类 以 Abstract 或 Base 开头
    • 异常类以 Exception 结尾
    • 测试类以 Test 结尾
  2. boolean 变量不加 is 前缀

  3. 包名小写
    • 每一级只允许一个单词
    • 单数形式
  4. 避免不规范的英文缩写

  5. 枚举名带 Enum 后缀
    • 枚举成员名 大写, 单词间下划线分隔
  6. Service 等接口层方法命名
    • 获取多个对象
      • list 前缀
      • 复数结尾
      • 举例: listObjects
    • 获取对象数量
      • count 前缀
      • 举例: countObject

OOP 规约

  1. Integer 判等也应使用 equals 方法
    • IntegerCache 外的 Integer 判等相当于 引用判等
    • Objects.equals
  2. 货币类型均存储
    • 以最小单位存储
    • 整形
  3. 浮点数判等
    • 方式1: Math.abs
    • 方式2: BigDecimal.compareTo
  4. BigDecimal 判等
    • 使用 compareTo 而非 equals
      • compareTo 忽略精度 (1.0 等价于 1.00)
      • equals 不忽略精度 (1.0 不等价于 1.00)
  5. BigDecimal 不能直接由 float 或 double 构造
    • 方式1: String 构造
    • 方式2: BigDecimal.valueOf 创建
  6. POJO 属性字段要使用 包装类型, 由使用方进行 空引用 校验
    • 避免 空引用 与 默认值 含义冲突
  7. POJO 的 默认属性值 设置
    • 应该在逻辑方设置, 而不是在 POJO 类定义中设置
      • 避免逻辑中对默认值的要求不一致
  8. 类变更是, 需要序列化向下兼容时, 不要修改 serialVersionUID, 否则需要修改 serialVersionUID
    • @see java.io.Serializable

由于目前项目中通常不使用 ObjectStream 等原生序列化手段, 所以 serialVersionUID 应用较少

  1. 构造方法中不应有初始化逻辑, 初始化逻辑应该在单独的初始化方法中

  2. POJO 必须实现 toString()
    • 如果有基类, 必须先调用 super.toString()
  3. 同一属性, 禁止同时存在 isA() 与 getA() 方法

  4. String.split 字符串分隔多元素时, 校验元素数量
    • 如果有相邻的分隔符, 结果数组长度将小于 分隔符数量-1
  5. 多个构造, 或多个同名方法, 放在一起, 便于阅读

  6. 类内方法顺序
    1. public
    2. protected
    3. private
    4. getter / setter
  7. setter 方法
    • 参数名与字段名一致
    • 方法内只赋值, 没逻辑
  8. 在循环中拼接字符串, 应显示提前创建 StringBuilder
    • 避免在循环中 隐式 多次 创建 StringBuilder
  9. 能用 final 时尽量用 final
    • 包括局部变量
    • final 类
    • final 方法

时期时间

  1. pattern 中 应使用 y 而非 Y
    • yyyy 与 YYYY 含义不同
    • Y 的含义指当前周属于哪一年, 误用会存在跨年问题
  2. 月 M
    • 分 m
      • 24: H
      • 12: h
  3. 时刻及时间差处理 使用 Instant 类

  4. 禁用的 时间 API
    • java.sql.Date
      • API 需要自己处理异常
    • java.sql.Time
      • 继承自 java.sql.Date
      • 容易误用, 不易做异常处理
    • java.sql.Timestamp
      • 继承自 java.sql.Date
      • 误用构造可能抛异常
      • time 与 nanos 容易误解误用
  5. 闰年等时间问题 使用 LocalDate 等新的时间API处理, 而不是 Calendar

  6. 时间逻辑不要绑定具体的日期数值, 避免 后一年没有2月29日 等问题

  7. 月份使用枚举
    • Date 和 Calendar 等API获取月份数值为 0~11, 容易误用

集合处理

  1. hashcode 和 equals 必须同时重写
    • 需要用 Set, Map 存储的类, 必须实现 hashcode 和 equals
  2. 使用 isEmpty 判空, 而不是 size == 0

  3. 使用 Collectors.toMap 需要指定 mergeFunction, 避免 key冲突时抛异常
    • 甚至可指定用什么类型的 map
  4. Collectors.toMap 可能导致 NPE
    • why
      • Collectors.toMap 会调用 Map.merge
      • Map.merge 不支持 null value, 否则 NPE
        • 为什么不支持 null value
          • null value 在 merge 中的语义为 remove
          • 如果允许 null value 存在, 将于 remove 逻辑冲突
    • 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 处理

        • 不推荐

相关问题 - 如何判断 Map 是否支持 null value

  1. ArrayList.subList 返回值类型是 SubList, 而不是 ArrayList
    • 返回的 SubList 为原数据视图, 对视图的操作对原数据有效
    • 而且尽可能避免针对集合具体类型编程
  2. 不可同时对 SubList 及其父集合 进行 遍历操作或增删操作
    • 因为 SubList 是镜像, 不是副本, 所以可能引起并发修改异常
  3. 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;
    }
  1. 泛型 PECS 原则 (Producer Extends Consumer Super)
    • 生产具体的
    • 消费抽象的
  2. 实现 Comparator 一定要处理相等的情况, 以保证比较相关的数学运算性质
    • 互反性
    • 传递性
    • 等价替换性
  3. 集合初始化时, 尽可能指定容量大小, 避免频繁扩容 (数据越多影响越大)

  4. 先遍历 Map.keySet 再 Map.get 相当于 1次遍历 + N次查找
    • 可以替换成遍历 entrySet, 或 Map.forEach
  5. 对 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
  1. 集合的 有序性 sort 和 稳定性 order
    • 稳定性: 每次遍历, 次序相同
    • ArrayList: unsort, order
    • HashSet: unsort, unorder
    • TreeSet: sort, order

并发处理

  1. 创建线程使用 ThreadPoolExecutor, 不用 Executors
    • 更明确指定线程运行规则
    • 明确限制线程数量, 避免 OOM
      • newCachedThreadPool
    • 明确限制请求队列长度, 避免 OOM
      • newCachedThreadPool
      • newSingleThreadExecutor
      • newFixedThreadPool
      • newScheduledThreadPool
  2. SimpleDateFormat 线程不安全, 不能作为静态单例使用
    • solve
      • Instant 代替 Date
      • LocalDateTime 代替 Calendar
      • DateTimeFormatter 代替 SimpleDateFormat
    • why
      • simple
      • beautiful
      • strong
      • immutable
      • thread-safe
  3. ThreadLocal 变量必须 及时 回收
    • 避免内存泄漏
    • 避免脏数据影响后续业务
    • 使用 try 块, 在 finally 中回收, 避免中途异常导致回收失败
  4. 尽量不用锁, 少用锁, 尽量缩小锁定范围
    • 范围: 代码块锁 < 方法锁 < 对象锁 < 类锁
    • 锁定范围内不要存在异步调用
      • 避免锁定时间过长, 甚至死锁
  5. 锁定多表, 必须保证一致的锁定顺序, 避免死锁

  6. lock 用法
Lock lock = new XxxLock();
// lock 须在 try 外调动, 如果 lock 未调用, 直接调用 unlock 会抛异常
lock.lock();
// lock 与 try 之间不可再有额外代码, 避免未捕获的异常导致 unlock 未执行
try {
    // do something
} finally {
    // 确保解锁, 未解的锁, 重复锁定会抛异常
    lock.unlock();
}
  1. 如果是尝试获取锁, 需要判断是否锁定成功
Lock lock = new XxxLock();
boolean isLocked = lock.tryLock();
if (isLocked) {
    try {
        // do something
    } finally {
        lock.unlock();
    }
}
  1. 冲突概率小于 20% 使用 乐观锁, 否则使用悲观锁
    • 乐观锁的重试次数 不小于 3次
    • 乐观锁: 基于数据版本号
  2. Timer 运行多个 TimerTask, 如果一个task抛异常, 所有 task 都会中止
    • 使用 ScheduledExecutorService 来避免此问题
  3. 金融等敏感信息使用悲观锁
    • 悲观锁使用原则: 一锁二判三更四放 (详见第10条)
  4. CountDownLatch, 在 finally 中 countDown, 确保解锁成功 (各种锁的通用解锁规则)

  5. Random 实例避免多线程共享, 虽然线程安全, 但是会影响性能
    • 使用 ThreadLocalRandom
  6. 双检锁需要声明 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;
    }
    // ...
}
  1. LongAdder 相比于 AtomicLong
    • 优点:
      • 多线程分散热点, 高并发的写操作性能好
    • 缺点:
      • 需要更多内存
      • 总数读取基于多线程状态累计, 不具原子性, 无法做到数据的精确控制

LongAccumulator

  1. 如果误将 HashMap 用于多线程场景, resize 可能引发死链
    • JDK 1.7 采用头插法, 扩容后链表倒置
      • 扩容较慢的线程会发现, next 已经变成 previous, 成环而死锁
    • JDK 1.8 已将头插法改为尾插法
      • 相比头插法, 不仅需要头指针, 还需要尾指针
      • 但是保证了和原链表一致的相对顺序
  2. 用 static 修饰 ThreadLocal 变量
    • 步骤
      1. get
      2. do something
      3. 在 finally 中 remove
    • 为什么 static
      • 因为在 Thread.threadLocalMap 中会以该 ThreadLocal 实例为 key
        • 避免 key 的错误创建
        • 避免浪费不必要的内存

可以将 ThreadLocal 可以封装在 AutoCloseable 中, 通过 try-with-resources 使用

控制语句

  1. switch(String) 前 需要 null 判定

  2. 三目运算法可能隐含拆箱, 注意避免 NPE

  3. 并发场景的中止条件, 不要使用相等判断, 因为相等条件可能被击穿
    • 比如将 == 0 换成 <= 0 之类
  4. 方法超过 10 行时, return throw 等中断语句后面空一行

  5. 异常情况, 尽量将 if-else 改成 if-return

  6. 条件判断语句中不要执行复杂语句
    • 可以将条件结果赋值给 boolean 变量, 使语义更加清晰明确
  7. 赋值语句单独成行, 不要嵌套在其他赋值语句中

  8. 开销大的代码尽可能放在循环体外, 比如 IO, try-catch 等

  9. 尽量用正向逻辑, 少用取反, 使判断条件更易读

  10. public 接口需要入参维护

  11. 高频调用的内部方法, 确保安全时可以不做参数校验
    • 需要注释说明, 须在调用时确保外部已做过校验

注释规约

  1. Javadoc 使用 /** ... */ 而不是 // ...: 类, 属性, 方法
    • IDE 对 /** ... */ Javadoc 提供预览支持, 提供代码阅读效率, 不必每次查看方法定义
    • 支持生成 Javadoc 文档
  2. 抽象方法 必写 Javadoc
    • 作用
    • 参数
    • 返回值
    • 异常
  3. 类头必写作者日期

  4. 枚举字段必注释

  5. 半吊子英文不如中文
    • 专有名词可以英文
  6. 代码与注释要同步

  7. 注掉代码需要注释说明原因, 如果永久弃用就直接删掉

  8. 注释要点
    • 设计思想
    • 业务含义
  9. 注释精简准确, 表达到位
    • 好的命名和代码结构, 是自解释的
  10. 特殊注释标记
    • TODO
    • FIXME

前后端规约

  1. key: lowerCamelCase

  2. 超大整数用 String 代替 Long
    • JS Number 类型相当于 double, 64位浮点数
      • 尾数只有 53 位精度
  3. 翻页
    • <1: first page
    • >size: last page

其他

  1. 正则需要预编译

  2. 属性拷贝
    • ApacheBeanUtils 性能差
    • SpringBeanUtils 浅拷贝
    • CglibBeanCopier 浅拷贝
  3. enum 的属性必须私有 且 不可变

  4. 任何数据结构都应指定大小, 避免 OOM

  5. 及时清理弃用代码及配置
    • 临时弃用, 使用 /// ... 三斜线注释说明注释原因