一、编程规约
(一) 命名风格
布尔类型变量命名不要加is前缀
9.【强制】POJO 类中的任何布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误。 说明:本文 MySQL 规约中的建表约定第 1 条,表达是与否的变量采用 is_xxx 的命名方式,所以需要在
设置从 is_xxx 到 xxx 的映射关系。 反例:定义为布尔类型 Boolean(**怀疑此处打错, 应为 boolean**) isDeleted 的字段,它的 getter 方法也是 isDeleted(),部分框架在反向解析时,“误以 为”对应的字段名称是 deleted,导致字段获取不到,得到意料之外的结果或抛出异常。
尝试了以下代码, 注意其中 isPackedBoolean 是 Boolean, isPrimaryBoolean 为 boolean :
@Data
public class POJO {
private boolean isPrimaryBoolean;
private Boolean isPackedBoolean;
}
经过编译之后的类为:
public class POJO {
private boolean isPrimaryBoolean;
private Boolean isPackedBoolean;
public POJO() {
}
public boolean isPrimaryBoolean() {
return this.isPrimaryBoolean;
}
public Boolean getIsPackedBoolean() {
return this.isPackedBoolean;
}
public void setPrimaryBoolean(boolean isPrimaryBoolean) {
this.isPrimaryBoolean = isPrimaryBoolean;
}
public void setIsPackedBoolean(Boolean isPackedBoolean) {
this.isPackedBoolean = isPackedBoolean;
}
...
可见其中 isPrimaryBoolean
字段的 get 和 set 方法并未再添加 is 前缀, 而是直接生成 isPrimaryBoolean
和 setPrimaryBoolean
两个方法.
lombok 之中 @Data 注解对基本类型的 boolean 和包装类型的 Boolean 生成的 getter/setter 不同.
命名规约
-
获取单个对象的方法用 get 做前缀
-
获取多个对象的方法用 list 做前缀, 复数结尾, 比如 listItems()
-
获得统计值的方法用 count 做前缀
-
插入的方法用 save 做前缀
-
删除的方法用 remove 做前缀
-
修改的方法用 update 做前缀
(三) 代码格式
单个方法总行数不超过80行
代码逻辑分清红花和绿叶, 主干和分支, 分支代码抽取出来成为额外方法.
(四) OOP 规约
浮点数不能判断等值
计算机无法精确标识浮点数, 因此无法直接判断等值. 我个人的建议是, 不要在传值的地方用浮点数, 统一用 BigDecimal.
BigDecimal 使用 compareTo() 方法等值比较
bigDecimal 的 equals() 是带上值和精度的, 因此要使用忽略精度的 compareTo()
BigDecimal 仅使用 String 类型的参数初始化
如果使用 BigDecimal(double), 可能存在精度损失的风险, 在精确计算或者比较的场景下导致业务逻辑异常.
BigDecimal g = new BigDecimal(0.1F);实际的存储值为:0.100000001490116119384765625
基本数据类型与包装数据类型的使用标准
除了局部变量之外, 全部使用包装数据类型!
个人理解 ,因为包装数据类型默认为null, 可以提醒数据拿取方业务可能不正常, 但是如果是 integer, 那么只会返回0, 业务方并不知道是真的没有数据, 还是数据是0.
primary type 有默认值, 永远不为空这一点确实有利有弊.
在一个POJO类之中禁止同时存在 isXXX() 和 getXxx()
在框架, 比如 jackson 或者 hibernate 通过反射拿取属性的时候, 同时存在这两个方法, 其无法确定优先使用哪个方法!!!
比如:
public class Person {
private boolean employed;
public boolean isEmployed() {
return employed;
}
public Boolean getEmployed() {
return employed ? Boolean.TRUE : Boolean.FALSE;
}
public void setEmployed(boolean employed) {
this.employed = employed;
}
}
在使用jackson 序列化的时候, 模型可能选择 getEmployed(), 返回 Boolean 对象, 而不是 primary type 的 boolean.
对于布尔类型, 遵循单一 getter 规则:
- 对于 boolean 属性,始终使用 isXxx() 作为 getter 方法。
- 对于 Boolean 属性,使用 getXxx(),但避免与 isXxx() 共存。
使用下标访问 String 的 split 方法得到的数组时候, 注意最后一个分隔符之后是否有内容的检查和index检查.
String str = "a,b,c,,";
String[] arr = str.split(",");
//arr = ["a", "b", "c"], 长度只是3
System.out.println(arr.length); // 报错
在 split() 之中, 如果不传入 limit参数, 等于 limit = 0.
- 当 limit 为 0 时,遍历得到的数组,从尾部向前检查连续的空字符串,并将它们剔除,直到遇到非空的项为止
final 的使用
-
不允许继承的类, 比如 String
-
不允许修改引用的对象
-
不允许覆写 Override 的方法
(五) 日期时间
日期格式化之中 pattern 标识年份, 统一用小写的 y
日期格式化的时候, yyyy 标识那一天所在的年份, YYYY 是 week in which year.
某程序员因使用 YYYY/MM/dd 进行日期格式化,2017/12/31 执行结果为 2018/12/31,造成线上故障。
(六) 集合处理
hashCode 和 equals 的处理
-
覆写 equals 必须 覆写 hashCode
-
Set 或者是 Map 的key的对象类型, 必须覆写 hashCode 和 equals()
java.util.stream.Collectors 类的 toMap() 一定要加入mergeFunction避免重复key异常
在处理集合的时候, 无法保证内部是否有一样的数值, 所以要自定义mergeFunction. 一般情况下mergeFunction 都是用新的替换旧的(list里面有多个重复元素对), 不然要仔细想想,是否能用map?
java.util.stream.Collectors 类的 toMap() 要注意 value 为null 时候会抛出NPE
List<Pair<String, Double>> pairArrayList = new ArrayList<>(2);
pairArrayList.add(Pair.of("version1", 8.3));
pairArrayList.add(Pair.of("version2", null));
// 抛出 NullPointerException 异常
Map<String, Double> map = pairArrayList.stream().collect(Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));
原因: 其底层实现为 java.util.HashMap#merge. 方法的意义是将新的KV对插入到现有的map里面, 基本理解为:
-
K 不存在或者现有 map 中 K 对应的 V 为 null, 直接将新的 V 插入
-
K 存在, 使用 remappingFunction 合并旧的 V 和新的 V.
那么如果允许value 为 null, null 本身可能没有值, 也可能标识有值但是值显式的设置为null, 到底这个新的值要怎么去做合并呢?
merge 方法专注将两个非空的值做合并.
ArrayList 的 subList 结果不可强转成 ArrayList,否则会抛出 ClassCastException 异常
因为其实现其实是内部的 java.util.ArrayList.SubList
, 可以看做 arraylist 的一个视图, 对于sublist 所有的操作都会反映在这个视图上面而已.
Map 的方法 keySet() / values() / entrySet() 返回集合对象时, 不可添加元素
从宏观角度讲, 这三个反应的是视图, 视图可以减少元素, 并且会被之前的 map 的元素修改同步影响, 但是视图不应该添加元素, 因为视图之中添加了元素, 对应backed的map并不知道应该怎么同步处理.
个人理解, keySet() 和 values() 只是map的一部分(K 或者 V), 单纯对任何一个添加元素, 那么之前的map就不一致了, 所以不能加.
entrySet() 之中, 添加一个键值对的同时必须更新map的key和value, 这个也是在视图之中难以完成的. 比如万一这个key重复了? value 为null了? 使用什么样的mergeFunction? 都是难以单纯抉择的.
不要使用 Collections 类返回 emptyList() / singletonList(), 其均不可操作
如果查询无结果,返回 Collections.emptyList() 空集合对象,调用方一旦在返回的集合中进行了添加元素的操作,就会触发 UnsupportedOperationException 异常。
subList 之中对父集合的元素的修改, 会导致子列表的修改报 ConcurrentModificationException 异常
很好理解, 父列表做完操作, 那么正在遍历的子列表是跟着更新还是不更新? 咋都不对, 不如直接异常了事
集合转数组, 必须使用 toArray(T[] array), 传入为类型一致, 长度为0的空数组
如果直接调用 toArray()
, 返回的是Object[]
, 强转其他类型的数组, 就会报 ClassCastException.
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
// 下面代码会报 ClassCastException
String[] array = (String[]) list.toArray();
正解:
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
String[] array = list.toArray(new String[0]);
为什么参数传0? 动态创建和 size 相同的数组, 性能最好
Arrays.asList() 返回的 list 不能使用 add/remove/clear 修改集合相关方法
Arrays.asList() 返回的是 java.util.Arrays.ArrayList!!! 是一个内部类, 并没有实现集合的修改这些方法,使用会直接报错 java.lang.UnsupportedOperationException
Arrays.asList 体现的是适配器模式, 只是给外界看看, 底层还是数组, 没有集合的方法
PECS原则,<? extends T> 只能接收返回数据, <? super T> 只能写入数据
<? extends T> 只能确认内部的元素都是T的子类, 那么T的不同子类都符合条件, 如果允许塞入的话, 编译器是不知道内部具体类型的. 但是整个列表一定可以按照T类型来进行读取.
<? super T> 只能确认内部的元素都是T的父类, 所以只要是T或者T的子类都可以进行塞入, 但是读取的时候并不知道其中元素的实际类型, 所以无法进行读取
public class PECSExample {
static class Animal {
@Override public String toString() { return "Animal"; }
}
static class Dog extends Animal {
@Override public String toString() { return "Dog"; }
}
static class Cat extends Animal {
@Override public String toString() { return "Cat"; }
}
static class Poodle extends Dog {
@Override public String toString() { return "Poodle"; }
}
public static List<? extends Animal> printAnimalNames(List<? extends Animal> animals) {
//下面代码异常
animals.add(new Dog());
}
public static void addAnimals(List<? super Animal> animals) {
//下面代码异常
for (Animal animal : animals) {
System.out.println(animal);
}
}
}
未定义泛型的集合类型赋值给泛型类型的集合, 在使用集合元素时, 需要判断 instanceOf 来避免 ClassCastException 异常
个人认为, 这种只会出现在很老的代码里面, 新代码之中一定会使用泛型
泛型由来:
jdk5 前没有泛型的概念, 集合内部只存储了 Object 类型的对象. 这意味着可以向同一个 List 之中添加各种类型的对象, 比如 String, Integer 或者任何其他类型. 但是在取出元素的时候, 需要进行强制的类型转换, 且在编译的时候无法得到编译器的检查, 容易 ClassCastException
下面这段代码可以运行:
List notGenerics = new ArrayList(); notGenerics.add("abcd"); notGenerics.add(1); notGenerics.add(new Object()); for (Object o : notGenerics) { System.out.println(o); } // abcd // 1 // java.lang.Object@35fb3008
在jdk5引入泛型之后, 下面是保持兼容的措施:
类型擦除: 编译之后去掉泛型信息, 变成该类extend的类或者Object类, 让字节码可以和旧版JVM兼容. 编译器在编译的时候进行类型检查, 且自动插入类型转换
原始类型: 仍然允许使用原始类型的泛型类, 比如 List, 在牺牲编译时类型安全的情况下允许新代码和旧代码兼容,
桥接方法: 当一个类继承或实现泛型类或接口, 类型擦除可能导致方法签名在字节码上变化. 为了保持多态性, 编译器会自动生成一个具有原始类型签名的桥接方法, 内部会实际调用自己编写的具有泛型签名的方法.
下面是例子:
class Node<T> { public T data; public void setData(T data) { this.data = data; } } // 自己实现的方法 class MyNode extends Node<String> { @Override public void setData(String data) { // 覆盖了泛型方法 System.out.println("MyNode setData: " + data); super.setData(data); } }
在编译
MyNode
时,编译器会发现Node<T>
的setData(T data)
在擦除后变成setData(Object data)
。而MyNode
中定义的是setData(String data)
。为了让通过Node
引用调用setData
时能够正确调用到MyNode
的覆盖版本,编译器会为MyNode
生成一个桥接方法:// 由编译器在 MyNode.class 中自动生成的桥接方法(示意) public void setData(Object data) { setData((String) data); // 调用实际编写的 setData(String) 方法 }
我们再回到正题, 那么将jdk5之前写的不带泛型的集合赋值给新的带泛型的集合的时候, 我们就需要在使用的时候和jdk5之前一样, 要带上 instanceof
List notGenerics = new ArrayList();
notGenerics.add("abcd");
notGenerics.add(1);
notGenerics.add(new Object());
List<String> rawList = notGenerics;
for (Object s : rawList) {
// 注意此处
if (s instanceof String) {
System.out.println(s);
}
}
元素的remove 要使用 iterator 方式, 需要并发操作则给 iterator 加锁.
不可以在foreach 之中对集合做增加和删除操作, 要做的话需要用iterator.
iterator 通过 modCount 来记录集合被修改的次数, 如果和期望不匹配直接抛异常, 从而实现状态一致
一个有趣的小情况: 集合只有在第一个元素遍历之后才会检查是否modified, 所以下面两种情况输出不同:
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); for (String s : list) { if ("1".equals(s)) { list.remove(s); } } System.out.println(list); // 输出 [2] for (String s : list) { if ("2".equals(s)) { list.remove(s); // 报错 java.util.ConcurrentModificationException } }
使用 entrySet 来遍历Map的KV, 而不是 keySet
keySet 对map做了两次操作, 一次是转为 iterator, 一次是从hashMap之中取出 keySet 的对应value
Map类集合不能存储null的情况
个人认为, 实际上null 作为key 不具有业务含义, 应该在写入map的时候就过滤掉key为null的情况.
结论: HashMap 可以有一个为null的key, value可以为null.
ConcurrentHashMap 之中的key和value都不可以为null
原因: map.get(key)==null 有两种情况
-
这个key在map中不存在
-
这个key在map之中存在, 但是显式的设置成了null
HashMap 是单线程使用的情况, 可以用 containsKey 来做检查:
if (map.containsKey(key)) {
// 键存在,值为null
} else {
// 键不存在
}
但是在多线程环境之中, 在检查 containsKey的时候, 可能有其他线程直接写入了对应的值, 所以无法判断到底是哪一种情况, 因此禁止 concurrentHashMap 的 value 为null
用 Set 的元素唯一性来做集合的去重操作
(七) 并发处理
线程必须通过ThreadPoolExecutor 的形式显式参数处理生成
SimpleDateFormat 线程不安全, 不能定义成 static 变量. 建议使用 java.time.format.DateTimeFormatter
SimpleDateFormat 内部维护一个线程不安全的 calendar 类, 所以在多线程情况下可能导致日期格式化或者日期生成错乱. 使用 java.time.format.DateTimeFormatter
的话, 其内部是不可变且线程安全的 ,且性能更好
Instant 相关使用方式
instant 内部维护一个long ,代表 epoch 秒, 和一个int,代表当前秒之中的纳秒.
instant 是不可变的, 每次操作之后其会返回一个新的 instant
ThreadLocal 之中的值必须手动回收
在线程池等情况下, 线程是复用的, 那么threadLocal 之中的值也是复用的, 如果不手动清除, 会造成业务混乱
尽量在代码之中 try … finally …之中回收
在尽量小的代码块之中加锁
加锁的区域尽量小, 这样让需要竞争状态下面的代码尽量少的发生竞争
在同时加锁时, 按照顺序对不同对象加锁
如果线程1 加锁顺序为 A,B,C, 线程2 和线程1不同, 那么可能出现死锁
Java 之中阻塞等待获取锁的正确使用方式
1. 在try代码块之外调用加锁方法 lock.lock()
加锁操作必须在代码块的外面, 而不能在代码块的里面.
- 加锁方法和try 代码块之间不能有任何可能抛出异常的方法调用
为什么?
首先我们明确, 加锁之后的 try 代码块之中是业务逻辑, 在 finally 之中需要执行 lock.unlock() 逻辑.
-
为什么加锁方法和try 代码块之间不能有任何可能抛出异常的方法调用? 如果先执行 lock.lock(), 成功之后执行这个可能抛出异常的方法调用, 那么如果这个调用抛出异常, finally() 之中的方法不会被执行, 整个锁就无法被其他线程获取, 导致事故.
-
如果在 try 块之内调用 lock.lock(), 会有什么后果? try 内部调用lock.lock(), 如果在其之前抛出异常, 那么会对还未加锁的对象 unlock, 导致出现 IllegalMonitorStateException 异常. lock.lock() 本身也可能出现异常, 那么也会和上面情况一样, 给出 IllegalMonitorStateException 异常
Lock lock = new ReentrantLock(); // 创建一个锁对象,例如 ReentrantLock
// ...
lock.lock(); // 在 try 块之外加锁
try {
doSomething(); // 执行一些操作
doOthers(); // 执行其他操作
} finally {
lock.unlock(); // 在 finally 块中释放锁
}