什么是异常?--------->带你认识异常

一、异常的概念与体系结构

1.异常的概念

在Java中,将程序执行过程中发生的不正常行为称为异常。比如之前写代码经常遇到的:

  1. 算术异常
 public static void main(String[] args) {
        System.out.println(10/0);
        
    }
//运行结果
Exception in thread "main" java.lang.ArithmeticException: / by zero
at day1010.test2.Exception.main(Exception.java:5)   
  1. 数组越界异常
int [] arr={1,2,3};
System.out.println(arr[100]);
//运行结果
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100
at day1010.test2.Exception.main(Exception.java:8) 
  1. 空指针异常
public int num=10;
    public static void main(String[] args) {
        //System.out.println(10/0);

       /* int [] arr={1,2,3};
        System.out.println(arr[100]);*/
        Exception t=null;
        System.out.println(t.num);
    }
//Exception in thread "main" java.lang.NullPointerException
at day1010.test2.Exception.main(Exception.java:11)

从上面的内容可以看到,Java中不同类型的异常,都有与其对应的类来进行描述。

2.异常的体系结构

异常种类繁多,为了对不同异常或者错误进行很好的分类管理,Java内部维护了一个异常的体系结构:
在这里插入图片描述
从上图可以看到:

  1. Throwable:是异常体系的顶层类,其派生出两个重要的子类,Error和Expection。
  2. Error: 指的是Java虚拟机无法解决的严重问题。比如:JVM的内部错误、资源耗尽等,典型代表:StackOverflowError和OutOfMemoryError,一旦发生,无药可救。
  3. Expection:异常产生后程序员可以通过代码进行处理,使程序继续执行。比如:感冒、发烧。我们平时所说的异常就是Expection。

3.异常的分类

异常可能在编译时发生,也可能在程序运行时发生,根据发生的时机不同,可以将异常分为:

  1. 编译时异常
    在程序编译期间发生的异常,称为编译时异常,也称为受检查异常。
public class Person {
    private String name;
    private String gender;
    int age;
    //想让该类支持深拷贝,重写Object类的clone方法即可

    @Override
    protected Person clone()  {
        return (Person)super.clone();
    }
}
//编译时报错
java: 未报告的异常错误java.lang.CloneNotSupportedException; 必须对其进行捕获或声明以便抛出
  1. 运行时异常

  在程序执行期间所发生的异常,称为运行时异常,也称为非受检查异常。
  RunTimeExpection以及子类对应的异常,都称为运行时异常。比如:NullPointerExpection、ArithmeticExpection。

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

二、异常的处理方式

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;
        }

缺陷:正常流程和错误处理流程代码混在一起,代码整体显得比较混乱。

  1. EAFP:It’s Easier to Ask Forgiveness than Permission.意思是先操作,遇到问题再处理。即:事后认错型
   try{
            登陆游戏();
            开始匹配();
            游戏确认();
            载入游戏画面();
        }catch (登陆游戏异常){
            处理登陆游戏异常;
        }catch (开始匹配异常){
            处理开始匹配异常;
        }catch (游戏确认异常){
            处理游戏确认异常;
        }catch (载入游戏画面异常){
            处理载入游戏画面异常;
        }

优势:正常流程和错误流程是分开的,程序员更关注正常流程,代码更清晰,容易理解代码异常处理的核心思想就是EAFP。
在java中,异常处理的5个主要关键字throw、try、catch、final、throws

2.异常的抛出

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

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

需求:实现一个获取数组中任意位置元素的方法。

public class test {
    public static int getElement(int[] array,int index){
        if(null==array){
            throw new NullPointerException("传递的数组为null");
        }
        if(index<0||index>=array.length){
            throw new ArrayIndexOutOfBoundsException("传递的数组下标越界");
        }

        return array[index];
    }

    public static void main(String[] args) {
        int[] array={1,2,3};
        System.out.println( getElement(array,0));
    }
}

