1. Java的异常
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表示严重的错误,程序对此一般无能为力,例如:
1. OutOfMemoryError:内存溢出;
2. NoClassDefFoundError:无法加载某个Class;
3. StackOverflowError:栈溢出。
而Exception则是运行时的错误,它可以被捕获并处理。某些异常是应用程序逻辑处理的一部分,应该捕获并处理。例如:
1. NumberFormatException:数值类型的格式错误;
2. FileNotFoundException:未找到文件;
3. SocketException:读取网络失败。
还有一些异常是程序逻辑编写不对造成的,应该修复程序本身。例如:
1. NullPointerException:对某个null的对象调用方法或字段;
2. IndexOutOfBoundsException:数组索引越界
Exception又分为两大类:
- RuntimeException 以及它的子类;
- 非RuntimeException(包括IOException、ReflectiveOperationException等等)
Java规定:
必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception
不需要捕获的异常,包括Error及其子类,RuntimeException及其子类
Java 的非检查性异常
异常 | 描述 |
ArithmeticException | 当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零"时,抛出此类的一个实例。 |
ArrayIndexOutOfBoundsException | 用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。 |
ArrayStoreException | 试图将错误类型的对象存储到一个对象数组时抛出的异常。 |
ClassCastException | 当试图将对象强制转换为不是实例的子类时,抛出该异常。 |
IllegalArgumentException | 抛出的异常表明向方法传递了一个不合法或不正确的参数。 |
IllegalMonitorStateException | 抛出的异常表明某一线程已经试图等待对象的监视器,或者试图通知其他正在等待对象的监视器而本身没有指定监视器的线程。 |
IllegalStateException | 在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下。 |
IllegalThreadStateException | 线程没有处于请求操作所要求的适当状态时抛出的异常。 |
IndexOutOfBoundsException | 指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。 |
NegativeArraySizeException | 如果应用程序试图创建大小为负的数组,则抛出该异常。 |
NullPointerException | 当应用程序试图在需要对象的地方使用 null 时,抛出该异常 |
NumberFormatException | 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。 |
SecurityException | 由安全管理器抛出的异常,指示存在安全侵犯。 |
StringIndexOutOfBoundsException | 此异常由 String 方法抛出,指示索引或者为负,或者超出字符串的大小。 |
UnsupportedOperationException | 当不支持请求的操作时,抛出该异常。 |
Java 定义在 java.lang 包中的检查性异常类
异常 | 描述 |
ClassNotFoundException | 应用程序试图加载类时,找不到相应的类,抛出该异常 |
CloneNotSupportedException | 当调用 Object 类中的 clone 方法克隆对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常 |
IllegalAccessException | 拒绝访问一个类的时候,抛出该异常 |
InstantiationException | 当试图使用 Class 类中的 newInstance 方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常 |
InterruptedException | 一个线程被另一个线程中断,抛出该异常 |
NoSuchFieldException | 请求的变量不存在 |
NoSuchMethodException | 请求的方法不存在 |
在Java中定义了两种类型的异常和错误:
- JVM(Java虚拟机) 异常:由 JVM 抛出的异常或错误。例如:NullPointerException 类,ArrayIndexOutOfBoundsException 类,ClassCastException 类
- 程序级异常:由程序或者API程序抛出的异常。例如 IllegalArgumentException 类,IllegalStateException 类
捕获异常
捕获异常使用 try...catch 语句,把可能发生异常的代码放到 try {...} 中,然后使用catch捕获对应的Exception及其子类:
public class JavaExceptionDemo {
public static void main(String[] args) throws UnsupportedEncodingException {
byte[] bs = toGBK("中文看看矿库");
System.out.println(Arrays.toString(bs));
}
private static byte[] toGBK(String s) throws UnsupportedEncodingException {
// try {
// // 用指定编码转换String为byte[]:
// return s.getBytes("GBK");
// } catch (UnsupportedEncodingException e) {
// // 如果系统不支持GBK编码,会捕获到UnsupportedEncodingException:
// e.printStackTrace(); // 打印异常信息
// return s.getBytes(); // 尝试使用用默认编码
// }
return s.getBytes("GBK");
}
}
如果我们不捕获 UnsupportedEncodingException ,会出现编译失败的问题:
像 UnsupportedEncodingException 这样的 Checked Exception ,必须被捕获
在 toGBK( ) 方法中,因为调用了 String.getBytes(String) 方法,就必须捕获UnsupportedEncodingException
我们也可以不捕获它,而是在方法定义处用throws表示toGBK( )方法可能会抛出 UnsupportedEncodingException ,就可以让toGBK()方法通过编译器检查:
代价就是一旦发生异常,程序会立刻退出
什么也不做是非常不好的,即使真的什么也做不了,也要先把异常记录下来
Error是无需捕获的严重错误,Exception是应该捕获的可处理的错误;
2. 捕获异常
在Java中,凡是可能抛出异常的语句,都可以用 try ... catch 捕获。把可能发生异常的语句放在try { ... }中,然后使用 catch 捕获对应的Exception及其子类。
多catch语句
可以使用多个catch语句,每个catch分别捕获对应的 Exception 及其子类。JVM在捕获到异常后,会从上到下匹配 catch 语句,匹配到某个catch后,执行catch代码块,然后不再继续匹配。简单地说就是:多个catch语句只有一个能被执行。例如:
存在多个catch的时候,catch的顺序非常重要:子类必须写在前面。例如:
public static void main(String[] args) {
try {
process1();
} catch (IOException e) {
System.out.println(e);
} catch (UnsupportedEncodingException e) {
// 永远捕获不到,因为它是IOException的子类,当抛出UnsupportedEncodingException异常时,会被catch (IOException e) { ... }捕获并执行。
System.out.println("Bad encoding");
} catch (NumberFormatException e) {
System.out.println(e);
}
}
// 正确写法
public static void main(String[] args) {
try {
process1();
} catch (UnsupportedEncodingException e) {
System.out.println("Bad encoding");
} catch (IOException e) {
System.out.println("IO error");
}
}
finally:Java的 try ... catch 机制还提供了finally语句,finally语句块保证有无错误都会执行
public static void main(String[] args) {
try {
process1();
} catch (UnsupportedEncodingException e) {
System.out.println("Bad encoding");
} catch (IOException e) {
System.out.println("IO error");
} finally {
System.out.println("END");
}
}
注意finally的特点:
- finally 语句不是必须的,可写可不写;
- finally 总是最后执行。
如果没有发生异常,就正常执行 try { ... } 语句块,然后执行finally。如果发生了异常,就中断执行 try { ... } 语句块,然后跳转执行匹配的catch语句块,最后执行 finally 。
某些情况下,可以没有 catch,只使用 try ... finally 结构。因为方法声明了可能抛出的异常,所以可以不写catch。例如:
void process(String file) throws IOException {
try {
...
} finally {
System.out.println("END");
}
}
捕获多条异常(可以使用 | 将其合并在一起,一起捕获异常)
如果某些异常的处理逻辑相同,但是异常本身不存在继承关系,那么就得编写多条catch子句;但是因为处理IOException和NumberFormatException的代码是相同的,所以我们可以把它两用 | 合并到一起:
public static void main(String[] args) {
try {
process1();
} catch (IOException | NumberFormatException e) { //IOException或NumberFormatException
System.out.println("Bad input");
} catch (Exception e) {
System.out.println("Unknown error");
}
}
使用 try ... catch ... finally 时:
- 多个catch语句的匹配顺序非常重要,子类必须放在前面;
- finally 语句保证了有无异常都会执行,它是可选的;(也可以只写 try...finally)
- 一个 catch 语句也可以匹配多个非继承关系的异常
3. 抛出异常
当发生错误时,例如,用户输入了非法的字符,我们就可以抛出异常。如何抛出异常?参考 Integer.parseInt( ) 方法,抛出异常分两步:1. 创建某个Exception的实例;2. 用throw语句抛出
void process2(String s) {
// NullPointerException e = new NullPointerException(); throw e;
if (s==null) { throw new NullPointerException(); } // 合并为一行
}
如果方法捕获了某个异常后,又在catch子句中抛出新的异常,就相当于把抛出的异常类型“转换”了:
void process1(String s) {
try {
process2();
} catch (NullPointerException e) {
throw new IllegalArgumentException();
}
}
void process2(String s) {
if (s==null) {
throw new NullPointerException();
}
}
// 当process2()抛出NullPointerException后,被process1()捕获,然后抛出IllegalArgumentException()。如果在main()中捕获IllegalArgumentException,我们看看打印的异常栈:
java.lang.IllegalArgumentException
at Main.process1(Main.java:15)
at Main.main(Main.java:5)
// 这说明新的异常丢失了原始异常信息,我们已经看不到原始异常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();
}
}
// 打印出的异常栈
java.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)
// 注意到Caused by: Xxx,说明捕获的IllegalArgumentException并不是造成问题的根源,根源在于NullPointerException,是在Main.process2()方法抛出的。
- 在 catch 中抛出异常,不会影响 finally 的执行。JVM 会先执行 finally ,然后抛出异常。
- 调用 printStackTrace( ) 可以打印异常的传播栈,对于调试非常有用;
- 捕获异常并再次抛出新的异常时,应该持有原始异常信息;
- 通常不要在 finally 中抛出异常。如果在finally中抛出异常,应该原始异常加入到原有异常中。调用方可通过Throwable.getSuppressed( ) 获取所有添加的 Suppressed Exception 。
4. 自定义异常
抛出异常时,尽量复用 JDK 已定义的异常类型;例如,参数检查不合法,应该抛出IllegalArgumentException:
自定义异常体系时,推荐从 RuntimeException 派生“根异常”,再派生出业务异常;
自定义异常时,应该提供多种构造方法。
5. NullPointerException
NullPointerException 即空指针异常,俗称NPE。Null Pointer更确切地说是Null Reference,不过两者区别不大。如果一个对象为 null,调用其方法或访问其字段就会产生NullPointerException,这个异常通常是由JVM 抛出的,例如:
String s = null;
System.out.println(s.toLowerCase());
使用空字符串 "" 而不是默认的 null 可避免很多 NullPointerException ,编写业务逻辑时,用空字符串""表示未填写比 null 安全得多。返回空字符串""、空数组,而不是null:这样使得调用方无需检查结果是否为 null
如果产生了 NullPointerException ,JVM 可以给出详细的信息告诉我们null对象到底是谁
public class Main {
public static void main(String[] args) {
Person p = new Person();
System.out.println(p.address.city.toLowerCase());
}
}
class Person {
String[] name = new String[2];
Address address = new Address();
}
class Address {
String city;
String street;
String zipcode;
}
可以在NullPointerException的详细信息中看到类似... because "<local1>.address.city" is null,意思是city字段为null,这样我们就能快速定位问题所在。
这种增强的NullPointerException详细信息是Java 14新增的功能,但默认是关闭的,我们可以给JVM添加一个-XX:+ShowCodeDetailsInExceptionMessages参数启用它:
// java -XX:+ShowCodeDetailsInExceptionMessages Main.java
6. 使用断言(Assertion)
断言(Assertion)是一种调试程序的方式。在Java中,使用assert关键字来实现断言。
public static void main(String[] args) {
double x = Math.abs(-123.45);
assert x >= 0;
System.out.println(x);
}
// 语句assert x >= 0;即为断言,断言条件x >= 0预期为true。
// 如果计算结果为false,则断言失败,抛出AssertionError
// 使用assert语句时,还可以添加一个可选的断言消息:
// 这样,断言失败的时候,AssertionError会带上消息x must >= 0,更加便于调试。
assert x >= 0 : "x must >= 0";
Java 断言的特点是:断言失败时会抛出AssertionError,导致程序结束退出。因此断言不能用于可恢复的程序错误,只应该用于开发和测试阶段。对于可恢复的程序错误,不应该使用断言。例如:
int x = -1;
assert x > 0;
System.out.println(x);
执行上述代码,发现程序并未抛出 AssertionError ,而是正常打印了x的值。这是因为JVM默认关闭断言指令,即遇到assert语句就自动忽略了,不执行。
要执行assert语句,必须给Java虚拟机传递 -enableassertions(可简写为-ea)参数启用断言
所以,上述程序必须在命令行下运行才有效果: java -ea Main.java
还可以有选择地对特定地类启用断言,命令行参数是:-ea:com.itranswarp.sample.Main,表示只对com.itranswarp.sample.Main这个类启用断言。
或者对特定地包启用断言,命令行参数是:-ea:com.itranswarp.sample...(注意结尾有3个.),表示对com.itranswarp.sample这个包启动断言。
断言是一种调试方式,断言失败会抛出AssertionError,只能在开发和测试阶段启用断言;
对可恢复的错误不能使用断言,而应该抛出异常;
断言很少被使用,更好的方法是编写单元测试 JUnit