前言
《Java开发手册》是阿里巴巴集团技术团队的集体智慧结晶和经验总结,经历了多次大规模一线实战的检验及不断完善,公开到业界后,众多社区开发者踊跃参与,共同打磨完善,系统化地整理成册。现代软件行业的高速发展对开发者的综合素质要求越来越高,因为不仅是编程知识点,其它维度的知识点也会影响到软件的最终交付质量。比如:数据库的表结构和索引设计缺陷可能带来软件上的架构缺陷或性能风险;工程结构混乱导致后续维护艰难;没有鉴权的漏洞代码易被黑客攻击等等。所以本手册以Java开发者为中心视角,划分为编程规约、异常日志、单元测试、安全规约、MySQL数据库、工程结构、设计规约七个维度,再根据内容特征,细分成若干二级子目录。另外,依据约束力强弱及故障敏感性,规约依次分为强制、推荐、参考三大类。在延伸信息中,说明”对规约做了适当扩展和解释;“正例”提倡什么样的编码和实现方式;“反例”说明需要提防的雷区,以及真实的错误案例。
手册的愿景是码出高效,码出质量。现代软件架构的复杂性需要协同开发完成,如何高效地协同呢?无规矩不成方圆,无规范难以协同,比如,制订交通法规表面上是要限制行车权,实际上是保障公众的人身安全,试想如果没有限速,没有红绿灯,谁还敢上路行驶?对软件来说,适当的规范和标准绝不是消灭代码内容的创造性、优雅性,而是限制过度个性化,以一种普遍认可的统一方式一起做事,提升协作效率,降低沟通成本。代码的字里行间流淌的是软件系统的血液,质量的提升是尽可能少踩坑,杜绝踩重复的坑,切实提升系统稳定性,码出质量。
这些规范你遵守了么?
一、编程规约
(一)命名规范
- 反例:DaZhePromotion[打折]/getPingfenByName()[评分]
- 反例:Boolean isDeleted;
- 反例:inta的随意命名方式。
- 正例:startTime /workQueue /nameList /TERMINATED_THREAD_COUNT
- 反例:startedAt /QueueOfWork /listName /COUNT_TERMINATED_THREAD
- 正例:接口方法签名voidcommit(); 接口基础常量StringCOMPANY="alibaba";
- 反例:接口方法定义publicabstractvoidf();
(二) 常量定义
- 反例:String key = "Id#taobao_" + tradeId;
- 说明:Long a = 2l; 写的是数字的 21,还是 Long 型的 2。
(三) 代码格式
- 反例:if (空格 a == b 空格)
- 说明:运算符包括赋值运算符=、逻辑运算符&&、加减乘除符号等。
- 正例: // 这是示例注释,请注意在双斜线之后有一个空格
- 1)第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进,参考示例。
- 2)运算符与下文一起换行。
- 3)方法调用的点符号与下文一起换行。
- 4)方法调用中的多个参数需要换行时,在逗号后进行。
- 5)在括号前不要换行,如sb.append("Jack").append("Ma")...append
("alibaba"); // 超过 120 个字符的情况下,不要在括号前换行
- 正例:method(args1, args2, args3);
(四) OOP 规约
- 说明:可变参数必须放置在参数列表的最后。(提倡同学们尽量不用可变参数编程)
- 正例:"test".equals(object);
- 反例:object.equals("test");
- 反例:
- 正例:
- 说明:BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。
- 正例:优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部其实执行了Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。
BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommend2 = BigDecimal.valueOf(0.1);
- 1) 【强制】所有的 POJO 类属性必须使用包装数据类型。
- 2) 【强制】RPC 方法的返回值和参数必须使用包装数据类型。
- 3) 【推荐】所有的局部变量使用基本数据类型。
- 反例:POJO 类的 createTime 默认值为 new Date(),但是这个属性在数据提取时并没有置入具体值,在 更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。
- 说明:
- 说明:下例中,反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行 append 操作,最后通过 toString 方法返回 String 对象,造成内存资源浪费。
- 反例:
- 1) 如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private。
- 2) 工具类不允许有 public 或 default 构造方法。
- 3) 类非 static 成员变量并且与子类共享,必须是 protected。
- 4) 类非 static 成员变量并且仅在本类使用,必须是 private。
- 5) 类 static 成员变量如果仅在本类使用,必须是 private。
- 6) 若是 static 成员变量,考虑是否为 final。
- 7) 类成员方法只供类内部调用,必须是 private。
- 8) 类成员方法只对继承类公开,那么限制为 protected。
- 说明:任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解耦。
(五) 集合处理
- 1) 只要覆写 equals,就必须覆写 hashCode。
- 2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两个方法。
- 3) 如果自定义对象作为 Map 的键,那么必须覆写 hashCode 和 equals。
- 说明:String 已覆写 hashCode 和 equals 方法,所以我们可以愉快地使用 String 对象作为 key 来使用。
- 反例:直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现ClassCastException 错误。
- 说明:在 ArrayList#addAll 方法的第一行代码即 Object[] a = c.toArray(); 其中 c 为输入集合参数,如果 为 null,则直接抛出异常。
- 说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适 配器模式,只是转换接口,后台的数据仍是数组。
- 反例:
- 说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应 的 value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8, 使用 Map.forEach 方法。
集合类 | Key | Value | Super | 说明 |
---|---|---|---|---|
Hashtable | 不允许为 null | 不允许为 null | Dictionary | 线程安全 |
ConcurrentHashMap | 不允许为 null | 不允许为 null | AbstractMap | 锁分段技术(JDK8:CAS) |
TreeMap | 不允许为 null | 允许为 null | AbstractMap | 线程不安全 |
HashMap | 允许为 null | 允许为 null | AbstractMap | 线程不安全 |
(六) 并发处理
- 说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问 题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
- 说明:Executors 返回的线程池对象的弊端如下:
- 1) FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
- 2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
- 说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar, DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。
- 说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。
- 说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。
- 说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次。
- 说明:如果是 count++操作,使用如下类实现:AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观 锁的重试次数)。
(七) 控制语句
- 说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件 来代替。
- 反例:判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数, 这样的话,活动无法终止。