注意:

  1. throw必须写在方法体内部。
  2. 抛出的对象必须是Expection或者Expection的子类对象。
  3. 如果抛出的是RunTimeExpection或者RunTimeExpection的子类,则可以不用处理,直接交给JVM来处理。
  4. 如果抛出的是编译时异常,用户必须处理,否则无法通过编译。
  5. 异常一旦抛出,其后的代码就不会执行。

3.异常的捕获

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

3.1异常声明throws

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

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

需求:加载指定的配置文件config.ini

public class Config {
    File file;
    //FileNotFoundException:编译时异常,表明文件不存在,此处不处理,也没有能力处理,
    // 应将错误信息报告给调用者,让调用者检查文件名字是否给错了
    public void OpenConfig(String filename) throws FileNotFoundException{
        if(filename.equals("config.ini")){
            throw new FileNotFoundException("配置文件名字不对");
        }
        //打开文件
    }
    public void readConfig(){
        
    }
}

注意事项:

  1. throws必须跟在方法的参数列表之后
  2. 声明的异常必须是Expection或者Expection的子类
  3. 方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可。
public class Config {
    File file;
    //FileNotFoundException:编译时异常,表明文件不存在,此处不处理,也没有能力处理,
    // 应将错误信息报告给调用者,让调用者检查文件名字是否给错了
    public void OpenConfig(String filename) throws IOException,FileNotFoundException {
        if(filename.endsWith(".ini")){
            throw new IOException("文件不是.ini文件");
        }
        if(filename.equals("config.ini")){
            throw new FileNotFoundException("配置文件名字不对");
        }
        //打开文件
    }
    public void readConfig(){

    }
}

  1. 调用声明抛出异常的方法时,调用者必须对该异常进行处理,或者继续使用throws抛出
public static void main(String[] args)   {
        Config config=new Config();
        config.OpenConfig("config.ini");
    }

将光标放在抛出的异常方法上,Alt+Enter快速处理:
在这里插入图片描述
处理之后的代码:


 public static void main(String[] args) throws IOException {
        Config config=new Config();
        config.OpenConfig("config.ini");
    }

3.2 try-catch捕获并处理

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

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

注意

  • []中表示可选项,可以添加,也可以不用添加
  • try中的代码可能会抛出异常,也可能不会

代码示例
读取该配置文件,如果配置文件名字不是指定名字,抛出异常,调用者进行异常处理。

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

    }

    public static void main(String[] args)   {
        Config config=new Config();
        try{
            config.OpenConfig("config.txt");
            System.out.println("文件打开成功");
        }catch(IOException e){
            //异常的处理方式
            System.out.println(e.getMessage());//只打印异常信息
            System.out.println(e);//打印异常类型
            e.printStackTrace();//打印信息最全面
        }
        
        //一旦异常被捕获处理了,此处的代码会执行
        System.out.println("异常被处理了");
    }
}

关于异常的处理方式,我们需要分情况而定:

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

注意事项:

  1. try块内抛出异常位置之后的代码将不会被执行
  2. 如果抛出异常类型与catch时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往外抛,知道JVM收到后中断程序----异常是按照类型来捕获的。
public static void main(String[] args) {
        try{
            int[] array={1,2,3};
            System.out.println(array[3]);//此处会抛出数组越界异常
        }catch(NullPointerException e){ //捕获时捕获的是空指针异常---真正的异常无法捕获到
            e.printStackTrace();
        }
        System.out.println("后续代码");
    }
//运行结果
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
at day1010.test3.Test1.main(Test1.java:7)
  1. try中可能会抛出多个不同的异常对象,则必须用多个catch来捕获-----即多种异常,多次捕获。
public static void main(String[] args) {
        int[] arr={1,2,3};

        try{
            System.out.println("before");
            //arr=null;
            System.out.println(arr[100]);
            System.out.println("after");
        }catch(ArrayIndexOutOfBoundsException e){
            System.out.println("这是个数组下标越界异常");
            e.printStackTrace();
        }catch(NullPointerException e){
            System.out.println("这是个空指针异常");
            e.printStackTrace();
        }
        System.out.println("after try catch");
    }

