二、异常处理
1、Java的异常
在计算机程序运行的过程中,总是会出现各种各样的错误。
有些错误是用户造成的,比如,希望用户输入一个int
类型的年龄,但是用户的输入是abc
:
// 假设用户输入了abc:
String s = "abc";
int n = Integer.parseInt(s); // NumberFormatException!
还有一些错误是随机出现,并且永远不可能避免的。比如:
- 网络突然断了,连接不到远程服务器;
- 内存耗尽,程序崩溃了;
所以,一个健壮的程序必须处理各种各样的错误。
Java对于错误的处理是内置了一套异常处理机制,总是使用异常来表示错误。
异常是一种class
,因此它本身就带有类型信息。异常可以在任何地方抛出,但是需要在上层捕获,这样就和方法调用分离了:
try {
String s = processFile(“C:\\test.txt”);
// ok:
} catch (FileNotFoundException e) {
// file not found:
} catch (SecurityException e) {
// no read permission:
} catch (IOException e) {
// io error:
} catch (Exception e) {
// other error:
}
Java的异常是class
,它的继承关系如下:
从继承关系可知:Throwable
是异常体系的根,它继承自Object
。Throwable
有两个体系:Error
和Exception
,
Error
表示严重的错误,程序对此一般无能为力,例如:
OutOfMemoryError
:内存耗尽NoClassDefFoundError
:无法加载某个ClassStackOverflowError
:栈溢出
而Exception
则是运行时的错误,它可以被捕获并处理。
某些异常是应用程序逻辑处理的一部分,应该捕获并处理。例如:
NumberFormatException
:数值类型的格式错误FileNotFoundException
:未找到文件SocketException
:读取网络失败
还有一些异常是程序逻辑编写不对造成的,应该修复程序本身。例如:
NullPointerException
:对某个null
的对象调用方法或字段IndexOutOfBoundsException
:数组索引越界
总结:
Error
是十分严重的错误,程序对此无能为力,不需要捕获。RuntimeException
是运行时的错误,应该修改代码本身,不强制捕获。- 非
RuntimeException
(Checked Exception)是逻辑处理的错误,必须要捕获处理。
不推荐捕获了异常但不进行任何处理。
2、捕获异常
在Java中,凡是可能抛出异常的语句,都可以用try ... catch
捕获。
2.1 多catch语句
可以使用多个catch
语句,每个catch
分别捕获对应的Exception
及其子类。
JVM在捕获到异常后,会从上到下匹配catch
语句,匹配到某个catch
后,执行catch
代码块,然后不再继续匹配。
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (IOException e) {
System.out.println(e);
} catch (NumberFormatException e) {
System.out.println(e);
}
}
注意:因为JVM在配置到某个
catch
后,就不再继续匹配。因此,存在多个catch
时,catch
的顺序很重要:子类必须写在前面。否则会永远捕获不到。例如:public static void main(String[] args) { try { process1(); process2(); process3(); } catch (IOException e) { System.out.println("IO error"); } catch (UnsupportedEncodingException e) { // UnsupportedEncodingException是IOException的子类,所以永远捕获不到 System.out.println("Bad encoding"); } }
2.2 finally语句
我们我们希望无论是否有异常发生,都执行一些语句,例如关闭数据库等,可以使用 finally
语句。
finally
语句块保证有无错误都会执行。
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (UnsupportedEncodingException e) {
System.out.println("Bad encoding");
} catch (IOException e) {
System.out.println("IO error");
} finally {
System.out.println("END");
}
}
finally
有几个特点:
finally
语句不是必须的,可写可不写;finally
总是最后执行。
某些情况下,可以没有
catch
,只使用try ... finally
结构
2.3 捕获多种异常
如果某些异常的处理代码相同,但是异常本身不存在继承关系,可以把它两用|
合并到一起:
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (IOException | NumberFormatException e) { // IOException或NumberFormatException
System.out.println("Bad input");
} catch (Exception e) {
System.out.println("Unknown error");
}
}
3、抛出异常
3.1 异常的传播
当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个try ... catch
被捕获为止。
public class Main {
public static void main(String[] args) {
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
}
static void process1() {
process2();
}
static void process2() {
Integer.parseInt(null); // 会抛出NumberFormatException
}
}
通过printStackTrace()
可以打印出方法的调用栈,类似:
java.lang.NumberFormatException: null
at java.base/java.lang.Integer.parseInt(Integer.java:614)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at Main.process2(Main.java:16)
at Main.process1(Main.java:12)
at Main.main(Main.java:5)
上述信息表示:
NumberFormatException
是在java.lang.Integer.parseInt
方法中被抛出的;- 调用层次从上到下依次是:
main()
调用process1()
;process1()
调用process2()
;process2()
调用Integer.parseInt(String)
;Integer.parseInt(String)
调用Integer.parseInt(String, int)
。
查看Integer.java
源码可知,抛出异常的方法代码如下:
public static int parseInt(String s, int radix) throws NumberFormatException {
if (s == null) {
throw new NumberFormatException("null");
}
...
}
3.2 抛出异常
当发生错误时,例如,用户输入了非法的字符,我们就可以抛出异常。
如何抛出异常?参考Integer.parseInt()
抛出异常的方法,抛出异常分两步:
- 创建某个
Exception
的实例; - 用
throw
语句抛出。
例子:
void process2(String s) {
if (s==null) {
throw new NullPointerException();
}
}
如果一个方法捕获了某个异常后,又在catch
子句中抛出新的异常,就相当于把抛出的异常类型“转换”了:
public class Main {
public static void main(String[] args) {
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
}
static void process1() {
try {
process2();
} catch (NullPointerException e) {
throw new IllegalArgumentException();
}
}
static void process2() {
throw new NullPointerException();
}
}
在上面的代码中,当process2()
抛出NullPointerException
后,被process1()
捕获,然后抛出IllegalArgumentException()
。最后,在main()
中捕获IllegalArgumentException
并打印异常栈,如下:
java.lang.IllegalArgumentException
at Main.process1(Main.java:19)
at Main.main(Main.java:9)
这说明新的异常丢失了原始异常信息,我们已经看不到原始异常NullPointerException
的信息了。
为了能追踪到完整的异常栈,在构造异常的时候,把原始的Exception
实例传进去,新的Exception
就可以持有原始Exception
信息。如下代码:
public class Main {
//...
static void process1() {
try {
process2();
} catch (NullPointerException e) {
throw new IllegalArgumentException(e);
}
}
//...
}
这次,打印出来的异常栈类似:
java.lang.IllegalArgumentException: java.lang.NullPointerException
at Main.process1(Main.java:19)
at Main.main(Main.java:9)
Caused by: java.lang.NullPointerException
at Main.process2(Main.java:24)
at Main.process1(Main.java:17)
... 1 more
由输出可以知道:
Caused by: Xxx
知道造成问题的根源是NullPointerException
,是在Main.process2()
方法抛出的。
4、自定义异常
Java标准库定义的常用异常包括:
Exception
│
├─ RuntimeException
│ │
│ ├─ NullPointerException
│ │
│ ├─ IndexOutOfBoundsException
│ │
│ ├─ SecurityException
│ │
│ └─ IllegalArgumentException
│ │
│ └─ NumberFormatException
│
├─ IOException
│ │
│ ├─ UnsupportedCharsetException
│ │
│ ├─ FileNotFoundException
│ │
│ └─ SocketException
│
├─ ParseException
│
├─ GeneralSecurityException
│
├─ SQLException
│
└─ TimeoutException
在需要排除异常时,尽量使用已定义的异常类型。
在一个大型项目中,可以自定义新的异常类型,但是,保持一个合理的异常继承体系是非常重要的。
一个常见的做法是自定义一个BaseException
作为“根异常”,然后,派生出各种业务类型的异常。
BaseException
需要从一个适合的Exception
派生,通常建议从RuntimeException
派生:
public class BaseException extends RuntimeException {
}
其他业务类型的异常就可以从BaseException
派生:
public class UserNotFoundException extends BaseException {
}
public class LoginFailedException extends BaseException {
}
//...
自定义的BaseException
应该提供多个构造方法:
public class BaseException extends RuntimeException {
public BaseException() {
super();
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
public BaseException(String message) {
super(message);
}
public BaseException(Throwable cause) {
super(cause);
}
}
上述构造方法实际上都是原样照抄RuntimeException
。这样,抛出异常的时候,就可以选择合适的构造方法。通过IDE可以根据父类快速生成子类的构造方法。