目录
初始异常
在我们从学习编程开始 , 学习过程中难免会遇到一些 “异常” , 比如常见的
数组越界异常
int[] arr = {1,2,3};
for(int i = 0; i < 5; i++){
System.out.print(arr[i]+" ");
}
//运行结果:
//1 2 3 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
at fu.main(fu.java:44)
null指针异常
public static void main(String[] args) {
String str = null;
System.out.println(str.length());
}
// 运行结果
Exception in thread "main" java.lang.NullPointerException
at Test.main(Test.java:16)
算数异常
public static void main(String[] args) {
int a = 10;
System.out.println(a / 0);
}
// 运行结果
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Test.main(Test.java:16)
所谓异常指的就是程序在运行时**出现错误时通知调用者的一种机制 , 但在 Java 中有些异常是能够进行避免的 , 有些则不能。
异常体系
在 JAVA 中异常的种类有很多, 不同种类的异常具有不同的含义, 也有不同的处理方式 , 每个异常都是 Java 中的一个类 , 每个异常类都直接或间接继承了 Throwable 类 , Throwable 类下有两个直接子类 Exception 类和 Error 类。 继承关系如下图
- 其中 Error 指的是 Java 运行时内部错误和资源耗尽错误. 应用程序不抛出此类异常. 这种内部错误一旦出现,除了告知用户并使程序终止之外, 再无能无力. 这种情况很少出现.
- Exception 是我们程序猿所使用的异常类的父类.
- 其中 Exception 有一个子类称为 RuntimeException , 这里面又派生出很多常见的异常类
异常一般分为两大类 , 受查异常 和 非受查异常 , Java语言规范将派生于 Error 类或 RuntimeException 类的所有异常称为 非受查异常 , 所有的其他异常称为 *受查异常.
受查异常
编译期间会被检测 , 受查异常 是必须进行异常处理的 , 如果不进行异常处理 , 代码会编译不通过 , 例如
public static void main(String[] args) {
// 尝试打开文件, 并读其中的一行.
File file = new File("d:/test.txt");
// 使用文件对象构造 Scanner 对象.
Scanner sc = new Scanner(file);
System.out.println(sc.nextLine());
}
// 编译期间抛出异常
Error:(21, 22) java: 未报告的异常错误java.io.FileNotFoundException; 必须对其进行捕获或声明以便抛出
非受查异常
编译期间不会被检测 , 代码在运行期间抛出的异常叫做 非受查异常 , 程序员可以选择是否交给代码去处理 非受查异常 , 例如
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5};
for (int i = 0; i < 6; i++) {
System.out.println(array[i]);
}
System.out.println("hello world");
}
// 运行时 , 抛出了一个数组越界异常程序终止 , hello world 未打印
1 2 3 4 5 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 5
at Test.main(Test.java:20)
对于 受查异常 我们是必须要做出异常处理的 , 对于 非受查异常 , 我们也可以选择去做出异常处理。
异常基本用法
1.捕获异常
try-catch-finally 语句
在对于一些可能出现异常的地方 , 我们可以使用 try catch 关键字去捕获可能出现的异常 , 然后对发生异常后做进一步的处理 , 捕获异常的好处就是即使代码运行中出现了异常 , 也不会终止当前代码 , 而是会继续往下走。
语法
try {
有可能出现异常的语句
} catch (异常类型 异常对象) {
异常捕获后的下一步处理
} finally {
异常出口 //finally 不管再任何时候都执行
}
- try 代码块中放的是可能出现异常的代码.
- catch 代码块中放的是出现异常后的处理行为.
- finally 代码块中的代码用于处理善后工作, 在最后无论如何都会执行
- 其中 catch 和 finally 都可以根据情况选择加或者不加.
- catch 关键字可以有多个 , 类似 else if 一样
对于上面使用到的代码 , 如果对在循环中的输出语句不进行异常处理的话 , 则 array[i] 就会导致数组越界抛出异常 , 使得后面的代码无法继续执行下去 , 我们就可以使用 try catch 来进行异常捕获
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5};
for (int i = 0; i < 10; i++) {
System.out.println(array[i]);
}
System.out.println("hello world");
}
// 运行时 , 抛出了一个数组越界异常程序终止 , hello world 未打印
1 2 3 4 5 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 5
at Test.main(Test.java:20)
代码示例1
使用 try catch 对可能出现异常的输出语句进行捕获 , 可以使用异常对象调用 printStackTrace( ) 方法打印错误信息 , 可以看到 , 在出现异常后 , 并不会影响 hello world 正常打印
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5};
for (int i = 0; i < 10; i++) {
// 捕获异常
try {
System.out.print(array[i] + " ");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("捕获到数组越界异常");
// 打印异常的详细信息
e.printStackTrace();
}
}
System.out.println("hello world");
}
// 运行结果
1 2 3 4 5 捕获到数组越界异常
hello world
java.lang.ArrayIndexOutOfBoundsException: 5
at Test.main(Test.java:21)
代码示例2
在 catch 异常捕获类型中 , 需要是对应的异常类型才能进行正确捕获 , 否则 , 一旦异常类型不对应 , 程序依然会由于发生异常而终止 , 例如我们将 catch 中的异常捕获类型改成空指针异常 , 可以看到由于程序抛出了数组越界异常 , 后续的 hello world 并没有成功打印 , 由此可知 , 由于异常类型不匹配 , catch 中空指针异常并没有被捕获
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5};
for (int i = 0; i < 6; i++) {
try {
System.out.print(array[i] + " ");
} catch (NullPointerException e) {
System.out.println("捕获到null指针异常");
// 打印异常的详细信息
e.printStackTrace();
}
}
System.out.println("hello world");
}
// 运行结果
1 2 3 4 5 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 5
at Test.main(Test.java:21)
代码示例3
不过如果对于可能出现多个异常的代码 , 也可以选择多增加几个 catch 来进行捕获多个异常 , 例如在上列代码中增加一个 catch 捕获数组越界异常 , 当代码执行发生了哪个异常 , 则会跳到对应捕获到异常的 catch 代码块中执行 , 且发生异常后不影响程序往下执行
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5};
for (int i = 0; i < 6; i++) {
try {
System.out.print(array[i] + " ");
} catch (NullPointerException e) {
System.out.println("捕获到null指针异常");
// 打印异常的详细信息
e.printStackTrace();
} catch (ArrayIndexOutOfBoundsException e){
System.out.println("捕获到数组越界异常");
// 打印异常的详细信息
e.printStackTrace();
}
}
System.out.println("hello world");
}
// 运行结果
1 2 3 4 5 捕获到数组越界异常
hello world
java.lang.ArrayIndexOutOfBoundsException: 5
at Test.main(Test.java:21)
向上传递
对于 try 代码块中可能会出现异常的语句 , 如果发生了异常 , 则会结束 try 代码块中的代码 , 而是直接转到 catch 中查看是否有匹配的异常捕获 , 如果没有对应的异常捕获就会将异常向上传递到上层调用者
如果在方法中没有合适的处理异常方式或者没处理异常 , 那么该异常就会就会沿着调用栈向上传递 , 例如在 func 方法中在抛出异常后 , 没对异常进行处理 , 该异常就会向上传递到 main 方法中 , 被 main 方法给捕获了 , 然后在 main 方法中进行了对异常的处理
private static void func() {
int[] array = {1,2,3,4,5};
System.out.println(array[5]); // 发生数组越界的异常
}
public static void main(String[] args) {
try {
func();
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("捕获到 ArrayIndexOutOfBoundsException 异常");
// 打印异常详细信息
e.printStackTrace();
}
System.out.println("hello world");
}
// 运行结果
捕获到 ArrayIndexOutOfBoundsException 异常
hello world
java.lang.ArrayIndexOutOfBoundsException: 5
at Test.func(Test.java:20)
at Test.main(Test.java:25)
如果一直向上传递到 main 也不对异常进行处理的话或者没有捕捉到该异常的话 , 最终就会交给 JVM 处理 , 而一旦交给 JVM 处理 , 那么 JVM 就会使用最直接的方法 , 将程序终止 , 由于程序直接终止了 , 代码就无法执行到输出 hello world
private static void func() {
int[] array = {1, 2, 3, 4, 5};
System.out.println(array[5]); // 发生数组越界的异常
}
public static void main(String[] args) {
func();
System.out.println("hello world");
}
// 运行结果
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 5
at Test.func(Test.java:20)
at Test.main(Test.java:24)
finally 关键字
在 异常处理 流程中 , finally 也是很重要的 , finally 跟 try catch 一样也是包含了一个代码块 , 功能是无论是否发生异常都会执行 finally 代码块中的语句
public static void main(String[] args) {
try {
System.out.println("我是 try 代码块");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("我是 catch 代码块");
// 打印异常的详细信息
e.printStackTrace();
} finally {
System.out.println("我是 finally 代码块");
}
System.out.println("hello world");
}
// 运行结果
我是 try 代码块
我是 finally 代码块
hello world
由于 finally 代码块无论如何都会执行 , 所以在程序中 finally 代码块一般表示最后的善后工作 , 例如释放资源 , 无论代码是否存在异常, 在 finally 中的代码一定都会执行. 保证最终一定会执行到 Scanner 的 close 方法.
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
try {
System.out.println("我是 try 代码块");
System.out.println(10 / 0);
} catch (ArithmeticException e) {
System.out.println("我是 catch 代码块");
System.out.println("捕获 ArithmeticException 异常");
// 打印异常的详细信息
e.printStackTrace();
} finally {
System.out.println("我是 finally 代码块");
// 释放 Scanner 资源
sc.close();
}
System.out.println("hello world");
}
// 运行结果
我是 try 代码块
我是 catch 代码块
捕获 ArithmeticException 异常
我是 finally 代码块
hello world
java.lang.ArithmeticException: / by zero
at Test.main(Test.java:21)
关于finally代码块的注意事项
由于 finally代码块中无论代码是否存在异常, finally 中的代码一定都会执行到 , 所以如果在写方法需要用返回值时 , 尽量不要在 finally 代码块中写返回值 , 可能会影响最终结果 , 例如以下代码 , 按照常规的执行代码阅读 , 在碰到第一个 return 时代码就应该结束了 , 可是因为后面跟了一个 finally 代码块 , 代码最终执行了第二个 return , 所以最后的结果是 -10
private static int func(int a,int b) {
try {
return a + b;
} finally {
return a - b;
}
}
public static void main(String[] args) {
System.out.println(func(10,20));
}
// 运行结果
-10
异常处理流程
- 程序先执行 try 中的代码
- 如果 try 中的代码出现异常, 就会结束 try 中的代码 , 看和 catch 中的异常类型是否匹配
- 如果找到匹配的异常类型, 就会执行 catch 中的代码
- 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者
- 无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行)
- 如果上层调用者也没有处理的了异常, 就继续向上传递.一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止
try-with-resources语句
try-with-resources 语句可以自动去处理关闭资源的场景 , 它能够很容易地关闭在 try-catch 语句块中使用的资源 , 所谓的资源 resource 是指在程序完成后,必须关闭的对象 , 使用方法跟 try-catch-finally 类似 , 用来代替 try-catch-finally 语句中的 finally 是程序不管是否出现异常都能正确执行代码
语法
try (使用的资源) {
有可能出现异常的语句
} catch (异常类型 异常对象) {
异常捕获后的下一步处理
}
代码示例
来看一下 try-finally 关闭资源的写法 , 是使用 finally 代码块进行资源关闭的 , 需要手动写代码进行关闭
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
try {
// .....
} finally {
sc.close();
}
}
再看一下 try-with-resources 自动关闭资源的方法 , 直接将 Scanner 对象定义在 try ( ) 中 , 这样不管代码是如何结束的 , 都能保证 Scanner 对象能被关闭 , 况且这种写法会更简洁
public static void main(String[] args) {
try (Scanner sc = new Scanner(System.in)) {
// .....
}
}
为什么要有 try-with-resources 语句 ?
-
这是因为 , 在 java 中有些资源使用完了之后就必须关闭 , 而有的时候程序猿可能会忘记写关闭资源的代码 ,导致资源没有被关闭就造成了资源泄露导致可能下一次就打不开该资源了 , 所以在处理必须关闭的资源时 , 优先使用try-with-resources 语句替代try-finally语句。
什么类型可以使用 try-with-resources 语句 ?
-
JAVA 标准库中有个 Closeable 接口 , 在 Closeable** 中有个 close 方法 , 因为 try-with-resources 语句就是在程序执行完成后自动调用资源的 close 方法 ,所以只要有类实现了 Closeable 接口并重写了 close 方法就能使用 try-with-resources 语句
2.抛出异常
除了 JAVA标准库 中内置的一些类会抛出异常之外 , 我们也可以手动去抛出一个异常 , 在针对有些实际场景下 , 我们可以使用 throw 关键字手动去抛出异常
语法
throw new 异常类型
代码示例
如果我们要写一个计算两个变量相除的结果的方法 , 我们知道 0 是不能作为除数的 , 所以在方法内部我们可以判断除数是否是 0 , 当传入的参数 b 为 0 时则手动抛出一个异常做为提示 , 在抛出异常时可以构造一些描述性信息
public static int func(int a,int b) {
if (b == 0){
throw new ArithmeticException("除数不能为 0");
}
return a / b;
}
public static void main(String[] args) {
System.out.println(func(10,0));
}
// 运行结果
Exception in thread "main" java.lang.ArithmeticException: 除数不能为 0
at Test.func(Test.java:20)
at Test.main(Test.java:26)
3.声明异常
对于某些方法可能会出现异常 , 我们需要提醒调用者知道该方法可能会产生异常 , 就可以在方法声明中使用 throws 关键字来声明该方法可能出现的异常
语法
throws 异常类型
代码示例1
如果声明的是一个非受查异常 , 也就是运行时异常 , 在调用者调用该方法时 , 编译器并不会去报错
调用者可以选择是否去处理这个异常
代码示例2
但如果声明的是一个受查异常 , 编译器就会提醒调用者采取异常处理
此时解决方法有两个:
一、使用 try - catch - finally 语句 , 将该方法进行异常处理
public static void main(String[] args) {
int a = 0;
try {
a = func(10, 0);
System.out.println(a);
} catch (Exception e) {
e.printStackTrace();
}
}
二、在当前方法中不进行异常处理 , 继续使用 throws 关键字声明该异常向上传递
public static void main(String[] args) throws Exception {
int a = func(10, 0);
System.out.println(a);}
throw 和 throws 的区别
- 写法上 , throw 关键字用于方法内部 , throws 用于方法声明上
- 用法上 , throw 是直接抛出具体的一个异常 , throws 只是声明了一个异常并没有做任何处理 , 向上传递给调用者表示方法内可能会抛出异常
自定义异常
Java 标准库中虽然已经内置了丰富的异常类, 但是我们实际场景中可能还有一些情况需要我们对异常类进行扩展, 创建符合我们实际情况的异常 , 格式如下
格式
class 自定义异常 extends Exception / RuntimeException{
public 自定义异常(){
super();
}
public 自定义异常(String message){
super(message);
}
}
关于自定义异常 , 一般都是自定义一个类 , 然后去继承 Exception 或者 RuntimeException 类 , 例如我们自定义一个异常
class MyException extends Exception {
public MyException(){
super();
}
public MyException(String message) {
super(message);
}
}
在代码中运用自定义的异常和普通的异常用法没什么区别
一、使用 throw 关键字抛出自定义异常
public static void main(String[] args) {
throw new MyException("抛出自定义异常");
}
// 运行结果
Exception in thread "main" MyException: 抛出自定义异常
at Test.main(Test.java:35)
二、使用 throws 关键字声明异常
public static void main(String[] args) throws MyException {
throw new MyException("抛出自定义异常");
}
自定义异常注意事项
- 自定义异常通常会继承自 Exception 或者 RuntimeException
- 继承自 Exception 的异常默认是受查异常
- 继承自 RuntimeException 的异常默认是非受查异常.