一、异常简介
异常本质上是程序上的错误,包括程序逻辑错误和系统错误。通过异常机制,我们可以更好地提升程序的健壮性
二、异常体系结构

Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类,所有异常类型都是内置类Throwable的子类。
Throwable两个不同的分支
- Error类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。例如,Java虚拟机运行错误(Virtual MachineError),当JVM不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError,发生这些异常时,JVM会选择终止程序
- Exception表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常
Exception两个不同分支
- checked exception 受检异常,也称作非运行期异常或者编译异常
- unchecked exception 非受检异常,也称作运行时异常(RuntimeException)
两者异同点:
- 受检异常种类:IOExeption,SQLException
- 非受检异常种类:ArrayIndexOutOfBoundsException(数组下标越界)、NullPointerException(空指针异常)、ArithmeticException(算术异常)、MissingResourceException(丢失资源)、ClassNotFoundException(找不到类)等异常
- 受检异常从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过
- 非受检异常程序中可以选择捕获处理,也可以不处理,不处理的话最终会抛出到虚拟机,导致虚拟机终止程序。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生
三、异常处理机制
Java的异常处理本质上是抛出异常和捕获异常
抛出:
- 方法上使用throws,表示该方法有可能抛出异常
- 代码内抛出异常对象 throw new Exeception()
捕获:
使用try/catch/finally
异常处理流程:
1、当程序出现异常,JVM自动根据异常的类型实例化一个与之类型匹配的异常类对象。
2、根据异常对象判断是否存在异常处理。如果不存在异常处理,则由JVM对异常默认处理:输出异常信息,结束程序调用。
3、如果存在异常捕获操作,try语句捕获异常类实例化对象,再与catch语句进行异常类型匹配,并处理异常。
4、不管是否匹配到catch语句,如果存在finally语句,就会执行finally语句代码。
5、finally语句后面的代码根据之前是否匹配到catch语句进行处理。如果匹配到catch语句,也即捕获到异常,则继续执行finally后的语句。如果没有匹配到catch语句,则将异常交由JVM默认处理。
四、关键字 try catch finally 执行流程
问题1: try{} 里有一个 return 语句,那么紧跟在这个 try 后的 finally{} 里的 code 会不会被执行,什么时候被执行,在 return 前还是后?
回答: 会执行,在方法返回调用者前(即return前面)执行
public int test() {
try {
int i = 1;
return i;
} catch (Exception e) {
return -1;
} finally{
System.out.println(-2);
}
}
结果:
-2
返回值还是-1
问题2: try,catch,finally语句中都有return语句,那么执行流程是啥?
回答: finally语句一定会执行,并且finally语句的return语句会覆盖try和catch语句的return值
public int test() {
try {
int i = 1;
i = i / 0;
return i;
} catch (Exception e) {
return -1;
} finally{
return -2;
}
}
结果: -2
这里如果注释掉 i = i / 0;,还是会返回-2,原因上面已经说过
五、自定义异常
public class SelfException extends RuntimeException{
//自定义异常编码
private final String errorCode;
public SelfException(String errorCode) {
super();
this.errorCode = errorCode;
}
public SelfException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public SelfException(String errorCode, Throwable cause) {
super(cause);
this.errorCode = errorCode;
}
public SelfException(String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
当java提供的异常类型不能满足开发需要时,可以自己开发一个异常类,规定不同类型的异常编码。实现自定义异常类,只需要继承Exception或RuntimeException父类即可
比如在代码有会有很多参数校验,就可以自定义异常,给定异常编码,然后在校验参数的方法中抛出自定义异常接口
public class SelfException extends RuntimeException{
//自定义异常编码
private final String errorCode;
public SelfException(String errorCode) {
super();
this.errorCode = errorCode;
}
public SelfException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public SelfException(String errorCode, Throwable cause) {
super(cause);
this.errorCode = errorCode;
}
public SelfException(String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
public class ValidateParameter {
public static void main(String[] args) {
String param = "";
try {
validate(param);
} catch (SelfException e) {
System.out.println(e.getErrorCode()+ " "+e.getMessage());
}
}
private static void validate(String parameter) {
if("".equals(parameter)) {
throw new SelfException("EXP001", "参数非空异常");
}
}
}
运行结果:
EXP001 参数非空异常
自定义异常类SelfException,参数检验不通过抛出该异常对象,使用try/catch捕获该异常,可以获取自定义的异常编码和异常信息。
Tips:
我们可以使try/catch捕获程序抛出的异常,在catch代码块中转化成自定义异常,保留异常堆栈信息,然后抛出自定义异常,在最上层捕获该异常,将其异常编码和信息封装入接口返回值,这样可以让接口调用间异常处理更加简洁
六、异常使用注意事项
1.只在必要使用异常的地方才使用异常
不要用异常去控制程序的流程,这是因为新建异常对象比创建一个普通对象是要更加的耗时。
2.子类重写父类方法的时候,如何确定异常抛出声明的类型
- 父类的方法没有声明异常,子类在重写该方法的时候不能声明异常;
- 如果父类的方法声明一个异常exception,则子类在重写该方法的时候声明的异常不能是exception的父类;
- 如果父类的方法声明的异常类型只有非运行时异常(运行时异常),则子类在重写该方法的时候声明的异常也只能有非运行时异常(运行时异常),不能含有运行时异常(非运行时异常)
3.切忌使用空catch块
在捕获了异常之后什么都不做,相当于忽略了这个异常。千万不要使用空的catch块,空的catch块意味着你在程序中隐藏了错误和异常,并且很可能导致程序出现不可控的执行结果。如果你非常肯定捕获到的异常不会以任何方式对程序造成影响,最好用Log日志将该异常进行记录,以便日后方便更新和维护
4.注意catch块的顺序
不要把父类的异常放在最前面的catch块
try {
FileInputStream inputStream = new FileInputStream("");
int ch = inputStream.read();
System.out.println("aaa");
return "step1";
} catch (IOException e) {
System.out.println("io exception");
return "step2";
}catch (FileNotFoundException e) {
System.out.println("file not found");
return "step3";
}finally{
System.out.println("finally block");
//return "finally";
}
第二个catch的FileNotFoundException将永远不会被捕获到,因为FileNotFoundException是IOException的子类
5.异常处理尽量放在高层进行
尽量将异常统一抛给上层调用者,由上层调用者统一之时如何进行处理。如果在每个出现异常的地方都直接进行处理,会导致程序异常处理流程混乱,不利于后期维护和异常错误排查。由上层统一进行处理会使得整个程序的流程清晰易懂
6.避免多次在日志信息中记录同一个异常
只在异常最开始发生的地方进行日志信息记录。很多情况下异常都是层层向上跑出的,如果在每次向上抛出的时候,都Log到日志系统中,则会导致无从查找异常发生的根源
七、异常信息打印
使用e.getMessage()来获取异常信息,但是这样获取到的信息内容并不全,而且有时候为空,比如NullPointerException空指针异常,就不能获取堆栈信息。
可以使用下面代码获取:
public static String getStackTrace(Throwable ex, int len) {
StringWriter stringWriter = new StringWriter();
String retStr = null;
try (PrintWriter printWriter = new PrintWriter(stringWriter)) {
ex.printStackTrace(printWriter);
retStr = stringWriter.toString();
if (null != retStr && retStr.length() >= len) {
retStr = retStr.substring(START_NUM_0, len);
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return retStr;
}
将异常堆栈信息写入到PrintWriter 流中,转换成StringWriter再转换成String字符串。我们传入异常对象e和长度len两个参数,就可以获取我们需要的异常堆栈信息,然后保存到服务器日志。