Java的异常
Java的异常是class,它的继承关系如下:
Throwable是异常体系的根,它继承自Object。Throwable有两个体系:Error和Exception,Error表示严重的错误,程序对此一般无能为力,例如:
- OutOfMemoryError:内存耗尽
- NoClassDefFoundError:无法加载某个Class
- StackOverflowError:栈溢出
而Exception则是运行时的错误,它可以被捕获并处理。
Exception分为两大类: - RuntimeException以及它的子类;
- 非RuntimeException(包括IOException、ReflectiveOperationException等等)
Java规定: - 必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception。
- 不需要捕获的异常,包括Error及其子类,RuntimeException及其子类。
捕获异常
捕获异常使用try...catch语句,把可能发生异常的代码放到try {...}中,然后使用catch捕获对应的Exception及其子类
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
byte[] bs = toGBK("中文");
System.out.println(Arrays.toString(bs));
}
static byte[] toGBK(String s) {
try {
// 用指定编码转换String为byte[]:
return s.getBytes("GBK");
} catch (UnsupportedEncodingException e) {
// 如果系统不支持GBK编码,会捕获到UnsupportedEncodingException:
System.out.println(e); // 打印异常信息
return s.getBytes(); // 尝试使用用默认编码
}
}
}
如果我们不捕获UnsupportedEncodingException,会出现编译失败的问题
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
byte[] bs = toGBK("中文");
System.out.println(Arrays.toString(bs));
}
static byte[] toGBK(String s) {
return s.getBytes("GBK");
}
}
编译器会报错,错误信息类似:unreported exception UnsupportedEncodingException; must be caught or declared to be thrown,并且准确地指出需要捕获的语句是return s.getBytes("GBK");。意思是说,像UnsupportedEncodingException这样的Checked Exception,必须被捕获。
这是因为String.getBytes(String)方法定义是
public byte[] getBytes(String charsetName) throws UnsupportedEncodingException {
...
}
在方法定义的时候,使用throws Xxx表示该方法可能抛出的异常类型。调用方在调用的时候,必须强制捕获这些异常,否则编译器会报错。
在toGBK()方法中,因为调用了String.getBytes(String)方法,就必须捕获UnsupportedEncodingException。我们也可以不捕获它,而是在方法定义处用throws表示toGBK()方法可能会抛出UnsupportedEncodingException,就可以让toGBK()方法通过编译器检查
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
byte[] bs = toGBK("中文");
System.out.println(Arrays.toString(bs));
}
static byte[] toGBK(String s) throws UnsupportedEncodingException {
return s.getBytes("GBK");
}
}
上述代码仍然会得到编译错误,但这一次,编译器提示的不是调用return s.getBytes("GBK");的问题,而是byte[] bs = toGBK("中文");。因为在main()方法中,调用toGBK(),没有捕获它声明的可能抛出的UnsupportedEncodingException。
修复方法是在main()方法中捕获异常并处理
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
try {
byte[] bs = toGBK("中文");
System.out.println(Arrays.toString(bs));
} catch (UnsupportedEncodingException e) {
System.out.println(e);
}
}
static byte[] toGBK(String s) throws UnsupportedEncodingException {
// 用指定编码转换String为byte[]:
return s.getBytes("GBK");
}
}
可见,只要是方法声明的Checked Exception,不在调用层捕获,也必须在更高的调用层捕获。所有未捕获的异常,最终也必须在main()方法中捕获,不会出现漏写try的情况。这是由编译器保证的。main()方法也是最后捕获Exception的机会。
如果是测试代码,上面的写法就略显麻烦。如果不想写任何try代码,可以直接把main()方法定义为throws Exception
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
public static void main(String[] args) throws Exception {
byte[] bs = toGBK("中文");
System.out.println(Arrays.toString(bs));
}
static byte[] toGBK(String s) throws UnsupportedEncodingException {
// 用指定编码转换String为byte[]:
return s.getBytes("GBK");
}
因为main()方法声明了可能抛出Exception,也就声明了可能抛出所有的Exception,因此在内部就无需捕获了。代价就是一旦发生异常,程序会立刻退出。
所有异常都可以调用printStackTrace()方法打印异常栈,这是一个简单有用的快速打印异常的方法。
static byte[] toGBK(String s) {
try {
return s.getBytes("GBK");
} catch (UnsupportedEncodingException e) {
// 先记下来再说:
e.printStackTrace();
}
return null;
多catch语句
可以使用多个catch语句,每个catch分别捕获对应的Exception及其子类。JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个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);
}
}
存在多个catch的时候,catch的顺序非常重要:子类必须写在前面。
finally语句
- finally语句不是必须的,可写可不写;
- finally总是最后执行。
无论是否有异常发生,如果我们都希望执行一些语句,例如清理工作,怎么写?
public static void main(String[] args) {
try {
process1();
process2();
process3();
System.out.println("END");
} catch (UnsupportedEncodingException e) {
System.out.println("Bad encoding");
System.out.println("END");
} catch (IOException e) {
System.out.println("IO error");
System.out.println("END");
}
}
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");
}
}
捕获多种异常
如果某些异常的处理逻辑相同,但是异常本身不存在继承关系,那么就得编写多条catch子句
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (IOException e) {
System.out.println("Bad input");
} catch (NumberFormatException e) {
System.out.println("Bad input");
} catch (Exception e) {
System.out.println("Unknown error");
}
}
因为处理IOException和NumberFormatException的代码是相同的,所以我们可以把它两用|合并到一起
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");
}
}
抛出异常
异常的传播
当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个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
}
}
抛出异常
1.创建某个Exception的实例
2.用throw语句抛出
void process2(String s) {
if (s==null) {
NullPointerException e = new NullPointerException();
throw e;
}
}
void process2(String s) {
if (s==null) {
throw new NullPointerException();
}
}
为了能追踪到完整的异常栈,在构造异常的时候,把原始的Exception实例传进去,新的Exception就可以持有原始Exception信息。
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(e);
}
}
static void process2() {
throw new NullPointerException();
}
}
output:
ava.lang.IllegalArgumentException: java.lang.NullPointerException
at Main.process1(Main.java:15)
at Main.main(Main.java:5)
Caused by: java.lang.NullPointerException
at Main.process2(Main.java:20)
at Main.process1(Main.java:13)
在catch中抛出异常,不会影响finally的执行。JVM会先执行finally,然后抛出异常。
自定义异常
Java标准定义库的常用异常包括:
- 抛出异常时,尽量复用JDK已定义的异常类型;
- 自定义异常体系时,推荐从RuntimeException派生“根异常”,再派生出业务异常;
- 自定义异常时,应该提供多种构造方法。