1. 异常的概念与体系结构
1.1 异常的概念
在Java中,将程序执行过程中发生的不正常行为称为异常。
public class Test {public static void main(String[] args) {//算术(ArithmeticException)异常
// int a = 5/0;
// System.out.println(a);//输出结果:Exception in thread "main" java.lang.ArithmeticException: / by zero// at Test.main(Test.java:11)//数组下标越界(ArrayIndexOutOfBoundsException)异常
// int array[] = {1, 2, 3};
// System.out.println(array[10]);//输出结果:Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 3// at Test.main(Test.java:20)//空指针(NullPointerException)异常int array1[] = null;System.out.println(array1[0]);//输出结果:Exception in thread "main" java.lang.NullPointerException: Cannot load from int array because "array1" is null// at Test.main(Test.java:26)}
}
1.2 异常的体系结构
这些异常是一个一个的类,并且这三种异常都是继承于RunException 类,属于运行时异常。
- Throwable:是异常体系的顶层类,其派生出两个重要的子类, Error 和 Exception
- Error:指的是Java虚拟机无法解决的严重问题,比如:JVM的内部错误、资源耗尽等,典型代表:
StackOverflowError和OutOfMemoryError,一旦发生回力乏术。 - Exception:异常产生后程序员可以通过代码进行处理,使程序继续执行。
1.3 异常的分类
1.3.1 编译时异常
在程序编译期间发生的异常,称为编译时异常,也称为受检查异常(Checked Exception)
Person类:
public class Person implements Cloneable{public String name;@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}
}
Test类:
public class Test {public static void main(String[] args) throws CloneNotSupportedException{Person person1 = new Person();Person person2 = (Person) person1.clone();//注意:clone出来的对象是Object类型的,需要强转。}
}
这段代码主要是克隆了一个Person对象,要想实现克隆,需要满足下面几点:
- 在Person类上实现Cloneable接口。
- 在Person类中重写clone方法。
- 在main函数中处理clone异常。
1.3.2 运行时异常
在程序执行期间发生的异常,称为运行时异常,也称为非受检查异常(Unchecked Exception)
RunTimeException以及其子类对应的异常,都称为运行时异常。比如:NullPointerException、
ArrayIndexOutOfBoundsException、ArithmeticException。
2. 异常的处理
2.1 防御式编程
2.1.1 事前防御型(LBYL)
在每一个可能发生异常的语句后面都紧接着进行异常的处理。
boolean ret = false;ret = 登陆游戏();if (!ret) {处理登陆游戏错误;return;}ret = 开始匹配();if (!ret) {处理匹配错误;return;}ret = 游戏确认();if (!ret) {处理游戏确认错误;return;}ret = 选择英雄();if (!ret) {处理选择英雄错误;return;
}
ret = 载入游戏画面();
if (!ret) {
处理载入游戏错误;
return;
}
......
缺陷:正常流程和错误处理流程代码混在一起, 代码整体显的比较混乱。
2.1.2 事后认错型(EAFP)
使用try-catch语句,将正常的流程语句放在一起,然后再添加对异常捕捉的语句。
try {登陆游戏();开始匹配();游戏确认();选择英雄();载入游戏画面();...} catch (登陆游戏异常) {处理登陆游戏异常;} catch (开始匹配异常) {处理开始匹配异常;} catch (游戏确认异常) {处理游戏确认异常;} catch (选择英雄异常) {处理选择英雄异常;} catch (载入游戏画面异常) {处理载入游戏画面异常;}......
优势:正常流程和错误流程是分离开的, 程序员更关注正常流程,代码更清晰,容易理解代码
2.2 异常的抛出
所有的受查时异常都要在后面throws一下,或者使用try catch。必须!!!!
- throw必须写在方法体内部
- 抛出的对象必须是Exception 或者 Exception 的子类对象
- 如果抛出的是 RunTimeException 或者 RunTimeException 的子类,则可以不用处理,直接交给JVM来处理
public class Test {public static void func(int[] array){if (array == null){throw new NullPointerException();}else {System.out.println("array != null");}}public static void main(String[] args) {func(null);}
}
输出结果:
Exception in thread "main" java.lang.NullPointerExceptionat Test.func(Test.java:11)at Test.main(Test.java:18)
- 如果抛出的是编译时异常,用户必须处理,否则无法通过编译
这里抛出了一个编译时异常(克隆异常),如果不对整个异常进行处理,那么抛出这条异常语句下面的红线就不会消失。可以像下面这样处理:
虽然现在func方法内不报错了,但是异常具有传递性,main方法调用了func,所以main方法也会报错。所以,此时也要在main方法中处理一下这个异常,还挺麻烦的。
- 异常一旦抛出,其后的代码就不会执行
public class Main {public static void func(int[] array){if (array == null){throw new NullPointerException();}else {System.out.println("array != null");}}public static void main(String[] args) {func(null);System.out.println("我执行了......");}
}
输出结果:
Exception in thread "main" java.lang.NullPointerExceptionat Main.func(Main.java:11)at Main.main(Main.java:19)
2.3 异常的捕获
异常的捕获,也就是异常的具体处理方式,主要有两种:异常声明throws 以及 try-catch捕获处理。
2.3.1 异常声明throws
方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型
具有父子关系,直接声明父类即可。
public class Main {public static void func(int[] array) throws CloneNotSupportedException,NullPointerException{if (array == null){throw new CloneNotSupportedException();}else {System.out.println("array != null");}}public static void main(String[] args) throws CloneNotSupportedException{func(null);}
}
这种在方法参数后面通过throws来处理异常的方式,其实并没有真正处理异常,只是骗过了编译器,
正常情况下,编译器的退出码都是0,但此时却是1,就表明程序是有问题的。要想真正去处理异常,需要使用try-catch。
2.3.2 try-catch捕获并处理
public class Main {public static void main(String[] args) {try {int[] array = {1,2,3};System.out.println(array[10]);//一旦程序抛出异常了,抛出异常的后面就不会执行了//当上面那行代码遇见数组下标越界异常就会直接来到下面的catch,// 走完catch,程序不会再回过头来执行下面的代码System.out.println("hahaha.....");//不会走这段代码}catch (ArrayIndexOutOfBoundsException e){e.printStackTrace();//打印出来当前所发生的异常信息System.out.println("数组下标越界异常.....");}catch (NullPointerException e){e.printStackTrace();System.out.println("空指针异常......");}//但是抛出的异常不影响外面的程序执行System.out.println("程序继续执行......");//执行}
}
输出结果:
java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 3at Main.main(Main.java:12)
数组下标越界异常.....
程序继续执行......
问:如果程序没有捕捉到异常呢?
public class Main {public static void main(String[] args) {try {int[] array = {1,2,3};System.out.println(array[10]);System.out.println("hahaha.....");//不会走这段代码}catch (NullPointerException e){e.printStackTrace();System.out.println("空指针异常......");}//但是抛出的异常不影响外面的程序执行System.out.println("程序继续执行......");//执行}
}
输出结果:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 3at Main.main(Main.java:12)
答:. 如果抛出异常类型与catch时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往外抛,直到JVM收到后中断程序----异常是按照类型来捕获的
问:会不会同时打印数组下标越界异常和空指针异常?
答:不会。如果同时打印,就意味着这两个异常是同时抛出的。
问:会不会同时抛出两个异常?
答:不会。
public class Main {public static void main(String[] args) {try {int[] array = {1,2,3};System.out.println(array[10]);System.out.println("hahaha.....");//不会走这段代码}catch (ArrayIndexOutOfBoundsException | NullPointerException e){//这里的异常|可以写多个,但是变量e只有一个e.printStackTrace();//打印出来当前所发生的异常信息}//但是抛出的异常不应下外面的程序执行System.out.println("程序继续执行......");//执行}
}
输出结果:
java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 3at Main.main(Main.java:12)
程序继续执行......
如果异常之间具有父子关系,一定是子类异常在前catch,父类异常在后catch,否则语法错误。
遇到异常的时候,程序是从上往下来进行匹配的,此时父类RuntimeException在上面,就相当于下面的两个没有异常没有用。
2.3.3 finally
在写程序时,有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库连接、IO流等,在程序正常或者异常退出时,必须要对资源进进行回收。另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的。
public class Main {public static void main(String[] args) {try {int[] array = {1,2,3};System.out.println(array[1]);System.out.println("hahaha.....");//不会走这段代码}catch (ArrayIndexOutOfBoundsException e){e.printStackTrace();//打印出来当前所发生的异常信息System.out.println("数组下标越界异常.....");}catch (NullPointerException e){e.printStackTrace();System.out.println("空指针异常......");}finally {System.out.println("finnal被执行了......");//无论有没有异常发生都会被执行}System.out.println("程序继续执行......");//执行}
}
public class Main {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);try {int a = scanner.nextInt();System.out.println(a);}catch (InputMismatchException e){System.out.println("输入类型匹配异常.....");}finally {scanner.close();//无论输入是否成功,都需要关闭这个输入资源}}
}
问:输入99,程序会返回99,还是100?
public class Main {public static int main(String[] args) {Scanner scanner = new Scanner(System.in);try {int a = scanner.nextInt();//输入99return a;}catch (InputMismatchException e){e.printStackTrace();System.out.println("输入类型匹配异常.....");}finally {return 100;}}
}
答:返回100。如果try和finally中都有return语句,最终return的值是以finally中的值为准的。建议不要在finally中进行return。此时编译器会有警告,黄色部分。
【面试题】:
- throw 和 throws 的区别?
throw是抛出一个异常对象,throws是声明。 - finally中的语句一定会执行吗?
一定会执行!!!!
2.4 异常的处理流程
关于 “调用栈”:
方法之间是存在相互调用关系的, 这种调用关系我们可以用 “调用栈” 来描述. 在 JVM 中有一块内存空间称为
“虚拟机栈” 专门存储方法之间的调用关系. 当代码中出现异常的时候, 我们就可以使用e.printStackTrace();
方式查看出现异常代码的调用栈.
如果本方法中没有合适的处理异常的方式, 就会沿着调用栈向上传递。
【异常处理流程总结】
- 程序先执行 try 中的代码
- 如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配.
- 如果找到匹配的异常类型, 就会执行 catch 中的代码
- 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者.
- 无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行).
- 如果上层调用者也没有处理的了异常, 就继续向上传递.
- 一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止.
3. 自定义异常类
Java 中虽然已经内置了丰富的异常类, 但是并不能完全表示实际开发中所遇到的一些异常,此时就需要维护符合我们实际情况的异常结构
实现一个登录用户登录功能:
PasswordException类:
//public class PasswordException extends RuntimeException{//继承RuntimeException,就是运行时异常,也就是非受查异常
public class PasswordException extends Exception{//继承Exception,就是编译时异常,也就是受查异常public PasswordException(){}public PasswordException(String msg){super(msg);}
}
UserNameException类:
//public class UserNameException extends RuntimeException{
public class UserNameException extends Exception{public UserNameException(){}public UserNameException(String msg){super(msg);}
}
Login类:
public class Login {public String userName = "admin";public String password = "123456";public void loginInfo(String userName, String password)throws PasswordException,UserNameException{if(!this.userName.equals(userName)){throw new UserNameException("用户名错误异常.....");
// System.out.println("用户名错误");
// return;}if (!this.password.equals(password)){
// System.out.println("密码错误");
// return;throw new PasswordException("密码错误异常......");}System.out.println("登录成功!!!");}
}
Main类:
public class Main {public static void main(String[] args) {Login login = new Login();try {login.loginInfo("zhangsan","123456");}catch (PasswordException e){System.out.println("PasswordException");}catch (UserNameException e){System.out.println("UserNameException");}}
}
注意事项:
- 自定义异常通常会继承自 Exception 或者 RuntimeException
- 继承自 Exception 的异常默认是受查异常
- 继承自 RuntimeException 的异常默认是非受查异常