[Java]剖析异常处理机制与常见面试题

专栏简介 :java语法

创作目标:从不一样的角度,用通俗易懂的方式,总结归纳java语法知识.

希望在提升自己的同时,帮助他人,与大家一起共同进步,互相成长.

学历代表过去,能力代表现在,学习能力代表未来!


目录

前言

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

1.1异常的概念

 1.2异常的体系结构 

1.3异常的分类

二.异常的处理

 2.1 throws

 2.2 try_catch

 2.3 防御式编程

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

2.EAFP:It is Easier to Ask Forgiveness than Permission.事后认错比事前申请更容易.即:事后认错型.

 2.4 深入理解 try_catch

 2.5异常中的两个重要方法

 2.6finally子句

2.6.1finally语句通常使用在什么情况下

2.6.2 try与finally连用没有catch

2.6.3 finally 子句失效.

 2.7 final finally finalize 的区别

 2.9 常见面试题

1.try_catch_finally中如果catch中return了,finally还会执行吗?

2.Error和Exception的区别是什么?

3.JVM是如何处理异常的?

4.throw与throws的区别是什么?

5.说出下面代码的执行结果---出自<>编程思想>

6.finally语句块中可以使用return吗?

三.自定义异常

总结



前言

异常是java知识体系中较为零散的知识点,日常学习中总是遇到一点学习一点,很难形成自己的知识体系,鉴于以上原因博主系统的整理了异常的知识点希望可以帮到你!


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

1.1异常的概念

        异常是一个十分广泛的概念,不仅仅存在于程序的运行过程中,例如:日常生活中我们去滑雪场滑雪,当我们脚踩滑雪板遇到雪坡,高高跃起时稍有不慎落地就会有摔倒的危险,当我们在空中的瞬间大脑就会向身体抛异常.大脑: 哥们!一定要小心啊!不然你就要摔倒了! 身体:收到!谢谢提醒!(并作出相应反应).设想如果大脑不向身体抛异常,我们就这么傻傻的落下去,必定会摔的很惨!

 


 

        而思维缜密追求完美的程序员在日常开发中,即使绞尽脑汁把程序写的尽善尽美,也难免会出错.有时通过代码很难去控制,比如数据格式不对,数组越界,算数异常.....因此java把程序运行过程中出现的不正常行为称为异常,例如我们在写代码时经常遇到的:

1.算数异常

public static void main(String[] args) {
        System.out.println(10/0);
    }
Exception in thread "main" java.lang.ArithmeticException

2.数组越界异常

public static void main(String[] args) {
       int[]arr = {1,2,3};
       arr[100] = 10;
    }
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException

3.空指针异常 

public static void main(String[] args) {
       int[]arr = null;
        System.out.println(arr.length);
    }
Exception in thread "main" java.lang.NullPointerException

从上述例子可以看出:

  • java中不同类型异常,都有其所对应的
  • 异常的作用:增强程序的健壮性.

 


 

 1.2异常的体系结构 

异常的种类繁多,为了对不同种类的异常进行更好的分类管理,java内部维护了一个异常的体系结构.


1.3异常的分类

        由上文可知每一个异常都对应一个类,因此异常的发生就是new异常对象,所以异常都是在运行阶段发生的,因为只有在运行阶段才能new对象,

       1.编译时异常

         要求程序员在编写程序阶段必须对这些异常进行处理,如果不处理编译器会报错,因此得名编译期异常,也称受查时期异常.(Checked Execption)

        2.运行时异常

         程序员在编写程序阶段可以预处理也可以不管,称为运行时期异常,也称非受查时期异常.(UnChecked Execption)

注意:编译时异常与运行时异常的区别:

  • 编译时异常并不意外着异常发生在编译时期,只是提醒程序员在编写程序的时期必须预先对这种异常进行处理,如果不处理就会报错,因为编译时异常发生概率比较高
  • 运行时期异常指程序以通过编译得到class文件了,再由JVM执行过程中出现的错误.运行时异常发生概率比较低.

eg1:运行时异常

