Java语法之异常

1.异常的概念以及体系结构

        1.1 异常的概念

        在生活中人会生病,比如咳嗽流鼻涕,头晕等,程序也一样,比如:数据格式不匹配,网络不通畅,内存报警等.在Java中,我们把程序执行的不正常行为称为异常.

        比如:

        1. 算数异常

System.out.println(10 / 0);

// 执行结果

Exception in thread "main" java.lang.ArithmeticException: / by zero

        2.数组越界异常 

nt[] arr = {1, 2, 3}; System.out.println(arr[100]);

// 执行结果

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100

        3.空指针异常

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

// 执行结果

Exception in thread "main" java.lang.NullPointerException

        从以上例子我们可以看出,java中不同类型的异常,都有对应的类来进行描述,简而言之,异常其实就是类 

        1.2 异常的体系结构

        为了对异常进行一个比较好的分类,Java内部维护了一个异常的体系结构:

由图可知:

1. Throwable是所有异常的父类,然后由它派生出俩个子类: Error 和 Exception

2.  Error: java虚拟机无法解决的严重错误, 比如: JVM的内部错误,资源耗尽(栈溢出:StackOverflowError和OutOfMemoryError)一旦发生,没有任何东西能阻止.如:人的死亡

3.  Exception: 异常产生之后程序员可以通过代码处理,使程序继续执行,比如:普通的感冒,发烧.

        1.3 异常的分类

        1. 编译时异常

        在编译时期发生的异常(写在编译器上就会冒红(非语法错误)),也成为受查异常(Checked Exception)

        比如之前写的clone()方法

         编译时出现的语法性错误,不能称之为异常。例如将 System.out.println 拼写错了, 写成了system.out.println. 此时编译过程中就会出错, 这是 "编译期" 出错。而运行时指的是程序已经编译通过得到class 文件了, 再由 JVM 执行过程中出现的错误

         2. 运行时异常

        在程序执行期间发生的异常,也成为非受查异常(Unchecked Exception)

        比如算数异常

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

具体想知道运行谁是运行时异常的子类可以自行去java官方文档查看

       

2.异常的处理

        2.1 防御式编程

        错误在代码中是客观存在的. 因此我们要让程序出现问题的时候及时通知程序猿. 主要的方式

                1. 事前防御型: LBYL: Look Before You Leap. 在操作之前就做充分的检查.

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

        缺点: 代码比较混乱

                2. 事后认错型: It's Easier to Ask Forgiveness than Permission. "事后获取原谅比事前获取许可更容易". 也就是先操作, 遇到问题再处理. (有点先斩后奏的味道了)
 

try {
登陆游戏();
开始匹配();
游戏确认();
选择英雄();
载入游戏画面();
...
        } catch (登陆游戏异常) {
处理登陆游戏异常;
} catch (开始匹配异常) {
处理开始匹配异常;
} catch (游戏确认异常) {
处理游戏确认异常;
} catch (选择英雄异常) {
处理选择英雄异常;
} catch (载入游戏画面异常) {
处理载入游戏画面异常;
}
        ......
        throw new XXXException("异常产生的原因");
