乐趣区

《阿里巴巴 Java开发手册》读后感

前言
只有光头才能变强
前一阵子一直在学 Redis,结果在黄金段位被虐了,暂时升不了段位了,每天都拿不到首胜(好烦)。
趁着学校校运会,合理地给自己放了一个小长假,然后就回家了。回到家才发现当时 618 买了一堆书,这堆书还有没撕包装的呢 …. 于是我翻出了最薄的一本《阿里巴巴 Java 开发手册》

这本书一共就 90 多页,一天就可以通读完了,看完之后我又来水博文了。
注意:

书上很多的规范是可以用 IDE 来避免的,也有很多之前已经知道的了。
所以,这篇文章只记录我认为比较重要,或者说是我之前开发时没有注意到的一些规范(知识点)。
该文章的内容肯定没有书上写得那么全的,如果感兴趣的同学可以去买一本来读一下~

PDF 官方地址:
https://github.com/alibaba/p3c
一、Java 相关

POJO 是 DO/DTO/BO/VO 的统称,禁止命名为 xxxPOJO
获取多个对象的方法中 list 作为前缀
获取统计值的方法用 count 作为前缀

POJO 类中的布尔类型 (Boolean) 的变量都不要加 is 前缀,否则部分框架解析会引起序列化错误
如果你的变量名带 is 的话,比如 isActive,框架解析的时候可能就当成 active 了。

如果是形容能力的接口名称,取对应的形容词为接口名(通常是 -able 的形式)
不允许任何魔法值 (未经预先定义的常量) 直接出现在代码中
Object 的 euqals 方法容易抛出空指针异常,应使用常量或者有值的对象来调用 equals。推荐使用 java.util.Object#equals 工具类

所有 POJO 类的属性全部使用包装数据类型,RPC 的返回值和参数必须使用包装数据类型,所有的局部变量都使用基本数据类型。定义 VO/DTO/DO 等 POJO 类时,不要设定任何属性的默认值
如果你的类属性使用 int 这样的基本数据类型,默认值是 0。一般情况下该变量没有赋值,一般想表达的是不存在(null),而不是 0。

构造方法禁止加入任何的业务逻辑,如果初始化逻辑可以放在 init 方法中。set/get 方法也不要增加业务逻辑。
如果 set/get 方法放入业务逻辑,有时候排查问题就变得很麻烦了

工具类 Arrays.asList()把数组转成 List 时,不能使用其修改集合的相关方法。比如说 add、clear、remove

在 JDK7 以及以上版本中,Comparator 要满足三个条件,不然调用 Arrays.sort()或者 Collections.sort()会报异常。

x,y 的比较结果和 y,x 的比较结果相反
传递性:x>y 并且 y >z,那么 x 一定大于 z
对称性:x=y, 则 x,z 比较结果和 y,z 比较结果相同

使用 entrySet 遍历 Map 类集合 K /V,而不是用 keySet 方式遍历
keySet 遍历了两次,一次是转成 Iterator 对象,一次是从 hashMap 中取出 key 所对应的 value,如果 JDK8 可以使用 Map.foreach 方法

线程资源必须由线程池提供,不允许在应用中自行显示创建线程。线程池不允许用 Executors 创建,通过 ThreadPoolExecutor 的方式创建,这样的处理方式能够让编写代码的工程师更加明确线程池的运行规则,规避资源耗尽的风险。

SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类
如果是 JDK8 应用,可以使用 Instant(针对时间统计等场景)代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat

避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed 导致性能下降
在 JDK7 之后,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要编码保证每个线程持有一个实例。

类、类属性、类方法的注释必须使用 Javadoc 规范,使用 /** 内容 */ 格式,不得使用 //xxx 方式
所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释,除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。所有的类都必须添加创建者和创建日期。
对于暂时被注释掉,后续可能恢复使用的代码片断,在注释代码的上方,使用三个斜杠 /// 来说明注释代码的理由
保证单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试之间不能互相调用,也不能依赖执行的先后顺序。
高并发服务器建议调小 TCP 协议的 time_await 超时时间,调大最大事件句柄数(fd),

1.1 值得说明的点
一、不允许任何魔法值 (未经预先定义的常量) 直接出现在代码中
例子:

Negative example:
//Magic values, except for predefined, are forbidden in coding.
if (key.equals(“ 关注公众号:Java3y”)) {
//…
}

Positive example:
String KEY_PRE = “ 关注公众号:Java3y”;
if (KEY_PRE.equals(key)) {
//…
}

ps: 我猜是把先常量定义出来,后续引用 / 修改的时候就很方便了。

二、Object 的 euqals 方法容易抛出空指针异常,应使用常量或者有值的对象来调用 equals。推荐使用 java.util.Object#equals 工具类
java.util.Object#equals 的源码(已经判断 null 的情况了)

