文章目录
前言
做Java开发的,大多数可能都有看过阿里的Java后台开发手册,里面有关于Java后台开发规范的一些内容,基本覆盖了一些通用、普适的规范,但大多数都讲的比较简洁,本文主要会用更多的案例来对一些规范进行解释,以及结合自己的经验做补充!
其他类型的规范
【Java后台开发规范】— 不简单的命名
【Java后台开发规范】— 日志的输出
【Java后台开发规范】— 线程与并发
【Java后台开发规范】— 长函数、长参数
【Java后台开发规范】— 设计原则
【Java后台开发规范】— 圈复杂度
【Java后台开发规范】— Null值处理
异常
异常处理应该算是一种我们非常熟悉的话题了,Java中对于异常的处理也非常便捷、灵活,但往往越是简单的东西,越容易忽视它,恰巧异常也存在很多容易忽视的陷阱,一起来看看吧!
异常架构
Throwable
Throwable
是所有异常的错误的父类,printStackTrace()
方法就是由Throwable
提供的。
Error
Error
表示程序遇到了无法处理的问题,出现了严重的错误,常见的比如:OutOfMemoryError,*Error
Exception
程序本身可以处理的异常,Exception
类本身又分为两类:运行时异常和编译时异常。
运行时异常
RuntimeException
类及其子类产生的异常,编译时不会进行检查,只有在程序运行时才会产生,也可以通过try-catch
来进行处理,但通常不需要我们这样做,因为运行时异常一般都是我们代码本身编写存在问题,应该在处理逻辑上进行修正。
常见的有:NullPointerException,ArrayIndexOutBoundException,ClassCastException
编译时异常
Exception
下除了RuntimeException
类型的其他异常都是编译时异常,这类异常在编译时就会进行检查,并强制要求对其进行处理,否则无法通过编译。
常见的有:ClassNotFoundException、IOException
异常使用的误区
忽视异常
绝大多数情况下都不应该像如下这样忽视异常的存在,因为这样会让你无法发现问题。
try{
doSomething();
}catch(Exception e){
// 什么也不做
}
当然也有例外
如果选择了忽略异常,那么最好在catch
中通过注释的方式给出原因,并且变量名使用ignored
下面两个案例,都将变量名改为了ignored
,但都没有在catch
中给出具体的原因。
原因写在了方法注释上。
这个解释绝绝子,不可能发生
标准化异常
统一语言、统一认知一直是我们强调的,让异常标准化也算其实现手段之一,得益于标准化好处,当你看到如下这些异常时,会感到非常的熟悉:NullPointerException、IllegalArgumentException、IllegalStateException、ClassCastException、IllegalFormatConversionException、IndexOutOfBoundsException
如果没有这些标准化的异常分类,实际上所有的异常都可以归为IllegalStateException
(非法状态)或者IllegalArgumentException
(非法参数)。
TreeMap
中的Key
不允许为null
HashTable
中的value
不允许为null
以上两个案例,实际上都可以按照IllegalArgumentException
(非法参数)来处理,但是作者并没有这样做,IndexOutOfBoundsException
异常也一样,并没有用IllegalArgumentException
来替代。
常见的一些标准异常:
IllegalArgumentException
IndexOutOfBoundsException
NullPointerException
ClassCastException
IllegalFormatConversionException
UnsupportedOperationException
正确的使用异常
一种基于异常的循环控制,这种做法的原因是因为有人认为JVM底层就是这样终止的。
List<User> userList = new ArrayList<>();
userList.add(new User("a"));
userList.add(new User("b"));
userList.add(new User("c"));
userList.add(new User("d"));
try {
int i = 0;
while (true) {
User user = userList.get(i++);
System.out.println(user.getName());
}
} catch (IndexOutOfBoundsException e) {
// 什么也不做
}
比如,正常你应该会写成像下面这样,那JVM又是怎么判断数据边界的呢?
for (User user : userList) {
System.out.println(user.getName());
}
为了省去每次的边界检查,所以采用异常捕获的方式,这明显是错误的,实际上测试对比后,后者比前者快很多,原因主要在于以下两点:
- 写在
try-catch
中的代码,JVM一般不会对其进行优化。 - 而数组的遍历,经过JVM优化后不会造成多余的边界检查。
基于上述这个案例,也告诫我们在做设计时,不要企图让你的调用者通过异常控制的方式来完成正常的流程。
再来看一个案例
Iterator<User> iterator = userList.iterator();
while(iterator.hasNext()){
User user = iterator.next();
}
假如Iterator
没有提供hasNext
方法,那可能你只能通过try-catch
的方式来解决了。
Iterator<User> iterator = userList.iterator();
try{
while(true){
User user = iterator.next();
}
} catch(NoSuchElementException e){
}
让异常保持原子性
这条原则的含义是指,当调用某行代码产生异常时,应该使当前对象仍能保持异常前的数据状态。
通常有下面几种方式:
让异常前置
举一个list集合移除元素的例子,其中rangeCheck
方法中对当前集合的size
做了检查,如果index>=size
则抛出异常
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
其实通过这个简单的rangeCheck
方法就能让异常保持原子性,因为它使得在modCount
在修改之前就已经抛出了异常,假设你没有提前做rangeCheck
检查,那么你在调用E oldValue = elementData(index)
这一行时,仍然会遇到IndexOutOfBoundsException
异常,但modCount
状态却已经被修改了,你不得不再去维护它的状态。
不可变对象
很多场景中不可变对象总是安全的,异常也不例外。
临时拷贝
如果你每次操作的都是新拷贝出来的对象,那么即使失败了,也并没有对原数据产生影响。
补偿
通过手动补偿的方式来保证失败后状态的正确性,就有点像如何解决分布式事务的问题,在遇到失败后,主动调用一段事先准备好的回滚逻辑,使数据回到失败前的状态。
其他补充
- 处于事务中的流程,如果catch了异常,要注意事务的回滚。
- 尽量避免在循环体中try-catch异常。
- 不要用异常来控制流程
- 使用try-with-resources替代try-catch-finally