初始Java篇(JavaSE基础语法)(9)认识异常

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页:我要学编程(ಥ_ಥ)-CSDN博客

所属专栏:JavaSE

目录

异常的体系结构

异常的分类

编译时异常

运行时异常 

异常的处理

防御式编程

异常的抛出

异常的捕获 

异常声明throws

try-catch捕获并处理

finally 

面试题:

异常的处理流程

自定义异常类 


异常的体系结构

Java程序在运行时,会遇到各种阻碍。这些阻碍如果在强制阻止程序运行,那么这些阻碍就会被JVM给捕获到。被捕获到的阻碍分为两种:一种是错误,一种是异常。可能有小伙伴会好奇:异常不就是错误吗?平时我们在写代码时,程序报错,这不就是错误或者说异常吗?其实不然。我们目前遇到的大部分问题都是异常。例如:

上面就是报的异常:空指针异常。而如果报的是错误的话,应该是以Error为后缀的。

Java中的错误指的是JVM无法解决的严重问题。通常是由于 JVM故障或其他系统级问题引起的,程序本身无法处理。而异常产生后程序员可以通过代码进行处理,使程序继续执行。

最常见的就是:栈溢出(StackOverflowError)

那么到底异常是个啥呢?在Java中,将程序执行过程中发生的不正常行为称为异常。

Java中异常的种类繁多,为了对不同异常或者错误进行很好的分类管理,Java内部维护了一个异常的体系结构。

异常的分类

Java 中的异常主要分为两大类:checked exceptions(受查异常,也称为编译时异常)和 unchecked exceptions(非受查异常,也称为运行时异常)。顾名思义,编译时异常,就是在编译的时候,发生的异常;运行时异常,是编译阶段良好,但是程序在运行时,会发生的异常。

编译时异常

我们在学习拷贝时,面对的深拷贝和浅拷贝问题。那时我们是通过重写clone方法实现了深拷贝的。而我们用clone方法时,加了一个语句。

没加语句时的报异常,我们就称为编译时异常。

注意:

运行时异常 

上面举的空指针异常。包括下面这个数组越界异常。

这种在编译时,看不出来。

异常的处理

既然,使用clone方法时加了一个语句就没问题了,那么这个语句是什么呢?具体作用呢?我们接下来学习:异常的处理。

在此之前,我们先来深入了解异常。异常是一个类。在JavaAPI中可以查到。

Java API(应用程序编程接口,Application Programming Interface)是Java平台提供的一套预定义的类和接口的集合。这些类和接口构成了Java语言的核心库,允许开发者利用现成的、经过严格测试的功能来编写程序,而无需从头开始实现所有基本功能。Java API覆盖了从基本的数据结构、文件操作、网络编程、多线程到高级功能如数据库连接、图形用户界面(GUI)开发、XML解析、加密解密等众多领域。

JavaAPI,点击进入

刚刚,我们学习了异常包括了编译时异常和运行时异常,而且异常时一个类。那么是不是意味着:这个类是异常类的子类呢?确实,是子类。 

防御式编程

当出现异常时,首先,应该告诉程序员,让其来解决异常。因此,在操作之前就做充分的检查。我们在玩游戏,就是这个逻辑。

        boolean ret = false;
        ret = 登陆游戏();
        if (!ret) {
            处理登陆游戏错误;
            return;
        }
        ret = 开始匹配();
        if (!ret) {
            处理匹配错误;
            return;
        }
        ret = 游戏确认();
        if (!ret) {
            处理游戏确认错误;
            return;
        }
        ret = 选择英雄();
        if (!ret) {
            处理选择英雄错误;
            return;
        }
        ret = 载入游戏画面();
        if (!ret) {
            处理载入游戏错误;
            return;
        }
        ......

一步一步的检查。当我们登录游戏,选择错误时,游戏就会立即处理这个问题。

因此这个防御式编程,也叫做:事前防御型。 

还有一种是:事后认错型。

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

这个的逻辑是先运行游戏,当遇到错误了,我们在去处理。

