本文为社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

前言

在抱负国际中,程序永远不会出现问题,用户输入的数据永远是正确的,逻辑没有任何问题 ,挑选翻开的文件也一定是存在的,内存永远是够用的……!可是实践国际里一旦出现这些问题,假如处理欠好,程序就不能正常运转了,导致影响用户体验,用户就有或许再也不运用这个程序了。

出现反常时,对外要给出清晰友好的提示消息。对内,程序自己尽量做好补救措施,真实不行了要及时开释占有的资源,防止影响其他线程的使命形成整个程序的溃散。所以程序的反常处理非常重要。

在一开端入门学习编程的时分,为了快速学习语法点咱们写的都是一些契合预期能履行下去的程序,不过在实践开发项目的时分就不能太单纯了,一定要处处当心,多质疑自己写的还有别人的程序,即使是其他三方库的程序也要质疑一下–犯错了该怎么办?直接忽视会不会让我在公司就无了?

在程序犯错的时分,Java 运用的是反常机制,支撑将过错信息封装起来,并让程序跳出正常的处理流程,交给反常处理部分去处理

说到反常处理,许多人都会想起 Try- Catch语法,下面咱们先经过一个简单的代码示例,经过运转和解读这个示例来快速了解一下什么是反常,以及反常处理具有行为习惯。

Try Catch句子

在Java程序中,运用Try Catch句子来进行程序的反常处理,先看下面这个简单的例子,了解相同Try Catch句子的履行流程。

package com.example.learnexception;
public class ExceptionFirstExpression {
    public static void main(String[] args) {
        try {
            int[] arr = new int[1];
            arr[1] = 9;
        } catch (Exception ex) {
            int abc = 999;
            ex.printStackTrace();
        }
        try {
            String str = "";
            str.substring(9, 10);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        System.out.println("程序履行完毕");
    }
}

在解读上面的程序履行成果前,先了解一下依靠 try catch 句子做反常处理的履行流程。

  • 在 try 句子中假如发生了不契合正确预期的反常情况(Exception),那么程序的履行流程会跳转到 catch 句子中。
  • Java会将反常相关信息封装在一个反常类的实例中,上面 catch 代码块中 ex 变量便是指向这个反常实例的引用。
  • 处理反常最简单的办法,便是调用反常实例的printStackTrace办法将反常信息打印出来输出到操控台或许日志。
  • 当 catch 块内的句子履行完毕后,程序会持续向下顺序履行。

上面的程序有两个tray catch 块,能够看到两个try块中的句子都有问题,第一个是数组索引越界(数组长度为1索引只有0),第二个是字符串索引的越界。他们都会导致程序抛出反常,咱们履行程序看一下成果:

java.lang.ArrayIndexOutOfBoundsException: 1
	at com.qsc.ebao.insureplan.controller.front.ExceptionFirstExpression.main(ExceptionFirstExpression.java:7)
java.lang.StringIndexOutOfBoundsException: String index out of range: 10
	at java.lang.String.substring(String.java:1963)
	at com.qsc.ebao.insureplan.controller.front.ExceptionFirstExpression.main(ExceptionFirstExpression.java:16)
程序履行完毕

能够看到程序仍然履行到了最后,不过在两个catch 句子分别捕获了java.lang.ArrayIndexOutOfBoundsException 和 java.lang.StringIndexOutOfBoundsException 两个反常,并把他们的实例赋值给了 ex 。在例程的catch块的反常处理顶用,则是用 ex.printStackTrace 打印出了反常明细和导致反常的调用栈。

由于正常捕获到反常并进行了处理,所以上面的例程仍然能平稳履行到最后。

了解了 try catch 句子的履行流程后,咱们再来深化地看一下关于反常实例所代表的反常(Exception)在 Java 中的所具有的体系。

反常的分类

Java中的反常由 Throwable 类及其子类来描绘。Throwable类是Java反常类型的顶层父类,一个目标只有是 Throwable 类或许其子类的实例,它才是一个反常目标,才能被Java的反常处理机制辨认。

Java 供给的体系反常有许多,咱们还能够自界说反常,不过 Java 的反常分类中咱们只需求记住几个要害的分类即可,不必一切反常类的行为全都得背一遍。

下图是一个反常类族的层级图。

Java 异常处理通关指南
Throwable 有两个直接子类,Error类和Exception类

