Hey, Nice to meet You. 

必有过人之节.人情有所不能忍者,匹夫见辱,拔剑而起,挺身而斗,此不足为勇也,天下有大勇者,猝然临之而不惊,无故加之而不怒.此其所挟持者甚大,而其志甚远也.          ☆☆☆所谓豪杰之士,

夯实Java基础(十二)----异常处理

1、异常处理概述

在Java程序执行过程中, 总是会发生不被期望的事件, 阻止程序按照程序员预期正常运行, 这就是Java程序出现的异常。

异常处理是基于面向对象的一种运行错误处理机制,通过对异常问题的封装,实现对用户的非法操作、参数设置异常,硬件系统异常,网络状态改变异常等运行态中可能出现的异常信息的处理机制。

如果某个方法不能按照正常的途径完成任务,就可以通过另一种路径退出方法。在这种情况下会抛出一个封装了错误信息的对象。此时,这个方法会立刻退出同时不返回任何值。另外,调用这个方法的其他代码也无法继续执行,异常处理机制会将代码执行交给异常处理器来处理,这就是Java异常的处理。Java为我们提供了非常完美的异常处理机制,我们看下面的图。

image

从图的结构我们可以知道,所有的异常都是继承自Throwable,有两个子类Error和Exception,它们分别表示错误和异常。下面来看看Throwable、Error与Exception 的具体描述:

  • Throwable:它是 Java 语言中所有异常的超类。Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。Throwable 包含两个子类:Error(错误)和 Exception(异常),它们通常用于指示发生了异常情况。
  • Error:表示程序中无法处理的错误,说明运行应用程序中出现了严重的错误。比如VirtualMachineError(虚拟机运行错误)、OutOfMemoryError(内存不足错误)、ThreadDeath(线程锁死)、StackOverflowError(栈溢出错误)等。当这些异常发生时, Java虚拟机一般会选择线程终止。
  • Exception:是程序本身可以处理的异常。这种异常它们又分为两大类RunTimeException(运行时异常)和非RunTimeException(非运行时异常),在程序中我们应当尽可能去处理这些异常。
    • RunTimeException(运行时异常):表示 JVM 在运行期间可能出现的异常。这种异常Java 编译器不会检查它,当程序中可能出现这类异常时,倘若既"没有通过 throws 声明抛出它",也"没有用 try-catch 语句捕获它",还是会编译通过。当遇到异常时会由 Java 虚拟机自动抛出并自动捕获(就算我们没写异常捕获语句运行时也会抛出错误!!),此类异常的出现绝大数情况是代码本身有问题应该从逻辑上去解决并改进代码。
    • 非RunTimeException(非运行时异常):这种异常首先 Java 编译器会检查它。如果程序中出现此类异常,比如 ClassNotFoundException(没有找到指定的类异常),IOException(IO 流异常),要么通过 throws 进行声明抛出,要么通过 try-catch 进行捕获处理,否则不能通过编译。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。该异常我们必须手动在代码里添加捕获语句来处理该异常
  • CheckException:受检查异常,它发生在编译阶段。所有CheckException都是需要在代码中处理的,所以这对我们在编码时是非常有帮助的。它们的发生是可以预测的,是可以合理的处理。要么使用try-catch-finally语句进行捕获,要么用throws子句抛出,否则编译就会报错。在Exception中,除了RuntimeException及其子类以外,其他都是都是CheckedException。
  • UncheckedException:不受检查异常,它发生只有在运行期间。所有是无法预先捕捉处理的,主要是由于程序的逻辑错误所引起的。Error也是UncheckedException,也是无法预先处理的,它们都难以排查。所以在我们的程序中应该从逻辑角度出发,尽可能避免这类异常的发生。

既然有些时候错误和异常不可避免,那么我们可以在程序设计中认真的考虑,设计出更加高质量的代码,这样即使产生了异常,也能尽量保证程序朝着有利方向发展。

2、Java常见的异常

在Java中异常的种类非常的多,所以我们就列出比较常见的异常。

Error异常:

  • OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。
  • StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出时抛出该错误。
  • VirtualMachineError:虚拟机错误。用于指示虚拟机被破坏或者继续执行操作所需的资源不足的情况。
  • ThreadDeath:线程结束。当调用Thread类的stop方法时抛出该错误,用于指示线程结束。
  • UnknownError:未知错误。用于指示Java虚拟机发生了未知严重错误的情况。

