读书笔记 之 《阿里巴巴Java开发手册》

一、前言

本文参考《阿里巴巴Java开发手册》,这本书主要定义了一些代码的规范以及一些注意事项。我只根据我自己的不足,摘录了一些内容,方便以后查阅。

二、读书笔记

  • 命名

1、代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。

2、常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
 
3、抽象类命名使用 Abstract 或 Base 开头; 异常类命名使用 Exception 结尾; 测试类命名以它要测试的类的名称开始,以 Test 结尾。
 
4、中括号是数组类型的一部分,数组定义如下: String[] args;
 
5、POJO 类中布尔类型的变量,都不要加 is,否则部分框架解析会引起序列化错误。
反例: 定义为基本数据类型 Boolean isDeleted; 的属性,它的方法也是 isDeleted()。 RPC框架在反向解析的时候, “以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。
 
6、包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。
 
7、如果使用到了设计模式,建议在类名中体现出具体模式。有利于阅读者快速理解架构设计思想。
 
8、接口类中的方法和属性不要加任何修饰符号(public 也不要加) ,保持代码的简洁性,并加上有效的 Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法相关,并且是整个应用的基础常量。
 
9、枚举类名建议带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。
说明: 枚举其实就是特殊的常量类,且构造方法默认强制为私有。
正例: 枚举名字: DealStatusEnum, 成员名称: SUCCESS / UNKOWN_REASON。
 
10、各层命名规约:
A) Service/DAO 层方法命名规约
1) 获取单个对象的方法用 get 做前缀。
2) 获取多个对象的方法用 list 做前缀。
3) 获取统计值的方法用 count 做前缀。
4) 插入的方法用 save(推荐) 或 insert 做前缀。
5) 删除的方法用 remove(推荐) 或 delete 做前缀。
6) 修改的方法用 update 做前缀。
B) 领域模型命名规约
1) 数据对象: xxxDO, xxx 即为数据表名。
2) 数据传输对象: xxxDTO, xxx 为业务领域相关的名称。
3) 展示对象: xxxVO, xxx 一般为网页名称。
4) POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO。 
 
  • 常量定义
1、不允许任何未经定义的常量直接出现在代码中。
反例: String key = "Id#taobao_" + tradeId;
cache.put(key, value);
 
2、long 或者 Long 初始赋值时,必须使用大写的 L,不能是小写的 l,小写容易跟数字1 混淆,造成误解。
 
3、不要使用一个常量类维护所有常量,应该按常量功能进行归类,分开维护。如:缓存相关的常量放在类: CacheConsts 下; 系统配置相关的常量放在类: ConfigConsts 下。
 
  • OOP规约
1、避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。
 
2、外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加@Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么。
 
3、不能使用过时的类或方法。
 
4、Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用
equals
正例: "test".equals(object);
反例: object.equals("test");
 
5、所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。对于 Integer var = ? 在-128 至 127 范围内的赋值, Integer 对象是在IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。
 
6、关于基本数据类型与包装数据类型的使用标准如下:
1) 【强制】 所有的 POJO 类属性必须使用包装数据类
2) 【强制】 RPC 方法的返回值和参数必须使用包装类
3) 【推荐】 所有的局部变量使用基本数据类型。
说明:用基本数据类型数据默认值是0,而包装数据类型默认值是null,数据库的查询结果可能为null,因为自动拆箱,用基本数据类型接收有NPE风险。
 
7、定义 DO/DTO/VO 等 POJO 类时,不要设定任何属性默认值(在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。)
 
8、序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败; 如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。注意 serialVersionUID 不一致会抛出序列化运行时异常。
 
9、构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中。
 
10、类内方法定义顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter方法。
 
11、循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。
 
12、慎用 Object 的 clone 方法来拷贝对象。对象的 clone 方法默认是浅拷贝,若想实现深拷贝需要重写 clone 方法实现属性对象的拷贝。
 
13、工具类不允许有 public 或 default 构造方法。
 
  • 集合处理