public static void main(String[] args) {
        System.out.println(100/0);
        //这里的hello world既没有输出也没有执行.
        System.out.println("hello world");
    }

当程序执行到 System.out.println(100/0);

此处发生了ArithmeticException异常,底层会new一个ArithmeticException异常对象然后抛出.

由于(100/0)是在main方法中调用的,所以异常会抛给main方法,但main方法不会理睬,只能交给JVM以最粗暴的方式解决,那就是终止程序.

所以此时System.out.println("hello world");并不会执行.

注意:

ArithmeticException继承自RuntimeException属于运行时异常,程序预处理阶段不需要对这种异常进行处理.

eg2:编译时异常 

class Teacher implements Cloneable{
    private String name;
    private int age;

    //想让该类支持深拷贝,重写Object类的clone()方法即可.
    
    @Override
    protected Object clone()  {
        return super.clone();
    }
}
    /** 
     * 编译时报错
     * Unhandled exception: 未报告的异常错误java.lang.CloneNotSupportedException; 必须对其进行捕获或声明以便抛出
     */

这段异常表示clone()方法在执行过程中可能会出现CloneNotSupportedException异常.

叫做克隆不支持异常,这个异常的直接父类是Exception,所以CloneNotSupportedException属于编译时异常.

解决方式一.throws上报给方法调用者(推卸责任,调用者知道)也叫Ducks

class Teacher implements Cloneable {
    private String name;
    private int age;

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

解决方式二.try-catch捕捉处理(调用者不知道)

class Teacher implements Cloneable {
    private String name = "zhangsan";
    private int age = 38;
    
    @Override
    protected Object clone(){
        try {
            return super.clone();

        }catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        return null;
    }
}


二.异常的处理

 2.1 throws

 在方法的声明上使用throws关键字抛出,谁调用这个方法就由谁来解决这个异常,由方法的调用者来解决.

这种处理异常的方式叫上报也叫潜水(Duck)

 2.2 try_catch

 这个异常不会上报,自己把异常处理了.异常到此为止了.

注意:

  • 只要异常没捕捉,采用上报的方式让JVM知道,此方法的后续代码不会执行.
  • try语句块中某行代码出现异常,该行后面的代码不会执行.
  • try_catch语句块捕获异常后,后续代码会执行.

eg: 

public static void FileTest()throws FileNotFoundException {
        System.out.println("打开文件");
        FileMyData();//该方法可能有异常

        //以上代码出现异常这里将无法执行
        System.out.println("关闭文件");
    }
public static void FileTest(){
        try{
            FileMyData();
            System.out.println("打开文件");
            }catch(FileNotFoundException e){
                     System.out.println("打开文件失败");
                     System.out.println(e);//以程序语言输出异常
            }
            System.out.println("关闭文件");//会执行
    }

注意:

