异常
异常是什么?
程序不可能是没有错误的,当程序遇到问题时,会抛出异常,此时应终止程序,或处理异常。
异常层次
Throwable是所有异常的父类,往下为
- Error:运行时系统的内部错误和资源消耗尽错误,不会由应用程序产生
- Exception:又分为RuntimeException和其他Exception,由程序错误导致的异常属于RuntimeException,而程序无问题但外部(如I/O)导致的异常属于其他异常
如果出现RuntimeException,那一定是代码写错了,如常见的算术、空指针、数组溢出
应该处理或抛出何种异常?
将上述层次分为两大类:
- unchecked异常:继承Error和RuntimeException类的异常
- checked异常:除上面外的其他异常,所有checked异常都需提供异常处理器
一个方法应该处理或抛出其所有可能发生的checked异常,因为
- Error异常由Java内部产生,我们无法对其控制和修正
- RuntimeException可通过修改代码逻辑避免发生,而不应该关注出错后的处理
声明异常
方法不仅需要告诉调用者其返回的值,同时还需要告诉其可能发生的错误,通过throws关键字声明其可能发生的异常
class FileLoad {
public void load() throws IOException {
}
}
class ImageLoad extends FileLoad {
public void load() {
}
}
class TextLoad extends FileLoad {
public void load() throws FileNotFoundException, EOFException {
}
}
声明一个异常,并不代表只能抛出那个异常,也有可能抛出其子类异常(如上FileLoad的load()方法声明为IOException,但可能抛出其子类FileNotFoundException)
Tip:
- 子类重写的方法可选择不声明异常,但不能声明比父类更通用的异常,即只能声明父类异常或其子类
- 若父类方法没有声明checked异常,子类重写的方法也不能声明
抛出异常
方法经过声明后,内部即可能出现异常,需要在异常出现的地方使用throw关键字抛出异常
由于大多数异常是在运行时才能发生的,故下面模拟在读取一个10行的文件时,读到第5行因线程抢占导致线程中止,此时会抛出IO中断异常
class FileLoad {
public void load() throws IOException {
for (int i = 0; i < 10; i++) {
if (i == 5) {
throw new InterruptedIOException();
}
}
}
}
自定义异常
若系统自带的异常类不够用,可通过继承Exception或其子类创建自己的异常类,通常需要两个构造方法,一个为默认构造方法,一个为带有详细描述信息的构造方法(通过toString打印)
class divideOneException extends ArithmeticException {
public divideOneException() {
}
public divideOneException(String s) {
super(s);
}
}
class numDivide {
void divide() throws divideOneException {
int i = 2;
int j = 1;
if (j == 1) {
throw new divideOneException();
}
i /= j;
}
}
如上自定义了除1异常,当j==1时抛出异常(实际情况下j应由外部输入)
捕获异常
如果异常抛出后没有捕获,则会抛给上层调用者,一直往上抛,若无法解决,程序就会终止并打印错误信息,其包括异常类型和堆栈内容,如不想让程序终止,可通过try-catch关键字捕获并处理
int i = 2;
int j = 0;
int[] k = new int[1];
try {
i /= j;
k[2] = 1;
} catch (ArithmeticException | IndexOutOfBoundsException e) {
e.printStackTrace();
}
Tips:
- 将可能发生异常的代码放进try子句中
- 可以捕获多个异常,e为final类型
- 若try子句中的某行代码未抛出异常,跳过catch子句
- 若try子句中的某行代码抛出了在catch子句中说明的异常类,将不再执行try子句中的后续代码,转而执行catch子句中的代码
- 若try子句中的某行代码抛出了在catch子句中未说明的异常类,相当于未捕获异常,程序终止
异常链和异常包装
在catch中可以抛出一个异常,可在此改变异常的类型
class numDivide {
void divide() throws IOException {
int i = 2;
int j = 0;
int[] k = new int[1];
try {
i /= j;
} catch (ArithmeticException e) {
IOException throwable = new IOException("除0异常");
throwable.initCause(e);
throw throwable;
}
}
}
如上将ArithmeticException转换为IOException ,并通过Throwable抛出,当捕获到异常时,可通过下面的语句得到原始异常
ArithmeticException e = throwable.getCause();
finally
当代码抛出异常,程序终止,若当前方法获得了一些资源,则需在finally子句中进行释放。不管是否有异常被捕获,finally子句都会执行
private String load() {
FileInputStream inputStream = null;
BufferedReader bufferedReader = null;
StringBuilder stringBuilder = new StringBuilder();
try {
inputStream = openFileInput("data");
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line = "";
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return stringBuilder.toString();
}
如上是利用bufferedReader读取文件的代码,在finally子句中需要对bufferedReader流关闭,但关闭流的同时,也会出现异常,这个异常可能会覆盖掉原有的异常,所以需要对关闭操作可能产生的异常进行捕获
不要在finally子句中调用return
调用下面方法输入2,将会返回0,在try中的return返回前会调用finally子句,此时finally子句的return会覆盖掉原来的返回值。
int finallyReturn(int n) {
try {
int r = n * n;
return r;
} finally {
if (n == 2) {
return 0;
}
}
}
try-with-resources
为了解决finally中可能产生异常导致的异常覆盖及代码繁琐,对于所有实现类AutoCloseable接口的类,可使用try-resources语句简写,将上面的load方法改为如下,就不必用finally中关闭流了
private String load() {
StringBuilder stringBuilder = new StringBuilder();
try (
FileInputStream inputStream = openFileInput("data");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))
) {
String line = "";
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
}
return stringBuilder.toString();
}
Tips:
- try-with-resources内部会捕获close方法抛出的异常,由addSuppressed方法增加到原来的异常
- 可通过getSuppressed方法获取close方法抛出的异常列表
应该选择抛出还是捕获异常
通常,应该捕获那些知道如何处理的异常,而将那些不知道怎样处理的异常继续传递。如果要传递异常,需在方法首部添加throws标识此方法可能抛出异常。
但在前面曾说到若父类方法没有声明checked异常,子类覆盖后的方法也不能声明,故此时子类覆盖的方法必须捕获异常,不允许在子类throws抛出超过父类方法所列出的异常类的范围。
异常使用技巧
早抛出,当栈空时就应该抛出EmptyStackException,而不是返回null,因为接下来的调用仍会抛NullPointException
Object o = stack.pop();
o.toString();
晚捕获,传递异常可以让更高层调用对象知道可能会发生的错误
private String load() throws IOException {
StringBuilder stringBuilder = new StringBuilder();
try (
FileInputStream inputStream = openFileInput("data");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
) {
String line = "";
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
}
}
return stringBuilder.toString();
}