文章目录
- 异常
- 5.1 初识异常
- 5.1.1 NullPointerException
- 5.1.2 NumberFormatException
- 5.2 异常类
- 5.2.1 Throwable
- 5.2.2 异常类体系
- 5.2.3 自定义异常
- 5.3 异常处理
- 5.3.1 try/catch匹配
- 5.3.2 重新抛出异常
- 5.3.3 finally
- 5.3.4 try-with-resources
- 5.3.5 throws
- 5.3.6 对比受检和未受检异常
- 5.4 如何使用好异常
- 5.4.1 异常应该且仅用于异常情况
- 5.4.2 异常处理的目标
- 5.4.3 异常处理的一般逻辑
异常
本文为书籍《Java编程的逻辑》1和《剑指Java:核心原理与应用实践》2阅读笔记
程序中,基本类型、类、接口、枚举都是在表示和操作数据,操作的过程中可能有很多出错的情况,出错的原因可能是多方面的,有的是不可控的内部原因,比如内存不够了、磁盘满了,有的是不可控的外部原因,比如网络连接有问题,更多的可能是程序的编写错误,比如引用变量未初始化就直接调用实例方法。这些非正常情况在Java
中统一被认为是异常,Java
使用异常机制来统一处理。接下来我们详细讨论Java
中的异常机制,首先介绍异常的初步概念,以及异常类本身,然后主要介绍异常的处理。
5.1 初识异常
为认识异常,我们先认识一下两个具体的异常:NullPointerException
和NumberFormatException
。
5.1.1 NullPointerException
package com.ieening.learnException;public class TestException {public static void main(String[] args) {String str = null;int strLength = str.length();System.out.println(strLength);}
}
运行代码,没有得出结果,反而输出:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "str" is nullat com.ieening.learnException.TestException.main(TestException.java:6)
输出是告诉我们:在TestException
类的main
函数中,代码第 6 6 6行,出现了空指针异常(java.lang.NullPointerException
)。但,具体发生了什么呢?当执行str.length()
的时候,Java
虚拟机发现str
的值为null
,没有办法继续执行了,这时就启用异常处理机制,首先创建一个异常对象,这里是类NullPointerException
的对象,然后查找看谁能处理这个异常,在示例代码中,没有代码能处理这个异常,因此Java
启用默认处理机制,即打印异常栈信息到屏幕,并退出程序。异常栈信息就包括了从异常发生点到最上层调用者的轨迹,还包括行号,可以说,这个栈信息是分析异常最为重要的信息。Java
的默认异常处理机制是退出程序,异常发生点后的代码都不会执行,所以示例代码中的System.out.println(strLength)
不会执行。
5.1.2 NumberFormatException
package com.ieening.learnException;public class TestException {public static void main(String[] args) {int integer = Integer.parseInt("a");System.out.println(integer);}
}
运行上面代码,得到结果:
Exception in thread "main" java.lang.NumberFormatException: For input string: "a"at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)at java.base/java.lang.Integer.parseInt(Integer.java:662)at java.base/java.lang.Integer.parseInt(Integer.java:778)at com.ieening.learnException.TestException.main(TestException.java:5)
根据异常栈信息,我们首先定位,NumberFormatException.java:67
代码:
之后,回到Integer.java:662
代码:
再查看Integer.java:778
行代码:
最后回到TestException.java:5
处。
将NumberFormatException.java:67
和Integer.java:662
合在一起可以知道:
throw new new NumberFormatException("For input string: \"" + s + "\"" + (radix == 10 ? "" : " under radix " + radix))
new NumberFormatException
是容易理解的,含义是创建了一个类的对象,只是这个类是一个异常类。throw
是什么意思呢?就是抛出异常,它会触发Java
的异常处理机制。在之前的空指针异常中,我们没有看到throw
的代码,可以认为throw
是由Java
虚拟机自己实现的。throw
关键字可以与return
关键字进行对比。return
代表正常退出,throw
代表异常退出;return
的返回位置是确定的,就是上一级调用者,而throw
后执行哪行代码则经常是不确定的,由异常处理机制动态确定。异常处理机制会从当前函数开始查找看谁“捕获”了这个异常,当前函数没有就查看上一层,直到主函数,如果主函数也没有,就使用默认机制,即输出异常栈信息并退出,这正是我们在屏幕输出中看到的。对于屏幕输出中的异常栈信息,程序员是可以理解的,但普通用户无法理解,也不知道该怎么办,我们需要给用户一个更为友好的信息,告诉用户,他应该输入的是数字,要做到这一点,需要自己“捕获”异常。“捕获”是指使用try/catch
关键字,如下代码所示:
public class ExceptionTest {public static void main(String[] args) {if(args.length<1){System.out.println("请输入数字");return;}try{int num = Integer.parseInt(args[0]);System.out.println(num);}catch(NumberFormatException e){System.err.println("参数" + args[0] + "不是有效的数字,请输入数字");}}
}
上述代码使用try/catch
捕获并处理了异常,try
后面的花括号{}
内包含可能抛出异常的代码,括号后的catch
语句包含能捕获的异常和处理代码,catch
后面括号内是异常信息,包括异常类型和变量名,这里是NumberFormatException e
,通过它可以获取更多异常信息,花括号{}
内是处理代码,这里输出了一个更为友好的提示信息。捕获异常后,程序就不会异常退出了,但try
语句内异常点之后的其他代码就不会执行了,执行完catch
内的语句后,程序会继续执行catch
花括号外的代码。
至此,我们就对异常有了一个初步的了解。异常是相对于return
的一种退出机制,可以由系统触发,也可以由程序通过throw
语句触发,异常可以通过try/catch
语句进行捕获并处理,如果没有捕获,则会导致程序退出并输出异常栈信息。
5.2 异常类
NullPointerException
和NumberFormatException
都是异常类,所有异常类都有一个共同的父类Throwable
,我们首先学习这个父类,接着学习Java
中的异常类体系,最后学习怎么自定义异常。
5.2.1 Throwable
追溯NullPointerException
和NumberFormatException
的继承体系,可以发现,两者的超类都是Throwable
类。观察Throwable
类,其有五个构造函数:
/*** Constructs a new throwable with {@code null} as its detail message.* The cause is not initialized, and may subsequently be initialized by a* call to {@link #initCause}.** <p>The {@link #fillInStackTrace()} method is called to initialize* the stack trace data in the newly created throwable.*/public Throwable() {fillInStackTrace();}/*** Constructs a new throwable with the specified detail message. The* cause is not initialized, and may subsequently be initialized by* a call to {@link #initCause}.** <p>The {@link #fillInStackTrace()} method is called to initialize* the stack trace data in the newly created throwable.** @param message the detail message. The detail message is saved for* later retrieval by the {@link #getMessage()} method.*/public Throwable(String message) {fillInStackTrace();detailMessage = message;}/*** Constructs a new throwable with the specified detail message and* cause. <p>Note that the detail message associated with* {@code cause} is <i>not</i> automatically incorporated in* this throwable's detail message.** <p>The {@link #fillInStackTrace()} method is called to initialize* the stack trace data in the newly created throwable.** @param message the detail message (which is saved for later retrieval* by the {@link #getMessage()} method).* @param cause the cause (which is saved for later retrieval by the* {@link #getCause()} method). (A {@code null} value is* permitted, and indicates that the cause is nonexistent or* unknown.)* @since 1.4*/public Throwable(String message, Throwable cause) {fillInStackTrace();detailMessage = message;this.cause = cause;}/*** Constructs a new throwable with the specified cause and a detail* message of {@code (cause==null ? null : cause.toString())} (which* typically contains the class and detail message of {@code cause}).* This constructor is useful for throwables that are little more than* wrappers for other throwables (for example, {@link* java.security.PrivilegedActionException}).** <p>The {@link #fillInStackTrace()} method is called to initialize* the stack trace data in the newly created throwable.** @param cause the cause (which is saved for later retrieval by the* {@link #getCause()} method). (A {@code null} value is* permitted, and indicates that the cause is nonexistent or* unknown.)* @since 1.4*/public Throwable(Throwable cause) {fillInStackTrace();detailMessage = (cause==null ? null : cause.toString());this.cause = cause;}/*** Constructs a new throwable with the specified detail message,* cause, {@linkplain #addSuppressed suppression} enabled or* disabled, and writable stack trace enabled or disabled. If* suppression is disabled, {@link #getSuppressed} for this object* will return a zero-length array and calls to {@link* #addSuppressed} that would otherwise append an exception to the* suppressed list will have no effect. If the writable stack* trace is false, this constructor will not call {@link* #fillInStackTrace()}, a {@code null} will be written to the* {@code stackTrace} field, and subsequent calls to {@code* fillInStackTrace} and {@link* #setStackTrace(StackTraceElement[])} will not set the stack* trace. If the writable stack trace is false, {@link* #getStackTrace} will return a zero length array.** <p>Note that the other constructors of {@code Throwable} treat* suppression as being enabled and the stack trace as being* writable. Subclasses of {@code Throwable} should document any* conditions under which suppression is disabled and document* conditions under which the stack trace is not writable.* Disabling of suppression should only occur in exceptional* circumstances where special requirements exist, such as a* virtual machine reusing exception objects under low-memory* situations. Circumstances where a given exception object is* repeatedly caught and rethrown, such as to implement control* flow between two sub-systems, is another situation where* immutable throwable objects would be appropriate.** @param message the detail message.* @param cause the cause. (A {@code null} value is permitted,* and indicates that the cause is nonexistent or unknown.)* @param enableSuppression whether or not suppression is enabled* @param writableStackTrace whether or not the stack trace is writable** @see OutOfMemoryError* @see NullPointerException* @see ArithmeticException* @since 1.7*/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;}
Throwable
类有两个主要参数:一个是message
,表示异常消息;另一个是cause
,表示触发该异常的其他异常。异常可以形成一个异常链,上层的异常由底层异常触发,cause
表示底层异常。Throwable
还有一个public
方法用于设置cause
:
/*** Initializes the <i>cause</i> of this throwable to the specified value.* (The cause is the throwable that caused this throwable to get thrown.)** <p>This method can be called at most once. It is generally called from* within the constructor, or immediately after creating the* throwable. If this throwable was created* with {@link #Throwable(Throwable)} or* {@link #Throwable(String,Throwable)}, this method cannot be called* even once.** <p>An example of using this method on a legacy throwable type* without other support for setting the cause is:** <pre>* try {* lowLevelOp();* } catch (LowLevelException le) {* throw (HighLevelException)* new HighLevelException().initCause(le); // Legacy constructor* }* </pre>** @param cause the cause (which is saved for later retrieval by the* {@link #getCause()} method). (A {@code null} value is* permitted, and indicates that the cause is nonexistent or* unknown.)* @return a reference to this {@code Throwable} instance.* @throws IllegalArgumentException if {@code cause} is this* throwable. (A throwable cannot be its own cause.)* @throws IllegalStateException if this throwable was* created with {@link #Throwable(Throwable)} or* {@link #Throwable(String,Throwable)}, or this method has already* been called on this throwable.* @since 1.4*/public synchronized Throwable initCause(Throwable cause) {if (this.cause != this)throw new IllegalStateException("Can't overwrite cause with " +Objects.toString(cause, "a null"), this);if (cause == this)throw new IllegalArgumentException("Self-causation not permitted", this);this.cause = cause;return this;}
Throwable
的某些子类没有带cause
参数的构造方法,就可以通过这个方法来设置,这个方法最多只能被调用一次。
在所有构造方法的内部,都有一句重要的函数调用:fillInStackTrace()
。它会将异常栈信息保存下来,这是我们能看到异常栈的关键。Throwable
有一些常用方法用于获取异常信息,比如:
void printStackTrace()
:打印异常栈信息到标准错误输出流;void printStackTrace(PrintStream s)
和void printStackTrace(PrintWriter s)
:打印栈信息到指定的流;String getMessage()
:获取设置的异常message
;Throwable getCause()
:获取异常的`cause;StackTraceElement[] getStackTrace()
:获取异常栈每一层的信息, 每个StackTraceElement
包括文件名、类名、函数名、行号等信息;
5.2.2 异常类体系
以Throwable
为根,Java
定义了非常多的异常类,表示各种类型的异常,部分类如下图所示。
Throwable
是所有异常的基类,它有两个子类:Error
和Exception
。Error
表示系统错误或资源耗尽,由Java
系统自己使用,应用程序不应抛出和处理,比如上图中列出的虚拟机错误(VirtualMacheError
)及其子类内存溢出错误(OutOfMemoryError
)和栈溢出错误(StackOverflowError
)。Exception
表示应用程序错误,它有很多子类,应用程序也可以通过继承Exception
或其子类创建自定义异常,上图中列出了三个直接子类:IOException
(输入输出I/O
异常)、RuntimeException
(运行时异常)、SQLException
(数据库SQL
异常)。RuntimeException
比较特殊,它的名字有点误导,因为其他异常也是运行时产生的,它表示的实际含义是未受检异常(unchecked exception
),相对而言,Exception
的其他子类和Exception
自身则是受检异常(checked exception
),Error
及其子类也是未受检异常。
受检(checked
)和未受检(unchecked
)的区别在于Java
如何处理这两种异常。对于受检异常,Java
会强制要求程序员进行处理,否则会有编译错误,而对于未受检异常则没有这个要求。
如此多不同的异常类其实并没有比Throwable
这个基类多多少属性和方法,大部分类在继承父类后只是定义了几个构造方法,这些构造方法也只是调用了父类的构造方法,并没有额外的操作。那为什么定义这么多不同的类呢?主要是为了名字不同。异常类的名字本身就代表了异常的关键信息,无论是抛出还是捕获异常,使用合适的名字都有助于代码的可读性和可维护性。
5.2.3 自定义异常
除了Java API
中定义的异常类,也可以自己定义异常类,一般是继承Exception
或者它的某个子类。如果父类是RuntimeException
或它的某个子类,则自定义异常也是未受检异常;如果是Exception
或Exception
的其他子类,则自定义异常是受检异常。我们通过继承Exception
来定义一个异常,代码如下所示。
public class AppException extends Exception {public AppException() {super();}public AppException(String message, Throwable cause) {super(message, cause);}public AppException(String message) {super(message);}public AppException(Throwable cause) {super(cause);}
}
和很多其他异常类一样,我们没有定义额外的属性和代码,只是继承了Exception
,定义了构造方法并调用了父类的构造方法。
5.3 异常处理
Java
语言对异常处理的支持,包括catch
、throw
、finally
、try-with-resources
和throws
。
5.3.1 try/catch匹配
如下代码所示,使用try/catch
捕获异常,其中catch
可以捕获一条或者多条,每条对应一种异常类型。
try{//可能触发异常的代码
}catch(NumberFormatException e){System.out.println("not valid number");
}catch(RuntimeException e){System.out.println("runtime exception "+e.getMessage());
}catch(Exception e){e.printStackTrace();
}
异常处理机制将根据抛出的异常类型找第一个匹配的catch
块,找到后,执行catch
块内的代码,不再执行其他catch
块,如果没有找到,会继续到上层方法中查找。需要注意的是,抛出的异常类型是catch
中声明异常的子类也算匹配,所以需要将最具体的子类放在前面,如果基类Exception
放在前面,则其他更具体的catch
代码将得不到执行。代码中也利用了异常信息,e.getMessage()
获取异常消息,e.printStackTrace()
打印异常栈到标准错误输出流。这些信息有助于理解为什么会出现异常,这是解决编程错误的常用方法。示例是直接将信息输出到标准流上,实际系统中更常用的做法是输出到专门的日志中。
在示例中,每种异常类型都有单独的catch
语句,如果多种异常处理的代码是类似的,这种写法比较烦琐。自Java 7
开始支持一种新的语法,多个异常之间可以用|
操作符,形如:
try {//可能抛出 ExceptionA和ExceptionB
} catch (ExceptionA | ExceptionB e) {e.printStackTrace();
}
5.3.2 重新抛出异常
在catch
块内处理完后,可以重新抛出异常,异常可以是原来的,也可以是新建的,如下所示:
try{//可能触发异常的代码
}catch(NumberFormatException e){System.out.println("not valid number");throw new AppException("输入格式不正确", e);
}catch(Exception e){e.printStackTrace();throw e;
}
对于Exception
,在打印出异常栈后,就通过throw e
重新抛出了。而对于NumberFormatException
,重新抛出了一个AppException
,当前Exception
作为cause
传递给了AppException
,这样就形成了一个异常链,捕获到AppException
的代码可以通过getCause()
得到NumberFormatException
。
为什么要重新抛出呢?因为当前代码不能够完全处理该异常,需要调用者进一步处理。为什么要抛出一个新的异常呢?当然是因为当前异常不太合适。不合适可能是信息不够,需要补充一些新信息;还可能是过于细节,不便于调用者理解和使用,如果调用者对细节感兴趣,还可以继续通过getCause()
获取到原始异常。
5.3.3 finally
异常机制中还有一个重要的部分,就是finally
。catch
后面可以跟finally
语句,语法如下所示:
try{//可能抛出异常
}catch(Exception e){//捕获异常
}finally{//不管有无异常都执行
}
finally
内的代码不管有无异常发生,都会执行,具体来说:
- 如果没有异常发生,在
try
内的代码执行结束后执行。 - 如果有异常发生且被
catch
捕获,在catch
内的代码执行结束后执行。 - 如果有异常发生但没被捕获,则在异常被抛给上层之前执行。
由于finally
的这个特点,它一般用于释放资源,如数据库连接、文件流等。try/catch/finally
语法中,catch
不是必需的,也就是可以只有try/finally
,表示不捕获异常,异常自动向上传递,但finally
中的代码在异常发生后也执行。
finally
语句有一个执行细节,如果在try
或者catch
语句内有return
语句,则return
语句在finally
语句执行结束后才执行,但finally
并不能改变返回值,我们来看下面的代码:
public static int test(){int ret = 0;try{return ret;}finally{ret = 2;}}
这个函数的返回值是 0 0 0,而不是 2 2 2。实际执行过程是:在执行到try
内的return ret
;语句前,会先将返回值ret
保存在一个临时变量中,然后才执行finally
语句,最后try
再返回那个临时变量,finally
中对ret
的修改不会被返回。
如果在finally
中也有return
语句呢?try
和catch
内的return
会丢失,实际会返回finally
中的返回值。finally
中有return
不仅会覆盖try
和catch
内的返回值,还会掩盖try
和catch
内的异常,就像异常没有发生一样,比如:
public static int test(){int ret = 0;try{int a = 5/0;return ret;}finally{return 2;}
}
以上代码中,5/0
会触发ArithmeticException
,但是finally
中有return
语句,这个方法就会返回 2 2 2,而不再向上传递异常了。finally
中,如果finally
中抛出了异常,则原异常也会被掩盖,看下面的代码:
public static void test(){try{int a = 5/0;}finally{throw new RuntimeException("hello");}
}
finally
中抛出了RuntimeException
,则原异常ArithmeticException
就丢失了。所以,一般而言,为避免混淆,应该避免在finally
中使用return
语句或者抛出异常,如果调用的其他代码可能抛出异常,则应该捕获异常并进行处理。
5.3.4 try-with-resources
对于一些使用资源的场景,比如文件和数据库连接,典型的使用流程是首先打开资源,最后在finally
语句中调用资源的关闭方法,针对这种场景,Java 7
开始支持一种新的语法,称之为try-with-resources
,这种语法针对实现了java.lang.AutoCloseable
接口的对象,该接口的定义为:
public interface AutoCloseable {void close() throws Exception;
}
没有try-with-resources
时,使用形式如下:
public static void useResource() throws Exception {AutoCloseable r = new FileInputStream("hello"); //创建资源try {//使用资源} finally {r.close();}
}
使用try-with-resources
语法,形式如下:
public static void useResource() throws Exception {try(AutoCloseable r = new FileInputStream("hello")) { //创建资源//使用资源}
}
资源r
的声明和初始化放在try
语句内,不用再调用finally
,在语句执行完try
语句后,会自动调用资源的close()
方法。资源可以定义多个,以分号分隔。在Java 9
之前,资源必须声明和初始化在try
语句块内,Java 9
去除了这个限制,资源可以在try
语句外被声明和初始化,但必须是final
的或者是事实上final
的(即虽然没有声明为final
但也没有被重新赋值)。
5.3.5 throws
异常机制中,还有一个和throw
很像的关键字throws
,用于声明一个方法可能抛出的异常,语法如下所示:
public void test() throws AppException,SQLException, NumberFormatException {//主体代码
}
throws
跟在方法的括号后面,可以声明多个异常,以逗号分隔。这个声明的含义是,这个方法内可能抛出这些异常,且没有对这些异常进行处理,至少没有处理完,调用者必须进行处理。这个声明没有说明具体什么情况会抛出什么异常,作为一个良好的实践,应该将这些信息用注释的方式进行说明,这样调用者才能更好地处理异常。
对于未受检异常,是不要求使用throws
进行声明的,但对于受检异常,则必须进行声明,换句话说,如果没有声明,则不能抛出。对于受检异常,不可以抛出而不声明,但可以声明抛出但实际不抛出。这主要用于在父类方法中声明,父类方法内可能没有抛出,但子类重写方法后可能就抛出了,子类不能抛出父类方法中没有声明的受检异常,所以就将所有可能抛出的异常都写到父类上了。
如果一个方法内调用了另一个声明抛出受检异常的方法,则必须处理这些受检异常,处理的方式既可以是catch
,也可以是继续使用throws
,如下所示:
public void tester() throws AppException {try {test();} catch(SQLException e) {e.printStackTrace();}
}
对于test
抛出的SQLException
,这里使用了catch
,而对于AppException
,则将其添加到了自己方法的throws
语句中,表示当前方法处理不了,继续由上层处理。
5.3.6 对比受检和未受检异常
通过以上介绍可以看出,未受检异常和受检异常的区别如下:
受检异常必须出现在throws
语句中,调用者必须处理,Java
编译器会强制这一点,而未受检异常则没有这个要求。
为什么要有这个区分呢?我们自己定义异常的时候应该使用受检还是未受检异常呢?对于这个问题,业界有各种各样的观点和争论,没有特别一致的结论。一种普遍的说法是:未受检异常表示编程的逻辑错误,编程时应该检查以避免这些错误,比如空指针异常,如果真的出现了这些异常,程序退出也是正常的,程序员应该检查程序代码的bug
而不是想办法处理这种异常。受检异常表示程序本身没问题,但由于I/O
、网络、数据库等其他不可预测的错误导致的异常,调用者应该进行适当处理。但其实编程错误也是应该进行处理的,尤其是Java
被广泛应用于服务器程序中,不能因为一个逻辑错误就使程序退出。所以,目前一种更被认同的观点是:Java
中对受检异常和未受检异常的区分是没有太大意义的,可以统一使用未受检异常来代替。
这种观点的基本理由是:无论是受检异常还是未受检异常,无论是否出现在throws
声明中,都应该在合适的地方以适当的方式进行处理,而不只是为了满足编译器的要求盲目处理异常,既然都要进行处理异常,受检异常的强制声明和处理就显得烦琐,尤其是在调用层次比较深的情况下。其实观点本身并不太重要,更重要的是一致性,一个项目中,应该对如何使用异常达成一致,并按照约定使用。
5.4 如何使用好异常
针对异常,我们学习了try/catch/finally
、catch
匹配、重新抛出、throws
、受检/未受检异常,那到底该如何使用异常呢?下面从异常的适用情况、异常处理的目标和一般逻辑等多个角度进行介绍。
5.4.1 异常应该且仅用于异常情况
异常应该且仅用于异常情况,是指异常不能代替正常的条件判断。比如,循环处理数组元素的时候,应该先检查索引是否有效再进行处理,而不是等着抛出索引异常再结束循环。对于一个引用变量,如果正常情况下它的值也可能为null
,那就应该先检查是不是null
,不为null
的情况下再进行调用。另一方面,真正出现异常的时候,应该抛出异常,而不是返回特殊值。
5.4.2 异常处理的目标
异常大概可以分为三种来源:用户、程序员、第三方。用户是指用户的输入有问题;程序员是指编程错误;第三方泛指其他情况,如I/O
错误、网络、数据库、第三方服务等。每种异常都应该进行适当的处理。处理的目标可以分为恢复和报告。恢复是指通过程序自动解决问题。报告的最终对象可能是用户,即程序使用者,也可能是系统运维人员或程序员。报告的目的也是为了恢复,但这个恢复经常需要人的参与。对用户,如果用户输入不对,可以提示用户具体哪里输入不对,如果是编程错误,可以提示用户系统错误、建议联系客服,如果是第三方连接问题,可以提示用户稍后重试。对系统运维人员或程序员,他们一般不关心用户输入错误,而关注编程错误或第三方错误,对于这些错误,需要报告尽量完整的细节,包括异常链、异常栈等,以便尽快定位和解决问题。用户输入或编程错误一般都是难以通过程序自动解决的,第三方错误则可能可以,甚至很多时候,程序都不应该假定第三方是可靠的,应该有容错机制。比如,某个第三方服务连接不上(比如发短信),可能的容错机制是换另一个提供同样功能的第三方试试,还可能是间隔一段时间进行重试,在多次失败之后再报告错误。
5.4.3 异常处理的一般逻辑
如果自己知道怎么处理异常,就进行处理;如果可以通过程序自动解决,就自动解决;如果异常可以被自己解决,就不需要再向上报告。如果自己不能完全解决,就应该向上报告。如果自己有额外信息可以提供,有助于分析和解决问题,就应该提供,可以以原异常为cause
重新抛出一个异常。总有一层代码需要为异常负责,可能是知道如何处理该异常的代码,可能是面对用户的代码,也可能是主程序。如果异常不能自动解决,对于用户,应该根据异常信息提供用户能理解和对用户有帮助的信息;对运维和开发人员,则应该输出详细的异常链和异常栈到日志。这个逻辑与在公司中处理问题的逻辑是类似的,每个级别都有自己应该解决的问题,自己能处理的自己处理,不能处理的就应该报告上级,把下级告诉他的和他自己知道的一并告诉上级,最终,公司老板必须要为所有问题负责。每个级别既不应该掩盖问题,也不应该逃避责任。
马俊昌.Java编程的逻辑[M].北京:机械工业出版社,2018. ↩︎
尚硅谷教育.剑指Java:核心原理与应用实践[M].北京:电子工业出版社,2023. ↩︎