之前面试的时候,被问到关于异常的继承关系,自定义异常,断言异常怎么处理的。
对这块知识不太了解,刚好做个博客整理一下。
异常的继承关系
- 运行时异常和非运行时异常的区别,分别含义是什么?
从继承关系图中可以看出,运行时异常包含了一些常见异常,如:NullPointerException,ClassNotFoundException等,一般爆出这些异常的时候,系统会中断运行,并且使用idea等编辑器不会提示需要进行try catch的操作。
但是非运行时异常又是什么?当时我说的是,在编译期间能够被检查出来的异常。
百度了一下答案,
检查异常(checked exception)
检查异常通常是用户错误,程序员并不可预见的问题。例如,如果一个文件被打开,但该文件无法找到,则会出现异常。这些异常并不能在编译时被发现。
运行时异常(runtime exception也叫unchecked exception)
运行时异常时本来可以由程序避免的异常。而不是已检查异常,运行时异常是在编译时被忽略。这里的运行时异常并不是我们所说的运行期间产生的异常,只是Java中用运行时异常这个术语来表示而已。另外,所有Exception异常都是在运行期间产生的。
错误(error)
无法处理的异常,比如OutOfMemoryError,一般发生这种异常,JVM会选择终止程序。因此我们编写程序时不需要关心这类异常。
1.运行时异常是不需要捕获的,程序员可以不去处理,当异常出现时,虚拟机会处理。常见的运行时异常有空指针异常。
2.非运行时异常就必须得捕获了,否则编译不过去,java编译器要求程序员必须对这种异常进行catch,Java认为Checked异常都是可以被处理(修复)的异常,所以Java程序必须显式处理Checked异常。
常见的非运行异常有io异常和sql异常。
参考优快云博客链接:java 运行时异常与非运行时异常理解
自定义异常
自定义异常一般继承Exception或者RuntimeException
然后定义构造方法,在构造方法需要显式用super调用父类的构造方法。
- 为什么自定义异常的构造方法需要显式用super调用父类的构造方法?
跟踪了一下调用父类的构造函数,自定义异常继承自RuntimeException ,RuntimeException 继承 Exception,Exception继承Throwable,在Throwable之前,每个异常类的构造函数都使用了super函数。那看看Throwable的构造函数有什么特别的。
public Throwable() {
fillInStackTrace();
}
public Throwable(String message) {
fillInStackTrace();
detailMessage = message;
}
public Throwable(String message, Throwable cause) {
fillInStackTrace();
detailMessage = message;
this.cause = cause;
}
public Throwable(Throwable cause) {
fillInStackTrace();
detailMessage = (cause==null ? null : cause.toString());
this.cause = cause;
}
protected Throwable(String message, Throwable cause,
boolean enableSuppression,
boolean writableStackTrace) {
if (writableStackTrace) {
fillInStackTrace();
} else {
stackTrace = null;
}
detailMessage = message;
this.cause = cause;
if (!enableSuppression)
suppressedExceptions = null;
}
值得关注的是,fillInStackTrace()函数,那跟踪一下源码,看看这个函数做了什么
/**
* Fills in the execution stack trace. This method records within this
* {@code Throwable} object information about the current state of
* the stack frames for the current thread.
*
* <p>If the stack trace of this {@code Throwable} {@linkplain
* Throwable#Throwable(String, Throwable, boolean, boolean) is not
* writable}, calling this method has no effect.
*
* @return a reference to this {@code Throwable} instance.
* @see java.lang.Throwable#printStackTrace()
*/
public synchronized Throwable fillInStackTrace() {
if (stackTrace != null ||
backtrace != null /* Out of protocol state */ ) {
fillInStackTrace(0);
stackTrace = UNASSIGNED_STACK;
}
return this;
}
private native Throwable fillInStackTrace(int dummy);
翻译一下,这个方法的注释信息。
填充执行堆栈跟踪。此方法在{@code Throwable}对象中记录当前线程堆栈帧的当前状态的信息。
如果这个{@code Throwable}{@linkplain的堆栈跟踪Throwable#Throwable(String,Throwable,boolean,boolean)不是可写},调用此方法无效。
返回值: 对这个{@code Throwable}实例的引用。
参见: java.lang.Throwable打印堆栈跟踪()
重点是java.lang.Throwable#printStackTrace()这个方法,查看除了Throwable的异常类,源码都很简单,只是处理了构造函数,那也就是,当我们在对异常try catch打印堆栈信息的时候,调用的是父类Throwable的printStackTrace方法。
printStackTrace的翻译信息: 注:源码的printStackTrace注释信息有很多,感兴趣的可以自行查看源码
*打印这个可丢弃的东西和它的回溯到
*标准错误流。此方法为此打印堆栈跟踪
*错误输出流上的{@code Throwable}对象
*字段{@code的值系统错误}. 第一行
*输出包含的{@link\#toString()}方法的结果
*这个物体。其余行表示以前记录的数据
*方法{@link#fillInStackTrace()}。
大概就是要把之前存入堆栈的信息打印出来,所以按我的理解,之前构造的时候需要让Throwable绑定一个堆栈信息,因为fillInStackTrace最后的时候前面的标识是native,调用的本地方法(JVM将控制调用本地方法的所有细节)。所以所有自定义异常类都需要继承Throwable,并且在构造函数中使用super调用父类的构造方法。否则无法打印异常的堆栈信息。
断言异常怎么处理
一般业务异常,我们会使用断言处理,给用户良好的错误信息提示。提高用户使用感受。
那断言异常又是怎么定义的?而且对于前端而言,我们不能使用直接接口报错的方式抛出异常,一般是自定义异常码值,抛出错误的message。
自定义异常类:
public class AssertException extends RuntimeException {
private String errorMessage;
private String logMessage;
private Integer errCode;
public AssertException(String errorMessage, String logMessage) {
super(errorMessage);
this.errorMessage = errorMessage;
this.logMessage = logMessage;
}
public AssertException(String errorMessage, String logMessage, Integer errCode) {
super(errorMessage);
this.errorMessage = errorMessage;
this.logMessage = logMessage;
this.errCode = errCode;
}
public Integer getErrCode() {
return this.errCode;
}
public void setErrCode(Integer errCode) {
this.errCode = errCode;
}
public String getErrorMessage() {
return this.errorMessage;
}
public String getLogMessage() {
return this.logMessage;
}
}
可以看到自定义了三个私有属性
private String errorMessage; // 错误信息(断言提示语)
private String logMessage; // 日志信息
private Integer errCode; // 错误码
- 怎么使用自定义的断言异常呢?
一、定义如何使用自定义异常类的类以及对应方法,在需要使用方法中,用throw抛出自定义异常。
StackTraceElement methodCaller = Thread.currentThread().getStackTrace()[2];
StringBuffer logMessage = new StringBuffer();
logMessage.append("\nBusiness Exception:{");
logMessage.append("\n\tServices Name: " + methodCaller.getClassName());
logMessage.append("\n\tMethod Name: " + methodCaller.getMethodName());
logMessage.append("\n\tLine Num: " + methodCaller.getLineNumber());
logMessage.append("\n\tService Error Code: " + errorCode);
logMessage.append("\n\tService Error Message: " + message);
logMessage.append("\n}\n");
throw new AssertException(message, logMessage.toString(), errorCode);
二、在统一返回结果的处理类中,去接收这个异常
try catch捕获异常,利用instanceof 关键字 判断当前的异常是否为自定义异常的实例,做对应的处理,如设置特定码值,以及把错误信息放到返回信息中。
if (ex instanceof AssertException) {
AssertException serviceAssertException = (AssertException)ex;
this.outputInfoLog(sb, ex);
response.setCode(402);
response.setMessage(serviceAssertException.getMessage());
}