1、ArrayList的subList结果不可强转成ArrayList,否则会抛出 ClassCastException异常: java.util.RandomAccessSubList cannot be cast to java.util.ArrayList。subList 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList ,而是ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。

2、在 subList 场景中, 高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均产生 ConcurrentModificationException 异常。

3、使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一样的数组,大小就是 list.size()。

说明: 使用 toArray 带参方法,入参分配的数组空间不够大时, toArray 方法内部将重新分配内存空间,并返回新数组地址; 如果数组元素大于实际所需,下标为[ list.size() ]的数组元素将被置为 null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素个数一致。
正例:
List<String> list = new ArrayList<String>(2);
list.add("guan");
list.add("bao");
String[] array = new String[list.size()];
array = list.toArray(array);
反例: 直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现 ClassCastException 错误

4、使用工具类 Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。

说明: asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。

5、泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用 add 方法, 而<? super T>不能使用 get 方法,做为接口调用赋值时易出错。

说明: 扩展说一下 PECS(Producer Extends Consumer Super)原则: 1) 频繁往外读取内容的,适合用上界 Extends。 2) 经常往里插入的,适合用下界 Super。

6、不要在 foreach 循环里进行元素的 remove/add 操作。 remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。

正例:
Iterator<String> it = a.iterator();
while (it.hasNext()) {
String temp = it.next();
if (删除元素的条件) {
it.remove();
}
}
反例:
List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
for (String temp : a) {
if ("1".equals(temp)) {
a.remove(temp);
}
}

7、集合初始化时, 指定集合初始值大小。

说明: HashMap 使用 HashMap(int initialCapacity) 初始化,
正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loaderfactor) 默认为 0.75, 如果暂时无法确定初始值大小, 请设置为 16。

9、使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。

说明: keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出key 所对应的 value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用 Map.foreach 方法。
正例: values()返回的是 V 值集合,是一个 list 集合对象; keySet()返回的是 K 值集合,是一个 Set 集合对象; entrySet()返回的是 K-V 值组合集合。

10、在 java 集合类库中,List 的 contains 方法普遍时间复杂度是 O(n) ,如果在代码中需要频繁调用 contains 方法查找数据,可以先将 list 转换成 HashSet 实现,将 O(n) 的时间复杂度降为 O(1) 。

11、高度注意 Map 类集合 K/V 能不能存储 null 值的情况,如下表格:

Hashtable 不允许为 null 不允许为 null Dictionary 线程安全
ConcurrentHashMap 不允许为 null 不允许为 null AbstractMap 分段锁技术
TreeMap 不允许为 null 允许为 null AbstractMap 线程不安全
HashMap 允许为 null 允许为 null AbstractMap 线程不安全
反例: 由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,而事实上,存储 null 值时会抛出 NPE 异常。 
 
 
 
  • 控制语句

1、在 if/else/for/while/do 语句中必须使用大括号。 即使只有一行代码,避免使用单行的形式: if (condition) statements;

2、表达异常的分支时, 少用 if-else 方式。如果非得使用 if()...else if()...else...方式表达逻辑,避免后续代码维护困难, 请勿超过 3 层。

3、除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。

正例:
final boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed) {
...
}
反例:
if ((file.open(fileName, "w") != null) && (...) || (...)) {
...
}

 

  • 异常处理

1、Java 类库中定义的一类 RuntimeException 可以通过预先检查进行规避,而不应该通过 catch 来处理,比如: IndexOutOfBoundsException, NullPointerException 等等。

正例: if (obj != null) {...}
反例: try { obj.method() } catch (NullPointerException e) {...}
 
2、异常不要用来做流程控制,条件控制,因为异常的处理效率比条件分支低。
 
3、有 try 块放到了事务代码中, catch 异常后,如果需要回滚事务,一定要注意手动回滚事务。
 
4、不能在 finally 块中使用 return, finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句。
 
5、防止空指针异常,是程序员的基本修养。
 
