- Java异常的体系结构
①在Java当中,Throwable是异常体系的顶层类,其派生出2个子类,一个是错误Error,一个是异常Exception。
②错误Error是指Java虚拟机无法解决的严重问题,比如:JVM的内部错误、资源耗尽等。代表:StackOverFlowError栈溢出错误,这是是逻辑上的错误,必须由程序员排查代码。
③异常Exception分为 编译时的异常(受查异常)和运行时的异常(非受查异常、RuntimeException),所有的运行时异常都会继承RuntimeException类。
④异常产生后程序员可以通过代码处理,使程序继续执行。
⑤编译时出现的语法错误,不能称之为异常,比如:System.out.println写成了system.out.println。
- 异常的处理
异常的处理通常有2种方法
①事前防御型:LBYL:Look Before You Leap. 在操作之前就充分检查。做一步检查一步。
②事后认错型:EAFP:It’s Easier to Ask Forgivness than Permission. 事后获取原谅比事前获取许可更容易。也就是先操作,遇到问题再处理。
try{
//一些列操作
}catch(捕获操作可能产生的异常类型 变量){
//处理异常
}catch(捕获操作可能产生的异常类型 变量){
//处理异常
}catch(捕获操作可能产生的异常类型 变量){
//处理异常
}
...
优势:正常流程和错误流程分开,程序员更关注正常流程,代码也更清晰,异常处理的核心思想就是EAFP。
- 要想处理异常,首先要学会抛出异常,抛出异常的关键字为:throw。
在Java中,可以借助throw关键字,抛出一个指定的异常对象,将错误信息告知给调用者。
注意:throw必须写在方法体内部,并且throw抛出的对象必须是Exception或者Exception的子类。
①抛出运行时异常(非受查异常)RuntimeException,可以不用处理,直接交给JVM来处理。
public class Test {
public static void main(String[] args) {
func(0);
}
public static void func(int a) {
if (a == 0) {
throw new NullPointerException();
}
}
}
运行结果:
分析:一般在运行结果中提示行数的最上面一行(本例中的第8行)就是我们需要解决的,一般的当解决这个异常后,下面的异常行数就也随之解决了。
②抛出编译时异常(受查异常),用户必须处理,否则无法通过编译
如图所示,CloneNotSupportedException是一个编译时异常,所以在编写代码的时候必须处理这个异常(本例中采用throws声明异常)。
public static void main(String[] args) throws CloneNotSupportedException {
Person person = new Person();
Person person1 = (Person) person.clone();
}
③异常一旦抛出,其后的代码不会被执行
public static void main(String[] args) {
func1(0);
}
public static void func1(int a) {
if (a == 0) {
throw new NullPointerException();
}
System.out.println("方法结束");
}
运行结果:
分析:打印语句 System.out.println("方法结束");
因为在它之前抛出了异常,所以这条语句没有被执行。
-
异常的具体处理方式,也就是异常的捕获,主要有2种方式:异常声明throws以及try-catch捕获处理。
-
异常声明throws
①throws可以理解为一种较为不负责的做法,它只是告诉调用者我这里有异常,声明了异常,但是并没有真正的去解决处理这个异常。即,当方法种抛出编译时异常,用户不想处理该异常,此时就可以借助throws将异常抛给方法的调用者来处理。当我们没有解决这个异常的时候,就会把这个异常交给JVM处理,一旦交给JVM,程序就崩溃了。
②throws的位置是在方法声明参数列表之后
③throw声明的异常必须是Exception或者Exception子类。
public static void main(String[] args) throws NullPointerException,ArithmeticException{
func(null,0);
}
public static void func(int[] array, int a) throws NullPointerException,ArithmeticException{
if (array == null) {
throw new NullPointerException();
}
if (a == 0) {
throw new ArithmeticException();
}
}
分析:在func中可能会抛出NullPointerException,ArithmeticException两种异常,所以在func的参数列表后面紧跟着使用throws进行声明,并且方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,假如抛出的多个异常之间具有父子关系,直接声明父类就可以。
- 异常捕获try-catch
①throws对异常并没有真正处理,而是将异常报告给抛出异常方法的调用者,由调用者处理。如果真正要对异常进行处理,就需要try-catch。
public static void main(String[] args) {
try {
func(0);
System.out.println("我会不会被执行呢?");
}catch (ArithmeticException e){
System.out.println("捕获到异常了");
}
System.out.println("捕获异常处理后正常流程的语句");
}
public static void func(int a){
if (a == 0) {
throw new ArithmeticException();
}
}
运行结果:
分析:try块内抛出异常位置之后的代码 System.out.println("我会不会被执行呢?");
,不会被执行。
②问题:try-catch能不能处理编译时异常(受查异常)?
答:可以的,对于受查异常来说,当try中没有当初catch的受查异常的时候,catch检测不到就会报错。因此,对于catch中如果写受查异常,则try中一定要出现受查异常。
public static void main(String[] args) {
try {
func(0);
System.out.println("我会不会被执行呢?");
}catch (ArithmeticException e){
System.out.println("捕获到异常了");
}catch (CloneNotSupportedException e){
System.out.println("受查异常被捕获");
}
System.out.println("捕获异常处理后正常流程的语句");
}
public static void func(int a) throws CloneNotSupportedException{
if (a == 0) {
throw new CloneNotSupportedException();
}
}
运行结果:
分析:在func中会抛出受查异常CloneNotSupportedException,但是func并没有处理这个受查异常,所以在func方法参数列表之后使用throws声明这个异常,因此真正的处理时在main中的catch捕获这个异常的时候,这个时候才叫真正意义上的处理异常。
③如果try抛出异常类型和catch的异常类型不匹配,则异常也不会被成功捕获,也就不会被处理,则会继续往外抛,直到JVM处理,一旦交给JVM处理,程序就会立即中止。【异常是按照类型来进行捕获的】
public static void main(String[] args) {
try {
int[] array = null;
int a = array.length;
}catch (ArithmeticException e) {
System.out.println("算术异常被捕获");
}
}
运行结果:
分析:try当中抛出的是空指针异常,而catch捕获处理的是算术异常,因此最终异常会交给JVM处理,程序中止。
④问:try会不会在同一时间同时抛出2个及以上的异常?
答:不会的,当try当中存在多个异常的时候,从上往下执行,谁先抛出异常就捕获哪个异常。
public static void main(String[] args) {
try {
int[] array = null;
int a = array.length;
int ret = 10 / 0;
}catch (ArithmeticException e) {
System.out.println("算术异常被捕获");
}catch (NullPointerException e){
System.out.println("空指针异常被捕获");
}
System.out.println("正常流程语句");
}
运行结果:
分析:在try中会存在抛出空指针异常和算术异常两种情况,所以必须用多个catch来捕获,即多种异常,多次捕获。但是由于try中先会抛出空指针异常,所以不会继续抛其后的算术异常,但是我们发现catch的算术异常是写在空指针异常前面的,这也说明catch中程序的书写不影响异常的捕获,而是取决于try中抛哪个异常。
⑤如果多个异常的处理方式是完全相同的,可以使用 | 连接。
catch (ArithmeticException | NullPointerException e) {
System.out.println("算术异常或者空指针异常被捕获");
}
⑥问:能不能把Exception异常写在最前面
答:不可以,因为所有的异常都是继承Exception的,此时Exception作为所有异常的父类,它能捕获所有的异常,如果把它放到catch的第一个,那么后续的异常没有任何作用了。
错误写法:Exception放在最前面
正确做法:把Exception放到最后面。
⑦可以通过一个catch捕获所有的异常,catch Exception,因为Exception是所有异常类的父类,因此可以用这个一类型表示捕获所有异常。
注意:catch进行类型匹配的时候,不光会匹配相同类型的异常对象,也会捕获目标异常类型的子类对象。
try {
int[] array = null;
int a = array.length;
int ret = 10 / 0;
}catch (Exception e) {
System.out.println("异常被捕获");
}
分析:这种做法不推荐,不可取。
- 有些特定的代码,不论程序是否发生异常,都需要执行,比如对资源的回收释放,而异常会引发程序的跳转,导致有些语句执行不到,finally就是用来解决这个问题的。
try{
//可能会发生异常的代码
}catch(异常类型 e){
//对捕获到的异常进行处理
}finally{
//此处的语句无论是否发生异常,都会被执行
}
//如果没有抛出异常,或者异常被捕获处理了,这里的代码也会被执行
注意:fnally中的代码一定会执行,一般在finnally中进行一些资源清理的扫尾工作。
分析:下面程序输出结果:
// 下面程序输出什么?
public static void main(String[] args) {
System.out.println(func());
}
public static int func() {
try {
return 10;
} finally {
return 20;
}
}
答:输出20,因为本来要返回10,但是finally中的return语句要在方法结束之前执行,所以在返回10之前,变成了返回20。
分析:finally执行的时机是方法返回之前(try或者catch中如果由return,会在这个return之前执行finally),但是如果finally中存在return语句,那么就会执行finally中的return,从而不会执行到try中原有的return。【一般不建议在finally中写return语句,会被编译器当成一个警告。】并且finally中的语句一定会被执行。
-
异常处理流程总结
①程序先执行try中的代码
②如果try中的代码出现异常,就会结束try中的代码,看和catch中的异常类型是否匹配
③——Ⅰ:如果找到匹配的异常类型,就会执行catch中的代码
③——Ⅱ:如果没有找到匹配的异常类型,就会将异常向上传递到上层调用者
④无论是否找到匹配的异常类型,finally中的代码都会被执行到(在该方法结束之前执行)
⑤如果上层调用者也没有处理异常,就继续向上传递
⑥一直到main方法也没有合适的代码处理异常,就会交给JVM来进行处理,此时程序就会异常终止 -
自定义异常:为什么要自己取定义异常?
Java中虽然已经内置了丰富的异常类,但是并不能完全表示实际开发中所遇到的一些异常,此时就需要维护符合实际情况的异常结构,基于已有的异常类进行扩展(继承)创建和业务相关的异常类。
具体方式:
①自定义异常类,继承Exception(编译时异常)或者RuntimeException(运行时异常)。
②实现一个带有String类型参数的构造方法,参数含义:出现异常的原因。
注意事项:
①自定义异常通常会继承Exception类或者RuntimeException类
②继承自Exception的异常默认是受查异常
③继承自RuntimeException的异常默认是非受查异常
实现:实现一个简单的控制台版用户登陆程序, 程序启动提示用户输入用户名密码. 如果用户名密码出错, 使用自定义异常的方式来处理
用户名异常类:
public class NameExcepetion extends RuntimeException{
public NameExcepetion(String message) {
super(message);
}
public NameExcepetion() {
}
}
密码异常类:
public class PasswordExcepetion extends RuntimeException{
public PasswordExcepetion() {
}
public PasswordExcepetion(String message) {
super(message);
}
}
测试类:
import java.util.Scanner;
public class Test {
//异常练习
public static void login() {
String name = "123";
String password = "567";
Scanner scanner = new Scanner(System.in);
System.out.println("请输入用户名");
String uname = scanner.nextLine();
System.out.println("请输入密码");
String upassword = scanner.nextLine();
if (!name.equals(uname)) {
throw new NameExcepetion("兄弟用户名错误了");
}
if (!password.equals(upassword)) {
throw new PasswordExcepetion("兄弟密码错误了");
}
}
public static void main(String[] args) {
try {
login();
} catch (NameExcepetion e) {
e.printStackTrace();
System.out.println("用户名错误");
} catch (PasswordExcepetion e) {
e.printStackTrace();
System.out.println("密码错误");
}
}
}
运行结果:
分析:自定义异常类是一个闪电的图标。