一、编程规约
(一) 命名风格
布尔类型变量命名不要加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 既需要在对象层面的判断相等的 equals(), 也需要在值层面判断相等的 compareTo()
BigDecimal 仅使用 String 类型的参数初始化
如果使用 BigDecimal(double), 可能存在精度损失的风险, 在精确计算或者比较的场景下导致业务逻辑异常.
BigDecimal g = new BigDecimal(0.1F);实际的存储值为:0.100000001490116119384765625
还是一样的, java 内部的浮点数天生有坑, 根源还是二进制转换无法精确转换十进制的小数部分.
基本数据类型与包装数据类型的使用标准
除了局部变量之外, 全部使用包装数据类型!
个人理解 ,因为包装数据类型默认为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 时,遍历得到的数组,从尾部向前检查连续的空字符串(空格不算空字符串, 所以不会被干掉) ,并将它们剔除,直到遇到非空的项为止
简言之, split() 帮我们自动做了 trim, 所以在使用的时候应该注意下标, 而不是最后一个元素是否为空(如果是空, 已经trim掉了)
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 块中释放锁
}
在尝试执行上锁段代码之前, 必须先判断当前线程是否持有锁
使用 tryLock() 来进行检测, 确定当前线程持有锁, 才能继续对应代码段逻辑
Lock lock = new ReentrantLock();
System.out.println("here is some logic");
boolean isLocked = lock.tryLock();
if (isLocked) {
try {
System.out.println("here executing all logics");
} finally {
lock.unlock();
}
}
使用 ScheduledExecutorService 来进行定时任务的调度
java.util.Timer 内部是一个单线程执行所有任务, 所以一旦一个任务抛出异常, 就会影响所有任务;但是 ScheduledExecutorService 内部是一个线程池, 所以其在任何一个任务抛出异常的时候都没有问题;
Timer 有问题的例子:
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
throw new RuntimeException("Task1 failed!"); // 未捕获异常
}
}, 1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Task2 running"); //永远走不到
}
}, 2000);
悲观锁: 一锁二判三更新四释放
CountDownLatch 正确使用方法
CountDownLatch 本身是计数器, 主线程需要等待各个在线程结束计算返回后的汇总值进行下一步动作. 每个子线程在结束自己的任务之后需要调用 countDown() 来让计数器 -1.
需要注意的点:
-
countDown() 必须被子线程调用到, 所以要放在 try…catch…finally 的 finally 里面. 不然主线程可能卡死
-
主线程的 await() 一定要用带超时的版本, 这样方便兜底. 不然子线程万一未正常调用 countDown() 就会卡死
-
主线程的 try…catch… 和子线程的 try…catch… 本身是两个东西, 所以要分别的 try..catch
Random 实例不可多线程使用, 会因竞争同一个seed导致性能下降
多线程之中需要使用 ThreadLocalRandom 来在多线程之中使用Random
Random 包括 java.util.Random 的实例或者 Math.random() 的方式.
Random 之中随机数的获取是多线程安全的, 但是其会原子更新需要的 seed 值, 大概流程如下:
newSeed = f(oldSeed)
随机数 = g(newSeed)
问题就出在这”原子更新seed值”, 多线程条件下更新种子, 需要每一个线程都去竞争更新seed值, 那么在高并发的时候, 对 seed 值的更新就成为卡点, 导致大量线程阻塞
双重校验锁的volatile作用
老面试题了.
例子:
public class DoubleCheckLockingTest {
private volatile Item item;
private Item getItem() {
if (item == null) {
synchronized (this) {
if (item == null) {
item = new Item();
}
}
}
return item;
}
}
-
两次检验 item 的作用, 是防止多个线程第一次检验 item 的时候均为空, 导致多个线程进入 synchronized 结构体, 从而初始化多个对象
-
volatile 的作用, 因为
item = new Item();
这个步骤不是原子化的, 对象分配是:-
allocate: 分配 item 的内存空间
-
construct: 调用构造函数以初始化变量
-
assign: 把分配的内存地址赋值给 item 的引用变量
如果不加 volatile, 那么这三步的顺序可能不对, 比如 1->3->2, 那么在一个对象还没初始化完成的时候就有可能被其他线程发现且返回, 导致对象不可用, 出现异常
ThreadLocal 使用 Static 修饰
private static final ThreadLocal<SharedResource> resourceThreadLocal = new ThreadLocal<>();
上面的使用方法是其正确方法, 原因有以下几个:
-
-
threadLocal 既然作为key, 那么就是全局唯一的, static 可以保证变量只被初始化一次. 所有访问这个类的线程, 都拿取的是同样的一个 threadLocal 实例
-
防止内存泄漏: 如果每个线程都有自己的 threadLocal 实例, 那么假设有 M 个线程和 N 个类的实例, 一共会有 M*N 个线程局部变量的类实例!
-
便于全局访问: 便于同一个线程之中的不同组件进行访问. 比如在服务器模型之中, 用户的单个请求是一个线程从头到尾执行的, 但是这个线程会使用不同组件, 比如 controller, 然后调用 service 的方法等等. 这样的情况下会在 thradLocal 之中放入一些公共信息, 比如鉴权,header 或者 traceId 等. 那如果每个组件拿取的是不同的 threadLocal 变量, 自然也就无法获得相同信息
为什么不同线程对同一个 threadLocal 对象可以取出不同的值
每个线程的 threadLocal 的值, 实际上是存储在线程内部的 threadLocalMap 当中的, threadLocal 对象更像是一个统一的key, 每个线程去自己的 threadLocalMap 之中取值的时候, 都用同样的一个key 来做读写操作, 但是每个map本身不同, 自然读写的值也不同
threadLocal 无法解决共享对象的更新问题
threadLocal 只能作为key , 但是如果threadLocal 中对于这个key 拿取的值是一个共享的对象, 其也无法解决竞态问题, 还是要进行上锁操作
(八) 控制语句
switch 块之中, 必须用 continue/break/return 终止, 且哪怕default语句什么也没有, 也必须附上
switch 括号之中之中不能为null, 所以必须先判空
比如 switch(s), 如果 s == null 的话, 其解析的时候会先调用 s.hashCode(), 所以会直接报 java.lang.NullPointerException
. 其他的类型, 比如 integer, 也会因为类似的原因报 java.lang.NullPointerException
.
三目运算符 condition ? 表达式 1:表达式 2 中, 需要对表达式1和表达式2进行判空, 避免自动拆箱出现NPE
三目表达式的 表达式1 和 表达式2 之中, 只要一个是原始类型, 另一个包装类型就会被拆箱, 这时候如果包装类型对应的是null, 就会出现 NPE
Integer a = 1;
Integer b = 2;
Integer c = null;
Boolean flag = false;
Integer result = flag ? a * b : c; // a*b 进行算术运算操作, 自动拆箱变成int, c也被自动拆箱, 导致NPE
高并发场景不可以使用等于作为判定条件, 而要是范围判定
如果并发处理没有做好, 会发生击穿操作, 比如 奖品数量 == 0 的时候, 不再允许用户抽奖, 但是并发过高, 导致奖品数量变成负数, 造成活动无法终止
在条件判断之中不要执行复杂语句, 而是将其结果赋值给一个有意义的Boolean, 提高可读性
(九) 注释规约
所有抽象方法都必须给出 javaDoc 注释
所有的枚举类型字段必须要有注释,说明每个数据项的用途。
(十) 前后端规约
超大整数使用 String 类型返回, 避免 JS 之中 Number 类型可表示长度不够从而数值错误
JavaScript中Number类型的原理类似于 java 的 double, 由三部分组成:1位符号位、11位指数位和52位小数位3。对于整数来说,安全整数范围是-2^53+1到2^53-1,因为此时刚好用完了52位小数位加上隐含的1位整数位3。
但是 java long 类型最大为 2^63-1, 那么这部分差值就可能被截断从而造成数值错误
http 请求通过url 传递参数时候, 不能超过 2048 字节
URL 之中的非 ASCII 和 特殊字符需要进行编码, 会增加实际字节长度, 比如:
-
一个英文字母或数字通常占用1个字节
-
一个空格编码为”%20”,占用3个字节
-
一个汉字通常编码为”%XX%XX%XX”,占用9个字节左右
因此,当URL包含非英文字符或特殊字符时,实际可传输的信息量将显著减少。
http 的 body 内容需要控制长度, 不然后端解析会出错
不只是 nginx, tomcat 等有最大的内容限制 ,很多CDN也有自己的长度限制, 这个需要注意.
server 内部重定向必须使用 forward, 外部重定向地址必须使用URL 统一代理模块生成
前后端的时间格式统一为 “yyyy-MM-dd HH:mm:ss GMT”
(十一) 其他
正则表达式使用预编译功能
不要在调用方法内部进行编译, 而是在类之中使用 private static final 的成员变量来创建.
Math.random() 返回的是 double 类型, 如果要获取整数类型的随机数, 使用 Random.nextInt()
二、异常日志
(二) 异常处理
Java 类库之中可以通过预检查方式规避的 RuntimeException 应该通过检查筛出, 比如 NullPointerException, IndexOutOfBoundsException 等
能提前检查出来的就提前检查, 不要用catch 的方式再来做检验, 不然 catch 承载的工作太多(预期外+预期内都要检查)
异常捕获后不要用来做流程控制或者条件控制
一个异常非常消耗资源:
- throw 操作会创建异常对象, 且填充堆栈跟踪
语义职责也不同, 异常是用来处理程序运行之中的非预期错误
catch 时候需要分清永远不会出错的代码和非稳定代码, 对于非稳定代码的catch 要尽量区分异常类型, 再做相应处理
不要一个大 try catch 直接包裹住所有的代码段, 而是分清哪边可能抛出异常哪边不会, 再做对应的处理
事务之中如果异常被catch 之后需要回滚, 一定要注意手动回滚事务
我建议是在catch异常结束处理之后, 直接将异常再次抛出, 这样让最外层继续感知异常并且触发回滚
资源对象, 流对象等最好使用 try-with-resource 方式进行关闭.
为什么try…catch…finally 不好?
假设资源初始化异常或者调用 close() 异常, 那么finally 之中调用对象的 close() 就会出问题, 在finally 之中抛出异常. 为了防御, 需要进行冗余编码, 比如:
ResourceType resource = null;
try {
resource = new ResourceType(); // 初始化资源,这里可能抛出异常
// 使用资源... 这里也可能抛出异常
} catch (Exception e) {
// 处理使用资源时发生的异常
e.printStackTrace();
} finally {
if (resource != null) { // 1. 非空检查
try {
resource.close(); // 2. 关闭资源,可能抛出异常
} catch (Exception closeException) { // 3. 捕获并处理关闭时发生的异常
closeException.printStackTrace(); // 避免覆盖原始异常
}
}
}
如果
try-with-resource 解决了什么问题
解决了finally块在资源管理上面的笨重和冗余. 其实现了自动管理机制
在 try 后面的括号之中进行资源的初始化.
try (ResourceType resource1 = new ResourceType1(); // 分号分隔多个资源
ResourceType resource2 = new ResourceType2()) {
// 使用 resource1 和 resource2
// ... 代码块 ...
} catch (Exception e) {
// 处理 try 代码块中发生的异常
} finally {
// 可选的 finally 块,在资源关闭后执行
}
条件:
必须实现java.lang.AutoCloseable
接口(或者其子接口 java.io.Closeable
)
原理:
编译器自动生成字节码, 在try块结束完毕之后调用资源的 close() 方法.
优点
开发不用再去手动 close(). 减少样板代码和各种手动处理错误的工作
多个异常如何处理
-
try 块和 close() 都抛出异常 try 之中的异常会被正常抛出, close() 之中的会被抑制住(supressed) 且附加到主异常上, 可以通过 Throwable.getSuppressed() 来获得这些异常. 这样避免了异常屏蔽, 保留完整异常
-
只有 close() 抛出异常: 正常抛出
相对比, 在finally 之中, 如果 try 和 finally 都抛出异常, 那么一般是 finally 之中的异常会覆盖掉try 之中的原始异常!!
不要在finally 之中使用 return, 不然会覆盖掉代码块本身的 return
主线程和线程池的异常沟通方式
主线程和线程池之中的异步线程是独立的线程, 他们有各自独立的调用栈, 当异常在工作线程抛出的时候, 其会在对应的工作线程的调用栈传播. 主线程和异步工作线程是分离的, 所以工作线程的异常事件不会自动被捕获.
主线程如果要看到线程池之中的异常, 需要:
-
future,get(), 直接在需要的时候拿到 future 的结果, 如果异步线程是异常, 那么 get() 会抛出 ExecutionException
-
UncaughtExceptionHandler
: 全局默认的异常处理, 当线程因为未捕获异常而终止的时候, 其被调用 -
在异步任务内部进行 try catch, 当异步任务自己能处理异常的时候, 直接选择这个, 因为其封装了错误处理逻辑
-
重写
ThreadPoolExecutor.afterExecute()
: 这个钩子方法可以在任务执行之后检查是否有异常发生.
通常使用第一种 future.get() 和 第三种 异步任务内部进行 try catch
线程池对异常线程怎么处理
如果某个线程通过 execute() 抛出的异常未捕获且终止的时候, 线程池通常会移除这个死掉的线程, 然后创建一个新的工作线程来代替.
在使用外部依赖的时候, 异常使用 throwable 来进行拦截
反射机制调用方法时候, 如果找不到方法, 其会抛出 NoSuchMethodException. 找不到方法的可能原因为:
-
外部依赖的包在类冲突时候, 仲裁机制选择了错误的类, 从而导致真正使用的方法不存在
-
字节码修改框架之中修改了相应的方法名, 这种情况下哪怕代码编译期是正确的, 但是在运行时候也会抛出 NoSuchMethodError
NPE的可能场景
- 返回类型为基本数据类型, 但是return 其包装类型的对象的时候, 可能产生NPE
public int getInteger(){
return new Integer; // 这个地方如果 Integer 对应的对象是null, 触发自动拆箱, 直接NPE
}
-
数据库 db 查不到数据, 返回 null
-
集合本身 isNotEmpty(), 但是取出元素是null 集合本身非空, 但是其内部存储的元素都是null, 这样拿取时候可能也是null. 要注意
-
微服务调用或者RPC调用, 必须考虑连接失败等情况导致返回值为null
-
在 session 之中拿取的对象可能是null
-
级连调用非常非常 容易产生NPE! 级联调用 obj.getA().getB().getC();
在处理级联的时候, optional 确实更清晰好用:
public String getUserCountry_Optional(User user) {
return Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCountry)
.map(Country::getIsoCode)
.map(String::toUpperCase)
.orElse("UNKNOWN");
}
这样就避免了层层嵌套去找值.