  • Error类表明体系的内部过错和资源耗尽过错,这些过错发生于虚拟机自身、或许发生在虚拟机试图履行使用时,这些反常在使用程序的操控和处理能力之外,一旦发生,Java 虚拟机一般会挑选线程停止。
  • Exception类表明程序能够处理的反常,能够捕获且或许恢复。遇到这类反常,应该尽或许处理反常,使程序恢复运转,而不应该随意停止反常。其间 RuntimeException 和其子类为运转时反常(Runtime Exception),即非查看反常(Unchecked exception);而 Exception 的其它子类为非运转时反常,即受查看反常(Checked Exception)。

上面反常类族的分级,其实都不用故意记,搞不清楚的时分能够回来这儿再复习一下。对咱们写程序真实有影响,需求咱们牢记的是上面描绘里说到的非查看反常和受查看反常,这是依据 Java 对反常的处理要求进行的分类。

  • 非查看反常(Unchecked Exception):Error 和 RuntimeException 以及他们的子类都归于非查看反常。Java 不强制一定要在程序里用 try catch 或许 throws 处理这类反常。这样的反常发生的原因多半是代码写的有问题。比方除数为 0 过错 ArithmeticException,强制类型转化过错 ClassCastException,数组索引越界ArrayIndexOutOfBoundsException,运用了空目标NullPointerException等等。