  • 一般由main()函数调用的方法不建议throws,因为一但发生异常直接交给JVM,JVM只能终止程序.
  • 一般建议main()函数中的异常用try_catch捕捉.

try {
    
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

 注意:e引用中保存的是new出来的异常对象的地址.


 因此以后开发过程中要抛异常(throw)还是捕获(try_catch),视开发者的具体情况而定.

 2.3 防御式编程

         出现错误的代码再正常不过,及时的发现并解决问题才是好的程序员,因此出现错误并及时通知程序员才是最关键的,以下提供两种通知方式:

以我们常见的游戏王者荣耀举例,游戏的每一个环节所对应的方法都有可能出错,所以我们每一个环节都要注意.

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

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

缺点:该方法的正常代码和错误代码混在一起,整体代码显得非常凌乱.

2.EAFP:It is Easier to Ask Forgiveness than Permission.事后认错比事前申请更容易.即:事后认错型.

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

优点:正常流程与错误流程分开程序员更关注正常流程.代码清晰明了. 


 2.4 深入理解 try_catch

  1. catch后面的小括号类型可以是具体的异常类型也可以说异常的父类的类型,为了方便后续调试一般建议写具体的异常类型.
  2. catch可以写多个,为了方便后续的调试,建议一个一个精确的处理.
  3. catch写多个时异常类型从上到下必须遵循从小到大,如果第一个异常类型非常大后续catch将不再捕获.
  4. 自JDK1.8之后,一个catch中可以用 | 分割来捕获多个异常.
 public static void main(String[] args) {
        try {
            FileInputStream fileInputStream = new FileInputStream("D:\\data\1.txt");
        }catch (FileNotFoundException e){
            System.out.println("该文件不存在");
        }
        //等价于
        try {
            FileInputStream fileInputStream = new FileInputStream("D:\\data\1.txt");
        }catch (Exception e){//多态原理:Exception e = new FileNotFoundException
            System.out.println("该文件不存在");
        }
    }
public static void main(String[] args) {
        try {
            FileInputStream fileInputStream = new FileInputStream("D:\\data\1.txt");
        }catch (IOException m){
            System.out.println("读取文件不存在");
        }catch (FileNotFoundException e){
            System.out.println("该文件不存在");
        }
        //异常:Exception 'java.io.FileNotFoundException' has already been caught

由于IOException为FileNotFoundException的父类,如果IOException写在前面,那么FileNotFoundException将无法执行导致编译无法通过.

public static void main(String[] args) {
        try {
            FileInputStream fileInputStream = new FileInputStream("D:\\data\1.txt");
            System.out.println(100/0);
        }catch (IOException |ArithmeticException |NullPointerException e){
            System.out.println("读取文件不存在,算数异常,空指针异常");
        }

 2.5异常中的两个重要方法

方法名作用
String getMessage()返回异常的详细消息字符串
void printStackTrack()追踪堆栈异常信息(采用异步线程)

 


 2.6finally子句

2.6.1finally语句通常使用在什么情况下

  • 有些资源在打开的情况下发生了异常,那么关闭资源的操作就无法执行,所以finally通常使用在资源的释放或关闭.
  • 有些try语句块有多个catch,如果每个catch中都有相同的语句,那么为了使代码更简洁,可以统一存放在finally语句中.(除了异常的对象,异常的对象只能在catch语句中使用)
public static void main(String[] args) {
        FileInputStream fis = null;
        //声明到try的作用域之外,才能被finally执行
        try {
            fis = new FileInputStream("D:\\data\1.txt");
            String s = null;
            System.out.println("helloworld");
        }catch (FileNotFoundException e){
            e.printStackTrace();

        }
        finally {
            if (fis!=null){//避免空指针异常
                try {//close()可能有异常
                    fis.close();
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
        }
    }

2.6.2 try与finally连用没有catch

public static void main(String[] args) {
        try {
            System.out.println("hello world");
            return;
        }finally {
            System.out.println("finally执行了");
        }
}

以上代码执行顺序:

  1. 先执行try..
  2.  再执行finally.
  3. .最后return.

注意: try和finally都不能单独使用必须联合使用.finally语句块中的代码一定执行.


2.6.3 finally 子句失效.

只有System.exit(0),和断电可以使finally子句失效.

 public static void main(String[] args) {
        try {
            System.out.println("hello world");
            System.exit(0);//程序直接结束运行
        }finally {
            System.out.println("finally执行了");
        }
}

 2.7 final finally finalize 的区别

  • final关键字
  1. final修饰的无法被继承
  2. final修饰的方法无法被重写
  3. final修饰的变量无法被重新赋值
  4. final修饰的引用类型不能修改
  • finally关键字
  1. finally语句块必须被执行
  2. finally必须和try联合使用
  • finalize标识符
  1. 是一个Object类中的方法名
  2. 这个方法是垃圾回收器GC负责调用的

 2.9 常见面试题

1.try_catch_finally中如果catch中return了,finally还会执行吗?

答:会执行,且在return之前执行

public static int test3(int i){
        try{
            System.out.println(i/0);
            i = 20;
        }catch (ArithmeticException e){
            i = 30;
            return i;
            /**
             * 此时 return 30;已经形成返回路径,返回路径一但形成就不会被修改,
             * 对应return来讲i相等与一个常量30.当程序准备return时发现还有finally
             * 没有执行,会先去执行finally子句中的代码.但此时return已不会被修改.
             */
        }finally {
            i = 40;
        }
        return i;
    }

2.Error和Exception的区别是什么?

  • Error 类型的错误通常为虚拟机错误,如:系统崩溃,内存不足,栈溢出等,编译器不会对这类错误进行检测,JAVA应用也不会对这类错误进行捕获,一但发生程序就会终止,仅靠应用程序本身无法恢复.
  • Exception 类型的错误可以在应用程序中捕获并处理,处理后应用程序就能恢复正常.

3.JVM是如何处理异常的?

  • 一个方法中如果发生异常,这个方法会new一个异常对象并转交给JVM,该异常对象包含异常名称,异常描述以及异常发生时程序状态,创建异常对象并交给JVM对象的过程称为抛出异常,可能经过一系列的方法调用才进入抛出异常的方法,这一系列方法调用的有序列表称为调用栈.
  • JVM会顺着调用栈去查找是否有需要处理异常的代码.如果有,JVM会把发生的异常传递给它,如果没有,JVM会将异常交给默认的异常处理器(为JVM内部的一部分),默认异常处理器会打印异常信息并终止程序.

4.throw与throws的区别是什么?

  • throw 关键字用在方法内部,只能用来抛出一种异常,抛出一个代码块或方法的异常,受查异常和非受查异常都能被抛出.
  • throws 关键字用在方法声明上,可以抛出多个异常,用来表示该方法可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则该方法签名中也要用throws声明相关的异常.

5.说出下面代码的执行结果---出自<<java编程思想>>

class Annoyance extends Exception{

}
class Sneeze extends Annoyance{

}
class Human{
    public static void main(String[] args)throws Exception {
            try {
                try {
                    throw new Sneeze();
                }catch (Annoyance a){//多态的思想:父类捕获子类异常
                    //相当于 Annoyance a = new Sneeze();
                    System.out.println("Caught Annoyance");
                    throw a;
                }
            }catch (Sneeze s){//子类捕获子类异常一定成功
                System.out.println("Caught Sneeze");
                return;//先执行finally子句再return
            }finally {
                System.out.println("hello world");
            }
        }
    }
输出结果:Caught Annoyance
        Caught Sneeze
        hello world

6.finally语句块中可以使用return吗?

答:可以但不建议,因为可能影响catch中return的内容.

public static int test3(int i){
        try{
            System.out.println(i/0);
            i = 20;
        }catch (ArithmeticException e){
            i = 30;
            return i;
        }finally {
            i = 40;
            return i;//此时返回路径又变了,由于程序只能执行一个return所以直接返回40.
        }
    }
    public static void main(String[] args) {
        int i = 10;
        test3(i);
}

三.自定义异常

        在实际开发中会面临各种各样的问题,JDK内置的异常肯定是不够用的,这是就需要我们自定义异常,习惯上定义一个异常需要两个构造函数,一个无参构造函数,一个有详细信息的构造函数(Throwable中的toString()方法会打印这些信息调试时非常有用)

自定义异常创建步骤:

  1. 第一步:编写一个类继承Exception或者RuntimeException
  2. 第二步:提供两个构造方法,一个无参的,一个带有String参数的.
class FileDamage extends Exception{//自定义异常:文件损坏异常
    public FileDamage() {
        super();
    }

    public FileDamage(String message, Throwable cause) {
        super(message, cause);
    }
}


总结

        综上所述,当你抛出异常并捕获异常时由很多的情况需要考虑,尤其是在业务需求庞大的项目里,大部分异常都是为了改善代码的可读性和健壮性.了解异常的基本逻辑在后续的学习中才能有跟深入的理解.码字不易,您的支持是我创作的不懈动力!

评论 36
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Node_Hao

您的支持是我创作的不懈动力

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

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

打赏作者

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

抵扣说明:

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

余额充值