public static int getElement(int[] array, int index){
    if(null == array){
        throw new NullPointerException("传递的数组为null");

                优势: 正常流程和错误流程是分开的,代码更加清晰.

        在Java中,异常处理主要用的关键字: throw,try,catch,final,throws. 接下来我们要具体介绍这几个关键字.

        2.2 异常的抛出

        抛出异常的方式有很多:

        1. 手动触发

        2. 某段程序触发

       我们主要讲1, 手动触发异常,我们要使用一个关键字: throw , 抛出一个指定的异常对象,将错误信息告知给调用者(一般抛出的是自定义异常)

        语法格式:

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

        比如这数组指向为空的异常.就是我们手动跑出来的

        注:

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

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

        3. 如果抛出的是编译时异常,用户必须处理,不然用户无法通过编译.

        4. 异常一旦抛出,后面的代码就不能执行了

         2.3 异常的捕获

                我们捕获异常一般有俩种方法:  throws 和 try-catch捕获处理

                1. 异常申明throws

                  当用户在面对一个编译时(受查异常)的时候选择不处理,就可以借助throws来把异常抛给方法的调用者来处理.告诉方法的调用者,调用这个方法,就会抛出一个XXX异常.当前方法不处理异常,提醒方法的调用者处理异常.

                   语法格式:

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

 放在参数列表的后面

        比如上面的clone().

        我们再看看Object里面的clone()方法

因为所有的类都默认继承Object类,又因为Object里面抛出了CloneNotSupportedException,所以继承它的类重写它的clone方法都要throws这个异常(如果不处理).

        注:

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

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

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

public class Config {
    File file;
    // public void OpenConfig(String filename) throws IOException,FileNotFoundException{
// FileNotFoundException 继承自 IOException
    public void OpenConfig(String filename) throws IOException{
        if(filename.endsWith(".ini")){
            throw new IOException("文件不是.ini文件");
        } if(
                filename.equals("config.ini")){
            throw new FileNotFoundException("配置文件名字不对");
        } // 打开文件
    }
    public void readConfig(){
    }
}

                2. 使用try-catch来进行捕获并且处理

                上面的throws并没有对异常进行处理,如果真的要由调用者来处理的话就要使用try-catch.

                语法格式

                try{

                        此处是有可能出现异常的代码

                }catch(要捕获的类型1 e){

                        如果try抛出的异常和catch捕获的异常类型一致,或者是try的父类,就会被捕获到,然后对异常进行正确处理,处理完之后,跳出try-catch,继续执行后续代码

                }catch(要捕获的类型2 e){

                        同上

                }finally{

                这里的代码一定会执行到(一般用来关闭资源)

                }

                后续代码,当异常捕获到,异常被处理了,这里的代码一定会执行,但是捕获到了,但是捕获的类型不对,这里的代码就不会被执行.

        注意: try中的代码可能会抛出异常,也可能不会

        我们来看一个例子:

   public static void main(String[] args) {
        System.out.println("before......");
        try {
            System.out.println(10/0);
            System.out.println("11234");//这里的代码不能打印
        }catch (ArithmeticException e) {//这里面的参数就是你要捕获的异常(要匹配),捕获到了才会执行catch当中的内容
           //这个玩意是使用其他工具来打印的,不是用System.out.println
            e.printStackTrace();
            System.out.println("我来处理ArithmeticException异常了");

        }catch (Exception e) {//我们可以通过catch捕获多个异常,但是同一时刻只能抛出一个异常
            System.out.println("Exception异常了");//这个相当于充当了殿后的角色
        }
        System.out.println("after......");//这个代码不try catch就是jvm来处理的

    }

运行结果:

注意:

1. try里面异常代码后面的正常代码是不会被执行的

2. try里面捕获了才会执行catch续代码,catch里面的异常和你try里面捕获的异常要匹配(异常是按照类型来捕获的),如果抛出异常类型与catch时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,就会层层往上抛直到main没处理就会交给JVM.JVM收到后中断程序,这个和异常不进行(throws,try-catch)处理的结果一样

这里有张我自己总结的图:

3. e.printStackTrace(),打印的是这一块信息

4. 一般我们习惯把父类异常写在子类异常之后当作兜底,比如ArithmeticExceptionn继承于Exception,我们就把后者写在最后一个catch里面.(如果反着写就会报错)

5. 由于 Exception 类是所有异常类的父类. 因此可以用这个类型表示捕捉所有异常

异常的种类有很多, 我们要根据不同的业务场景来决定.

对于比较严重的问题(例如和算钱相关的场景), 应该让程序直接崩溃, 防止造成更严重的后果对于不太严重的问题(大多数场景), 可以记录错误日志e.printStackTrace(), 并通过监控报警程序及时通知程序猿对于可能会恢复的问题(和网络相关的场景), 可以尝试进行重试.

在我们当前的代码中采取的是经过简化的第二种方式. 我们记录的错误日志是出现异常的方法调用信息, 能很快速的让我们找到出现异常的位置. 以后在实际工作中我们会采取更完备的方式来记录异常信息.

        3. 关于finally

        一般,程序员在写代码的时候,有些代码不管程序发布发生异常都要执行,比如:程序种打开的资源: 网络连接,数据库连接,IO流等,不管在申明情况下都要对资源进行回收. 另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的。

语法格式:

try{

// 可能会发生异常的代码

}catch(异常类型 e){

// 对捕获到的异常进行处理

}finally{

// 此处的语句无论是否发生异常,都会被执行到

} / / 如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行

比如文件操作:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class TryCatchFinallyExample {
    public static void main(String[] args) {
        BufferedReader reader = null;
        try {
            // 打开文件并创建BufferedReader对象
            reader = new BufferedReader(new FileReader("example.txt"));

            // 逐行读取文件内容并打印
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            // 处理可能的IO异常
            System.err.println("发生IO异常: " + e.getMessage());
        } finally {
            // 确保资源被关闭
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    System.err.println("关闭文件时发生异常: " + e.getMessage());
                }
            }
        }
    }
}

        这里面: reader.close(); 就是在回收资源.

        2.4 常考面试题

                1. throw和throws的区别

                throw是当你需要在代码中手动抛出一个异常时,可以使用 throw.手动抛出一个异常

                throws是把这个异常往上层调用抛(不进行处理),用try-catch才进行处理.主要用于声明一个方法可能抛出的异常类型。(一般面向是受查异常)

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

                正常情况下:try-catch-finally,无论能否捕获/类型匹不匹配,都会执行finally

                特殊情况: 如果在try或者catch里面调用了System.exit(0),程序就会立即终止,不会执行finally.再如:在try-catch里面代码进入了死循环或者阻塞Thread.sleep(很长时间),都不会执行finally.

