前言❤️❤️
hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹
在日常写代码时,异常是我们几乎每天都会遇到的东西,例如数组下标越界,空指针异常…
Java中的异常规则还是比较重要的,为什么有的异常编译器强制要求处理,有的异常代码却可以直接编译运行?try‑catch 到底什么时候该写、什么时候不该写?finally 一定执行吗?多个 catch 的匹配规则是什么?
这篇博客就是详细的解析这些知识点🐵
🎇个人主页:洋不写bug的博客
🎇所属专栏:Java学习之旅,从入门到进阶
🎇铁汁们对于Java的各种常用核心语法(不太常用的也有😆),都可以在上面的Java专栏学习,专栏正在持续更新中🐵🐵,有问题可以写在评论区或者私信我哦~
1,Java异常简介
异常,跟程序的错误处理是非常相关的,在写代码的时候,要未言胜,先言败
就拿王者荣耀来说,代码可能分为许多操作:登录,开始匹配,接受比赛,选择英雄,加载比赛地图…
那用户玩的时候,这些操作就一定都能顺利执行吗,会不会因为网卡了,或者服务器过载,造成中间的一个操作执行失败呢,当然是可能的。
因此,每一步操作都要充分考虑失败的情况,并且失败后要执行一定的操作帮助恢复正常
如果按照C语言的逻辑,那就是每次都判断下,如果失败的话,就执行相应的处理
这样写,正常流程的逻辑和错误流程的逻辑是混在一起的,代码就比较凌乱
Java中就通过异常处理,把正常的逻辑和错误处理的逻辑,分隔开了
Java中就通过try-catch语句,来达到这样的效果
将这些操作写在try语句中,当一个环节在执行过程中出错时,就会抛出异常,触发异常后,try中的代码就不会向下继续执行,就会跳转到下方的catch语句中执行相应的错误处理逻辑
一些异常可能我们平时写代码的时候经常遇到,例如算术异常
如下,报出的异常包括异常名和异常原因,下面时出现异常的位置
public class Test {
public static void main(String[] args) {
int a = 10;
int b = 0;
System.out.println(a/b);
}
}

还有数组越界异常,会报出原因:下标100对于长度3来说越界了
public class Test {
public static void main(String[] args) {
int []arr = {1,2,3};
System.out.println(arr[100]);
}
}

再比如空指针异常,前后加个打印语句看一下,会发现出现异常后,程序就终止,不再运行了
public class Test {
public static void main(String[] args) {
String s = null;
System.out.println("出现异常前");
System.out.println(s.length());
System.out.println("出现异常后");
}
}

Java中异常的继承关系如下所示:

