背景
本文主要介绍一些Java开发中的基础规范,目标是统一团队内的代码风格,利于阅读、维护与扩展。
编程规约
类(接口)注释
/**
* 描述信息
*
* @author name(推荐英文全拼)
* @date ${DATE} ${TIME}
*/
Idea(Mac OS)设置方法:
Preferences->Editor->File and Code Templates->File Header添加如下代码:
/**
* @author yourname
* @date ${DATE} ${TIME}
*/
命名风格
- 包名:统一使用小写,点分隔符之间有且仅有一个自然语义的英语单数单词
- 类名:驼峰写法,大写字母开头
- 变量名:驼峰写法,小写字母开头
- 方法名:驼峰写法,小写字母开头
- 常量名:统一使用final static修饰,全大写字母,下划线分隔
- 抽象类名:Abstract开头
- 异常类名:Exception结尾
- 枚举类名: Enum结尾,枚举成员名称需要全大写,单词间下划线分隔
- 单测类名:以要测试的类的名称开始,以 Test 结尾
- 为了达到代码自解释的目标,任何自定义编程元素在命名时,尽量使用完整的单词组合来表达其意,做到看见名字就能确切地知道是什么意思。杜绝完全不规范的缩写,避免望文不知义。
代码格式
- 变量与操作符之间要有空格,如int a = 0
- 左小括号和字符之间不出现空格;同样,右小括号和字符之间也不出现空格,如(a == b)
- 注释的双斜线与注释内容之间有且仅有一个空格,如
- 方法参数在定义和传入时,多个参数逗号后边必须加空格
- 类、类属性、类方法的注释必须使用 Javadoc 规范,使用/*内容/格式,不得使用//xxx 方式
- 不要简单的注释掉代码,若必须在其上方增加注释说明原因。若确定无用,则删除之。
代码规范
-
常量类使用final class,不要使用interface。不要使用一个常量类维护所有常量,按常量功能进行归类,分开维护。
-
所有的覆写方法,必须加@Override 注解
-
构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中
-
循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展
-
不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
-
不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。
-
在 JDK7 版本及以上,Comparator 要满足三个条件(大于、小于、等于),不然 Arrays.sort,Collections.sort 会报 IllegalArgumentException 异常。注意:若没有处理相等的情况,实际使用中可能会出现异常。
-
对于金钱等对精度要求很高的场景,不能使用double和float存储,应当使用BigDecimal类型。
BigDecimal BigDecimal(double d); //不允许使用,会造成精度丢失 BigDecimal BigDecimal(String s); //常用,推荐使用 static BigDecimal valueOf(double d); //常用,推荐使用
-
关于 hashCode 和 equals 的处理,遵循如下规则:
- 只要重写 equals,就必须重写 hashCode
- equals方法意在判断两个对象是否等价,因此equals方法必须满足自反性、对称性和传递性。
- 自反性:a.equals(a)一定为true。
- 对称性:若a.equals(b)为true,则b.equals(a)也为true。
- 传递性:若a.equals(b)为true,b.equals(c)为true,则a.equals(c)也为true。
- 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法
- 如果自定义对象做为 Map 的键,那么必须重写 hashCode 和 equals。
- 说明:String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象作为 key 来使用。
-
集合初始化时,指定集合初始值大小
示例:HashMap 使用 HashMap(int initialCapacity) 初始化, 说明:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。 反例:HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容量 7 次被迫扩大,resize 需要重建 hash 表,严重影响性能。
-
使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。
- keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出key 所对应的 value。 而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。 如果是 JDK8,使用 Map.foreach 方法。
-
线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
-
高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
-
并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。
-
在业务/线上代码中,尽量避免使用JDK8+的Stream,因为目前Stream性能欠佳,通常会消耗传统循环的5倍时间。如为一次性代码(如跑数据需要用到的)或离线代码可随意。
-
不要使用已废弃的类或者方法。库的作者有义务对于已废弃的东西给出替代方案,同样地,使用方也有义务找出
日志与异常
异常处理
- 对于自己写的代码,需要自行保证不会抛出非运行时异常(特别是NPE),这是程序员的基本素养。
- 避免对大段代码进行 try-catch,这是不负责任的表现。
- 只处理需要关注的异常,不要直接catch Exception。
- finally 块必须对资源对象、流对象进行关闭。
- 不能在 finally 块中使用 return,finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句。
日志使用规范
-
应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架SLF4J 中的 API。
-
对 trace/debug/info 级别的日志输出,必须使用条件输出形式或者使用占位符的方式。
if (logger.isDebugEnabled()) { logger.debug("Processing trade with id: " + id + " and symbol: " + symbol); } 或 logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol);
-
对于WARN和ERROR日志的取舍:从调用方或者客户端的角度看,是否可以达成期望的效果。
- 若未能达成用户期望的结果,则使用ERROR
- 若可以达成用户期望的结果,但是有其他需要注意的问题(如缓存失效等),则使用WARN。
-
只记录必要的日志,防止把服务器磁盘打满。
单元测试
- 单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下
- 核心业务、核心应用、核心模块的增量代码确保单元测试通过。
MySQL数据库
命名规范
- 数据库名、表名、字段名使用小写字母+下划线方式命名,不允许出现任何大写字母。
- 数据库名:尽量与应用名称一致
- 表名:表名为t_表名,不使用复数名词,多个单词下划线隔开。
- 字段名:字段名全小写,多个单词下划线隔开。
- 索引名:主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名。
建库与建表规范
- 统一使用utf8mb4编码。
- 每一个数据库表,和表中的每一个字段,务必用comment说明的用途。
- 表中必有三个字段:id, create_time, update_time。
- 其中 id 为主键,类型为 unsigned bigint、单表时自增、步长为 1。create_time,update_time 的类型均为datetime 类型
- 表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint( 1 表示是,0 表示否)
- varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text
- 单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
- 对于金钱等要求非常精确的字段,应当使用DECIMAL或整数,禁止使用double或float等浮点类型。
- 考虑将IP地址存为UNSIGNED INT。
索引规范
-
需要 join 的字段,数据类型必须绝对一致;多表关联查询时,保证被关联的字段有索引。
-
禁止超过三个表的join
-
如果有 order by 的场景,请注意利用索引的有序性。order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 file_sort 的情况,影响查询性能。
- 正例:where a=? and b=? order by c; 索引:(a,b,c)。
- 反例:索引中有范围查找,那么索引有序性无法利用,如:WHERE a>10 ORDER BY b; 索引(a,b) 无法排序。
-
建组合索引的时候,区分度最高的字段在最左边。
-
不要对形式离散、值域有限的量(例如状态、类别、语言、国家和地区、性别等)使用索引。
使用规范
- 使用ISNULL()来判断是否为NULL值。
- SQL SELECT语句中必须指出具体查询那些字段,不要SELECT *。
- 若在查询数据库时出现复杂运算(即加减乘除之外的),一律放在业务上,不要写在SQL语句中。
- 对于重要的查询,考虑对其EXPLAIN,时刻掌握其运行状况和可优化点。
- in操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在1000 个之内。
- 更新数据表记录时,必须同时更新记录对应的 update_time 字段值为当前时间。
- @Transactional 事务不要滥用。事务会影响数据库的 QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。
参考资料
【2】阿里巴巴Java开发手册