10- 认识异常

 

目录

一. 异常的体系

1.1 什么是异常

1.2 什么是错误

 1.3 二者的体系结构

二. 异常的处理

2.1 防御式编程

2.2 异常的抛出

2.3 异常的处理

2.3.1 异常声明throws

2.3.2 异常的处理try-catch

2.3.3 finally

2.4 异常的处理流程

三. 自定义异常类


一. 异常的体系

1.1 什么是异常

程序猿是一帮办事严谨、追求完美的高科技人才。在日常开发中,绞尽脑汁将代码写的尽善尽 美,在程序运行过程中,难免会出现一些奇奇怪怪的问题。有时通过代码很难去控制,比如:数据格式不对、网络 不通畅、内存报警等
Java 中,将程序执行过程中发生的不正常行为称为异常
比如我们之前遇到过的
算数异常:ArithmeticException
int result=10/0;

空指针异常:NullPointerException

int[] arr=null;
System.out.println(arr.length);

越界异常:ArrayIndexOutOfBoundsException

int[] arr=new int[3];
System.out.println(arr[5]);

 通过观察就会发现发现,异常前面都有一个包

 说明不同的异常就是一个不同的类

1.2 什么是错误

Error表示严重的问题,合理的应用程序不应该捕获

 栈溢出错误:java.lang.StackOverflowError

public class Test {
    public static void func() {
        func();
    }
    public static void main(String[] args) {
        func();
    }
}

 可以看到,错误也是java.lang包下的类

 1.3 二者的体系结构

 可以看到,Error和Exception都是Throwable派生出的子类

错误是JVM无法解决的严重问题,比如JVM的内部错误、资源耗尽等,一旦发生程序无法正常运行

异常可以被捕获,然后由程序员进行处理,使程序继续执行

异常根据发生的时间不同,可分为两类:

 1.受查异常

在程序编译期间发生的异常,称为编译时异常,也称为受检查异常

比如:

class Student implements Cloneable{
    String name;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class Test {

    public static void main(String[] args) {
        Student st=new Student();
        Student st2=(Student)st.clone();
    }
}

上面这行代码在编译器那里就会被标红:Unhandled exception:java.lang.CloneNotSupportedException

但是标红的不都是受查异常!

像上图这种情况只是语法错误,并不能称为异常 

2.非受查异常(运行时处理)

在程序执行期间发生的异常,称为运行时异常,也称为非受检查异常

RunTimeException 以及其子类对应的异常,都称为运行时异常 。比如: NullPointerException
ArrayIndexOutOfBoundsException ArithmeticExcepti hang

二. 异常的处理

2.1 防御式编程

程序猿处理异常的思维有两种:

1.LBYL : Look Before You Leap. 在操作之前就做充分的检查 . 即: 事前防御型
boolean ret = false;
ret = 登陆游戏();
if (!ret) {
处理登陆游戏错误;
return;
}
ret = 开始匹配();
if (!ret) {
处理匹配错误;
return;
}
ret = 游戏确认();
if (!ret) {
处理游戏确认错误;
return;
}
...

2.EAFP: It's Easier to Ask Forgiveness than Permission. "事后获取原谅比事前获取许可更容易". 也就是先操作, 遇到问题再处理. 即:事后认错型

try {
登陆游戏();
开始匹配();
游戏确认();
选择英雄();
载入游戏画面();
...
} catch (登陆游戏异常) {
处理登陆游戏异常;
} catch (开始匹配异常) {
处理开始匹配异常;
}
...

相较于事前防御,事后认错型的正常流程和错误流程是分离开的, 程序员更关注正常流程,代码更清晰,容易理解代码 

异常的处理核心思维就是EAFP

2.2 异常的抛出

在编写程序时,如果程序中出现错误,此时就需要将错误的信息告知给调用者,比如:参数检测。 在Java 中,可以借助 throw 关键字,抛出一个指定的异常对象,将错误信息告知给调用者
语法格式如下:
throw new XXXE xception("异常产生的原因");

来看示例代码:

public class Test {
    public static void main(String[] args) {
        int[] arr=new int[3];
        for (int i = 0; i < 4; i++) {
            //主动抛出下标越界异常
            if(i>=3) throw new ArrayIndexOutOfBoundsException("数组下标越界");                                             
            arr[i]=i;
        }
    }
}

运行结果

注意

1. throw...是一条语句,必须写在方法内部

2. 抛出的对象必须是Exception或者其子类

3. 如果throw抛出的是受查异常,必须由该方法的调用者处理,否则编译不通过

比如,如果指定抛出CloneNotSupportedException

public static void main(String[] args) {

      Student st=new Student();

      if(st==null) {
            
           throw new CloneNotSupportedException();//编译器会在这条语句标红,除非处理这个异常
           
     }
}

4.如果throw抛出的是如果抛出的是 RunTimeException或者其子类,则可以不用处理,直接交给JVM来终止程序

5. 异常一旦抛出,后面的代码不会继续执行

2.3 异常的处理

2.3.1 异常声明throws

处在方法声明时参数列表之后,当方法中抛出编译时异常,用户不想处理该异常,此时就可以借助 throws 将异常抛 给方法的调用者来处理。即当前方法不处理异常,提醒方法的调用者处理异常

比如

class Student implements Cloneable{