- 左边的Error,是JVM内部使用的,我们写代码时不会主动抛出这样的异常,一般出现这样的错误后,代码无法进行进行程序必须终止,下面的StackOverflowError就是当递归结束条件那里出错了,无限递归,就会出现报出这个错误
- 右面的Exception,这些异常是和代码直接相关的,学习异常主要学的就是这部分,我们try-catch捕获异常,也是捕获这里面的异常
2,异常捕获
下面这段代码,执行后并不会报错,结果是Infinity(无穷大),这个是double除法中的一个特殊情况
public class Test { private static double divide(double a,double b){ return a / b; } public static void main(String[] args) { System.out.println(divide(10,0)); } }
我们就可以自己针对分母为0的情况抛出异常,限制这种做法
创建一个异常类(必须是实现了Throwable接口的类的实例,不能随便创建一个对象)的对象,通过throw关键字触发抛出异常的行为
这个对象可以传入异常原因,异常原因抛出异常后就会在控制台上显示
如下所示public class Test { private static double divide(double a,double b){ if(b == 0){ throw new ArithmeticException("不能用0作为除数"); } return a / b; } public static void main(String[] args) { System.out.println("抛出异常前"); System.out.println(divide(10,0)); System.out.println("抛出异常后"); } }
如果抛出异常不管的话,那程序就会终止,如果我们用catch捕获一下异常,在catch方法里面执行错误处理逻辑
catch异常后,就不会报出异常,程序也不会终止(这里只是演示下,很多异常是不建议我们去catch的,后面的异常分类会解析到)
如下所示:package package11; public class Test { private static double divide(double a,double b){ if(b == 0){ throw new ArithmeticException("不能用0作为除数"); } return a / b; } public static void main(String[] args) { System.out.println("抛出异常前"); try{ System.out.println(divide(10,0)); }catch (ArithmeticException e){ System.out.println("已捕获到异常"); System.out.println("执行异常处理逻辑"); } System.out.println("抛出异常后"); } }
如果是连续的方法调用的话,那异常在下面的层中捕获到后,上面的层也就不再捕获了
public class Test { private static double divide(double a,double b){ if(b == 0){ throw new ArithmeticException("不能用0作为除数"); } return a / b; } private static void func1(){ try { System.out.println(divide(10,0)); }catch (ArithmeticException e){ System.out.println("在func1中捕获异常"); } } private static void func2(){ try { func1(); }catch (ArithmeticException e){ System.out.println("在func2中捕获异常"); } } public static void main(String[] args) { System.out.println("抛出异常前"); try{ func2(); }catch (ArithmeticException e){ System.out.println("在main方法中捕获异常"); } System.out.println("抛出异常后"); } }
如果在func1中没有捕获异常,异常就会到func2中,如果func2中也没有捕获异常,异常就会到main方法中,如果在main方法中,还是没有捕获异常,那程序就会终止
异常抛出后,就会沿着调用栈,向上逐层处理,一般这个catch只写一层就够了
当catch捕获到异常后,处理错误逻辑如下:
- 相同方案再重试一次
- 尝试使用备用方案
- 通知程序员人工干预(打日志,发短信,打电话)
在当前学习阶段,我们在catch中最常写的就是打印异常信息
public class Test { private static double divide(double a,double b){ if(b == 0){ throw new ArithmeticException("不能用0作为除数"); } return a / b; } public static void main(String[] args) { System.out.println("抛出异常前"); try{ System.out.println(divide(10,0)); }catch (ArithmeticException e){ e.printStackTrace(); } System.out.println("抛出异常后"); } }
如果没有捕获异常,强制终结程序时,也会打印出异常的信息,只是那是JVM打印的,这个是程序员自己打印的,打印这个就是能更好的知道异常的具体情况
3,异常匹配
一个try后面是可以写多个catch的(因为同一个try是可能会抛出不同的异常的),每个catch里面可以声明不同的异常类型
当try抛出异常后,就会从上到下因此匹配每个catch的参数,哪个catch参数类型和抛出的异常类型匹配,就执行对应的catch逻辑
一个异常一旦匹配到某个catch,就不会继续往下匹配其他的catch了public class Test { public static void main(String[] args) { try { int a = 10 / 0; }catch (ArithmeticException e){ System.out.println("发生算术异常"); }catch (NullPointerException e){ System.out.println("发生空指针异常"); }catch (IndexOutOfBoundsException e){ System.out.println("发生数组越界异常"); } } }
进行异常类型匹配的时候,不一定要是完全匹配,如果抛出的异常是声明异常类型的子类,也是能匹配到的
下标越界异常的父类是RuntimeException(运行时异常),包括空指针异常,算术异常,它们的父类都是运行时异常

写成运行时异常,也是可以匹配到的,如下所示:
public class Test { public static void main(String[] args) { try { int []arr = {1,2,3}; System.out.println(arr[3]); }catch (ArithmeticException e){ System.out.println("发生算术异常"); }catch (NullPointerException e){ System.out.println("发生空指针异常"); }catch (RuntimeException e){ System.out.println("运行时异常"); } } }
当我们尝试空指针异常写到运行时异常下面时,就会报错,报错原因是空指针异常已经被捕获过了,如下图:
因为异常是跟catch是从上往下匹配的,空指针异常一定会被上面的运行时异常捕获,因此下面写的这个是没有意义的