  • 受查看反常(Checked Exception):除了上面描绘的非查看反常以外,其他反常类都归于受查看反常。Java 强制要求在程序的办法中经过 try catch 句子捕获处理它,或许是用 throws 句子抛出它给外层,否则编译不会经过。这类反常一般是由程序的运转环境导致的。程序或许被运转在各种未知的环境下,且无法干涉用户怎么运用咱们编写的程序,于是程序就应该为这样的反常做好处理预备。如SQLException, IOException, ClassNotFoundException 等。

下面的例程履行try块中的程序时会抛出 ClassNotFoundException, 它是受查看反常,假如不用try catch 处理或许声明要抛出这个反常,是不能经过编译的。

package com.example.learnexception;
public class MustChecked {
    // 这儿的throws和try catch 选其一即可, try catch 后 throws 也就没机会履行了。
    public static void main(String[] args) throws ClassNotFoundException  {
        try {
            Class clazz = Class.forName("com.example.learnexception.MustCheckedAbc");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在这个示例程序里,其实咱们现已了解了Java程序的办法里遇到反常情况跑出反常的语法,下面咱们把抛出反常的运用场景再好好剖析一下。

抛出反常

抛出反常,分为抛出办法内运用的其他包发生的反常(re-throw)–即不处理,再往外抛,交给上层处理。以及抛出自己代码的反常。

能够运用 throws 要害字在办法上声明办法或许会抛出的反常,抛出的反常类型能够是实践反常的父类或许自身。throws 要害字能够声明抛出多个类型的反常,用逗号分隔就能够。

public class ThrowIt {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        Class clazz = Class.forName("abc");
        clazz.getField("");
    }
}

上面是声明晰办法或许会抛出 ClassNotFoundException 和 NoSuchFieldException 反常。

咱们也能够自己创立一个反常然后抛出去,在代码里主动抛出反常,要运用 throw 句子(留意是单数方式,不要和办法声明上的throws搞混了)

package com.example.learnexception;
public class NewAndThrowIt {
    public static void causeException() throws Exception {
		int a = 1;
        if (a < 10) {
            throw new Exception("Custome Exception");
        }
    }
    public static void causeRuntimeException() throws RuntimeException {
        // 能够创立一个unchecked exception,然后用throw句子抛出去
        // 这时分,办法界说里能够有,也能够没有throws句子
        throw new RuntimeException("");
    }
}
  • 办法 causeException 创立了一个 checked exception 实例,然后用 throw 要害字扔出去,这时分就需求办法声明里用 throws 清晰声明办法会抛出的反常类型。
  • 办法 causeRuntimeException 创立一个unchecked exception,然后用throw要害字扔出去,这时分,办法声明里能够有,也能够没有throws句子。

在界说接口时,也能够在接口办法声明上加上 throws 句子,约束完成类假如抛出反常的话,有必要抛出 throws 声明的类或许其子类。

public interface IntfaceWithEx {
    void methodWithEx() throws Exception;
}

反常的传递和反常链

实践国际里,用Java开发的程序是由一个个类组成的,类的功用由一个个办法来完成,所以一个功用是靠着办法的层层调用完成的,在反常处理时最基本的 ex.printStackTrace() 便是输出发生反常时办法的调用栈–即能够了解为事发现场。

Java 的反常也是在办法调用栈上传递的,要么沿着办法调用栈一路往上层办法上抛,最终无人处理形成当时线程反常退出,要么被调用栈中的某个办法 Catch 住。

在反常处理中比较常见的场景是,比方底层 IO 出现反常,咱们的程序有或许会 Catch 住这个反常再依据自己的需求抛出新反常,假如这时只是简单地 throw 出创立的新反常,势必会导致原有的反常信息丢失,那么怎么保持整个反常的链路呢?在 Throwable 及其子类的重载结构办法中都能够承受一个 Throwable 类型的 cause 参数

public class Exception extends Throwable {
	public Exception(String message) {
        super(message);
    }
    public Exception(String message, Throwable cause) {
        super(message, cause);
    }
    ......
}
public class Throwable implements Serializable {
    ......
    public synchronized Throwable getCause() {
        return (cause==this ? null : cause);
    }   
}

该参数代表原有的反常,经过 getCause ()就能够获取该原始反常信息。经过这种办法能够保持整个反常链的连续性。

自界说反常

在写使用程序时经常会需求界说一些自己需求的反常。反常最重要的信息是三块:类型、过错信息和调用栈。

  • 反常类型,在catch、throws 、throw 这些要害字出现的地方都会进行反常类型的判别
  • 过错信息,描绘了反常的详细信息。
  • 调用栈,出现反常时的调用栈,调用栈是由反常的基类 Throwable 协助封装的,不需求程序员开发人员人为进行干涉。

下面是一个自界说的反常类 UserException,模仿Excption类的重载结构办法,咱们也给它界说了三个重载结构办法。

package com.example.learnexception.exceptions;
public class UserException extends Exception {
    public UserException() {
    }
    public UserException(String message) {
        super(message);
    }
    public UserException(String message, Throwable cause) {
        super(message, cause);
    }
    public UserException(Throwable cause) {
        super(cause);
    }
}

结构办法里的 Throwable 类型的参数 cause 在上节反常链里现已提过,表明导致当时这个反常的反常。 比方在创立User时,由于数据库网络问题导致了创立失败,那么在抛出 UserException 反常的时分就能够把数据库 IO 反常作为 cause 传给UserException的结构办法创立 UserException 反常实例。

下面是一个运用咱们自界说反常 UserException 的示例程序

package com.example.learnexception;
import com.example.learnexception.exceptions.UserException;
public class ExCaller {
    public void callThrowException() throws UserException {
        // 能够在这儿catch反常,然后封装成自己的反常,并增加相应的反常描绘
        System.out.println("ExCaller.callThrowException开端");
        try {
            Class.forName("com.example.TestForException");
        } catch (ClassNotFoundException ex) {
            throw new UserException("there's something wrong", ex);
        }
        System.out.println("ExCaller.callThrowException 办法调用完毕");
    }
}

上面的例程咱们在办法里创立一个 TestForException 类的实例,由于不存在这个类,所以try 块里会抛出 ClassNotFoundException 反常,在catch里咱们捕获了这个反常,封装成自己的反常,并增加了相应的反常描绘。

package com.example.learnexception;
import com.example.learnexception.exceptions.UserException;
public class CallExceptionAppMain {
    public static void main(String[] args) {
        ExCaller caller = new ExCaller();
        try {
            caller.callThrowException();
        } catch (UserException ex) {
            ex.printStackTrace();
        }
    }
}

catch 句子是依据反常类型匹配来捕捉相应类型的反常的。假如类型不匹配,catch句子是不会履行的,反常会持续抛出。

也便是说,假如运用 catch (Throwable) 会捕捉到一切的反常,包括Error,可是主张 catch 的规模尽量精准, 不要用规模太大的反常类父类,比方 Exception 在 catch 时就尽量少用,真实不行也最多只捕捉 Exception 类型的反常就行了,不要运用 Throwable 。

留意:

  1. 假如 catch 一个其实并没有或许被抛出的受查看反常,Java程序会报错,由于 Java清晰的知道这个类型的反常不会发生。
  2. 假如 catch 一个非查看反常,Java程序不会报错。
  3. 假如throws一个其实并没有被抛出的checked exception或许unchecked exception,Java程序不会报错。

掌握了自界说反常后,让咱们在回到Try-Catch语法上,上面咱们一直用的其实不是这个语法的彻底态,让咱们再下一节仔细说说。

Try Catch Finally

try catch 句子的方式能够扩展成try catch finally 的方式。下面经过一个示例程序,了解一下,加了 finally 之后的作用。

package com.example.learnexception;
public class TryCatchFinallyAppMain {
    private static int VAL = 0;
    public static void main(String[] args) {
        System.out.println(withFinally());
        System.out.println(VAL);
    }
    private static int withFinally() {
        int len = 0;
        try {
            String s = null;
            return s.length();
        } catch (Exception ex) {
            // 反常的处理:在有回来值的情况下,回来一个特别的值,代表情况不对,有反常
            len = -1;
            System.out.println("履行catch里的return句子");
            return len;
        } finally {
            // 能够认为finally句子会在办法回来后,后边的办法开端前,会在return句子后
            // 不管是由于return完毕仍是由于反常完毕,finally句子都会履行
            System.out.println("履行finally句子");
            // finally里最好不要有return句子
//            return -2;
            // finally里给前面 return 表达式里的变量值赋值没用
            len = -2;
            VAL = 999;
            System.out.println("finally句子履行完毕");
        }
    }
}
  • 不管是否有反常发生,finally块中代码都会履行,不管是由于 return 完毕仍是由于反常完毕,finally句子都会在办法退出后履行。
  • finally是在 return 后边的表达式运算后履行的,所以函数回来值是在 finally 履行前确定的。不管finally 中的代码怎么样,回来的值都不会改变,仍然是之前return句子中保存的值,所以咱们不要再 finally 的代码块里测验修改要 return 给调用者的回来值。
  • 再有一点还要留意,finally中最好不要包括return,否则程序会提前退出,回来值不再是 try 或 catch 中保存的回来值。

其实 finally 由于其最后必定履行的特色,特别适合于封闭文件、网络IO等开释程序占用资源的操作,不过由于仍是有或许会被人为忘记,所以 Java 又在 1.7 版本引入了新语法,防止忘记开释资源。

可主动收回资源的 Try 句子

和网络、文件等资源相关的反常处理起来比较繁杂,尤其是在处理多个资源的时分,比方读取文件内容再经过网络发送出去,在文件资源操作和网络资源操作的时分都或许犯错(文件找不到,权限不对,网络连接不上等等)
这种情况下就要需求在 finally 块中把资源主动封闭。 但有或许会被遗漏, 所以后来 java 1.7 就有了 try-with-resources —支撑主动封闭资源的 try 句子。

那怎么让 try 句子主动协助咱们封闭反常呢?首先这个资源要能支撑主动封闭,Java 里只要完成了 AutoCloseable 接口的类的实例,就会被认为是一个可被主动封闭的资源。

package com.example.learnexception;
import java.io.IOException;
public class AutoClosableTestResource implements AutoCloseable {
    private String resourceName;
    private int counter;
    public AutoClosableTestResource(String resName) {
        this.resourceName = resName;
    }
    // read 办法会随意的读取出数据,或许抛出IOException
    public String read() throws IOException {
        counter++;
        if (Math.random() > 0.1) {
            return "You got lucky to read from " + resourceName + " for " + counter + " times...";
        } else {
            throw new IOException("resource不存在哦");
        }
    }
    @Override
    public void close() throws Exception {
        System.out.println("资源开释了:" + resourceName);
    }
}

到时分 try 句子就会协助咱们主动调用 close 办法开释掉资源。接下来看怎么在 try-with-resources 语法里运用这个资源。

package com.example.learnexception;
public class TryWithResource {
    public static void main(String[] args) {
        try (
                AutoClosableTestResource res1 = new AutoClosableTestResource("res1");
                AutoClosableTestResource res2 = new AutoClosableTestResource("res2")
        ) {
            while (true) {
                System.out.println(res1.read());
                System.out.println(res2.read());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

跟常规的try catch句子相比,try 后的小括号里创立能够主动封闭的资源,这儿创立的资源能被 try 代码块里的代码运用,等出现反常或许正常退出的时分就会帮咱们开释掉这儿的资源。

咱们履行以下上面的例程,能够看到以输出。

You got lucky to read from res1 for 1 times...
You got lucky to read from res2 for 1 times...
You got lucky to read from res1 for 2 times...
You got lucky to read from res2 for 2 times...
资源开释了:res2
资源开释了:res1
java.io.IOException: resource不存在哦
	at com.example.learnexception.AutoClosableTestResource.read(AutoClosableTestResource.java:20)
    at com.example.learnexception.TryWithResource.main(TryWithResource.java:10)

try 后边括号里创立的资源,在程序退出前都被开释了。咱们后边再写与资源操作有关的代码时,尽量都运用这种方式,日常咱们开发能接触到的资源也都现已完成了 AutoCloseable,支撑被主动封闭。

总结

本篇文章,把 Java 的反常处理的知识点以及实践开发中的使用盘点了一遍,内容比较多,主张看完文章了解就行,不用非得悉数记住,需求要点关注的知识点在文章里现已做要点说明,后边实践开发项目时,也能够多拿这篇内容做参阅,提升咱们自己的开发体验,毕竟我一直觉得工作中写代码,就跟开卷考试相同,哈哈,你觉得呢!