异常、断言和日志
——笔记内容参考《Core JAVA Volume》 和 《Effective JAVA》
Java异常
在理想世界里,用户输入的数据格式永远都是正确的,选择打开的文件也一定存在,代码中永远不会出现bug。
@Core Java Volume
-
处理异常
Core Java Volume中指出:用户期望在出现错误的时候,程序能够采取合理的行为。如果因为错误某些操作无法完成,程序应该:
- 返回到一种安全的状态,并能够让用户执行其他命令。
- 或者允许用户保存所有工作的成果,并以妥善的方式终止程序。
如果一个方法无法通过正常的途径完成他的任务,那么还可以通过另外一种方式退出方法。
在这种情况下,方法将会通过throw 抛出一个异常(封装了错误信息的对象),这个方法将不会返回任何正常值,而且也不会从调用这个方法的地方继续执行。而是跳转到能够处理这种异常的异常处理器(Exception Handler,即Catch{})。 -
异常分类
所有的异常类均派生与 Throwable,从Throwable 类 分化出了 Error 和 Exception ,又从Exception分化出了RunTimeException和
其它异常
。java规范将所有的异常分为了两类,检查型异常和非检查型异常。其中UncheckedException(非检查型异常)包括:Error 和 RunTimeException。CheckedException(受检异常)包括:从Exception派生出来的异常中除了RunTimeException的所有其他异常都是受检异常。
-
(Error描述了java RunTimeSystem的内部错误和资源耗尽错误,你的应用程序中不该抛出这样的异常,如果产生了内部错误,除了通知用户,并尽力妥善地终止程序之外,你将无能为力。)
-
(RunTimeException是指由编程错误导致的异常,简单来说就是你自己代码写的有问题从而导致的那些异常。像数组下标越界异常和空指针异常,这些都可以通过提前做一些判断规避掉的异常就是RuntimeException。)
你可能有个问题,那岂不是所有的异常都是RunTimeException?并不是,比如:打开一个文件,而文件不存在导致的异常,即使你在打开前对它是否存在进行了判断,但它可能在你刚判断完就被删掉了,这样仍然会出现文件不存在异常。文件的是否存在,并不仅仅取决于你的代码,而取决于
外部环境
。
总结:
-
如何区分受检异常和未受检异常?
所有的Error和RunTimeException都属于未受检异常,除此之外的异常都属于受检异常。
-
如何区分受检异常和RuntimeException?
所有因代码编程错误导致的异常都属于RunTimeException,如果代码逻辑正确,仍然发生的异常就是受检异常(主要取决于环境,而不取决于或不仅仅取决于你的代码。)
-
Exception拓展出两条分支:RunTimeException和OtherException,受检异常就是这里的“其它”异常。
-
-
声明检查型异常(非检查型异常不会由你抛出,所以不用也不要在方法首部声明将会抛出非检查型异常)
非检查型异常不该被声明也可以这样理解:
- 首先,Error是java运行系统内部发生的错误或资源耗尽错误,无法预测
- 其次,RunTimeException是你自己代码的问题。你知道自己的代码有问题,为什么不想着怎么修改你代码里的错误,而是要告诉编译器我的代码有问题,诶我就是不改,就是玩。???
CoreJava中指出:
在方法首部声明检查型异常的道理很简单:一个方法不仅需要告诉编译器它将返回什么类型的值,也需要告诉编译器有可能发生什么样的错误。
如:Public FileInputStream(String name) throws FileNotFoundException
这是一个FileInputStream构造器。
总结:
-
哪些异常需要在方法首部声明?
检查型异常
-
什么情况下需要声明?
- 调用了一个抛出检查型异常的方法(如FileInputStream构造器)
- 检测到一个错误,并用throw语句抛出了一个检查型异常。(怎么检测到的,用try{})
-
如果一个方法可能抛出多个检查型异常呢?
那就在方法首部列出所有的检查型异常类,用逗号隔开。
如:public Image LoadImage(String s) throws FileNotFoundException, EOFException
-
如果没有声明所有可能抛出的检查型异常呢?
编译器会给你发出一个错误消息。
-
如果超类中的某个方法声明抛出了检查型异常,而子类又覆盖了这个方法,那子类该如何声明呢?
如果子类方法中要抛出这个异常,子类方法声明的检查型异常就要比超类方法中声明的异常更具体,更详细,不能比超类中的更加通用。当然子类方法中也可以不抛出异常(在方法中用try{…}catch{…}捕获并处理掉),那就不需要声明了。
-
如果子类覆盖父类中的方法并没有声明可能抛出异常,那子类中的这个方法就也不能抛出任何检查型异常,但如果子类中的确可能产生异常该怎么办呢?
用try/catch捕获它并处理掉。
-
如何抛出异常
步骤很简单:
- 找到一个合适的异常类
- 创建这个类的一个对象
- 抛出这个对象
💎:一旦抛出这个对象,这个方法就不会返回到调用者。(也就是说,不必操心设置一个错误码,或默认返回值)
💎:Java中只能抛出Throwable类型的对象,而C++中,可以抛出任何类型的值。
-
创建异常类
什么情况下需要创建一个异常类呢,就是所有的标准异常类都无法描述你遇到的错误,这时候你就可以定义你自己的异常类了。
但,这很没有必要。(根据Effective Java 第72条:优先使用标准的异常,代码重用的原则同样适用于异常类)
💎重用标准异常类有三个好处:
- 使API更易于学习,因为它与程序员已经习惯的用法一致
- 对于用到这些API的程序而言,它们的可读性更好,因为不会出现程序员不太熟悉的异常。
- 异常类越少,意味着内存占用就越小,装载这些类的开销就越小。
-
异常的捕获与处理 try/catch块
💎:CoreJava中指出: 一般经验,捕获那些你知道怎么处理的异常,继续传播那些你不知道怎么处理的异常。最好的选择是什么也不做,而是将异常传递给调用者。如果方法抛出了异常,还是让调用者去想该怎么处理吧。(但也不可以无限向高层传播,因为如果一个错误信息过于底层,其他人可能还要去看你的底层代码,这很不友好。)
try/catch执行过程
:- 如果try语句块中的任何一个地方发生了一个异常,将会跳过try语句块中的剩下代码。
- 程序将继续执行catch子句中的代码。
- 如果try子句中没有发生任何异常,将跳过catch子句。
- 如果方法中任何地方抛出了catch子句中没有声明的异常类型,那么这个方法将会立即退出(希望方法调用者为它提供了catch子句)
在一个try语句块中可以捕获多个异常,同理在一个catch子块中如果多个异常的处理方式一样那就可以放在同一个catch子块中。
//🌰 try { code that may throw exceptions. } catch(FileNotFoundException | UnknownHostException e) { emergency action for missing files and unknown hosts. } catch(IOException e) { emergency action for I/O problems. }
-
再次抛出异常&异常链
再次抛出异常,即包装技术,可以抛出高层异常,而非非常底层的异常。常常会遇到这种情况,程序抛出了一个异常信息,上面显示了很多异常,查看异常位置,这些异常又与我的代码没有直接关系,找了半天终于找到关于我代码的异常信息。此时,你可以包装异常,来直接获得高层异常。
或者像CoreJava中说的那样:你如果开发了一个子系统供其他程序员使用,其它程序员使用的时候这个子系统抛出了异常,但这个错误信息包含了子系统的很多细节原因。这对其他程序员来说很不友好,其他程序员可能只想知道是不是子系统出了问题,但并不想知道发生这个错误的细节原因。
//🌰ServletException就是这样一个异常类型的例子。执行一个servlet代码可能不想知道发生错误的细节原因,但希望明确地知道servlet是否有问题。 try { access the database } catch(SQLException orginal) { var e = new ServletException("database error"); e.initCause(orginal); throw e; } //捕获到异常时,可以使用下边语句获取原始异常对象: Throwable orginal = caughtException.getCause(); //这样可以在子系统中抛出高层异常,而且不会丢失原始异常的细节。 //💎注意Effective Java 第73条:虽然异常包装与不加选择地从底层传递异常的做法相比有所改进,但是也不能滥用它。如有可能,处理来自底层异常最好的做法是,在调用底层方法之前确保他们会成功执行,从而避免他们抛出异常。有时候,可以在给底层方法传递参数之前,检测更高层方法的参数的有效性,从而避免底层抛出异常。
-
finally的体 主要用于清理资源
注意: 当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值。如下:
public class Test { public static int f(int value) { try { return value * value; } finally { if (value == 2) { return 0; } } } } //如果调用 `f(2)`,返回值将是 0,因为 finally 语句的返回值覆盖了 try 语句块的返回值。
💎注意:因此不要把该表控制流的语句(return, throw , break , continue )放入finally子句中。
-
try-with-resources(带资源的try语句)
//资源一般都实现了AutoCloseable接口。 try(Resource res = ...) { work with res } //try块退出时,会自动调用res.close()。 //try块中可以指定多个资源。 //try-with-resources语句自身也可以有catch子句,甚至还可以有一个finally子句。这些字句会在关闭资源后执行。
。。。断言。。。日志。。。后续补充