Exception中出现的异常:

RunTimeException子类:

  • NullPointerException:空指针异常。
  • ArrayIndexOutOfBoundsException:数组索引越界异常。
  • ClassCastException:类型转换异常类。
  • NumberFormatException:字符串转换为数字抛出的异常。
  • ArithmeticException:算术条件异常。如:整数除零等。
  • NegativeArraySizeException:数组长度为负异常。
  • ArrayStoreException:数组中包含不兼容的值抛出的异常。
  • SecurityException:安全性异常。
  • IllegalArgumentException:非法参数异常。
  • NoSuchMethodException:方法未找到异常。

非RunTimeException子类:

  • IOException:输入输出流异常。
  • EOFException:文件已结束异常。
  • SQLException:操作数据库异常。
  • ClassNotFoundException:找不到类异常。
  • 自定义Exception:用户自己定义的异常。

3、异常处理的机制

在 Java 应用程序中,异常处理机制为:捕获异常,抛出异常,声明异常。

image

接下来先来学习怎么用try-catch-finally语句来捕获异常。

4、捕获处理 try-catch-finally

先看一下try-catch-finally语句的格式:

try{
    //可能生成异常的代码    
}catch(Exception e){
    //处理异常的代码
}catch(Exception e){
    //处理异常的代码
} finally {
    //一定会执行的代码          
}

用一个整数除以零为例,来使用try-catch-finally捕获异常:

public class Main {
    public static void main(String[] args) {
        int i = 10;
        try {
            int j = i / 0;//会出现异常的地方
            System.out.println(j);
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("代码出现了异常...");
        } finally {
            System.out.println("一定会执行...");
        }
    }
}

运行结果:

image

我们知道任何数是不可以除零,这个地方一定会抛出异常,所以我们用try-catch给它包起来,当真的出现异常之后就可以将其捕获打印出来。从结果可以看出来,当程序遇到异常时会终止程序的运行(即后面的代码不在执行),控制权交由异常处理机制处理。由catch捕获异常后,再执行catch中的语句。然后再执行finally中一定会执行的语句(finally语句块一般都是去释放占用的资源)。

TIPS:在一个 try-catch 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理

private static void tryCatchTest(String filePath) {
    try {
        // code
    } catch (FileNotFoundException | UnknownHostException e) {
        // handle FileNotFoundException or UnknownHostException
    } catch (IOException e){
        // handle IOException
    }
}

注:同一个 catch 也可以捕获多种类型异常,用 | 隔开,但是一般都不会这么干,因为可能会造成分不清是哪个异常导致的错误。


从上面的例子我们会不会认为try-catch-finally捕获异常非常的简单,然而它们真的非常简单吗?再来看下面这个例子。

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

    private static int test() {
        int num = 10;
        try {
            System.out.println("try...");
            return num += 80;
        } catch (Exception e) {
            System.out.println("error...");
        } finally {
            if (num > 20) {
                System.out.println("finally num>20 : " + num);
            }
            System.out.println("finally ...");
        }
        return num;
    }
}

运行结果:

image

看到这样的结果一定会非常的懵逼,你可以自己先思考一下,后面会介绍try-catch-finally中有无return的执行顺序。

5、抛出异常 throws+异常类型(异常链)

throws是在声明在方法的后面使用,表示此方法不处理异常,而交给方法调用者进行处理,然后一直将异常向上一级抛给调用者,而调用者可以选择捕获或者抛出,如果所有方法(包括main)都选择抛出。那么最终将会抛给JVM,由JVM来打印出信息(异常链)。

package com.thr.exception;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class ThrowsTest {
    public static void method1() throws IOException {
        File file = new File("hello.txt");
        FileInputStream fis = new FileInputStream(file);
        int data = fis.read();
        while (data != -1) {
            System.out.println(data);
            data = fis.read();
        }
        fis.close();
    }

    public static void method2() throws IOException {
        method1();
    }

    public static void main(String[] args) throws IOException {
        method2();
    }
}
//结果;java.io.FileNotFoundException: hello.txt (系统找不到指定的文件。)

image

上面代码的异常信息最终由JVM打印,同样我们也可以对异常进行捕获。

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

通过使用throws+异常类型(异常链),我们可以提高代码的可理解性、系统的可维护性。

6、定义异常 throw (手动抛出异常)