    String name;

    @Override

    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

上面的throws CloneNotSupportedException就表示clone()方法不处理这个异常,而是要交给它的调用方法main去处理

public class Test {

    public static void main(String[] args) throws CloneNotSupportedException{

        Student st=new Student();

        Student st2=(Student)st.clone();
    }

}

根据我们之前的解决方式,main方法将这个异常交给了它的调用者JVM去处理,如果Student类没有实现Cloneable接口,就会抛出Cloneable受查异常,JVM的处理方式就是不让通过编译

注意事项

1. throws+异常 必须跟在方法的参数列表之后

2. 声明的异常必须是Exce或其子类

3. 如果该方法需要声明多个异常,异常之间用' , '隔开,如果这些异常存在父子关系,只声明父类即可

比如

传入数组arr,下标index,给arr[index] 元素赋值为1

public static void func(int[] arr,int index) throws 

ArrayIndexOutOfBoundsException,NullPointerException{

        if(arr==null) {
            throw new NullPointerException();
        }

        if(index >= arr.length) {
            throw new ArrayIndexOutOfBoundsException();
        }

        arr[index]=1;
    }

4. 如果调用throws声明异常的方法,调用者必须处理这些异常,否则继续用throws声明,直到交给JVM

idea也提供了处理异常的方法:

光标点在抛出异常的方法那里

