Java中的异常(Exception),史上最全的教程来啦~

创作不易,如果觉得这篇文章对你有帮助,欢迎各位老铁点个赞呗,您的支持是我创作的最大动力!

1 前言

Java中的Exception也是很重要的一部分,虽然经常使用Exception,但是基础知识比较零散,这里笔者也做一下总结吧,有利于以后形成良好的知识体系。

2 什么是异常

定义:
在程序运行过程中出现的错误,称为异常。异常就是程序运行过程中出现了不正常现象导致程序的中断。

在Java中,把各种异常现象进行了抽象形成了异常类。

3 异常的分类

3.1 异常的类图结构

先来整体认识下异常的类图结构:
在这里插入图片描述

3.2 异常的分类

异常主要分为:错误一般性异常(受控异常)运行时异常(非受控异常)

  • 错误
    如果应用程序出现了Error,那么将无法恢复,只能重新启动应用程序,最典型的Error的异常是:OutOfMemoryError

  • 受控异常
    这种异常属于一般性异常,出现了这种异常必须显示的处理,不显示处理java程序将无法编译通过。编译器强制普通异常必须try…catch处理,或用throws声明继续抛给上层调用方法处理,所以普通异常也称为checked异常

  • 非受控异常
    非受控异常也即是运行时异常(RuntimeException),这种系统异常可以处理也可以不处理,所以编译器不强制用try…catch处理或用throws声明,所以系统异常也称为unchecked异常

    此种异常可以不用显示的处理,例如被0除异常,java没有要求我们一定要处理, 当出现这种异常时,肯定是程序员的问题,也就是说,健壮的程序一般不会出现这种系统异常。

4 异常的处理

运行时异常(系统异常)不需要预处理,通过规范的代码可以避免产生这种异常
受检异常(编译时异常)必须预处理,否则编译报错,有两种预处理方式 :

  • 捕获处理
  • 抛出处理

4.1 异常捕获处理

4.1.1 try、catch和finally

异常的捕获和处理需要采用trycatch来处理,具体格式如下:

	try {
	
	} catch (OneException e) {
	
	} catch (TwoException e) {
	
	} finally {
	
	}
  • try中包含了可能产生异常的代码
  • try后面是catch,catch可以有一个或多个,catch中是需要捕获的异常
  • 当try中的代码出现异常时,出现异常下面的代码不会执行,马上会跳转到相应的catch语句块中,如果没有异常不会跳转到catch中
  • finally表示,不管是出现异常,还是没有出现异常,finally里的代码都执行,finally和catch可以分开使用,但finally必须和try一块使用,如下格式使用finally也是正确的
	try {
	
	} finally {
	
	}

4.1.2 异常捕获处理

异常捕获处理详细说明:

    private static void testException2() {
        try {
            //1、对可能产生异常的代码进行检视
            //2、如果try代码块的某条语句产生了异常, 就立即跳转到catch子句执行, try代码块后面的代码不再执行
            //3、try代码块可能会有多个受检异常需要预处理, 可以通过多个catch子句分别捕获
        } catch (异常类型1 e1) {
            //捕获异常类型1的异常, 进行处理
            //在开发阶段, 一般的处理方式要么获得异常信息, 要么打印异常栈跟踪信息(e1.printStackTrace())
            //在部署后, 如果有异常, 一般把异常信息打印到日志文件中, 如:logger.error(e1.getMessage());
        } catch (异常类型2 e1) {
            //捕获异常类型2的异常, 进行处理
            //如果捕获的异常类型有继承关系, 应该先捕获子异常再捕获父异常
            //如果没有继承关系, catch子句没有先后顺序
        } finally {
            //不管是否产生了异常, finally子句总是会执行
            //一般情况下, 会在finally子句中释放系统资源
        }
    }

【代码示例】

private static void testException3() {
        int i1 = 100;
        int i2 = 0;
        //try里是可能出现异常的代码
        try {
            //当出现被0除异常时,程序流程会执行到“catch(ArithmeticException arithmeticException)”语句,这里是运行是异常,编译可以通过
            //被0除表达式以下的语句永远不会执行
            int i3 = i1 / i2;

            //永远不会执行
            System.out.println(i3);

            //采用catch可以拦截异常
            //arithmeticException代表了一个ArithmeticException类型的局部变量
            //采用arithmeticException主要是来接收java异常体系给我们new的ArithmeticException对象
            //采用arithmeticException可以拿到更详细的异常信息
        } catch (ArithmeticException arithmeticException) {
            System.out.println("被0除了");
            arithmeticException.printStackTrace();
        }
    }

执行结果:
在这里插入图片描述

4.1.3 异常的捕获顺序