使用throw是允许我们在程序中手动抛出异常的,那么这就操蛋了,我们都巴不得不出现任何异常,这咋还得自己来抛出异常呢!这是因为有些地方确实需要抛出异常,我们简单举例来看:

public class ThrowTest {

    public void show(int age) throws Exception {
        if (age > 0 && age < 256) {
            System.out.println(age);
        } else {
            //System.out.println("输入年龄有误!");
            throw new Exception("输入年龄有误!");
        }
        System.out.println(age);
    }

    public static void main(String[] args) {
        ThrowTest test = new ThrowTest();
        try {
            test.show(500);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

上面的例子中如果我们使用System.out.println("输入年龄有误!");输出信息,然而它仅仅是一个输出的功能,并不会终止后面代码的执行,所以这时我们就可以选择手动抛出异常来终止代码的运行。

7、自定义异常

上面介绍了几种异常的处理机制,但是那些异常都是Java本身经定义好的,在实际开发中一般都会自己定义一些异常,这样可以更加方便的处理异常。那么我们自己怎么定义异常呢?Java是允许让用户自己定义异常的,但是一定要注意的是:在我们自定义异常时,一定要是Throwable的子类,如果是检查异常就要继承自Exception,如果是运行异常就要继承自RuntimeException。

package com.thr.exception;

//自定义异常,继承Exception
public class MyException extends Exception {
    private static final long serialVersionUID = 1L;
    private String errorCode;
    private String errorMessage;

    //定义无参构造方法
    public MyException() {
    }

    // 定义有参构造方法
    public MyException(String errorMessage) {
        super(errorMessage);
        this.errorMessage = errorMessage;
    }

    public MyException(String errorCode, String errorMessage) {
        super(errorMessage);
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
    }
}

class Main {
    public void show(int i) throws Exception {
        if (i >= 0) {
            System.out.println(i);
        } else {
            throw new MyException("不能为负数...");
        }
    }

    public static void main(String[] args) {
        Main main = new Main();
        try {
            main.show(-5);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果:

image

8、try-catch-finally的执行顺序

对于try-catch-finally的执行顺序在我们的日常编码中可能不会用到,但是可能会出现在你笔试中,所以我们还是需要了解一下。

情况一:try、finally中没有return

public class TryTest1 {
    public static void main(String[] args) {
        System.out.println("main:" + test());
    }

    private static int test() {
        int num = 10;
        try {
            num = 20;
        } finally {
            num = 30;
        }
        return num;
    }
}

这种情况比较好理解:输出结果就是 30。


情况二:try中有return,finally中没有return

public class TryTest2 {
    public static void main(String[] args) {
        System.out.println("main:" + test());
    }

    private static int test() {
        int num = 10;
        try {
            return num += 20;
        } finally {
            System.out.println("finally--" + num);
        }
    }
}

输出结果如下:

image

特别注意:try中有return的情况下,在执行到return的时候会保留好要返回的值,先去执行finally中的操作,然后才会返回来执行return。

分析:当代码执行到return num += 20;的时候,JVM会将计算好的结果存储起来,此时并没有直接返回,因为之前说过,finally中代码一定是会执行,那么随后会去执行finally中的代码,执行完之后再将结果返回,此时num的值为30.


情况三:try和finally中均有return

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

    private static int test() {
        int num = 10;
        try {
            return num += 20;
        } finally {
            System.out.println("finally--" + num);
            return num;
        }
    }
}

输出结果如下:

image

分析:和上面的一样,当遇到try中的return时,计算好结果后会去执行finally中的代码。但是此时finally中有return,所以finally中的return语句先于try中的return语句执行,因而try中的return被”覆盖“掉了,不再执行。


情况四:finally中改变返回值num

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

    private static int test() {
        int num = 10;
        try {
            return num += 20;
        } catch (Exception e) {
            System.out.println("catch-error");
        } finally {
            System.out.println("finally-" + num);
            num = 100;
            System.out.println("finally-" + num);

        }
        return num;
    }
}

输出结果如下:

image

分析:如果前面几个理解了,那么这个也非常的简单,主要还是上面特别注意中的那句话。代码中虽然在finally中改变了返回值num,但因为finally中没有return该num的值,因此在执行完finally中的语句后,test()函数会得到try中返回的num的值,而try中的num的值依然是程序进入finally代码块前保留下来的值,因此得到的返回值为 30。


情况五:将num的值包装在Num类中

下面在来看看当值被包装在一个对象中的情况。

public class TryTest {
    public static void main(String[] args) {
        System.out.println(test().num);
    }

    private static Num test() {
        Num number = new Num();
        try {
            return number;
        } catch (Exception e) {
            System.out.println("catch-error");
        } finally {

            System.out.println("finally-" + number.num);
            number.num = 100;
            System.out.println("finally-" + number.num);
        }
        return number;
    }
}

class Num {
    public int num = 10;
}

输出结果如下:

image

从结果中可以看出,这里在finally中改变了返回值num的值,打印的结果也是finally中改变后的值,这是因为此时是对象引用类类型了。我们在finally中改变的是对象引用中内部属性的值,而try中return的确是对象的引用,所以对象Num number = new Num();本身是没有发生改变的,只是对内部属性num的值发生了改变。

更细的可以参考这篇文章:[面试官太难伺候?一个try-catch问出这么多花样](面试官太难伺候?一个try-catch问出这么多花样 (qq.com))


对于含有return语句的情况,这里我们可以简单地总结如下:

  • 特别注意:try中有return的情况下,在执行到return的时候会保留好要返回的值,先去执行finally中的操作,然后才会返回来执行return。
  • 如果try-finally没有return语句,那么值是什么就返回什么。
  • 如果try有return语句,且finally中也有return语句,则finally中return会将try中的return语句”覆盖“掉,直接执行finally中的return语句,得到返回值,这样便无法得到try之前保留好的返回值。
  • 如果try有return语句,且finally中没有return语句,也没有改变要返回值,则执行完finally中的语句后,会接着执行try中的return语句,返回之前保留的值。
  • 如果try有return语句,且finally中没有return语句,但是改变了要返回的值,这里有点类似与引用传递和值传递的区别,分以下两种情况,:
    • 如果return的数据是基本数据类型或文本字符串,则在finally中对该基本数据的改变不起作用,try中的return语句依然会返回进入finally块之前保留的值。
    • 如果return的数据是引用数据类型,而在finally中对该引用数据类型的属性值的改变起作用,try中的return语句返回的就是在finally中改变后的该属性的值。

9、try-with-resource的介绍

try-with-resources语句是一种声明了一种或多种资源的try语句。资源是指在程序用完了之后必须要关闭的对象。try-with-resources语句保证了每个声明了的资源在语句结束的时候都会被关闭。JAVA 7 提供了更优雅的方式来实现资源的自动释放,自动释放的资源需要是实现了 AutoCloseable 接口的类。任何实现了java.lang.AutoCloseable接口的对象,和实现了java.io.Closeable接口的对象,都可以当做资源使用。

在Java SE7之前,你可以用finally代码块来确保资源一定被关闭,无论try语句正常结束还是异常结束。下面的例子用finally代码块代:

static String readFirstLineFromFileWithFinallyBlock(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    } finally {
        if (br != null) br.close();
    }
}

而在Java SE7之后就可以使用try-with-resource简写的方式来处理资源的释放。

static String readFirstLineFromFile(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}

当try 代码块退出时,会自动调用 br.close 方法,和把 br.close 方法放在 finally 代码块中不同的是,若 br.close 抛出异常,则会被抑制,抛出的仍然为原始异常。被抑制的异常会由 addSusppressed 方法添加到原来的异常,如果想要获取被抑制的异常列表,可以调用 getSuppressed 方法来获取。

10、关于throw 和 throws 的区别

我们发现throws和throw这两个关键字非常的相似,来看看它们的区别是什么:

  • throws:该关键字用在方法声明上,用来声明一个方法可能产生的所有异常。该方法不做任何处理,而是一直将异常往上一级传递,由调用者继续抛出或捕获。它用在方法声明的后面,跟的是异常类名,可以跟多个异常类名,用逗号隔开,它表示的是向上抛出异常,由该方法的调用者来处理。如果一种没有处理异常,那么最终将会抛给JVM,最后由JVM打印出异常信息。
  • throw:该关键字用在方法内部,用来抛出一种异常,它用在方法体内部,抛出的异常的对象名称。它表示抛出异常,由方法体内的语句处理。
posted @ 2019-07-31 11:33  唐浩荣  阅读(407)  评论(0编辑  收藏  举报