 可以根据提示的快捷键选择异常的处理方法

2.3.2 异常的处理try-catch

刚才的throws只是把锅甩给了调用者,并没有真正地处理异常,如果要处理异常,需要使用try-catch关键字,语法格式如下

try {
// 将可能出现异常的代码放在这里
catch ( 要捕获的异常类型 e ) {
// 如果 try 中的代码抛出异常了,此处 catch 捕获时异常类型与 try 中抛出的异常类型一致时,或者是 try 中抛出异常的基类 时,就会被捕获到
// 对异常就可以正常处理,处理完成后,跳出 try-catch 结构,继续执行后序代码
} catch ( 异常类型 e ) {
// 对异常进行处理
}

注意:

1.一旦异常被Catch捕捉到,就会立即执行Catch里面的语句,try抛出异常位置之后的代码块不会被执行

public class Test {

    public static void main(String[] args) {
        int[] arr={1,2,3};

        try {

            for(int i=0;i<5;i++) {
                System.out.println(arr[i]);
            }
            System.out.println("数组输出结束");//这句话在抛出异常的语句后面,不会被执行

        } catch(ArrayIndexOutOfBoundsException e) {

            System.out.println("数组越界异常");

        }
    }

}

输出结果

2. 如果try里面抛出多个异常,只会处理第一个被抛出的异常

 public static void main(String[] args) {

        int[] arr=null;

        try {

            System.out.println(arr.length);//此处会抛出空指针异常

            arr=new int[]{1,2,3};

            for(int i=0;i<5;i++) {

                System.out.println(arr[i]);//此处会抛出下标越界异常
            }

        } catch (ArrayIndexOutOfBoundsException e) {

            System.out.println("数组越界");

        } catch(NullPointerException e) {

            System.out.println("空指针异常");
        }
    }

输出结果

 原理和第一条注意事项是一样的,try中的异常一旦抛出,后面的代码不会被执行,后面的异常也会没有“作恶”的机会了

3. 如果try抛出的异常和catch()不匹配,异常不会被成功捕获,也不会被处理,会继续往外抛,直到交给JVM

解决这个问题有两种办法:

1.设置多个catch()来捕捉异常

2.用一个catch()捕捉多种不同类型的异常,这些异常之间用' | '连接

示例如下

public static void main(String[] args) {

        int[] arr=null;

        try {

            System.out.println(arr.length);//此处会抛出空指针异常
            arr=new int[]{1,2,3};

            for(int i=0;i<5;i++) {

                System.out.println(arr[i]);//此处会抛出越界异常
            }
        } catch (ArrayIndexOutOfBoundsException | NullPointerException e) {

            System.out.println("已成功捕获异常");
        }
    }

 4. catch不仅会捕捉()内的异常,还会捕捉这个类的子类,所以使用catch(Exception e)可以捕获所有异常(但不推荐,因为无法获取异常的确切信息)

如果try抛出的多个异常之间有父子关系,并且使用多个catch进行捕捉,一定要子类在前,父类在后;如果想使用一个catch捕捉,只需要写父类即可

所有的异常都是Exception的子类

博主以NullPointerException和Exception这一对父子举例:

public static void main(String[] args) {

        int[] arr=null;

        try {

            System.out.println(arr.length);

        } catch ( NullPointerException e) {

            System.out.println("空指针异常");

        } catch (Exception e) {

            System.out.println("已成功捕获异常");

        }
    }

上面这段代码中,catch(Exception e)这条语句不能放在catch(NullPointerException e)的前面,否则会报错

public static void main(String[] args) {

        int[] arr=null;

        try {

            System.out.println(arr.length);

        } catch ( NullPointerException| Exception e) {

            System.out.println("已成功捕获异常");

        }

    }

上面这段代码是错误的,catch在捕捉异常时不仅会捕捉()内的异常,还会捕捉这个异常的子类,所以捕捉子类异常没有存在的必要

5. 可以用printStackTrace()方法输出异常的具体信息

示例

public static void main(String[] args) {

        int[] arr=null;
        try {

            System.out.println(arr.length);

        } catch (  Exception e) {//直接使用Exception捕获异常

            e.printStackTrace();

        }


    }

输出结果

6. 抛出的异常一旦被catch捕获成功,表示该异常已被处理,后面的代码可以继续执行

public static void main(String[] args) {
        int[] arr=null;

        try {

            System.out.println(arr.length);

        } catch (  NullPointerException e) {

            e.printStackTrace();

        } catch (ArrayIndexOutOfBoundsException e) {

            e.printStackTrace();

        }

        System.out.println("hello");
    }

输出结果

 可以看到,和try抛出的异常匹配的catch执行完毕后,try-catch后面的语句会接着执行

2.3.3 finally

在写程序时, 有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源 :网络连接、数据库 连接、IO 流等, 在程序正常或者异常退出时,必须要对资源进进行回收 。另外,因为 异常会引发程序的跳转,可能 导致有些语句执行不到 finally 就是用来解决这个问题的。
语法格式
try {
// 可能会发生异常的代码
} catch ( 异常类型 e ){
// 对捕获到的异常进行处理
} finally {
// 此处的语句无论是否发生异常,都会被执行到
}
// 如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行

 示例

public static void main(String[] args) {

        int[] arr={1,2,3};

        try {

            for(int i=0;i<5;i++) {

                System.out.println(arr[i]);

            }

        } catch (NullPointerException e) {//catch异常类型不匹配,不能处理异常

            e.printStackTrace();

        } finally {

            System.out.println("finally 的代码已经被执行");
        }

        System.out.println("处理异常结束");
    }

来看一下运行结果

 可以看到,异常没有被处理,try-catch后面的语句也不会被执行,但不论异常是否被处理,finally的语句一定会执行

PS:至于为什么e.printStackTrace()的输出会在println的后面,是因为printStackTrace的底层处理机制,可以不去深究

现在就出现了一个新的问题:

如果异常没有被捕获,用finally回收资源可以理解;如果异常已经被成功捕获,finally的语句和try-catch后面的语句都会执行,这时候finally还有存在的必要吗?

来看一个特殊场景

(这个场景有点不当人看,嘴下饶人)

class TestFinally {

    public static int getData(){
        Scanner sc = null;
        try{
            sc = new Scanner(System.in);
            int data = sc.nextInt();
            return data;//输入的字符合法,就会直接return结束,否则会被catch捕获
        }catch (InputMismatchException e){

            e.printStackTrace();

        }finally {

            System.out.println("finally中代码");
        }

        System.out.println("try-catch之后代码");

        if(null != sc){
            sc.close();//进行输入流的关闭
        }
        return 0;
    }
    public static void main(String[] args) {
        int data = getData();
        System.out.println(data);
    }
}

 上面的代码有一个很大的问题,如果输入的字符是合法的,getData就会直接结束,输入流也就不会被关闭,造成资源泄露;如果不合法,才会执行catch及try-catch之后的语句,关闭输入流

最好的解决办法就是把资源扫尾的语句放在finally中

现在你肯定又有个问题:如果输入字符合法,就直接return结束该方法了,怎么还会用finally回收资源呢?

可以这么理解,finally的权力是要比return大的

在上面的代码中,如果执行合法的输入

 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中代码");

            return 10;//finally重新返回
        }
    }

public static void main(String[] args) {

        int data = getData();
        System.out.println(data);

    }

这样不论输入的data是什么,getData都只会返回10

2.4 异常的处理流程

关于 " 调用栈 "
方法之间是存在相互调用关系的 , 这种调用关系我们可以用 " 调用栈 " 来描述 . JVM 中有一块内存空间称为 "虚拟机栈 " 专门存储方法之间的调用关系 .

