目录
一、前言
我们在写程序中,难免会遇到各种各样的报错,我们将这些报错叫做异常。发生异常的原因多种多样,比如数据格式不对,内存报警等等。
二、初步认识异常
1.算术异常
public static void main(String[] args) {
int a = 10/0;
System.out.println(a);
}
运行后:
2.空指针异常
public static void main(String[] args) {
int[] array = null;
System.out.println(array[0]);
}
运行后:
3. 数组下标越界异常
public static void main(String[] args) {
int[] arr = {1,2,3};
System.out.println(arr[100]);
}
运行后:
那么问题又来了,到底有多少种异常,异常的体系是什么,异常之间的关系又是什么呢?
三、异常的体系结构
我们可以将异常体系总结为两大类: error(错误)和Exception(异常)。那我们在什么地方容易遇到异常呢?比如:
public static void func(){
func();
}
public static void main(String[] args) {
func();
}
运行结果:
我们在写递归代码时,容易犯无限递归的错误,这时就会报栈溢出的错误,当栈被挤爆时,代码停止运行。
那么问题又来了,Error 和 EXception 之间又有什么关系呢?
1. Throwable:是异常体系的顶类层,它派生出两个大的子类:Error 和 Exception。
2. Error:指的是JVM虚拟机无法处理的严重错误,比如JVM内部错误,资源损耗等等,典型案例就是:StackOverflowError (栈溢出错误)和 OutOfMemoryError(堆溢出错误),一旦发生,分身乏术。
3. Exception:异常产生后程序员可以通过代码经行处理,使程序继续执行。我们平时所说的异常就是Exception。
四、异常的分类
分类的依据就是执行的时间。
异常可能在编译时发生,也可能在程序运行时发生,根据发生的时机不同,可以将异常分为:编译时异常和运行时异常。
1.编译时异常
编译时异常也叫受查异常,运行时异常也叫非受查异常。那什么是受查异常呢?理解起来其实非常简单,我们曾经写过克隆方法,以此举例:
class Person implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class test {
public static void main(String[] args) {
Person person = new Person();
Person person1 = (Person) person.clone();
}
}
我们在正常编写代码时有时会遇到这种编译器自动划红线提示的情况,我们把这种情况叫做受查异常。但是要注意的是:受查异常不同于书写错误和语法错误,这不叫做异常。
受查异常是在编译时发生,所以如果想要运行的话需要先处理掉这个异常 。
2. 运行时异常
在程序执行期间发生的异常,称为运行时异常,也称为非受检查异常(Unchecked Exception)
RunTimeException以及其子类对应的异常,都称为运行时异常。比如:NullPointerException、
ArrayIndexOutOfBoundsException、ArithmeticException。
注意:编译时出现的语法性错误,不能称之为异常。例如将 System.out.println 拼写错了, 写成了
system.out.println. 此时编译过程中就会出错, 这是 "编译期" 出错。而运行时指的是程序已经编译通过得到class 文件了, 再由 JVM 执行过程中出现的错误
五、异常的处理
对于这种情况,程序一旦发生异常就会立刻终止。我们在日常生活中,经常使用的手机app也是会发生异常的,但发生异常时,手机app不可以直接崩溃掉,比如网络不佳,连接不上服务器,他也会尝试自动重连,所以我们要知道,在异常发生时如何处理异常。
2.1. 防御式编程
异常在编程中是不可避免的,我们需要做的是在出现异常时代码依然照常运行,然后将异常的代码汇报给程序员,主要方式有:
1. 事前防御型
以玩游戏为例:
boolean ret = false;
ret = 登陆游戏();if (!ret) {
处理登陆游戏错误;
return;
}ret = 开始匹配();
if (!ret) {
处理匹配错误;
return;
}ret = 游戏确认();
if (!ret) {
处理游戏确认错误;
return;
}ret = 选择英雄();
if (!ret) {
处理选择英雄错误;
return;}
一步一判断,就怕哪一步出现问题,每次在进行下一步时,上一步都是检查过的,。缺点也很明显:正常流程代码与错误代码都放在一起,代码整体比较混乱。
2. 事后认错型
事后认错型即先进行操作,遇到问题再进行处理。
try {
登陆游戏();
开始匹配();
游戏确认();
选择英雄();
载入游戏画面();
...
} catch (登陆游戏异常) {
处理登陆游戏异常;
} catch (开始匹配异常) {
处理开始匹配异常;
} catch (游戏确认异常) {
处理游戏确认异常;
} catch (选择英雄异常) {
处理选择英雄异常;
} catch (载入游戏画面异常) {
处理载入游戏画面异常;}
···
我们首先将可能出现异常的代码都放到 try 当中,然后用catch 捕获这些异常。这种方法相比于上一种的优势在于正常流程的代码和出现异常的代码分离开,增加代码的可读性,使程序员能够更加注重正常流程的代码。
在JAVA中,关于异常处理的关键字主要有5个:try ,catch , final , throw , throws。
2.2 异常的抛出
如何让程序抛出异常?
抛出异常的方式有很多种:
1. 某段程序触发
2. 通过关键字 throw 抛出异常
在编写程序时,如果程序中出现错误,此时就需要将错误的信息告知给调用者。在 java 中,可以通过 throw 来抛出一个指定的异常对象,然后将错误信息告知给调用者。
int a = 10;
if(a == 10){
throw new NullPointerException("HAHAHA");
}
手动抛出的异常,这个关键字一般用于抛出自定义的异常。
使用在方法的声明之后,作用是告诉方法的调用者,调用这个方法,会抛出一个xxx的异常。那为什么刚刚加了这个就不报错了呢,因为如果一个方法内部存在一个编译时异常(受查异常),此时这个编译时异常一定要处理,目前我们处理的方式是在方法定义的时候,通过 throws 关键字声明异常,最后这个异常是交给JVM 来处理的。
2.3 异常的捕获
异常的捕获,也就是异常的处理,处理方式主要有两种:异常声明 throws 和try-catch 。
1. 异常声明 throws
public static void main(String[] args) throws NullPointerException{
int a = 10;
if(a == 10){
throw new NullPointerException("HAHAHA");
}
}
注意:1. throws必须跟在方法的参数列表之后。
2.声明的异常必须是 Exception 或者 Exception 的子类
3. 方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可。
处在方法声明时参数列表之后,当方法中抛出编译时异常,用户又不想处理这个异常,就可以通过异常声明 throws 来抛出这个异常给异常的调用者来处理。即当前方法不处理该异常,只是提醒用户来处理异常,实际上还是交给了JVM,只不过在你看来他没有异常的红线了。
出现异常的快捷键:将光标放到异常上边,使用快捷键 alt + insert 就能快速处理异常。
那不想交给JVM怎么办,那就要使用try_catch了。
2. try-catch 方法
public static void main(String[] args){
System.out.println("before...");
try{
System.out.println(10/0);
}catch (ArithmeticException e){
System.out.println("我来处理异常了。");
}
System.out.println("after...");
}
}
catch 里面的参数就是你要捕获的异常,如果捕获到了才会执行 catch 当中的内容,同时catch可以捕获多个异常。异常与异常之间可以用 | 来分隔开。
要注意的是就算处理了异常,try 后边的异常代码也还是不会被执行,try_catch 后的代码会被执行。 但是不处理,后边所有代码都不会被执行。
同时,异常之间是有父子关系的,Exception 是所有异常的父类,它可以包括所有异常。使用try_catch 时,子类异常先catch,父类异常在最后 catch,此时 Exception 充当了一个垫后的作用。不能直接用Exception 接收所有的异常异常,这样得到的异常不精准。
关于异常的处理方式
异常的种类有很多, 我们要根据不同的业务场景来决定:
1.对于比较严重的问题(例如和算钱相关的场景), 应该让程序直接崩溃, 防止造成更严重的后果。
2.对于不太严重的问题(大多数场景), 可以记录错误日志, 并通过监控报警程序及时通知程序员。
3.对于可能会恢复的问题(和网络相关的场景), 可以尝试进行重试。
在我们当前的代码中采取的是经过简化的第二种方式. 我们记录的错误日志是出现异常的方法调用信息, 能很快速的让我们找到出现异常的位置. 以后在实际工作中我们会采取更完备的方式来记录异常信息。
六、finally
在写程序时,有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库连接、IO流等,在程序正常或者异常退出时,必须要对资源进行回收。另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的。
public static void main(String[] args) {
try {
int[] arr = null;
System.out.println(arr[0]);
} catch (NullPointerException e) {
System.out.println("发生空指针异常");
} finally {
System.out.println("finally中的代码一定会执行");
}
System.out.println("如果没有抛出异常,或者异常被处理了,try-catch后的代码也会执行");
}
运行结果:
把上述代码稍微变一变:
public static void main(String[] args) {
try {
int[] arr = {1,2,3};
System.out.println(arr[0]);
} catch (NullPointerException e) {
System.out.println("发生空指针异常");
} finally {
System.out.println("finally中的代码一定会执行");
}
System.out.println("如果没有抛出异常,或者异常被处理了,try-catch后的代码也会执行");
}
运行结果:
也就是说,无论是否捕获到异常,finally 中的代码都一定会被执行。
那问题又来了,我们发现不管是 finally 中的代码,还是 try_catch_finally 后的代码都是一定会执行的,那为什么还需要 finally 呢?
举个例子:
public static int getData() {
Scanner sc = null;
try {
sc = new Scanner(System.in);
int data = sc.nextInt();
return data;
} catch (InputMismatchException e) {
e.printStackTrace();
} finally {
System.out.println("finally中代码");
}
System.out.println("try-catch-finally之后代码");
if (null != sc) {
sc.close();
}
return 0;
}
public static void main(String[] args) {
int data = getData();
System.out.println(data);
}
运行结果:
在上面的代码中,当正常输入时,成功接收后代码就返回了,根本就没有执行 try_catch_finally之后的代码,输入流没有被成功释放,造成了资源泄露。
finally 中的代码是一定会执行的,所以 finally 一般进行一些资源清理的扫尾工作。
finally 执行的时机是在方法返回之前(try 或者 catch 中如果有 return 会在这个 return 之前执行 finally)。 但是如果finally 中也存在 return 语句, 那么就会执行 finally 中的 return, 从而不会执行到 try 中原有的 return。
一般我们不建议在 finally 中写 return (被编译器当做一个警告)。
问题:
throw 和 throws的区别是什么?
throw new 是抛出一个异常,throws 是在方法定义的时候声明一个异常。
七、自定义异常
我们首先进到一个异常的底层代码中,比如空指针异常:
我们发现,异常之间是有继承关系的,NullPointerException 就就继承自 RuntimeException,我们再进入RuntimeException 的底层代码:
发现 RuntimeException 又继承自 Exception,那 RuntimeException 和 Exception 有什么作用呢?
我们首先写一个例子:
具体方式:
1. 自定义异常类,然后继承自Exception 或者 RunTimeException
2. 实现一个带有String类型参数的构造方法,参数含义:出现异常的原因
class UserNameException extends Exception {
public UserNameException(String message) {
super(message);
}
}
class PasswordException extends RuntimeException {
public PasswordException(String message) {
super(message);
}
}
public class Login {
private String username = "admin";
private String password = "123456";
public static void logininfo(String username, String password) throws UserNameException, PasswordException {
if (!username.equals(username)) {
throw new UserNameException("用户名错误");
}
if (!password.equals(password)) {
throw new PasswordException("密码错误");
}
System.out.println("登录成功!");
}
public static void main(String[] args) {
try {
logininfo("admin", "123456");
} catch (PasswordException e) {
e.printStackTrace();
} catch (UserNameException e) {
e.printStackTrace();
}
}
}
注意事项:
1.自定义异常通常会继承自 Exception 或者 RuntimeException
2.继承自 Exception 的异常默认是受查异常
3.继承自 RuntimeException 的异常默认是非受查异常