如果多个异常的处理方式完全相同,也可以写成这样:

catch(ArrayIndexOutOfBoundsException|NullPointerException){
}

如果异常之间具有父子关系,一定是子类异常在前catch,父类异常在后catch,否则语法错误:

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

        try{
            System.out.println("before");
            arr=null;
            System.out.println(arr[100]);
            System.out.println("after");
        }catch(Exception e){//Expection可以捕获到所有的异常
            e.printStackTrace();
        }catch(NullPointerException e){
            System.out.println("这是个空指针异常");
            e.printStackTrace();
        }
        System.out.println("after try catch");
    }
//运行结果
java: 已捕获到异常错误java.lang.NullPointerException

正确代码:

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

        try{
            System.out.println("before");
            arr=null;
            System.out.println(arr[100]);
            System.out.println("after");
        }catch(NullPointerException e){
            System.out.println("这是个空指针异常");
            e.printStackTrace();
        }catch(Exception e){//Expection可以捕获到所有的异常
            e.printStackTrace();
        }
        System.out.println("after try catch");
    }
//运行结果
before
这是个空指针异常
after try catch
java.lang.NullPointerException
	at day1010.test3.Test1.main(Test1.java:10)

  1. 可以通过一个catch捕获所有的异常,即多个异常,一次捕获(不推荐)
public static void main(String[] args) {
        int[] arr={1,2,3};
        try{
            System.out.println("before");
            arr=null;
            System.out.println(arr[100]);
            System.out.println("after");
        }catch(Exception e){//Expection可以捕获到所有的异常
            e.printStackTrace();
        }
        System.out.println("after try catch");
    }
//运行结果
before
after try catch
java.lang.NullPointerException
at day1010.test3.Test1.main(Test1.java:9)

由于Expection类是所有异常类的父类。因此可以用这个类型表示捕捉所有的异常。
注:catch进行类型匹配的时候,不光会匹配相同类型的异常对象,也会捕捉目标异常类型的子类对象。比如前面的代码,NullPointerException和ArrayIndexOutOfBoundsExpection都是Expection的子类,因此都能被捕获到。

3.3 finally

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

语法格式:
try{
//可能会发生异常的代码
}catch(异常类型 e){
//对捕获到的异常进行处理
}finally{
//此处的语句无论是否发生异常,都会被执行到
}
//如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行
public static void main(String[] args) {
        try{
            int[] arr={1,2,3};
            arr[100]=10;
            arr[0]=10;
        }catch(ArrayIndexOutOfBoundsException e){
            System.out.println(e);
        }finally {
            System.out.println("finally中的代码一定会执行");
        }
        System.out.println("如果没有抛出异常,或者异常被处理了,try-catch后的代码也会执行");
    }

那么问题来了,既然finally和try-catch-finally后的代码都会执行,那为什么还要有finally呢?
需求:实现getData方法,内部输入一个整型数字,然后将该数字返回,并在main方法中打印。

public class TestFinally {
    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中的代码");
        }
        System.out.println("try-catch-finally之后的代码");
        if(null!=sc){
            sc.close();
        }
        return 0;
    }

    public static void main(String[] args) {
        int d=getData();
        System.out.println(d);
    }
}
//运行结果
2
finally中的代码
2

上面的程序,如果正常输入,成功接收之后程序就返回了,try-catch-finally之后的代码根本没有执行。即输入流就没有被释放,造成资源泄露。
注意:finally中的代码一定会执行的,一般在finally中进行了一些资源清理的扫尾工作。
接着,我们来看一段代码;
这段代码的输出结果是什么呢?

 public static int func(){
        try{
            return 10;
        }finally {
            return 20;
        }
    }

    public static void main(String[] args) {
        System.out.println(func());
    }

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

4.异常的处理流程

