说明
这是一个完善了但又不完善的笔记,或许以后会更新
可以参考但请务必超越
源文件
Tools
JAVA异常
异常,字面理解那就是出问题了呗
异常其实一直都有在接触,只不过没有去分类,去系统的讲述
1.异常的背景
曾经见过的异常:
除以0,算数异常
数组下标越界,数组越界异常
访问null对象,空指针异常
所以的异常,指的就是程序在运行时出现错误通知调用者的一种机制
注意关键字:运行时
关键字“运行时”
有些错误是这样的,例如将System.out.println拼写错了,写成了system.out.println,此时编译过程中就会出错,这是“编译期”出错。(当然除了大小写,还有中英文符号,忘写符号等)
而运行时指的是程序已经编译通过得到class文件了,再由JVM 执行过程中出现的错误。
异常的种类有很多,不同种类的异常具有不同的含义,也有不同的处理方式。
防御式编程
错误在代码中是客观存在的,所以我们要在程序出现问题的时候及时通知程序猿。有两种方式。
LBYL:Look Before You Leap. 在操作之前就做充分的检查。
EAFP:It's Easier to Ask Forgiveness than Permission. “事后获取原谅比事前获取许可更容易”。也就是先操作,遇到问题再处理。
这两个概念就没必要背了,很容易理解。
异常的好处
首先看一下LBYL风格的代码
比如游戏登陆,LBYL风格那就不使用异常呗
boolean flg = false;
flg = 登陆();
if (!flg) {
处理登陆错误;
return;
}
//...
EAFP风格的代码,就会使用异常
try {
登陆();
//...
} catch (登陆异常) {
处理登陆异常;
} //catch...
对比两种风格的代码,很明显第一种方式正常流程和错误处理混杂在一起,每一次都需要使用if判断一下先。第二种方式正常流程和错误处理是分开的,前面正常流程,把错误处理都放在后面,更容易理解查看。
这就体现出了异常的好处,哪怕只是观感和理解上,也相比不使用好的多。正常的项目工作,可要复杂的多
2.异常的基本用法
try {
有可能出现异常的语句;
}[catch (异常类型 异常对象) {
处理异常;
} ... ]
[finally {
异常的出口
}]
try
代码块中存放可能出现异常的代码。catch
代码块存放出现异常后处理的行为。finally
代码块存放处理善后工作的代码,在最后执行。catch
和finally
视情况选择加或不加。事实上一般情况都是会加catch
而不加finally
,不加catch
那么idea就会报错。
抛出异常
用栗子来证明理解是最好的方式
数组越界
public static void main(String[] args) {
int[] array = {1, 2, 3};
System.out.println(array[5]);
System.out.println("测试");
}
可以看到,程序抛出异常就立即终止了,后面的代码没有执行
那我们用try catch
包裹一下
public static void main(String[] args) {
int[] array = {1, 2, 3};
try {
System.out.println(array[5]);
System.out.println("test");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("捕捉到一个数组越界异常");
}
System.out.println("测试");
}
可以看到,try
代码块中的打印内容都没有打印,catch
捕获到异常后就做了处理打印了内容,最后的打印内容也正常打印了。
这样做就可以在异常抛出的时候,程序也还是能够运行下去
printStackTrace()
上面栗子的处理方式,可以使用这个方法
e.printStackTrace();
这个方法可以抛出异常,就是我们看到的红色字体
未捕捉到异常
上面的栗子catch
是成功捕捉到异常的
那要是有异常却没有捕捉到呢?
不处理异常
如果不处理异常的话,程序就会教给JVM处理
一旦交给JVM处理,程序就会立刻终止
注意,被catch
捕捉到也是对异常的处理。
catch (ArrayIndexOutOfBoundsException e) {
}
catch
中不写任何代码,意思是说捕捉到异常后,什么都不做
如果有异常缺没有捕获到,那就是真的不处理异常了
public static void main(String[] args) {
int[] array = {1, 2, 3};
try {
array = null;
System.out.println(array[5]);
System.out.println("test");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("捕捉到一个数组越界异常");
}
System.out.println("测试");
}
现在在try
中将数组引用array
置空
但是catch
捕捉的是数组越界异常
可以看到此时就是不处理异常
交给JVM后抛出空指针异常,然后终止程序
解决方式可以再添加一个catch
捕获空指针异常
public static void main(String[] args) {
int[] array = {1, 2, 3};
try {
array = null;
System.out.println(array[5]);
System.out.println("test");
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.out.println("捕捉到一个数组越界异常");
} catch (NullPointerException e) {
e.printStackTrace();
System.out.println("捕捉到一个空指针异常");
}
System.out.println("测试");
}
exit code
退出码,我们在执行完程序后,会显示一行信息
exit code 0
意味着程序正常执行完毕并退出
当我们有异常并处理掉了,后面正常运行完毕,也是会显示exit code 0
如果不处理异常交给JVM,那就会直接终止程序,并显示退出码exit code 1
。表示程序执行过程中遇到了某些问题或错误,非正常退出
exit code
在大多数编程语言都适用
捕捉多个异常
上面的例子有数组越界和空指针两个异常
再添加一个catch
是可以的,还可以使用|
符号捕捉多个异常
public static void main(String[] args) {
int[] array = {1, 2, 3};
try {
array = null;
System.out.println(array[5]);
System.out.println("test");
} catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
e.printStackTrace();
System.out.println("捕捉到一个数组越界/空指针异常");
}
System.out.println("测试");
}
异常信息栈
再举个栗子,用来学习下抛出的异常如何看
现在要输入一个数字,但是却输入了其他字符
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
System.out.println(n);
}
我的背景可能导致看不太清楚,那就直接亮度拉满,或者把他拿出来看一下
Exception in thread "main" java.util.InputMismatchException
at java.util.Scanner.throwFor(Scanner.java:864)
at java.util.Scanner.next(Scanner.java:1485)
at java.util.Scanner.nextInt(Scanner.java:2117)
at java.util.Scanner.nextInt(Scanner.java:2076)
at Test.main(Test.java:13)
InputMismatchException
:输入参数不匹配异常
遇到大段的异常不要慌,慢慢来看
- 首先确定异常的类型,在这里是
InputMismatchException
这部分 - 再看异常信息栈。这些以at开头的信息,就是异常信息栈
上面的图片看不太清没关系,只要知道以下几点即可
- idea中灰色的部分是Java的源代码出错。我们能怀疑和修改源码吗?肯定不行,所以找到最后一行蓝色的信息
- 蓝色的部分是程序最后一次出现错误的信息。可以看到
at Test.main(Test.java:13)
意思就是说在main函数的13行 - 因为蓝色部分的错误,导致了源码也错误
关于异常的处理方式
异常的种类有很多,我们要根据不同的业务场景来决定。
对于比较严重的问题(例如和算钱相关的场景,银行系统等)应该让程序直接崩溃,防止造成更严重的后果。
对于不太严重的问题(大多数场景),可以记录错误日志,并通过监控报警程序及时通知程序猿。
对于可能会恢复的问题(和网络相关的场景),可以尝试进行重试。
在我们当前的代码中采取的是经过简化的第二种方式,我们记录的错误日志是出现异常的方法调用信息,能很快速的让我们找到出现异常的位置。以后在实际工作中我们会采取更完备的方式来记录异常信息。
finally
在逻辑控制那一篇文章中提到过
Scanner
也是属于一种资源,不用了就关闭
现在我们结合异常写一段代码
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
try {
int n = scanner.nextInt();
System.out.println(10 / n);
} catch (InputMismatchException e) {
e.printStackTrace();
System.out.println("输入有误!");
} catch (ArithmeticException e) {
e.printStackTrace();
System.out.println("捕捉到一个算数异常,可能0作为了除数!");
} finally {
scanner.close();
}
System.out.println("测试");
}
首先正常运行,随便输入一个数据,catch
捕捉看看有没有异常
最后,不管前面经过结果,finally
都会执行
当然,执行完毕,依然按照正常流程走
所以,finally
一般用作资源的关闭
优先级
既然finally
一定会被执行,那么有这样一段代码
public static int func() {
int a = 10;
try {
return a;
} catch (ArithmeticException e) {
e.printStackTrace();
}finally {
return 20;
}
}
public static void main(String[] args) {
System.out.println(func());
}
方法遇到return
就会结束,那这里是返回try
的10还是finally
的20呢?
可以看到,本来应该返回的a应该是10,结果执行了finally
返回了20,这就是一定会执行finally
的含义
所以,尽量避免在finally
当中写return
小结
异常处理流程
- 程序先执行try中的代码。
- 如果try中的代码出现异常,就会结束try中的代码,看和catch中的异常类型是否匹配。
- 如果找到匹配的异常类型,就会执行catch中的代码。
- 如果没有找到匹配的异常类型,就会将异常向上传递到上层调用者。
- 无论是否找到匹配的异常类型,finally中的代码都会被执行到(在该方法结束之前执行)。
- 如果上层调用者也没有处理的了异常,就继续向上传递。
- 一直到main方法也没有合适的代码处理异常,就会交给JVM来进行处理,此时程序就会异常终止。
注意
打印信息是红色的,不一定是错误
如果有运行过Java程序的话,就会常见有很多红色的信息,但程序是正常运行的。红色的信息有可能是其他的信息
throws
前面的例子,异常都是在main函数中发生的
那么如果现在有一个方法,在方法内部出现了异常,会是什么情况?
public static void func(int n) {
System.out.println(10 / n);
}
public static void main(String[] args) {
func(0);
System.out.println("测试");
}
交给了JVM直接结束了程序,没有打印main函数中的“测试”
那么使用try catch
在方法内部和main函数中分别包裹看看
事实上,异常会沿着异常的信息调用栈进行传递
方法区没处理,那就会进行到main函数处理
main函数中处理,那关键的问题来了
怎么才能知道别人的方法中会出现什么异常呢?
使用throws
声明异常
throws ArithmeticException
声明异常
在面向对象章节中有使用过clone()
,下面画了红线
强转类型就不用多说了。强转之后,又使用alt+enter
快捷声明了异常
throws CloneNotSupportedException
使用throws
声明代码可能出现的异常。
告诉使用者,可能会出现什么样的异常
使用try catch
处理后就会正常进行,不处理异常则交给JVM处理
throw
还有一种抛异常的方法
使用throw
直接抛出异常
public static void func(int x, int y) throws RuntimeException {
if ((x - y) == 10) {
throw new RuntimeException("结果为" + (x - y));
}
}
public static void main(String[] args) {
func(20, 10);
}
一般来说即使是在方法内使用了throw
,还是应该要使用throws
声明一下
一般情况使用throw
用来抛一些自定义的异常
3.异常的体系结构
所有的异常都来自于父类Throwable
。没错,异常也是一个类
Throwable
又分为错误Error
和异常Exception
错误必须由程序猿自己去处理代码
比如无限递归
public static void func() {
func();
}
public static void main(String[] args) {
func();
}
StackOverflowError
:栈溢出错误(一般栈会溢出,堆溢出很少,因为现在内存都8个G以上,4个G都很少见)
异常分为运行时异常和编译时异常,也叫非受查异常和受查异常
异常就有太多了,一般是用到的时候查就行了
IOException
、ClassNotFoundException
和CloneNotSupportedException
属于编译时异常
运行时异常RuntimeException
派生出很多常见的异常类
我们可以看看他有很多异常,其中就有我们遇过的
还有异常的体系结构图,可以自行搜索。也可以在idea中一个一个的打开直到父类Throwable
旗舰版idea支持使用yFiles
生成可视化效果图,选中异常右键Diagrams
即可查看
比如:
捕获父类
现在明白我们前面使用catch
只是捕捉了其中的一个子类
那么是不是可以直接捕捉父类从而捕捉所有异常?
当然可以
catch (Exception e) {
e.printStackTrace();
}
一般不推荐捕捉父类,因为太大了
且如果父类异常和子类异常同时存在的话,父类异常在后面如果捕捉到了子类异常那父类异常就没用了,父类异常在前面那子类异常就直接报错。
catch
在捕获异常的时候,最好是从上往下。子类->父类,不要父类->子类
所以,最好捕捉具体的异常
4.自定义异常类
Java虽然已经提供了很多的异常类,但是实际情况可能会需要扩展一些其他的,符合我们实际情况的异常
直接举栗
举栗:登陆功能
private static final String name = "kirito";
private static final String password = "abc123";
public static void login(String name, String password) {
if (!Login.name.equals(name)) {
throw new NameException("无此用户名,请先注册!");
}
if (!Login.password.equals(password)) {
throw new PasswordException("密码错误!");
}
}
使用这个方法来判断输入的用户名和密码是否正确,不正确就抛出异常
这里的2个异常NameException
和PasswordException
就是我们要实现的异常类
实现条件
1.要在自定义的异常中写入信息
这个很简单,异常也是类嘛,直接重写一个调用一个参数的构造方法即可
2.要继承父类Throwable
当我们简单的写下2个自定义异常类时,肯定是会报错的
class NameException {
}
class PasswordException {
}
...
throw new NameException();
throw new PasswordException();
...
idea会提示我们应该抛出Throwable
,这当然好解决,只要继承就可以了
这个时候,就会产生一个问题
受查异常和非受查异常的区别
前面提到了,但是在这里结合自定义异常类来讲比较好
class NameException extends Exception {
}
class PasswordException extends RuntimeException {
}
上面的NameException
继承的是Exception
,这时他是个受查异常,必须使用try catch
包裹,并且使用throw
就要使用throws
声明
下面的PasswordException
继承的是RuntimeException
,这时他是个非受查异常,只会在运行时产生异常,就可以直接使用throw
而不用其他,当然如果想用也是可以的
这很简单,可以自己验证,这里就不验证了。
完善代码
整合在一起,就是这样
class NameException extends RuntimeException {
public NameException(String message) {
super(message);
}
}
class PasswordException extends RuntimeException {
public PasswordException(String message) {
super(message);
}
}
public class Login {
private static final String name = "kirito";
private static final String password = "abc123";
public static void login(String name, String password) {
if (!Login.name.equals(name)) throws NameException, PasswordException {
throw new NameException("无此用户名,请先注册!");
}
if (!Login.password.equals(password)) {
throw new PasswordException("密码错误!");
}
}
效果
用户名错误,就会直接抛出异常
用户名没错,就会再看密码
小头图版权:《Ganqing》by Aviary 2021年12月7日中午12点05分 pid:94623280