异常的捕获:一般按照由小到大的顺序,也就是先截获子异常再截获父异常
在这里插入图片描述
以上代码分析:
将IOException放到前面,会出现编译问题,因为IOExceptionFileNotFoundException的父类,所以截获了IOException异常后,IOException的子异常都不会执行到,所以再次截获FileNotFoundException没有任何意义

异常的截获一般按照由小到大的顺序,也就是先截获子异常,再截获父异常。

正确的捕获方式:

	try {
	  	 FileInputStream fis = new FileInputStream("test.txt");
	     fis.close();
	} catch (FileNotFoundException e) {
	     e.printStackTrace();
	} catch (IOException e) {
	     e.printStackTrace();
	}

这里只是为了说明捕获顺序,实际中,关闭流资源fis.close()是在finally语句块中处理的。

4.2 throws抛出处理

在定义方法时,如果方法体中有受检(编译时)异常需要预处理,可以捕获处理也可以抛出处理

处理异常时,使用throws抛出处理:

  • 谁调用这个方法,谁负责处理该异常
  • 在定义方法时,把异常抛出就是为了提醒方法的使用者,有异常需要预处理

在处理异常时,是选择捕获处理还是抛出处理

  • 一般情况下,在调用其他方法时,如果被调用的方法有受检(编译时)异常需要预处理,选择捕获处理,因为你调用了方法, 你负责处理该异常。
  • 在定义方法时,如果方法体中有受检异常需要预处理,可以选择捕获也可以选择抛出处理。如果方法体中通过throw语句抛出了一个异常对象,所在的方法应该使用throws声明该异常。

4.3 getMessage()和printStackTrace()

如何取得异常对象的具体信息,常用的方法主要有两种:

  • 获取异常描述信息
    使用异常对象的getMessage()方法,通常用于打印日志时

  • 取得异常的堆栈信息
    使用异常对象的printStackTrace()方法,比较适合于程序调试阶段

4.4 方法覆盖中的异常处理

方法覆盖(重写)规则:

  • 方法签名必须相同,方法名与参数列表就是方法签名

  • 方法的返回值类型可以相同 ,子类方法的返回值类型也可以是父类方法返回值类型的子类型

  • 子类方法的访问权限可以更宽泛(更大)
    a、如果父类方法使用public修饰,子类方法只能是public修饰
    b、如果父类方法使用protected修饰,子类方法可以是protected或者public修饰

  • 子类方法的异常要比父类方法的异常更小
    a、如果父类方法没有抛出异常,子类重写后也不能抛出异常
    b、如果父类方法抛出了异常,子类方法可以抛出相同的异常,也可以抛出父类异常的子异常,也可以不抛出异常

    代码示例:

    /**
     * 异常处理示例:
     * 子类方法的异常要比父类方法的异常更小
     * a、如果父类方法没有抛出异常,子类重写后也不能抛出异常
     * b、如果父类方法抛出了异常,子类方法可以抛出相同的异常,也可以抛出父类异常的子异常,也可以不抛出异常
     */
    public class ExceptionClass2 {
    
        public static void main(String[] args) {
        }
    
        class UserNotFoundException extends Exception {
    
        }
    
        /**
         * 定义一个接口,里面有一个抽象login方法,并且抛出了UserNotFoundException异常
         */
        interface UserManager {
            void login(String username, String password) throws UserNotFoundException;
        }
    
        /**
         * 正确写法,这里可以抛出UserNotFoundException异常,也可以不抛出异常
         */
        class UserManagerImpl1 implements UserManager {
            public void login(String username, String password) throws UserNotFoundException {
    
            }
        }
    
        /**
         * 不正确,因为UserManager接口中,login()方法没有要求抛出PasswordFailureException异常
         * 子类异常不能超出父类的异常范围
         */
        class UserManagerImpl2 implements UserManager {
            public void login(String username, String password) throws UserNotFoundException, PasswordFailureException {
    
            }
        }
    
        /**
         * 正确,因为MyException是UserNotFoundException子类
         * MyException异常没有超出接口的要求
         */
        class UserManagerImpl3 implements UserManager {
            public void login(String username, String password) throws UserNotFoundException, MyException {
    
            }
        }
    
        class PasswordFailureException extends Exception {
    
        }
    
        class MyException extends UserNotFoundException {
    
        }
        
    }
    

5 异常相关的面试题

5.1 谈谈你Java异常处理机制的理解?

Java对异常进行了分类,不同类型的异常分别用不同的Java类表示,所有异常的根类为 java.lang.Throwable,Throwable下面又派生了两个子类:ErrorException

Error: 表示应用程序本身无法克服和恢复的一种严重问题。