优势:正常流程和错误流程是分离开的, 程序员更关注正常流程,代码更清晰,容易理解代码。 

在Java中,异常处理主要的5个关键字:throw、try、catch、final、throws。

异常的抛出

在编写程序时,如果程序中出现错误,此时就需要将错误的信息告知给调用者,比如:参数检测。 在Java中,可以借助throw关键字,抛出一个指定的异常对象,将错误信息告知给调用者。具体语法如下:

throw new XXXException("异常产生的原因");

 例如:数组越界异常。

public class Test {
    public static void main(String[] args) {
        int[] array = new int[5];
        int n = 10;
        if (n > (array.length-1)) {
            throw new ArrayIndexOutOfBoundsException("代码有异常,是数组越界异常……");
        }else {
            System.out.println(array[n]);
        }
    }
}

通过throw new 异常 的语法,我们就可以自己书写可能会出现的异常。也就是说通过throw 可以抛出我们自定义的异常。

执行结果: 

注意:

1. throw必须写在方法体内部。

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

3. 如果抛出的是 RunTimeException(运行时异常) 或者 RunTimeException 的子类,则可以不用处理,直接交给JVM来处理。

4. 如果抛出的是编译时异常,用户必须处理,否则无法通过编译。

5. 异常一旦抛出,其后的代码就不会执行。 (上述代码可以看出,没有打印array[n])。

异常的捕获 

异常的捕获,也就是异常的具体处理方式,主要有两种:异常声明throws 以及 try-catch捕获处理。

异常声明throws

我们前面写的语句就是异常声明,声明可能会出现克隆不支持异常。

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

注意,前面我们在调用克隆方法时是把异常抛出了给调用者,也就是main方法,但是我们在main方法处也抛出了异常给到调用者,没有捕获这个异常,而是再次使用将异常声明抛出,这实际上不是一个标准做法。因为main方法是程序的入口点,没有哪个方法来调用main方法。然而,如果真的这样做了,并且没有合适的处理机制,程序会在此处终止执行,并打印出异常堆栈信息,这意味着异常最终是由JVM来“处理”的,但这通常不是我们希望看到的程序行为。正确的做法是在main方法中使用try-catch来捕获并适当处理或记录这个异常,以避免程序突然终止,并给予用户或系统管理员有用的反馈。待会我们就会学习到。

异常声明的语法格式:

修饰符 返回值类型 方法名(参数列表) throws 异常类型1,异常类型2...{ }

 注意:

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

2. 声明的异常必须是 Exception 或者 Exception 的子类。

3. 方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型 具有父子关系,直接声明父类即可。

4. 调用了 声明抛出异常 的方法时,调用者必须对该异常进行处理,或者继续使用throws抛出。

try-catch捕获并处理

throws对异常并没有真正处理,而是将异常报告给抛出给到了调用 异常方法 的 调用者,由调用者处理。如果真正要对异常进行处理,就需要try-catch。

语法格式:

        try {
            // 将可能出现异常的代码放在这里。
        } catch (要捕获的异常类型 e) {
            // 如果try中的代码抛出异常了,此处catch捕获的异常类型与try中抛出的异常类型一致时,
            // 或者是try中抛出异常的父类时,就会被捕获到。
            // 对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行后序代码。
        } catch (异常类型 e) {
            // 对异常进行处理。
        } finally {
            // 此处代码一定会被执行到(无论是否捕获到异常)。
        }
        // 后序代码:
        // 当异常被捕获到,就算异常被处理了,这里的后序代码一定会执行
        // 如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码就不会被执行

注意:

1. try 块必须至少有catch或finally或两者。你不能仅有一个try块而没有catch或finally。这是因为try块中的代码可能抛出异常,如果没有catch或finally来处理或清理资源,那么这个异常将会中断程序的正常执行流程。但是,从Java 7开始,引入了try-with-resources语句,这个语句自动管理资源,使得在某些情况下,你可能不需要显式的finally块来关闭资源。在这种结构中,虽然看起来像是只有try部分,但实际上finally的部分隐含在try-with-resources的语法中了。

