异常机制
异常机制指的是程序出现错误时,程序的处理方式。
程序的错误分为三种:
- 编译错误:由于没有遵循对于语言的语法规则,编辑器可以自动发现并提示的错误位置和原因。
- 逻辑错误:程序没有按照预期的顺序执行。
- 运行时错误:程序执行过程中,运行环境发现不能执行的操作,而异常就是指的程序运行时发生的错误。
走进异常
例子:算数异常+数组下标越界异常+空指针异常
public static void main(String[] args) {System.out.println(10/0); // 算数异常
// ----------------------------------------int[] a = {1,2,3,4,5}; // 数组下标越界异常System.out.println(a[10]);
// ----------------------------------------int[] a = null; // 空指针异常System.out.println(a[0]);}
上述的代码在分别运行时均会抛出异常,且抛出异常后不会去执行下面的代码。如下图
以空指针异常为例,根据空指针异常的源码,可以知道异常其实就是一个个类
异常的体系结构:
在Java中,所有的异常都有一个共同的祖先:Throwable(可抛出),Throwable有两个子类,分别为Error(错误)和Exception(异常),它们两者的区别是:Error不能够被处理,但是Exception可以被程序本身所处理。
Error(错误):是程序无法处理的错误,表示运行程序中较严重的问题。大多数错误与代码的编写者执行的操作无关,而表示代码运行时JVM出现的问题。因此我们在编写程序时不必关心这一类错误。常见的有StackOverflowError(栈溢出),NOClassDefFoundError(类定义错误)
Exception(异常):是程序本身可以处理的异常。在编写程序时我们要尽可能的去处理这些异常。有一个重要的子类RuntimeException。它以及它的子类表示jvm常用操作引发的异常。例如开头提到的算数异常,数组越界异常以及空指针异常。主要分为两大类:运行时异常和编译时异常(非运行时异常)。
- 运行时异常:时RuntimeException类及其子类的异常,这些异常称为非受查异常,程序可以选择处理,或者不进行处理。一般是由程序的逻辑错误导致,例如:数组下标越界,在编写程序时,应当尽可能的避免这类情况的发生。
- 编译时异常:RuntimeException以外的异常,这些都是必须在编译前必须处理的异常,如果不进行处理,那么编译时就不会通过。
通常,Java中的异常(Throwable)分为两大类:受查异常和非受查异常:
- 受查异常:除了Error类和RunTimeException类及其子类以外的所有类,在编译期间抛出,如何不进行处理,代码不会通过编译,当程序出现这里异常,要么使用try-catch语句进行捕获,要么用throw进行抛出
- 非受查异常:包括Error类和RunTimeException类及其子类,一般是对代码进行修改(因为大部分是代码本身的出错)而不是去捕获它们。
异常的处理
关键词throw:
异常可以通过jvm自动抛出,也可以通过程序员通过throw手动抛出。例如抛出一个自定义异常:
public class Main {public static void test(int a) {if(a == 1) {throw new ArithmeticException("a == 1");}}public static void main(String[] args) {test(1);}
}
表示如果是传入的是1,那么就会抛出算数异常(可以传参,也可以不传参),异常信息为a==1.
注意事项:
- throw必须写在方法体的内部。
- 抛出的对象必须是Exception或者Exception的子类对象。
- 如果抛出的是RunTimeException或者RunTimeException,则可以不用处理,直接交给jvm进行处理。
- 异常一旦抛出,后面的代码不会执行。
关键词throws:
语法:修饰符 返回类型 方法名(参数) throws 异常类型1,异常类型2...{
}
throws其实并没有处理异常,只是告诉调用此方法可能会抛出相对应的异常,交给了此方法的调用者进行处理,如果不进行处理,不断的throws,知道交给jvm进行处理,此时程序就会终止
例如: throws CloneNotSupportedException就是声明main方法和clone方法可能会抛出的异常
class Person implements Cloneable {@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}
}public class test1 {public static void main(String[] args) throws CloneNotSupportedException {Person person = new Person();Person person1 = (Person) person.clone();}
}
注意:
- throws必须位于方法的参数列表之后
- throws后的异常必须是Exception类及其子类
- 如果throws语句异常存在父子关系,只需要保留父类即可
- throws并没有真正的处理异常,只是将异常抛出给上层的调用者,最终交给jvm处理
关键词:try-catch:
try{
// 异常的代码
}catch(异常的类型 变量){
}
其中try内部写可能出现异常的代码,catch用于捕获指定的异常;如果try内部出现异常,catch进行捕获,捕获成功执行catch内部的代码,捕获失败,程序终止并报错。
public class Main {public static void test(int a) throws CloneNotSupportedException{if(a == 1) {throw new CloneNotSupportedException();}}public static void main(String[] args) {try {test(1);System.out.println("没有抛出异常");}catch (CloneNotSupportedException e) {System.out.println("捕获了异常");}System.out.println("程序到末尾");}
}
向test传参整形1,进入if语句,throw抛出异常,catch进行异常的捕获,最终执行打印:
捕获了异常,程序到末尾,但是如果注释掉if了的throw语句,那么就不会抛出异常,那么就不会被捕获,因此执行打印:没有抛出异常,程序到了末尾。
当打印了异常信息后,可能难以发现异常信息的出现位置,这是可以用到catch里面的变量信息e,利用e.printStackTrace()栈信息的打印,可以帮助追踪到出错位置;
public class Main {public static void test(int a) throws CloneNotSupportedException{if(a == 1) {throw new CloneNotSupportedException();}}public static void main(String[] args) {try {test(1);System.out.println("进入try");}catch (CloneNotSupportedException e) {System.out.println("捕获了异常");e.printStackTrace();}System.out.println("程序到末尾");}
}
由于PrintStackTrace打印逻辑不基于println,因此程序到末尾不是最后打印。
当一个程序可能出现多个异常时,需要多个catch语句进行捕获。
public static void main(String[] args) {int[] arr = {1,2,3};try {System.out.println(arr[1]);arr = null;System.out.println(arr.length);}catch (ArrayIndexOutOfBoundsException e) {System.out.println("处理 ArrayIndexOutOfBoundsException 异常");e.printStackTrace();}catch (NullPointerException e) {System.out.println("处理 NullPointerException 异常");e.printStackTrace();}System.out.println("-----------------------------------");}
这里时空指针异常,用第二个catch进行捕获。
但是不会一下子抛出多个异常,因为当捕获到一个异常后,catch进行处理,后面的异常不会被捕获。
一些不规范的异常抛出
1.存在一个catch语句并且待捕获异常为Exception,通过一个catch捕获所有,这样子写不能搞清楚到底是抛出了那些异常。但是Exception可以兜底。
public static void main(String[] args) {int[] arr = {1,2,3};try {System.out.println(arr[4]);}catch (Exception e) {System.out.println("处理 Exception 异常");e.printStackTrace();}}// ----------------------------------------------------------public static void main(String[] args) {int[] arr = {1,2,3};try {System.out.println(arr[4]);}catch (NullPointerException e) {System.out.println("处理 NullPointerException 异常");e.printStackTrace();}catch (Exception e) {System.out.println("处理 Exception 异常,兜底");e.printStackTrace();}}
2.存在一个catch语句并且捕获多个异常,用|进行连接,和上述相同,分不清到底抛出什么异常
public static void main(String[] args) {int[] arr = {1,2,3};try {System.out.println(arr[4]);}catch (ArrayIndexOutOfBoundsException | NullPointerException e) {System.out.println("处理 ArrayIndexOutOfBoundsException | NullPointerException 异常");e.printStackTrace();}}
3.前面的catch捕获Exception,后面的catch捕获了Exception的子类
public static void main(String[] args) {int[] arr = {1,2,3};try {System.out.println(arr[4]);}catch (Exception e) {System.out.println("处理 Exception 异常");e.printStackTrace();}catch (NullPointerException e) {System.out.println("处理 NullPointerException 异常");e.printStackTrace();}}
总结:
- try块内出现异常位置后的语句不会被执行
- 抛出的异常与catch不匹配,一直向上抛出,直到抛给jvm进行处理
- try中多个异常可以用多个catch进行一一捕获,可以用Exception进行兜底,但是不推荐直接用Exception
- 当catch出现父子关系,子类在上,父类在下
关键字:finally
在编写代码的过程中,有些代码无论是否发生异常都需要被执行,比如程序中打开的资源:网络连接,数据库,在程序正常或者异常退出时,必须对资源进行回收,另外,异常会引发程序的跳转,导致语句无法执行,针对这些语句,Java提供了finally关键字,和try-catch组合为try-catch-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中的代码");if(sc!=null) {sc.close();}}return 0;}public static void main(String[] args) {int data = getData();System.out.println(data);}
getData()方法为了获取整数,没有获取到,抛出异常,return 0,获取到打印获取的整数,无论是否成功获取,都会执行finally中的代码,其中.close()为了释放资源。
另一种方式,可以不写close().
try后面实现了一个AutoCloseavable或Closeable接口的资源,Java运行时会在try块执行完毕后自动调用close方法
public static int getData() {try (Scanner sc = new Scanner(System.in)){int data = sc.nextInt();return data;}catch (InputMismatchException e) {e.printStackTrace();}finally {System.out.println("finally中的代码");}return 0;}
避免在finally中出现return语句
public static int test() {try {return 10;}catch (NullPointerException e) {e.printStackTrace();}finally {return 20;}}public static void main(String[] args) {System.out.println(test());}
执行顺序:
- test执行,进入try。
- 执行return 10,但是try块中的return 语句实际返回值之前必须先执行finnaly。
- 执行finnaly,return 20,结束test方法,返回20
- 打印出20
throw和throws
- throw用于实际抛出的语句,throws表示该方法可能抛出的异常
- throw后面跟实例,throws后面跟异常类型
- throw可以用在任何方法体内,但是throws只能用在方法声明上
异常的处理流程
- 程序执行try中的代码
- 如果try中出现异常,结束执行try中的代码,catch进行捕获
- 如果捕获成功,执行catch中的代码,捕获失败向上传递给调用者,如果直到main方法没有合合适的代码处理异常,交给jvm,此时程序异常终止。
- 无论是否捕获成功,finally都会正常执行
自定义异常
在实际的开发中,Java中的异常不能完全表示遇到的一些异常,这是就需要自定义一些异常类来满足开发的需求。
我们根据空指针异常的源码对于自定义异常进行仿写
由此可得:
public class PassWordException extends Exception{public PassWordException() {}public PassWordException(String s) {super(s);}
}
public class userNameException extends Exception{public userNameException() {}public userNameException(String s) {super(s);}
}
那么总的实现是:
public class Login {private String userName;private String passWord;public void setUserName(String userName) {this.userName = userName;}public void setPassWord(String passWord) {this.passWord = passWord;}public void loginInfo(String userName, String passWord) throws userNameException, PassWordException {if(!this.userName.equals(userName)) {throw new userNameException("用户名错误!");}if(!this.passWord.equals(passWord)) {throw new PassWordException("密码错误!");}System.out.println("登录成功!");}
}
import java.util.Scanner;
class Test {public static void main(String[] args) {Login login = new Login();Scanner scanner = new Scanner(System.in);login.setUserName("abc");login.setPassWord("123");try {System.out.println("请输入用户名:");String userName = scanner.nextLine();System.out.println("请输入密码:");String passWord = scanner.nextLine();login.loginInfo(userName, passWord);}catch (userNameException e) {System.out.println("用户名错误!");e.printStackTrace();}catch (PassWordException e) {System.out.println("密码错误!");e.printStackTrace();}finally {scanner.close();}}
}