做一个小的笔记,有疑问时方便回顾。
如同大多数现代编程语言一样,Java语言有着健壮的错误处理机制,将控制权从出错点转移给强壮的错误处理器。
Java 支持异常处理
,而不是让错误代码冒泡似的从方法调用链上一层一层向上返回。在异常处理中,一个方法可以通过 “抛出异常” 来发出一个严重问题的信号。调用链的某一个方法,不一定是直接调用者,负责“捕获”异常。
异常处理的根本优点是:它将错误检测和错误处理解耦了。
异常处理要点:
1、当你抛出一个异常的时候,控制权转到最近的异常处理器
2、在 Java 中,由编译器跟踪已检查异常( checked exception )
3、使用 try / catch 结构处理异常
4、在正常执行完以后或者是异常发生时,try-with-resources 语句会自动关闭资源
5、使用 try/finally 结构处理那些无论执行是否正常地进行都要发生的行为
6、你可以捕获和重新抛出一个异常,或者将它和另外一个异常链起来
异常抛出
在某种情况下,一个方法,可能发现自己无法执行手头上的任务。可能是缺少必要的资源,可能是提供的参数不一致,这种情况下,最好抛出异常。
示例场景
假设一个方法返回一个给定的上下限之间的随机数。而调用者可能由于某种原因将最大范围和最小范围写反了。这种时候,我们应该抛出一个异常,提示方法调用者问题的所在。
public static int randInt(int low, int high) {
if (low > high) throw new IllegalArgumentException(String.
format("low should be <= high,but low is %d and high is %d", low, high));
return low + (int) (new Random().nextDouble() * (high - low + 1));
}
在这里,throw 语句抛出了一个带有错误信息指示的异常对象,方法体内的返回语句不再得到执行,控制权从错误发生点转移到了异常处理器(用户捕获异常的地方)
异常继承的层次
异常结构图(图片来自 diycode).png所有的异常类都派生自 Throwable
Error
Error用来表示编译时和系统错误,一般我们不用关心
CheckedException
我们在编码的时候,要么捕获检查异常,要么在方法头声明他们,编译器会检查这些异常是否被妥善的处理。这样的异常通常是可能被预知的情况,比如:输入输出,文件损坏,网络连接失败。在编码的时候,必须处理或者是抛出合理的异常信息来报告这样的错误。
UnCheckedException
我们程序的大部分问题其实都来自于 UnCheckedException , 因为编译器不强制要求我们对这样的异常进行处理,而且这样的异常都发生在运行时,在编译期间并不会被编译器检查出来。未检查异常表明是程序员造成的错误,不是不可避免的外部风险导致的,这样的异常往往是细心一点可以避免的。
已检查异常的声明
如果一个方法没有捕获一个检查性异常,那么该方法必须使用 throws 关键字来声明。throws 关键字放在方法签名的尾部。
也可以使用 throw 关键字抛出一个异常,无论它是新实例化的还是刚捕获到的。
下面方法的声明抛出一个 RemoteException 异常:
import java.io.*;
public class className
{
public void deposit(double amount) throws RemoteException
{
// Method implementation
throw new RemoteException();
}
//Remainder of class definition
}
一个方法可以声明抛出多个异常,多个异常之间用逗号隔开。在throws语句中,可以将异常合并到一个共同的父类。这样做好不好,取决于异常自身。例如,一个方法抛出多个 IOException
,那么将他们都在 throws IOException 中抛出是有意义的。反之,如果异常之间没有联系,则不能合并抛出,这样违背了异常检查的母的。
一些人认为承认方法抛出异常是很丢脸的一件事,相反,在方法内部处理异常不是更好吗?
实际上,我们应该允许每种异常找到她自己的处理器。异常处理的黄金法则是”早抛出,后捕获“。
当我们在覆盖方法的时候,不能够抛出比父类方法更多的已检查异常,或者是无关的已检查异常。如果父类方法没有 throws 关键字语句,则覆盖后的方法不能抛出已检查异常。
抛出了异常的方法,应该在文档中有所体现,比如:@throws:FileNotFoundException if there is no file with the filename
异常捕获
为了捕获异常,需要准备一个 try 代码块儿,最简单的形式如下:
try{
statements
}catch(ExceptionClass exceptionObject){
handler
}
当执行 try 语句块的时候,如果发生异常,控制转移到异常处理器。这种简单形式有两个变种:
变种一:
try {
// Code that might generate exceptions
} catch(Type1 t1){
// Handle exceptions of type1
} catch(Type2 t2){
// Handle exceptions of type2
} catch(Type3 t3){
// Handle exceptions of type3
}
catch语句按照从上到下的顺序执行,所以最具体的异常必须放在最前面。匹配具有唯一性。
变种二
try{
statements
}catch (IllegalArgumentException|IllegalStateException e){
handler
}
这种情况下,处理器只能在异常变量上调用所有异常共有的方法。这个变种是JDK1.7以后出现的,不过现在基本都是基于1.7/1.8开发的,倒是不用担心了。
try-with-resources
正常情况下我们释放资源的需通过finally,但在JDK1.7后,可以使用try-with-resources方式,它可以实现自动释放功能,而不需要加上finally子句。需要做的就是将需要释放的资源对象放在try语句:
public String read(String filename) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
StringBuilder builder = new StringBuilder();
String line = null;
while((line=reader.readLine())!=null){
builder.append(line);
builder.append(String.format("%n"));
}
return builder.toString();
}
}
并且可以放入多个需要释放的资源:当有多个资源需要释放的时候,先初始化的资源会被后释放,类似于栈的原理。
被try块包裹的资源必须属于一个实现了 AutoCloseable 接口的类,或者是 AutoCloseable的子接口,比如 Closeable,但是它抛出的是 IOException。
public void copyFile(String fromPath, String toPath) throws IOException {
try (InputStream input = new FileInputStream(fromPath);
OutputStream output = new FileOutputStream(toPath)) {
byte[] buffer = new byte[8192];
int len = -1;
while ((len = input.read(buffer)) != -1) {
output.write(buffer, 0, len);
}
}
}
finally子句
在上面,我们释放资源是通过 try-with-resources, 但是不是所有的资源都实现了 AutoCloseable 接口。这种情况下,我们需要使用 finally 子句来实现资源释放。
Java中主要通过finally把资源恢复到它们的初始状态,如:已打开的文件或网络链接等,总言之,就是与外界“世界”的某个开关。
这样做是因为无论程序是发生了异常还是正常结束,都会执行 finally 子句。
try{
do work
}finally {
clean up
}
我们应该避免在 finally 中抛出异常,因为如果 try 块儿中发生异常,这个异常会被 finally 中的异常所掩盖。参考案例:
BufferedReader in = null;
try{
in = Files.newBufferedReader(Paths.get(path), StandardCharsets.UTF_8);
}catch (IOException e){
System.err.println("caught IOException:"+e.getMessage());
}finally {
if (in!=null){
//这里也会抛出 IOException , 这时候,要么嵌套 try{}catch{}.要么是改用 try-with-resources
in.close();
}
}
重新抛出异常和异常链
这一小节的笔记是直接摘抄自 < https://www.diycode.cc/topics/208/>
有时我们在捕获到异常后,可能在捕获的地方不适合处理该异常,我们需要将它重新抛出:
catch(Exception e){
throw e;
}
这样有一个好处,我们可以将异常交给上一级环境处理,但是这样就会存在一个问题,抛出的的异常携带的信息,也就是printStackTrace()方法显示的是原来异常抛出点的调用栈信息,而非重新抛出点的信息,这样重新抛出点的调用信息就被掩盖了。如果想更新重新抛出点信息到这个异常调用栈中,就可以使用fillInStackTrace()方法:
catch(Exception e){
throw e.fillInStackTrace();
}
那么当前调用栈的信息就更新到了这个异常对象中了,还有一种情况,也会存在类似的丢失现象:
catch(Exception e){
throw new Exception();
}
这样我们上一级的抛出的异常信息就丢了,接收异常的地方就是只能得到new Exception()这个异常的信息。在JDK1.4以前如果你希望保存丢失的那个异常信息,只能通过编码的方式自己实现,而在JDK1.4后,Throwable类有一个构造方法接收一个Throwable类型的参数(文章上方方法汇总可以查看该构造方法)。那么这个传入的参数称为cause,它用来表示原始异常,那么就可以通过异常链从新的异常追踪到异常最初发生的位置。除了构造方法,我们还可以通过initCause(Throwable cause)方法传入一个Throwable对象,它的作用和构造函数传入一个Throwable对象是一样的。大家还记得之前介绍过finally字句吗?,它其实也会造成异常丢失:
class VeryImportantException extends Exception {
@Override
public String toString() {
return "A very important exception!";
}
}
class OtherException extends Exception {
@Override
public String toString() {
return "Other exception";
}
}
public class Test {
void f() throws VeryImportantException {
throw new VeryImportantException();
}
void dispose() throws OtherException {
throw new OtherException();
}
public static void main(String[] args) {
try {
Test test = new Test();
try {
test.f();
} finally {
test.dispose();
}
} catch (Exception e) {
System.out.println(e);
}
}
}/* Output:
Other exception
*///:~
我们把最外一层try看着是上一级程序的处理,在这个try里面发生了两次异常,但是我们只能获得从finally中抛出的异常信息,而在f()方法中的异常信息丢失,这种情况我们称上一个异常被抑制了。这在JDK1.7之前同样需要我们自己编码去解决这个问题,在JDK1.7之后,新加入了两个方法帮助我们能够很好的去解决这个问题了,那就是addSuppressed(Throwable exception)和getSuppressed(),对于上述问题的解决:
class VeryImportantException extends Exception {
@Override
public String toString() {
return "A very important exception!";
}
}
class OtherException extends Exception {
@Override
public String toString() {
return "Other exception";
}
}
public class Test {
void f() throws VeryImportantException {
throw new VeryImportantException();
}
void dispose() throws OtherException {
throw new OtherException();
}
public static void main(String[] args) {
try {
Test test = new Test();
Exception exception = null;
try {
test.f();
} catch (VeryImportantException e) {
exception = e;
} finally {
try {
test.dispose();
} catch (OtherException e) {
if (exception != null) {
exception.addSuppressed(e);
} else {
exception = e;
}
}
if (exception != null) {
throw exception;
}
}
} catch (Exception e) {
System.out.println(e);
for (Throwable throwable : e.getSuppressed()) {
System.out.println(throwable);
}
}
}
}/* Output:
A very important exception!
Other exception
*///:~
堆栈踪迹
如果没有在任何地方捕获异常,就会显示堆栈踪迹
(stack trace)——列出异常抛出时,所有未决方法的调用信息。堆栈踪迹信息会被推送到错误消息的流 System.err。
Android中常用的一种捕获未捕获异常的方式就是利用:Thread.getDefaultUncaughtExceptionHandler()。值得注意的是,未捕获异常会终止其所在的线程。
有时,我们捕获了异常,但是不知道怎么处理的时候,至少要打印出堆栈踪迹信息,而不是简单的忽略她。e.printStackTrace()
Objects.requireNonNull
public static void printMessage(String message){
String realMessage = Objects.requireNonNull(message,"message can not be null !");
System.out.println(realMessage);
}
上面的代码看起来好像没有什么实质性的作用,但是在开发截断,可以明显的帮助我们发现 NPE的位置。
未检查异常概览
未检查异常.png已检查异常概览
已检查异常.png异常方法
帮助我们更好的找到crash的原因
image.png
参考
[1]: https://www.diycode.cc/topics/208 "Java 异常详解"
[2]: http://wiki.jikexueyuan.com/project/java/exceptions.html "Java 异常处理"
[3]: http://www.runoob.com/java/java-exceptions.html "Java 异常处理"
[4]: 《 Java for the impatient 》
网友评论