public class Test {
    public static void main(String[] args) {
        int[] array = new int[5];
        try (Scanner scanner = new Scanner(System.in)){
            array[0] = scanner.nextInt();
            System.out.println(array[0]);
            //在这里,不需要手动关闭,会有一个默认的 finally语句来关闭
            // scanner.close(); 这个方法是被默认调用的
        }
    }
}

这个和普通的 try 块有区别 ,try 块后面跟了一个资源打开的的部分,后面自动关闭时,也是关闭try 块后面的部分。

2. try 部分的代码可能会抛出异常,也可能不会抛出异常。

3. try块内抛出异常位置之后的代码将不会被执行

4. 如果抛出异常类型与catch时异常类型不匹配(父类可以捕获子类),即异常不会被成功捕获,也就不会被处理,继续往外抛,直到 JVM收到后中断程序。异常是按照类型来捕获的。 

5. try中可能会抛出多个不同的异常对象,则必须用多个catch来捕获——即多种异常,多次捕获。如果多个异常的处理方式是完全相同, 也可以写成这样(不过用的很少):

catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
    ...
}

6. 如果异常之间具有父子关系,一定是子类异常在前catch,父类异常在后catch,否则语法错误。因为当父类异常在前面时,这个异常就直接被父类给捕获了,而后面的子类不会再去执行了,那么我们定义的子类也就毫无意义了。这里又有一个新的问题了:用Exception去捕获所有异常,这个写法,也是不推荐的。不过可以把Exce写到最后一个catch,这样即使前面的异常都没有接收,那么这个Exception也会接收。

例如: 

public class Test {
    public static void main(String[] args) {
        int[] array = new int[5];
        try{
            System.out.println(array.length);
            System.out.println(array[10]);
            System.out.println("当前面代码抛出异常时,这里的代码不会被执行");
        }catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("这里捕获到了前面代码抛出的异常,是数组越界异常,并进行了处理");
        }catch (NullPointerException e) {
            System.out.println("这里捕获到了前面代码抛出的异常,是空指针异常,并进行了处理");
        }catch (Exception e) {
            System.out.println("如果抛出了异常,并且异常没有被前面的捕获,那么这里的代码一定会执行,并进行了处理");
            return;
        }finally {
            System.out.println("无论前面是否会抛出异常,这里一定会执行");
        }
        System.out.println("由于前面的异常被捕获了,就算是处理了异常,因此这里的代码会被执行");
    }
}

执行结果: 

关于异常的处理方式:

异常的种类有很多, 我们要根据不同的业务场景来决定. 对于比较严重的问题(例如和算钱相关的场景), 应该让程序直接崩溃, 防止造成更严重的后果(为公司或者用户亏损钱财);对于不太严重的问题(大多数场景), 可以记录错误日志, 并通过监控报警程序及时通知程序员;对于可能会恢复的问题(和网络相关的场景,打游戏时断网后可重连), 可以尝试进行重试。在我们当前的代码中采取的是经过简化的第二种方式. 我们记录的错误日志是出现异常的方法调用信息,能很快速的让我们找到出现异常的位置。以后在实际工作中我们会采取更完备的方式来记录异常信息。

finally 

在写程序时,有些特定的代码,不论程序是否发生异常,都需要执行。比如程序中打开的资源:网络连接、数据库 连接、IO流等,在程序正常或者异常退出时,必须要对资源进进行回收。另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的。 

在前面的示例中,我们也已经知道了finally一定会被执行。 因此finally中的代码用来做一些资源清扫工作是再好不过了。

finally 执行的时机是在方法返回之前(try 或者 catch 中如果有 return 会在执行这个 return 之前执行 finally)。但是如果 finally 中也存在 return 语句, 那么就会执行 finally 中的 return, 从而不会执行到 try 或者 catch 中原有的 return。一般我们不建议在 finally 中写 return (被编译器当做一个警告)。