Exception: 表示程序还能够克服和恢复的问题,其中又分为系统异常普通异常

  • 系统异常
    系统异常是软件本身缺陷所导致的问题,也就是软件开发人员考虑不周所导致的问题,软件使用者无法克服和恢复这种问题,但在这种问题下还可以让软件系统继续运行或者让软件死掉,例如,数组下标越界(ArrayIndexOutOfBoundsException),空指针异常(NullPointerException)、类转换异常(ClassCastException)。

  • 普通异常
    普通异常是运行环境的变化或异常所导致的问题,是用户能够克服的问题,例如,网络断线,硬盘空间不够,发生这样的异常后,程序不应该死掉。

Java为系统异常和普通异常提供了不同的解决方案,编译器强制普通异常必须try…catch处理或用throws声明继续抛给上层调用方法处理,所以普通异常也称为checked异常,而系统异常可以处理也可以不处理,所以编译器不强制用try…catch处理或用throws声明,所以系统异常也称为unchecked异常

5.2 throw 和 throws 的区别?

  • throw
    throw 语句用在方法体内,表示抛出异常,由方法体内的语句处理。

    throw是具体向外抛出异常的动作,所以它抛出的是一个异常实例,执行throw一定是抛出了某种异常。

    throw一般用于抛出自定义异常。

  • throws
    throws语句是用在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理。

    throws主要是声明这个方法会抛出某种类型的异常,让它的使用者要知道需要捕获的异常的类型。

    throws表示出现异常的一种可能性,并不一定会发生这种异常。

5.3 final、finally、finalize 的区别?

  • final
    用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,被其修饰的类不可继承。

  • finally
    异常处理语句结构的一部分,表示总是执行。

  • finalize
    finalize 是Object 类的一个方法,所以Java对象都有这个方法,当某Java对象没有更多的引用指向的时候,会被垃圾回收器回收,该对象被回收之前,由垃圾回收器来负责调用此方法,通常在该方法中进行回收前的准备工作。该方法更像是一个对象生命周期的临终方法,当该方法被系统调用则代表该对象即将“死亡”,但是需要注意的是,我们主动行为上去调用该方法并不会导致该对象“死亡”,这是一个被动的方法(其实就是回调方法),不需要我们调用。

5.4 Java中异常分为哪些种类?

按照异常需要处理的时机,分为编译时异常(也叫受控异常)也叫 CheckedException运行时异常(也叫非受控异常)也叫 UnCheckedException。Java认为Checked异常都是可以被处理的异常,所以Java程序必须显式处理Checked异常。如果程序没有处理Checked 异常,该程序在编译时就会发生错误无法编译。这体现了Java 的设计哲学:没有完善错误处理的代码根本没有机会被执行。

Checked异常处理方法有两种:

  • 当前方法知道如何处理该异常,则用try…catch块来处理该异常。
  • 当前方法不知道如何处理,则在定义该方法时声明抛出该异常。

对于运行时异常,只有当代码在运行时才发行的异常,编译的时候不需要try…catch。
比如:除数是0和数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。当然如果你有处理要求也可以显示捕获它们。

5.5 error和exception的区别?

Error类和Exception类的父类都是Throwable类,他们的区别如下:

  • Error类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和预防,遇到这样的错误,建议让程序终止。

  • Exception类表示程序可以处理的异常,可以捕获且可能恢复。这种异常是由与程序设计的不完善而出现的问题,遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。

  • Exception类又分为未检查异常(UnCheckedException)受检查的异常(CheckedException)

    运行时异常ArithmeticException,IllegalArgumentException编译能通过,但是一运行就终止了,程序不会处理运行时异常,出现这类异常,程序会终止。

    而受检查的异常,要么用 try…catch 捕获,要么用throws字句声明抛出,交给它的父类处理,否则编译不会通过。

5.6 说出最常见的5个RuntimeException?

常见的异常有:

  • java.lang.NullPointerException 空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对象

  • java.lang.ClassCastException 数据类型转换异常

  • java.lang.ClassNotFoundException 指定的类找不到;出现原因:类的名称和路径加载错误;通常都是程序试图通过字符串来加载某个类时可能引发异常

  • java.lang.NoSuchMethodException 方法不存在异常

  • java.lang.NumberFormatException 字符串转换为数字异常;出现原因:字符型数据中包含非数字型字符

  • java.lang.IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生

  • java.lang.IllegalArgumentException 方法传递参数错误

  • java.lang.NoClassDefFoundException 未找到类定义错误

  • SQLException SQL 异常,常见于操作数据库时的 SQL 语句错误

  • java.lang.InstantiationException 实例化异常

5.7 调用下面的方法,得到的返回值是什么?