 比如下面这条语句

static void func2(){

    System.out.println("func2已被执行");
}

static void func1() {

    func2();//在func1方法中调用func2方法
    System.out.println("func1已被执行");
}

public static void main(String[] args) {

    func1();//在main方法中调用func1方法
    System.out.println("main已被执行");
}

上面的三个方法在被调用的时候,虚拟栈都会为它们开辟栈帧

现在假设func2中抛出空指针异常

static void func2(){

        try{
            throw new NullPointerException();

        }catch (NullPointerException e) {

            e.printStackTrace();//当代码中出现异常的时候, 
                              可以使用这种方式查看出现异常代码的调用栈
        }

        System.out.println("func2已被执行");
    }

1. 如果该异常被catch成功捕获,就在func2方法中进行处理

后面的代码会接着执行 

 

2. 如果func2没有捕获这个异常,或者catch与抛出的异常不匹配,该异常就会被接着抛给调用者,即func1

3. func1接着执行1,2的逻辑...如果main方法仍没有处理该异常,就会交给JVM,该程序会异常终止

与C语言相同,Java中如果程序异常终止,控制台的最后一行就会输出1,如果是正常终止,就会输出0 

三. 自定义异常类

在上文中我们提到过,throw一般用来抛出自定义异常。

为什么需要自定义异常呢?在很多情况下,代码的执行逻辑往往不符合我们的设想,这时候就需要自定义异常类

下面模拟实现用户登录功能,让我们看看如何自定义异常类:

class Login{

    String name="10086";//默认正确的用户名为10086
    String password="111111";//默认正确密码为111111
    
    void login(String name,String password) {

        if(!this.name.equals(name)) {

            throw new NameException("用户名输入错误");//如果用户名错误,就抛NameException
        }

        if(!this.password.equals(password)) {

            throw new PassWordException("密码输入错误");//如果密码错误,就抛PassWardException
        }
        System.out.println("登录成功!");
    }
    
}

那么NameException和PassWordException该怎么定义呢?

我们以NullPointerException为模板看一下

 通过throw我们可以知道,throw每次都要扔出一个异常对象,这个对象的产生肯定要调用它的构造方法。

NullPointerException的构造方法有上图两种,第一种无参构造很容易理解,第二种构造方法中接收了字符串s,是为了抛出异常时输出s作为提示信息

所以我们的自定义异常类也可以使用这样的构造方法

class NameException extends RuntimeException  {

    public NameException() {
    }

    public NameException(String message) {
        super(message);
    }
}
class PassWordException extends RuntimeException {

    public PassWordException() {
    }

    public PassWordException(String message) {
        super(message);
    }
}

你们肯定会有一个问题:为啥不继承Exception类,而是继承RuntimeException类呢?

上文中提到过,RuntimeException类及其子类都是非受查异常,如果定义成Exception类,则默认这个自定义异常类是受查异常

现在来模拟登陆一下

class test2 {
    public static void main(String[] args) {

        Login login=new Login();

        try {

            login.login("15563","24459");

        } catch (NameException e) {//用户名输入错误,会捕捉异常

            e.printStackTrace();

        } catch (PassWordException e) {//密码输入错误,会捕捉异常

            e.printStackTrace();
        }

    }
}

运行结果

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不 会敲代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值