因此,如果在catch中同时写子类异常和父类异常的话,就要求把父类异常放在下面注:在try语句中,有能力抛出不同的异常,但是一次执行只能抛出一种异常
下面这种就是根据我们的输入,try语句来抛出不同的异常
import java.util.Scanner; public class Test { public static void main(String[] args) { try { Scanner scanner = new Scanner(System.in); int num = scanner.nextInt(); if (num == 1) { int a = 10 / 0; } else if (num == 2) { String s = null; System.out.println(s.length()); } else { int[] arr = {1, 2, 3}; System.out.println(arr[3]); } } catch (ArithmeticException e) { System.out.println("发生算术异常"); } catch (NullPointerException e) { System.out.println("发生空指针异常"); } catch (IndexOutOfBoundsException e) { System.out.println("发生数组越界异常"); } catch (RuntimeException e) { System.out.println("运行时异常"); } } }
但是像下面这样写是没办法同时抛出三个异常的,也即是一旦 try 里面抛出异常,try 块剩下的代码直接作废,不会继续跑
public class Test { public static void main(String[] args) { try { int a = 10 / 0; String s = null; System.out.println(s.length()); int[] arr = {1, 2, 3}; System.out.println(arr[3]); } catch (ArithmeticException e) { System.out.println("发生算术异常"); } catch (NullPointerException e) { System.out.println("发生空指针异常"); } catch (IndexOutOfBoundsException e) { System.out.println("发生数组越界异常"); } catch (RuntimeException e) { System.out.println("运行时异常"); } } }
还有个万金油写法,这样所有的异常都能捕获到,如下:
public class Test { public static void main(String[] args) { try { } catch (Exception e){ System.out.println("捕获到异常"); } } }但是不推荐这样写,因为引入这些异常类型,就是因为不同的不同的异常情况下有不同的处理逻辑,这样写,就相当于所有异常的处理逻辑都相同,这是不合适的(就像有的人各种病都吃抗生素,是不合适的😅)
在catch最后再捕获个Exception,是为了防止遗漏一些异常,如下:
public class Test { public static void main(String[] args) { try { int a = 10 / 0; String s = null; } catch (ArithmeticException e) { System.out.println("发生算术异常"); } catch (NullPointerException e) { System.out.println("发生空指针异常"); } catch (IndexOutOfBoundsException e) { System.out.println("发生数组越界异常"); } catch (Exception e) { System.out.println("捕获到异常"); } } }也可以在catch声明中,不同的异常用|隔开,就是同时捕获多种异常(我们一般也不这样搞,这里只是演示下),如下所示:
try { } catch (ArithmeticException | IndexOutOfBoundsException | NullPointerException e) { System.out.println("同时捕获三种异常"); }4,finally
这里写个访问文件资源的代码(铁汁们大概看下即可,这是后续JavaEE的内容)
访问完成文件资源后,要用inputStream.close()关闭一下文件资源,这一步是非常重要的,如果不关闭,后面就可能这个进程打不开新的文件,造成程序崩溃
import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class Test { public static void func(){ try { InputStream inputStream = new FileInputStream("test.txt"); //return语句 //抛出异常 inputStream.close(); //关闭文件资源 } catch (IOException e) { throw new RuntimeException(e); } } }但是这里有个问题,这个关闭文件资源的语句放在try的末尾,一定会被执行到吗?
并不一定,前面有个return语句,就会跳出这个方法;或者中间的逻辑报出异常,那这个关闭资源的语句都运行不到这时候我们就可以使用finally,finally是写到catch后面的,无论如何(抛出异常,return),finally中的逻辑都会运行
这个关闭文件资源的操作也可能会报错,在finally语句中再try-catch一下
另外,把inputStream的定义放在try语句上面,这样在finally中才能访问到import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class Test { public static void func() { InputStream inputStream = null; try { inputStream = new FileInputStream("test.txt"); } catch (IOException e) { throw new RuntimeException(e); } finally { try { inputStream.close(); //关闭文件资源 } catch (IOException e) { throw new RuntimeException(e); } } } }这样就算前面写了return,那在退出方法前也会执行finally中的逻辑
如果finally中也有return的话,就会覆盖掉前面语句的return(了解下即可),如下:
public class Test { public static int func() { try { return 1; }catch (RuntimeException e){ }finally { return 2; } } public static void main(String[] args) { System.out.println(func()); } }
5, 异常分类
Java中的异常分为两大类,受查异常和非受查异常
- 受查异常:必须在代码中显示处理异常(try-catch,throws声明)
- 非受查异常:可以不显示处理
像前面打开关闭文件,抛出的异常就是IOException,这就是一种典型的受查异常(抛出这个异常就是文件可能找不到,让我们写下如果找不到文件采取什么措施),这种异常就必须进行显示处理,如下图

可以直接在func方法中进行try - catch
public static void func() { try { InputStream inputStream = new FileInputStream("test.txt"); } catch (IOException e) { System.out.println("文件打不开的处理逻辑"); } }当然,也可以直接声明可能会抛出的异常
拿示例来说,就是在func()方法中先不处理这个异常,直接声明出来,由后续这个方法的调用者来处理这个异常声明就是在方法名字后面用throws + 异常名,这样main方法调用func方法,就需要处理这个异常(如果在func方法中直接try-catch了,那就说明已经处理过异常了,main方法调用func就不需要处理了)

那在main方法中,同样也有两个选择:
一种就是try-catch,在catch中写下处理逻辑public static void main(String[] args) { try { func(); } catch (FileNotFoundException e) { System.out.println("异常处理逻辑"); } }那另一种就是直接在main方法中声明这个异常,表示不考虑这个异常了,也就是认为文件一定能打开,或者这个异常导致程序终止问题不大
public static void main(String[] args) throws FileNotFoundException { func(); }那像前面聊的那些数组越界、空指针、算术异常
这些都属于RunTimeException,就是非受查异常,我们不try-catch或者声明,程序是可以编译通过的(写出来没有爆红),这些就是非受查异常前面只是学习使用,正常情况下,很少会去主动捕获非受查异常,因为像空指针,数组越界,出现这些异常说明代码中有bug,那就需要修改代码了,这些问题越早暴露出来越好,捕获异常反而影响纠错代码
Error(StackOverflowError)和 RuntimeException(空指针,数组越界,算术异常)是非受查异常,IOException是受查异常
- 受查异常,一般都是对应着比较严重的问题,需要程序员一定重视的,此时通过受查异常显式的让程序员注意到这样的问题,通过合适的方式来处理
- 非受查异常本质是代码 bug。大部分情况下就是让程序直接崩溃即可,程序崩溃后让程序员开始改bug
6,自定义异常
在标准库中虽然提供了异常类,但是还是有一些情况,标准库中的类是无法满足的,就需要我们自己创建一个异常类,如果想要这个异常类是受查异常,就继承Exception,想要这个类是非受查异常,就继承RunTimeException
这里写个非受查异常,继承RunTimeException,要写个带参数的构造方法,抛出异常时就可以显示异常原因
public class MyException extends RuntimeException{ public MyException(String message) { super(message); } }这里因为是非受查异常,在main方法中直接抛出,编译器也不会报错


如果把MyException改为继承Exception,那编译器就会报错,就需要声明或者try-catch一下

用户登录时,可以写个用户名异常和密码异常(这里只是简单演示下,实际抛出异常后还会有处理逻辑),如下所示:
public class UsernameError extends Exception{ public UsernameError(String message) { super(message); } }public class PasswordError extends Exception{ public PasswordError(String message) { super(message); } }import java.util.Scanner; public class Test { public static void login() throws UsernameError, PasswordError { Scanner scanner = new Scanner(System.in); System.out.println("请输入用户名"); String username = scanner.next(); System.out.println("请输入密码"); String password = scanner.next(); if(!username.equals("admin")){ throw new UsernameError("用户名错误"); } if(!password.equals("123456")){ throw new PasswordError("密码错误"); } } public static void main(String[] args) throws UsernameError, PasswordError { login(); } }总结
在项目中,一般受查异常我们就是直接try-catch,非受查异常就让程序崩溃,然后去改bug,在JavaSE阶段简单学习下即可,具体情况的做法(比如说catch中要写什么错误处理逻辑)可以后期在项目中结合具体事例去学习🐵
以上就是今天的所有内容啦~完结撒花~🥳🎉🎉





443

被折叠的 条评论
为什么被折叠?