关于调用栈:
  方法之间是存在相互调用关系的,这种调用关系我们可以用"调用栈“来描述。在JVM中有一块内存空间称为"虚拟机栈"专门存储方法之间的调用关系。在代码中出现异常的时候,我们就可以使用e.printStackTrace();的方式查看出现异常代码的调用栈。
  如果本方法中没有合适的处理异常的方式,就会沿着调用栈向上传递。

public static void main(String[] args) {
        try{
            func();
        }catch(ArrayIndexOutOfBoundsException e){
            e.printStackTrace();
        }
        System.out.println("after try catch");
    }
    public static void func(){
       int[] arr={1,2,3};
        System.out.println(arr[100]);
    }
//运行结果
java.lang.ArrayIndexOutOfBoundsException: 100
at day1010.test3.TestFinally.func(TestFinally.java:17)
at day1010.test3.TestFinally.main(TestFinally.java:9)
after try catch

如果向上一直传递都没有合适的方法处理异常,最终就会交给JVM处理,程序就会异常终止(和我们最开始未使用try-catch时是一样的)。

public static void main(String[] args) {
        func();
        System.out.println("after try catch");
    }
public static void func(){
       int[] arr={1,2,3};
        System.out.println(arr[100]);
    }
//运行结果
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100
at day1010.test3.TestFinally.func(TestFinally.java:21)
at day1010.test3.TestFinally.main(TestFinally.java:8)

可以看到,程序以及异常终止了,没有执行 System.out.println("after try catch");

异常处理流程总结:

  • 程序先执行try中的代码;
  • 如果try中的代码出现异常,就会结束try中的代码,看和catch中的异常类型是否匹配;
  • 如果找到匹配的异常类型,就会执行catch中的代码;
  • 如果没有找到匹配的异常类型,就会将异常向上传递到上层调用者;
  • 无论是否找到匹配的异常类型,finally中的代码都会被执行到(在该方法结束之前执行);
  • 如果上层调用者也没有处理的了异常,就继续向上传递;
  • 一直到main方法也没有合适的代码处理异常,就会交给JVM处理,此时程序就会异常终止。

三、自定义异常类

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

public class Login {
    String name = "admin";
    String password = "123456";

    public void loginInfo(String n, String p){
        if(!name.equals(n)){
            throw new UserException("用户名输入有误");
        }

        if(!password.equals(p)){
            throw new PasswordException("密码输入有误");
        }

        //....
    }

    public static void main(String[] args) {
        Login login = new Login();
        login.loginInfo("admin","123456");
        System.out.println("login  ok");
    }
}

此时我们在处理用户名密码错误的时候可能就需要抛出两种异常。我们可以基于已有的异常类进行扩展(继承),创建和我们业务相关的异常类。
具体方式:

  1. 自定义异常类,然后继承自Expection或者RunTimeExpection
  2. 实现一个带有String类型参数的构造方法。参数含义:出现异常的原因。
class UserError extends Exception {
 public UserError(String message) {
 super(message);
 }
}
class PasswordError extends Exception {
 public PasswordError(String message) {
 super(message);
 }
}

此时login代码可以改写成

public static void main(String[] args) {
 try {
 login("admin", "123456");
 } catch (UserError userError) {
 userError.printStackTrace();
 } catch (PasswordError passwordError) {
 passwordError.printStackTrace();
 }
}
public static void login(String userName, String password) throws UserError, 
PasswordError {
 if (!Test.userName.equals(userName)) {
 throw new UserError("用户名错误");
 }
 if (!Test.password.equals(password)) {
 throw new PasswordError("密码错误");
 }
 System.out.println("登陆成功");

注意事项:

  1. 自定义异常通常会继承自Expection或者RunTimeExpection
  2. 继承自Exception的异常默认是受检查异常
  3. 继承自RunTimeException

一些总结

1.throw和throws的区别

  1. throw出现在方法体内部,throws在方法头,跟在参数列表之后。
  2. throws表示出现异常的一种可能性,并不一定会发生异常;throw则是抛出了异常,执行throw则一定会抛出某种异常。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值