public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}

三、工具类 Arrays.asList()把数组转成 List 时,不能使用其修改集合的相关方法。
因为返回的 ArrayList 是一个内部类,并没有实现集合的修改方法。后台的数据仍是数组,这里体现的是适配器模式。

四、在 JDK7 以及以上版本中,Comparator 要满足自反性,传递性,对称性,不然调用 Arrays.sort()或者 Collections.sort()会报异常。
The implementor must ensure that sgn(compare(x, y)) == -sgn(compare(y, x)) for all x and y. (This implies that compare(x, y) must throw an exception if and only if compare(y, x) throws an exception.)The implementor must also ensure that the relation is transitive: ((compare(x, y)>0) && (compare(y, z)>0)) implies compare(x, z)>0.
Finally, the implementor must ensure that compare(x, y)==0 implies that sgn(compare(x, z))==sgn(compare(y, z)) for all z.

1)x,y 的比较结果和 y,x 的比较结果相反。
2)传递性:x>y,y>z, 则 x>z。
3)对称性:x=y, 则 x,z 比较结果和 y,z 比较结果相同。

反例:下例中没有处理相等的情况,实际使用中可能会出现异常:

new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getId() > o2.getId() ? 1 : -1;
}
}

使用 entrySet 遍历 Map 类集合 K /V,而不是用 keySet 方式遍历
首先我们来看一下使用 keySet 是如何遍历 HashMap 的:

public static void main(String[] args) throws InterruptedException {

HashMap<String, String> hashMap = new HashMap<>();
hashMap.put(“ 关注公众号:”, “Java3y”);
hashMap.put(“ 坚持原创 ”, “Java3y”);
hashMap.put(“ 点赞 ”, “ 关注,转发,分享 ”);

// 得到 keySet,遍历 keySet 得到所有的 key
Set<String> strings = hashMap.keySet();
Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()) {

// HashMap 的每个 key
String key = iterator.next();

// 通过 key 可以获得对应的 value,如果有看过 HashMap 的同学知道 get 方法的时间复杂度是 O(1)
System.out.println(“key = ” + key + “, value = ” + hashMap.get(key));
}

}

再来看一下源码:

// 1. 得到 keySet,如果不存在,则创建
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}

// 2. 初始化 ks (实际上就是 Set 集合[HashMap 的内部类],在初始化时需要顺便初始化 iterator)
ks = new AbstractSet<K>() {
public Iterator<K> iterator() {
return new Iterator<K>() {
private Iterator<Entry<K,V>> i = entrySet().iterator();

public boolean hasNext() {
return i.hasNext();
}

public K next() {
return i.next().getKey();
}

public void remove() {
i.remove();
}
};
}

};

再来看一下 entrySet,可以直接拿到 key 和 value,不用再使用 get 方法来得到 value,所以比 keySet 更加推荐使用!

public static void main(String[] args) throws InterruptedException {

HashMap<String, String> hashMap = new HashMap<>();
hashMap.put(“ 关注公众号:”, “Java3y”);
hashMap.put(“ 坚持原创 ”, “Java3y”);
hashMap.put(“ 点赞 ”, “ 关注,转发,分享 ”);

// 得到 entrySet,遍历 entrySet 得到结果
Set<Map.Entry<String, String>> entrySet = hashMap.entrySet();
Iterator<Map.Entry<String, String>> iterator = entrySet.iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
System.out.println(“key = ” + entry.getKey() + “, value = ” + entry.getValue());
}
}
如果是 JDK8 的话,推荐直接使用 Map.forEach()就好了,我们也来看看用法:

public static void main(String[] args) throws InterruptedException {

HashMap<String, String> hashMap = new HashMap<>();
hashMap.put(“ 关注公众号:”, “Java3y”);
hashMap.put(“ 坚持原创 ”, “Java3y”);
hashMap.put(“ 点赞 ”, “ 关注,转发,分享 ”);

// forEach 用法
hashMap.forEach((key, value) -> System.out.println(“key = ” + key + “, value = ” + value));
}

其实在源码里边我们可以发现,forEach 实际上就是封装了 entrySet,提供 forEach 给我们可以更加方便地遍历 Map 集合

// forEach 源码
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}

五、SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。
有以下的例子可以正确使用 SimpleDateFormat:

// 1. 在方法内部使用,没有线程安全问题
private static final String FORMAT = “yyyy-MM-dd HH:mm:ss”;
public String getFormat(Date date){
SimpleDateFormat dateFormat = new SimpleDateFormat(FORMAT);
return dateFormat.format(date);
}