再如虚拟机崩溃,JVM被强制关闭(操作系统,硬件外部干预,如直接kill杀死进程),finally也不执行.

        2.5 异常的处理流程

        关于调用栈

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

这个东西之前我们就提到过:e.printStackTrace();可以看见异常代码的调用栈.如果本方法中没有合适的处理异常的方式, 就会沿着调用栈向上传递,如果向上一直传递都没有合适的方法处理异常, 最终就会交给 JVM 处理, 程序就会异常终止(和我们最开始未使用 try catch 时是一样的).层层向上直到mian为止还没有处理就给jvm直接终止这个程序.

        异常的处理流程小总结:

        1. 程序先执行try里面的代码

        2. 如果try种的代码出现异常就会结束try的代码,看和catch里面的异常代码是否匹配

        3. 如果找到匹配的异常类型,就会执行catch中的代码(否则就把异常传递给上层调用者,直到处理(正常运行)或者传递到main()还没处理,就交给JVM(直接终止程序))

        4. 无论是否找到匹配的异常类型,finally都会被执行

3.自定义异常

        虽然Java中提供了很多异常的类,但在实际开发中,我们需要面对不同的业务场景产生的异常,就需要自己定义一个异常类.

        我们来观察一下,java中自带的异常怎么写的

        1. 该类继承自Exception类/RuntimeException

        2. 在构造方法里面调用父类的构造方法

比如:我们来自己写一个用户登录功能

我们手动抛出的是一个受查异常需要通过throws来向上抛或者try-chatch进行处理

整体代码:


 class LogIn {
    private String userName = "admin";
    private String password = "123456";
    public static void loginInfo(String userName, String password) throws UserNameException {
        if (!userName.equals(userName)) {
        throw new UserNameException("用户名错误");
        }
        if (!password.equals(password)) {
            try {
                throw new PasswordException("密码错误");
            } catch (PasswordException e) {
                e.printStackTrace();
            }
        } System.out.println("登陆成功");
    }
    public static void main(String[] args) throws UserNameException {
        loginInfo("admin", "123456");
    }
}
class UserNameException extends Exception {
    public UserNameException(String message) {
        super(message);
    }
}
class PasswordException extends Exception {
    public PasswordException(String message) {
        super(message);
    }
}

如果继承的是RuntimeException


 class LogIn {
    private String userName = "admin";
    private String password = "123456";
    public static void loginInfo(String userName, String password) {
        if (!userName.equals(userName)) {
        throw new UserNameException("用户名错误");
        }
        if (!password.equals(password)) {

            throw new PasswordException("密码错误");
        }
         
    }
    public static void main(String[] args) {
        loginInfo("admin", "123456");
    }
}
class UserNameException extends RuntimeException {
    public UserNameException(String message) {
        super(message);
    }
}
class PasswordException extends RuntimeException {
    public PasswordException(String message) {
        super(message);
    }
}

注意:

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

继承自 Exception 的异常默认是受查异常(必须要进行处理)

继承自 RuntimeException 的异常默认是非受查异常(可以不处理,直接交给jvm)

4. 一些注意的点 

        

class Test2 {
    public static void a () throws ArithmeticException{
        int b = 1/0;
        System.out.println("这个是处理完异常之后才会调用出来的代码");
    }

    public static void main(String[] args) {
        try {
            a();
        }catch (ArithmeticException e){
            e.printStackTrace();

        }

        System.out.println("之后的代码");

    }
}

运行结果:

throws用在方法参数列表后面,注意的是,你如果调用该方法就必须对它进行处理,要么你把它继续往上抛,要么用try-catch来处理该异常.但是你如果都抛到了main方法里面,还想继续抛就直接给JVM处理,直接终止程序了,此时如果想打印在main里面调用该方法后面的代码就必须try-catch对该异常进行处理.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值