这个问题,估计大神也可能回答错误。

    public static int getNum() {
        try {
            int a = 1 / 0;
            return 1;
        } catch (Exception e) {
            return 2;
        } finally {
            return 3;
        }
    }

上述代码调用后,返回的结果是3,代码分析如下:

	public static void main(String[] args) {
        //代码走到第 3 行的时候遇到了一个MathException,这时第 4 行的代码就不会执行了,代码直接跳转到catch语句中,走到第 6 行的时候,
        //异常机制有一个原则:如果在catch中遇到了return或者异常等能使该函数终止的话那么有finally就必须先执行完finally代码块里面的代码然后再返回值。
        //因此代码又跳到第 8 行,可惜第 8 行是一个return语句,那么这个时候方法就结束了,因此第 6 行的返回结果就无法被真正返回。
        //如果finally仅仅是处理了一个释放资源的操作,那么该道题最终返回的结果就是2,因此上面这道题返回值是3。
        System.out.println(getNum());
    }

注:只有在 try 里面通过 System.exit(0) 来退出 JVM 的情况下 finally 块中的代码才不会执行,其他 return 等情况都会执行finally 代码块。

5.8 java 中如何编写自定义异常?

自定义异常通常继承于ExceptionRuntimeException,到底继承那个应该看具体情况来定。

自定义异常类可以有自己的变量和方法来传递错误代码传递其它异常相关信息。实际工作中,都会自定义异常类来处理异常。

  • 自定义受控异常

    /**
     * 自定义受控异常(编译时、检查时异常(checkedException)),继承Exception
     */
    class MyCheckedException extends Exception {
    
        public MyCheckedException() {
            //调用父类的默认构造函数
            super();
        }
    
        public MyCheckedException(String message) {
            //手动调用父类的构造方法
            super(message);
        }
    
    }
    
  • 自定义非受控异常

    /**
     * 自定义非受控异常(运行时异常-unCheckedException)
     */
    class MyRuntimeException extends RuntimeException {
    
        public MyRuntimeException() {
            //调用父类的默认构造函数
            super();
        }
    
        public MyRuntimeException(String message) {
            //手动调用父类的构造方法
            super(message);
        }
    
    }
    
  • 使用自定义异常

    /**
     * 使用自定义异常
     */
    public class MyExceptionClass {
    
        public static void main(String[] args) {
            testException1();
            testException2();
        }
    
        private static void testException1() {
            //【示例代码】自定义受控异常的使用
            try {
                method1(10, 0);
            } catch (MyCheckedException e) {
                //必须拦截,拦截后建议给出处理,如果不给出处理,就属于隐藏了该异常
                //系统将不给出任何提示,使程序的调试非常困难
                System.out.println(e.getMessage());
            }
        }
    
        private static void testException2() {
            method2(10, 0);
        }
    
        /**
         * 自定义受控异常的使用
         * 如果是受控异常必须throws声明,让调用方处理异常
         *
         * @param value1
         * @param value2
         * @throws MyCheckedException
         */
        private static void method1(int value1, int value2) throws MyCheckedException {
            if (value2 == 0) {
                throw new MyCheckedException("方法1,自定义受控异常,除数不能为0!");
            }
            int value3 = value1 / value2;
            System.out.println(value3);
        }
    
        /**
         * 【示例代码】,自定义非受控异常的使用
         *
         * @param value1
         * @param value2
         */
        private static void method2(int value1, int value2) {
            //throws MyRuntimeException {
            if (value2 == 0) {
                //抛出非受控异常,方法可以不使用throws进行声明,但也可以显示的声明
                throw new MyRuntimeException("方法2,自定义非受控异常,除数不能为0!");
            }
            int value3 = value1 / value2;
            System.out.println(value3);
        }
    
    }
    

    执行结果:
    在这里插入图片描述

6 总结

本文主要对Java中的异常相关的知识,进行了一下总结。

  • 异常的分类
  • 受控异常和非受控异常的区别
  • 异常的捕获和抛出处理
  • 异常的捕获顺序,先捕获小的,再捕获大的
  • 方法覆盖和异常的关系
  • 异常的5个关键字try、catch、finally、throws、throw

最后,上一个异常的图,来总结下Java中的异常,如下图所示:
在这里插入图片描述

写博客是为了记住自己容易忘记的东西,另外也是对自己工作的总结,希望尽自己的努力,做到更好,大家一起努力进步!

如果有什么问题,欢迎大家一起探讨,代码如有问题,欢迎各位大神指正!

给自己的梦想添加一双翅膀,让它可以在天空中自由自在的飞翔!

猜你喜欢

转载自blog.csdn.net/smilehappiness/article/details/106310868