// 2. 每次使用的时候加锁
private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”);
public void getFormat(){
synchronized (SIMPLE_DATE_FORMAT){
SIMPLE_DATE_FORMAT.format(new Date());
….;
}

// 3. 使用 ThreadLocal,每个线程都有自己的 SimpleDateFormat 对象,互不干扰
private static final ThreadLocal<DateFormat> DATE_FORMATTER = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat(“yyyy-MM-dd”);
}
};

// 4. 使用 DateTimeFormatter(This class is immutable and thread-safe.)

DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern(“yyyy-MM-dd HH:mm:ss”);
System.out.println(timeFormatter.format(LocalDateTime.now()));

如果是 JDK8 应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat。

二、数据库相关

表达是否概念的字段,必须使用 isxxx 的方式命名,数据类型是 unsigned tinyint(1 表示是,0 表示否)
小数类型用 decimal,禁止使用 float 和 double。
varchar 是可变字符串,不预选分配存储空间的话,长度不要超过 5000 个字符。如果超过则用 text,独立一张表,用主键对应,避免影响到其他字段的索引效率。
表必备的三个字段:id(类型是 unsigned bigint),gmt_create(创建时间),gme_modified(修改时间)
字段允许适当冗余,以提高查询性能,但必须考虑数据一致性。冗余的字段必须不是频繁修改的字段,不是 varhar 超长字段(更不能是 text 字段)。
单表行数超过 500 万行或者单表容量超过 2GB 才推荐进行分库分表(如果预计三年都达不到这个数据量,不要在创建表的时候就分库分表!)

超过三个表禁止使用 join,需要 join 的字段,数据类型必须保持一致,当多表关联查询时,保证被关联的字段需要有索引!

在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,页面搜索严禁左模糊或者全模糊,如果需要则通过搜索引擎来解决。
充分利用好最左前缀匹配特性!

利用延迟关联或者子查询优化超多也分场景。
如果有全球化需要,均以 utf- 8 编码。如果需要存储表情,选择 utf8mb4 进行存储。

2.1 值得说明的点
一、利用延迟关联或者子查询优化超多也分场景。
MySQL 并不是跳过 offset 行,而是取 offset+ N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。
例子:

// 优化前

SELECT id, cu_id, name, info, biz_type
, gmt_create, gmt_modified, start_time, end_time, market_type
, back_leaf_category, item_status, picuture_url
FROM relation
WHERE biz_type = ‘0’
AND end_time >= ‘2014-05-29’
ORDER BY id ASC
LIMIT 149420, 20;

// 优化后

SELECT a.*
FROM relation a, (
SELECT id
FROM relation
WHERE biz_type = ‘0’
AND end_time >= ‘2014-05-29’
ORDER BY id ASC
LIMIT 149420, 20
) b
WHERE a.id = b.id

解释:其实这里就是通过使用覆盖索引查询返回需要的主键, 再根据主键关联原表获得需要的数据。这样就是充分利用了索引!

三、未解决的问题
在看《手册》的时候还有一些知识点没看过、没实践过、涉及到的知识点比较多的,在这里先 mark 一下,后续再遇到或者有空的时候再回来补坑~

使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown 方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到,避免主线程无法执行至 await 方法,直到超时才返回结果。说明:注意,子线程抛出异常堆栈,不能在主线程 try-catch 到。
对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。如果是 count++ 操作,使用如下类实现: AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。
使用 JDK8 的 Optional 类来防止 NPE 问题。

当然了,如果你有比较好的资料阅读,也可以在评论区告诉我。我也会 mark 住好好看看。
比如说:“3y,我发现 Optional 类有篇文章写得很不错,url 是 xxxx(书籍的名称是 xxx)

由于现在没有一定的经验积累,所以以下的章节得回头看:
《手册》中的“日志规约”,“工程结构”、“设计规范”
最后
看我上面写的内容就知道,除了一些规范外,还有很多实用的小技巧,这些对我们开发是有帮助的。我这个阶段也有一些没怎么接触过的(“ 日志 ”,” 设计 ”,” 二方库 ”),这些都需要我在成长中不断的回看才行。
ps: 我会回来补坑的。
引用书上的一句话:
很多编程方式客观上没有对错之分,一致性很重要,可读性很重要,团队沟通效率很重要。程序员天生需要团队协作,而协作的正能量要放在问题的有效沟通上。个性化应尽量表现在系统架构和算法效率的提升上,而不是在合作规范上进行纠缠不休的讨论、争论,最后没有结论。
作者 (孤尽) 在知乎回答的一句话:

翻完了不代表记住了,记住了不代表理解了,理解了不代表能够应用上去,真正的知识是实践,实践,实践。
如果你觉得我写得还不错,了解一下:

坚持原创的技术公众号:Java3y
Java 交流群:742919422
文章的目录导航(精美脑图 + 海量视频资源):https://github.com/ZhongFuCheng3y/3y

退出移动版