一、编程规约
1、命名风格
- 代码中的命名均不能以 下划线或美元符号 开始及结束。
- 所有命名严禁使用拼音与英文混写,更不允许中文命名。
- 类名使用 UpperCamelCase 风格。
- 方法名、参数名、成员变量、局部变量使用 lowerCamelCase 风格。
- 常量命名全部大写,单词间下划线分隔。
- 抽象类命名使用 Abstract 或 Base 开头;异常类以 Exception 结尾;测试类以要测试的类名开头,以 Test 结尾。
- 类型与中括号紧挨相连来表示数组。
- POJO类中的任何布尔变量,都不能以 is 为前缀。
- 包名统一使用小写,分隔符间仅有一个自然语义的英文单词。
- 避免在子父类的成员变量之间、或者不同代码块的局部变量间采用完全相同的命名。
- 杜绝不规范的缩写,避免望文不知意。
- 在定义命名时,尽量使用完整的单词组合。
- 在常量与变量命名时,表示类型的名词放在词尾。
- 如:startTime、workQueue、nameList、TERMINATED_THREAD_COUNT
- 如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式。
- 如:OrderFactory、LoginProxy
- 接口中的方法和属性不要加任何修饰符号,并加上有效的 javadoc 注释。尽量不要在接口中定义变量。
- 接口和实现类有两套规则:
- 对于 Service 和 DAO 类,基于 SOA 理念,暴露出来的服务一定是接口,内部的实现类使用 Impl 后缀。
- 如果是形容能力的接口名称,取对应的形容词为接口名。
- 枚举类名带上 Enum 后缀,枚举成员全大写,以下划线分隔。
- 各层命名规范:
- Service/DAO 层方法命名规范:以get、list、count、save、insert、remove、delete、update做前缀
- 领域模型命名规范:
- 数据对象:xxDO,xx为数据表名
- 数据传输对象:xxDTO,xxx为业务领域相关的名称
- 展示对象:xxVO,xx一般为网阿爷名称
- POJO 是 DO/DTO/BO/VO 的统称,禁用 xxPOJO
2、常量定义
- 不允许任何魔法值直接出现在代码中。
- 在 long 或 Long 赋值时,以 L 结尾。
- 不要使用一个常量类维护所有常量,要按功能进行分类,分开维护。
- 常量的复用层次有五层:
- 跨应用共享常量:放置在二方库中
- 应用内共享常量:放置在一方库中
- 子工程内部共享常量:子工程的 constant 目录中
- 包内共享常量:在包下的 constant 目录中
- 类内共享常量:类内部使用 private static final 定义
- 固定范围内变化的变量值,可用 enum 定义。
3、代码格式
- 如果大括号内为空,直接使用 {} 即可。
- 使用4空格缩进,禁用 tab 字符。
- 单行字符限制为120个,超出需换行。
- IDE 的 text file encoding 设置为 UTF-8;IDE 中文换行符使用 Unix 格式,禁用 Windows 格式。
- 单个方法总行数不超过80行。
4、OOP规约
- 避免通过一个类的对象引用访问此类的静态变量或静态方法,直接使用类名访问即可。
- 所有覆写方法,必须加 @Override 注解。
- 相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Object。
- 外部调用或二方库依赖接口,不允许修改方法签名,即不允许重写方法,避免对调用发产生影响。过期方法必须使用 @Deprecated,并清晰地说明采用的新接口或新服务。
- 不能使用过时方法或类。
- 推荐使用 java.util.Objects#equals(JDK7中引入的工具类)类比较。
- 所有整型包装类对象之间值的比较,全部使用equals方法比较。
- 如属性 Integer var = ?,如果var 在 -128~127之间,则由 IntegerCache.cache 产生,会复用已有对象,可直接使用 == 判断。
- 但区间之外的所有数据,都会在堆上产生,不会复用已有对象。
- 推荐使用 equals 方法判断。
- 任何货币金额,均以最小货币单位且整型类型来进行存储。
- 浮点数之间的等值判断,基本数据类型不能用 == 比较,包装数据类型不能用 equals 判断。
- 定义数据对象 DO 类时,属性类型要与数据库字段类型相匹配。
- 禁止使用构造方法 BigDecimal(double) 的方式,将 double 值转化为 BigDecimal 对象。
- BigDecimal(double) 存在精度丢失风险
- 所有 POJO 类属性,必须使用包装数据类型;RPC 方法的返回值和参数必须是包装数据类型;所有局部变量使用基本数据类型。
- 定义 POJO 类时,不要设定任何属性默认值。
- 序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列化失败;如果完全不兼容升级,表面反序列化混乱,那么请修改 serialVersionUID 值。
- 构造方法中禁止加入任何业务逻辑,如有初始化逻辑,请放在init中。
- POJO类,必须有 toString 方法。
- 禁止在 POJO 类中,同时存在属性 xx 的 isXx() 和 getXx()。
- 使用索引访问用 String 的 split 方法得到的数组时,需对最后一个分隔符后有无内容做检查,否则可能会抛出 IndexOutOfBoundsException。
- 当一个类有多个构造方法,或多个同名方法,应按顺序放置在一起,便于阅读。
- 类内方法定义顺序为:公有方法 > 私有方法 > getter/setter。
- getter/setter 方法中不要增加业务逻辑,增加排查问题的难度。
- 循环体内,字符串连接使用 StringBuilder 的 append 方法。
for(int i=0;i<100;i++){ str += "hello"; }
示例中,在编译时每次循环会创建一个 StringBuilder ,然后进行 append 操作,最后通过 toString 方法返回 String 对象,造成内存资源浪费。
- final 可以声明类、成员变量、方法、本地变量:
- 不允许被继承的类
- 不允许修改引用的域对象
- 不允许被覆写的方法
- 不允许运行过程中重新赋值的局部变量
- 避免上下文重复使用一个变量,使用final可以强制重新定义一个变量,方便更好地进行重构
- 慎用 Object 的 clone 方法(浅拷贝)来拷贝对象。
5、日期时间
- 日期格式化时,正确格式应为:
yyyy-MM-dd HH:mm:ss
- 获取当前毫秒数,应该使用
System.currentTimeMillis()
- 不允许在程序中使用
java.sql.Date
、java.sql.Time
、java.sql.Timestamp
java.util.Date.after(Date)
入参是java.sql.Timestamp
时,会触发 JDK BUG,可能导致意外结果。
- 不要写死一年为365天。
LocalDate.now().lengthOfYear()
获取今年的天数LocaDate.of(2011, 1, 1).lengthOfYear()
获取指定某年的天数
- 避免公历闰月2月问题。
- 使用枚举值来代替月份。
6、集合处理
- 关于 hashCode 与 equals 的处理规则:
- 重写 equals,必须重写 hashCode
- Set 存储的是不重复的对象,因此 Set 中存储的对象 必须重写这两个方法
- 如果对象作为Map的Key,那么必须重写这两个方法
- 判断集合内部元素是否为空,使用 isEmpty() 方法,而非 size() == 0、
- 在使用 java.util.stream.Collectors 类的 toMap() 方法转为 Map 集合时,一定要使用含有参数类型为 BinaryOperator,参数名为 mergeFunction 的方法,否则当出现相同 key 时会抛出 IllegalStateException 异常。
- 在使用 java.util.stream.Collectors 类的 toMap() 方法转为 Map 集合时,一定要注意当 value 为 null 时会抛出 NullPointException。
- ArrayList 的 subList 结果不可强转为 ArrayList,否则会抛出 ClassCaseException。
- subList 返回的是 ArrayList 的内部类 SubList
- 使用 Map 的 keySet()/values()/entrySet() 返回集合对象时,不可对其进行添加操作,否则会抛出 UnsupportedOperationException 异常。
- Collections 返回的对象,都是immutable list,无法进行增删操作,如 emptyList()/singletonList() 等。
- 在 subList 场景中,对父元素的增删,均会导致子列表的遍历、增删产生 ConcurrentModificationException 异常。
- 使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致,长度为0的空数组。
- 数组长度等于0,动态创建于 size 相同的数组,性能最好
- 大于0但小于size,重新创建大小等于size的数组,增加GC负担
- 等于size,在高并发情况下,数组创建完成后,size正在变大的情况下,负面影响与2相同
- 大于size,空间浪费,且在size处插入null值,存在 NPE 隐患。
- 使用 Collection 接口任何实现类的 addAll() 方法,都要对集合参数进行 NPE 判断。
- 使用 Arrays.asList() 将数组转换为集合时,不能使用修改集合相关的方法。
- 泛型通配符
<? extends T>
来接收返回的数据,此写法的泛型集合不能使用add 方法,而<? super T>
不能使用 get 方法,两者在接口调用赋值的场景中容易出错。 - 在无泛型限制定义的集合,赋值给泛型限制的集合时,需要进行 instanceOf 判断,避免出现 ClassCaseException。
- 不要在 forEach 循环中进行元素的 remove/add 操作。
- 集合初始化时,指定集合初始值大小。
- 使用 entrySet 遍历 Map 类集合,而不是 keySet 方式遍历。
- 注意 Map 类集合中 KV 不能为空的情况,如 Hashtable、ConcurrentHashMap、TreeMap、HashMap。
- 利用 Set 集合唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的contains遍历去重或判断包含操作。
7、并发处理
获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
线程池不允许使用 Executors 创建,而是通过 ThreadPollExecutor 的方式。
- FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_Value,可能会堆积大量请求,从而导致OOM(OutOfMemory 内存溢出)
- CachedTheadPool:允许创建的线程数为 Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM
SimpleDateFormat 是线程不安全的类,一般不要定义为 static,如果定义为 static 必须加锁。或使用 DateUtils 工具类。
- Java8 中,可使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat。
必须回收自定义的 ThreadLocal 变量,尽量在 try-finally 快进行回收。
高并发时,同步调用应该去考量锁的性能消耗。能用无锁数据结构,就不要用锁;能用锁区块,就不要锁方法体;能用对象锁,就不要类锁。
- 锁的代码块尽可能小,避免在锁代码块中调用RPC方法。
对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。
在使用阻塞等待获取锁的方式中,必须在 tru 代码块之外,并且在加锁方法与 try 代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无法解锁。
// 正例: Lock lock = new XxxLock(); lock.lock(); try{ doSomething(); doOthers(); } finally { lock.unlock(); }
在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。
// 正例: Lock lock = new XxxLock(); boolean isLocked = lock.tryLock(); if(isLocked) { try{ doSomething(); doOthers(); } finally { lock.unlock(); } }
并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。
- 如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3次。
多线程并行处理定时任务时,Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService 则没有这个问题。
资金相关的金融敏感信息,使用悲观锁策略。
- 悲观锁遵循一锁二判三更新四释放的原则
使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown 方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到。
避免 Random 实例被多线程使用,虽然共享该实例时线程安全的,但会因竞争统一 seed 导致性能下降。
- Random 实例包括 java.util.Random 或 Math.random()
- JDK7之后,可直接使用 ThreadLocalRandom;而7之前,需要编码保证每个线程持有一个单独的 Random 实例。
通过双重检查锁实现延迟初始化的优化问题隐患,推荐解决方案中较为简单的一种:将目标属性声明为 volatile 类型。
volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但如果多写,同样无法解决线程安全问题。
HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升。
ThreadLocal 对象使用 static 修饰,ThreadLocal 无法解决共享对象的更新问题。
8、控制语句
- switch 括号内的变量类型为 string,且为外部参数时,需先进行 null 判断。