Java 开发手册笔记
基于嵩山版
一、OOP 规约
1.1【强制】如上所示 BigDecimal
的等值比较应使用 compareTo()
方法,而不是 equals()
方法。说明:equals()
方法会比较值和精度 (1.0 与 1.00 返回结果为 false) ,而 compareTo()
则会忽略精度
二、并发处理
2.1【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static ,必须加锁,或者使用 DateUtils 工具类
正例:注意线程安全,使用 DateUtils。亦推荐如下处理:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe
2.2【强制】必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally 块进行回收
正例:
objectThreadLocal.set(userInfo);
try {
// ...
} finally {
objectThreadLocal.remove();
}
2.3【强制】在使用阻塞等待获取锁的方式中,必须在 try 代码块之外,并且在加锁方法与 try 代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无法解锁
正例:
Lock lock = new XxxLock();
// ...
lock.lock();
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}
2.4【推荐】避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed 导致的性能下降
说明:Random 实例包括 java.util.Random 的实例或者 Math.random() 的方式。、
正例:在 JDK7 之后,可以直接使用 API ThreadLocalRandom
,而在 JDK7 之前,需要编码保证每个线程持有一个单独的 Random 实例
2.5【参考】 volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题
说明:如果是 count++操作,使用如下类实现:
AtomicInteger count = new AtomicInteger();
count.addAndGet(1);
如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)
2.6【参考】ThreadLocal 对象使用 static 修饰,ThreadLocal 无法解决共享对象的更新问题
说明:这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量
三、前后端规约
3.1【强制】前后端数据列表相关的接口返回,如果为空,则返回空数组[]或空集合 {}
3.2【强制】对于需要使用超大整数的场景,服务端一律使用 String 字符串类型返回,禁止使用 Long 类型
3.3【强制】HTTP 请求通过 body 传递内容时,必须控制长度,超出最大长度后,后端解析会出错
说明:nginx 默认限制是 1MB,tomcat 默认限制为 2MB,当确实有业务需要传较大内容时,可以通过调大服务器端的限制
3.4【强制】HTTP 请求通过 URL 传递参数时,不能超过 2048 字节
说明:不同浏览器对于 URL 的最大长度限制略有不同,并且对超出最大长度的处理逻辑也有差异,2048 字节是取所有浏览器的最小值
四、日期时间
【强制】日期格式化时,传入 pattern 中表示年份统一使用小写的 y。说明:日期格式化时,yyyy 表示当天所在的年,而大写的 YYYY 代表是 week in which year(JDK7 之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的 YYYY 就是下一年
正例:表示日期和时间的格式如下所示:
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
【强制】在日期格式中分清楚大写的 M 和小写的 m,大写的 H 和小写的 h 分别指代的意义。说明:日期格式中的这两对字母表意如下:
- 表示月份是大写的 M;
- 表示分钟则是小写的 m;
- 24 小时制的是大写的 H;
- 12 小时制的则是小写的 h
【强制】不要在程序中写死一年为 365 天,避免在公历闰年时出现日期转换错误或程序逻辑错误
正例:
// 获取今年的天数
int daysOfThisYear = LocalDate.now().lengthOfYear();
// 获取指定某年的天数
LocalDate.of(2011, 1, 1).lengthOfYear();
反例:
// 第一种情况:在闰年 366 天时,出现数组越界异常
int[] dayArray = new int[365];
// 第二种情况:一年有效期的会员制,今年 1 月 26 日注册,硬编码 365 返回的却是 1 月 25 日
Calendar calendar = Calendar.getInstance();
calendar.set(2020, 1, 26);
calendar.add(Calendar.DATE, 365);
【强制】判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size()==0 的方式
五、异常日志
5.1【强制】 catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理
5.2【强制】不要在 finally 块中使用 return
说明:try 块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally 块中的语句,如果此处存在 return 语句,则在此直接返回,无情丢弃掉 try 块中的返回点
5.3【强制】在调用 RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用 Throwable 类来进行拦截
5.4【推荐】防止 NPE ,是程序员的基本修养,注意 NPE 产生的场景
正例:使用 JDK8 的 Optional 类来防止 NPE 问题
5.5【强制】在日志输出时,字符串变量之间的拼接使用占位符的方式
说明:因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能
正例: logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
六、单元测试
6.1【强制】好的单元测试必须遵守 AIR 原则
说明:单元测试在线上运行时,感觉像空气(AIR)一样感觉不到,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点
- A:Automatic(自动化)
- I:Independent(独立性)
- R:Repeatable(可重复)
七、MySQL 数据库
7.1【强制】表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint(1 表示是,0 表示否)
说明:任何字段如果为非负数,必须是 unsigned
注意:POJO 类中的任何布尔类型的变量,都不要加 is 前缀,所以,需要在 设置从 is_xxx 到 Xxx 的映射关系。数据库表示是与否的值,使用 tinyint 类型,坚持 is_xxx 的命名方式是为了明确其取值含义与取值范围
正例:表达逻辑删除的字段名 is_deleted,1 表示删除,0 表示未删除
7.2【强制】表名、字段名必须使用小写字母或数字 , 禁止出现数字开头,禁止两个下划线中间只出现数字
7.3【强制】主键索引名为 pk_ 字段名;唯一索引名为 uk _ 字段名 ; 普通索引名则为 idx_ 字段名
说明:pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index 的简称
7.4【强制】小数类型为 decimal,禁止使用 float 和 double
说明:在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储
7.5【强制】如果存储的字符串长度几乎相等,使用 char 定长字符串类型
7.6【强制】 varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text ,独立出来一张表,用主键来对应,避免影响其它字段索引效率
7.7【强制】表必备三字段:id, create_time, update_time
说明:其中 id 必为主键,类型为 bigint unsigned、单表时自增、步长为 1。create_time, update_time 的类型均为 datetime 类型,前者现在时表示主动式创建,后者过去分词表示被动式更新
7.8【强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90% 以上,可以使用 count(distinct left (列名, 索引长度)) / count(*) 的区分度来确定
7.9【强制】不要使用 count(列名)或 count(常量)来替代 count(),count()是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关
说明:count(*) 会统计值为 NULL 的行,而 count(列名) 不会统计此列为 NULL 值的行
7.10【强制】count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1,col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0
7.11【强制】当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为 NULL,因此使用 sum()时需注意 NPE 问题
正例:可以使用如下方式来避免 sum 的 NPE 问题:SELECT IFNULL(SUM(column), 0) FROM table;
7.12【强制】使用 ISNULL() 来判断是否为 NULL 值
反例:在 SQL 语句中,如果在 null 前换行,影响可读性。select * from table where column1 is null and column3 is not null; 而ISNULL(column)
是一个整体,简洁易懂。从性能数据上分析,ISNULL(column)
执行效率更快一些
7.13【强制】POJO 类的布尔属性不能加 is,而数据库字段必须加 is_,要求在 resultMap 中进行字段与属性之间的映射
7.14【强制】 iBATIS 自带的 queryForList(String statementName , int start , int size) 不推荐使用
说明:其实现方式是在数据库取到 statementName 对应的 SQL 语句的所有记录,再通过 subList 取 start,size 的子集合
正例:
Map<String, Object> map = new HashMap<>(16);
map.put("start", start);
map.put("size", size);
7.15【强制】不允许直接拿 HashMap 与 Hashtable 作为查询结果集的输出
反例:某同学为避免写一个 xxx,直接使用 HashTable 来接收数据库返回结果,结果出现日常是把 bigint 转成 Long 值,而线上由于数据库版本不一样,解析成 BigInteger,导致线上问题