注意:finally 的出现必定伴随着 try 的出现。但是 try 可以单独出现(但是隐式地写了finally)。

面试题:

1. throw 和 throws 的区别?

简单点说就是两者的作用不同:throw 是 用来抛出一个异常实例,而throws 是 用来声明一个方法可能会抛出哪些异常。

下面是一些容易被忽视的点:

位置不同:throw 是在方法内部;throws 是在方法参数列表的后面。

语法格式不同:throw new ExceptionType("Exception message");          throws  ExceptionType1, ExceptionType2……{ …… }

作用效果不同:throw 当执行到 throw  语句时,当前方法立即停止执行,异常被抛出给到调用者。如果调用者没有处理掉这个异常,就会再次抛出给到调用这个方法的调用者(层层递增),直至这个异常被处理掉或者到了JVM这个层面,程序就被异常终止了。throws 在声明一个异常之后,是在告诉调用者,我可能会出现这种异常,你要么处理,要么再次声明这个异常,把这个异常给你自己的调用者。

2. finally中的语句一定会执行吗?

不一定。等等,之前不是说 finally 语句一定会被执行吗?怎么现在又说不一定呢?刚刚所说的只是正常情况,在正常情况下,finally 语句一定会被执行。但是如果执行finally 语句之前,程序就异常终止了的话,此时finally 语句就不会执行。异常终止,例如:JVM出了问题,此时程序就不会在执行了。我们的程序一定是在JVM中跑起来的。因此,JVM出了问题,程序肯定就直接终止了。

异常的处理流程

1. 程序先执行 try 中的代码。

2. 如果 try 中的代码出现异常,就会结束 try 中的代码,看和 catch 中的异常类型是否匹配。

3. 如果找到匹配的异常类型,就会执行 catch 中的代码。并执行后序代码

4. 如果没有找到匹配的异常类型, 就会将异常向上传递给到上层调用者。

5. 无论是否找到匹配的异常类型,finally 中的代码都会被执行到(在当前方法结束之前执行)。

6. 如果上层调用者也没有处理的了异常,就继续向上传递。

7. 如果一直到 main 方法也没有合适的代码处理异常,就会交给 JVM 来进行处理,此时程序就会异常终止。

自定义异常类 

Java 中虽然已经内置了丰富的异常类,但是并不能完全表示实际开发中所遇到的一些异常,此时就需要维护符合我们实际情况的异常结构。

例如:写登录代码时,根据登录的信息匹配情况,我们就需要有自定义的异常类来维护。

虽然这个代码也能很好地表达我们需要的功能,但是异常只是说运行时异常,不够明确,如果是 用户名异常 和 密码异常 就更完美了。

class UserNameExcetion extends RuntimeException{
    public UserNameExcetion(String str1) {
        super(str1);
    }
}
class PassWordException extends RuntimeException{
    public PassWordException(String str2) {
        super(str2);
    }
}
public class Test {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("UserNmae: ");
            String str1 = scanner.nextLine();
            if (!str1.equals("我要学编程")) {
                throw new UserNameExcetion("用户名异常~");
            }
            System.out.print("PassWord: ");
            String str2 = scanner.nextLine();
            if (!str2.equals("123456")) {
                throw new PassWordException("密码异常~");
            }
            System.out.println("登录成功~");
            break;
        }
    }
}

当然这里我只是简单的把异常 throw 了出来,并没有去处理抛出的异常。 

注意:

1. 自定义异常通常会继承自 Exception 或者 RuntimeException。

2. 继承自 Exception 的异常默认是受查异常。

3. 继承自 RuntimeException 的异常默认是非受查异常。 

好啦!本期 初始Java篇(JavaSE基础语法)(9)认识异常 的学习之旅就到此结束了!相信通过这篇文章的学习,你对Java中异常的了解将会更进一步!我们下一期再一起学习吧!

评论 92
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

我要学编程(ಥ_ಥ)

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

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

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

打赏作者

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

抵扣说明:

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

余额充值