6、避免出现重复的代码(Don’t Repeat Yourself) ,即 DRY 原则。
说明: 随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是共用模块。
 
  • 日志规约

1、应用中不可直接使用日志系统(Log4j、 Logback) 中的 API,而应依赖使用日志框架SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。

private static final Logger logger = LoggerFactory.getLogger(Abc.class);

2、日志文件推荐至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。

3、对 trace/debug/info 级别的日志输出,必须使用条件输出形式或者使用占位符的方式。

说明: logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
如果日志级别是 warn,上述日志不会打印,但是会执行字符串拼接操作,如果 symbol 是对象,会执行 toString()方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。
正例: (条件)
if (logger.isDebugEnabled()) {
logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
}
正例: (占位符)
logger.debug("Processing trade with id: {} symbol : {} ", id, symbol); 

4、避免重复打印日志,浪费磁盘空间,务必在 log4j.xml 中设置 additivity=false。

<logger name="com.taobao.dubbo.config" additivity="false">

5、谨慎地记录日志。生产环境禁止输出 debug 日志; 有选择地输出 info 日志; 如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志 。

  • 其他

1、在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。

说明: 不要在方法体内定义: Pattern pattern = Pattern.compile(规则);

2、后台输送给页面的变量必须加$!{var}——中间的感叹号。

说明: 如果 var=null 或者不存在,那么${var}会直接显示在页面上。

3、不要在视图模板中加入任何复杂的逻辑。

说明: 根据 MVC 理论,视图的职责是展示,不要抢模型和控制器的活。

4、任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存。

5、对于“明确停止使用的代码和配置”,如方法、变量、类、配置文件、动态配置属性等要坚决从程序中清理出去,避免造成过多垃圾。

6、new BigDecimal(double) 存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。请使用 BigDecimal.valueOf(double) 。

7、SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为static,必须加锁,或者使用 DateUtils 工具类。

正例: 注意线程安全,使用DateTools 。亦推荐如下处理: 
public class DateTools
{
private static ThreadLocal<SimpleDateFormat> t1=new ThreadLocal<>();
public static SimpleDateFormat getSdf(String pattern){
SimpleDateFormat simpleDateFormat = t1.get();
if (simpleDateFormat==null){
simpleDateFormat=new SimpleDateFormat(pattern);
}
t1.set(simpleDateFormat);
return simpleDateFormat;
} }
说明: 如果是 JDK8 的应用,可以使用 Instant 代替 Date, LocalDateTime 代替 Calendar,DateTimeFormatter代替Simpledateformatter,官方给出的解释:simple beautiful strong immutable thread-safe。
        // Instant 相当于 java.util.Date 类
Instant instant = Instant.now();
instant.getLong(INSTANT_SECONDS); //时间戳 // LocalDate 不包含时间的日期
LocalDate localDate = LocalDate.now();
// 自定义日期时间
LocalDate of = LocalDate.of(2019, 10, 1);
LocalDate parse = LocalDate.parse("2019-09-02");
// 日期加减
LocalDate days = parse.plusMonths(1L).minusDays(1L);
// 日期比较
System.out.println(of.isBefore(days));
System.out.println(of.isAfter(days));
System.out.println(of.equals(days)); // LocalTime 不包含日期的时间
LocalTime localTime = LocalTime.now();
System.out.println(LocalTime.MAX);
System.out.println(LocalTime.MIN); // LocalDateTime 包含了日期及时间,没有偏移信息(时区),相当于 java.util.Calendar 类
LocalDateTime localDateTime = LocalDateTime.now();
LocalDateTime atTime = localDate.atTime(LocalTime.MAX);
LocalDateTime atDate = localTime.atDate(LocalDate.of(2019, 10, 14)); // 时间日期格式化,相当于 java.text.SimpleDateFormat
LocalDateTime dateTime = LocalDateTime.now();
dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));

书籍链接: 阿里巴巴Java开发手册

上一篇:读阿里巴巴Java开发手册v1.2.0之工程结构有感【架构篇】


下一篇:netsh命令