- 说明:如果非使用 if()...else if()...else...方式表达逻辑,避免后续代码维护困难,【强制】请勿超过 3 层。
- 说明:很多 if 语句内的逻辑表达式相当复杂,与、或、取反混合运算,甚至各种方法纵深调用,理解成 本非常高。如果赋值一个非常好理解的布尔变量名字,则是件令人爽心悦目的事情。
- 正例:
// 伪代码如下
final boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed) {
...
}
- 反例:
public final void acquire(long arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
} }
- 说明:取反逻辑不利于快速理解,并且取反逻辑写法必然存在对应的正向逻辑写法。
- 正例:使用 if (x < 628) 来表达 x 小于 628。
- 反例:使用 if (!(x >= 628)) 来表达 x 小于 628。
(八) 注释规约
- 说明:代码被注释掉有两种可能性:1)后续会恢复此段代码逻辑。2)永久不用。前者如果没有备注信 息,难以知晓注释动机。后者建议直接删掉(代码仓库已然保存了历史代码)。
(九) 其它
- 说明:不要在方法体内定义:Pattern pattern = Pattern.compile(“规则”);
- 说明:如果想获取更加精确的纳秒级时间值,使用 System.nanoTime()的方式。在 JDK8 中,针对统计时 间等场景,推荐使用 Instant 类。
二、异常日志
(一) 异常处理
- 说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
- 说明:try 块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally 块中的语句,如果此处存 在 return 语句,则在此直接返回,无情丢弃掉 try 块中的返回点。
- 1) 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。 反例:public int f() { return Integer 对象}, 如果为 null,自动解箱抛 NPE。
- 2) 数据库的查询结果可能为 null。
- 3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
- 4) 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
- 5) 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
- 6) 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。 - 正例:使用 JDK8 的 Optional 类来防止 NPE 问题。
(二) 日志规约

- 说明:因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符 仅是替换动作,可以有效提升性能。
- 正例:logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
三、单元测试
- 反例:method2 需要依赖 method1 的执行,将执行结果作为 method2 的输入。
- 说明:源码编译时会跳过此目录,而单元测试框架默认是扫描此目录。
- 反例:删除某一行数据的单元测试,在数据库中,先直接手动增加一行作为删除目标,但是这一行新增数 据并不符合业务插入规则,导致测试结果异常。
四、安全规约
- 说明:防止没有做水平权限校验就可随意访问、修改、删除别人的数据,比如查看他人的私信内容、修改 他人的订单。
- 说明:中国大陆个人手机号码显示为:137**0969,隐藏中间 4 位,防止隐私泄露。
五、MySQL 数据库
(一) 建表规约
- 说明:MySQL 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写。因此,数据库名、表 名、字段名,都不允许出现任何大写字母,避免节外生枝。
- 说明:pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index 的简称。
- 说明:在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的 结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储。
- 说明:其中 id 必为主键,类型为 bigint unsigned、单表时自增、步长为 1。create_time, update_time 的类型均为 datetime 类型。
- 说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。
(二) 索引规约
- 说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外, 即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。
- 说明:即使双表 join 也要注意表索引、SQL 性能。
- 说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
- 说明:MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。
- 正例:先快速定位需要获取的 id 段,然后再关联: SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id
(三) SQL 语句
标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。
- 说明:count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。
- 正例:使用如下方式来避免 sum 的 NPE 问题:SELECT IFNULL(SUM(column), 0) FROM table;
(四) ORM 映射
- 说明:1)增加查询分析器解析成本。2)增减字段容易与 resultMap 配置不一致。3)无用字段增加网络 消耗,尤其是 text 类型的字段。
- 说明:参见定义 POJO 类以及数据库字段定义规定,在<resultMap>中增加映射,是必须的。在 MyBatis Generator 生成的代码中,需要进行对应的修改。
- 说明:resultClass=”Hashtable”,会置入字段名和属性值,但是值的类型不可控。
观阅原文件可免费下载java开发手册(阿里巴巴)