关于程序员:讲讲Java的异常

25次阅读

共计 8719 个字符,预计需要花费 22 分钟才能阅读完成。

在开发利用的过程中,现实的状态就是不要有谬误产生,就算要产生谬误,也要在编译期间检测进去。然而在事实中,程序在运行时呈现谬误都会时有发生,并不会被预料到,束之高阁的话,会导致程序解体。

在 C 语言中,程序会通过返回一个非正常值示意谬误,而程序员通过应用如 if-else 等操作检测可能产生异样的状况并解决,这样会使得代码变得十分麻烦臃肿。而 Java 提供一种异样解决的谬误捕捉机制处理程序中的异样,缩小谬误查看,提供牢靠的应用程序。

假如在一个 Java 程序运行期间呈现了一个谬误。这个谬误可能是因为文件蕴含了错误信息,或者网络连接呈现问题造成的,也有可能是因为应用有效的数组下标,或者试图应用一个没有被赋值的对象援用而造成的。

异样体系

Java 提供了以面向对象的思维为根底的一系列形容谬误类型的对象,自身带有类型信息。它是以 Throwable 作为异样的基类来示意的异样体系,而 java.lang.Throwable 类作为基类,能够代替所有异样类抛出,但并不举荐这样做。

Java 语言提供了三种 throwablerun-time exceptionchecked exceptionerror

  • error:用来形容 Java 运行时零碎的外部谬误和资源耗尽谬误,程序不应该抛出这种类型的对象。
  • run-time exception:运行时异样,RuntimeException 类及其子类在运行期间可能呈现的谬误。编译器不会查看此类异样。此类异样属于不可查异样,个别是由程序逻辑谬误引起的,在程序中能够抉择捕捉解决,也能够不解决。
  • checked exception:受检异样,Exception中除 RuntimeException 及其子类之外,编译器会查看此类异样,如果呈现程序中此类异样,必须申明异样或捕捉异样,否则编译无奈通过。

checked exceptionrun-time exception 都是 Exception 类层次结构的分支。

对于应用异样的准则是:如果冀望调用者可能适当地复原,对于这种状况就应该应用受检异样。

但申明要抛出的每个受检异样,都是强制程序员解决异样的条件,以此加强可靠性。但这种形式会减少程序员的累赘。当程序员无奈对受检异样正确的解决,那么未受检的异样可能会更适合。

因而,如果调用者无奈复原谬误,就应该抛出未受检异样。如果能够复原,并且想要迫使调用者解决异样的条件,返回一个 Optional 值。万一失败且无奈提供足够的信息时,才应该抛出受检异样。

留神:ExceptionError 的区别:Exception 能被程序自身解决,Error 是无奈解决的。

申明异样

如果在办法中不想解决异常情况,能够应用 throws 关键字在办法中申明要抛出的异样。如下例子:

public int read(File file) throws IOException {...}

在读取文件内容时,不论是文件不存在,还是内容为空,都会向外抛出 IOException 异样。

不是什么状况都须要应用 throws 申明要抛出的异样,需记住,遇到以下 4 种状况时应该抛出异样:

  1. 调用一个抛出受检异样的办法,例如,FileInputStream 结构器。
  2. 程序运行过程中发现错误,并且利用 throw 语句抛出一个受检异样。
  3. 程序呈现谬误,例如,a[-1]=0会抛出一个 ArrayIndexOutOfBoundsException这样的非受检异样。
  4. Java 虚拟机和运行时库呈现的外部谬误。

对于可能被别人应用的 Java 办法,应该依据异样标准,在办法的首部申明这个办法可能抛出的异样。

申明异样时,不要申明 Java 中的 Error 及其子类谬误,也不须要申明从 RuntimeException 继承的哪些非受检异样。

public static void get(int index) throws ArrayIndexOutOfBoundsException {···}

这些运行时异样齐全在管制之下。

总之,一个办法必须申明所有可能抛出的受检异样,而非受检异样要么不可管制(Error),要门就应该防止产生(RuntimeException)。如果办法没有申明可能产生的受检异样,编译器就会收回一个谬误音讯。

抛出异样

下面只是介绍了申明异样,然而异样是如何产生的呢?上面给出一个例子:

public File getFile(String path) throws FileNotFoundException {File file = new File(path);
    if (file.exists()) {return file;}
    throw new FileNotFoundException(path + "File Not Found!");
}

从下面的例子中,会理解到申明异样的产生是因为应用 throw 关键字抛出一个异样,示意文件没有找到。下面抛出异样的 throw new FileNotFoundException() 写法也能够换成如下写法:

FileNotFoundException ex = new FileNotFoundException();
throw e;

一旦办法抛出了异样,这个办法就不可能返回到调用者。也就是说,不用为返回的默认值或错误代码担心。

自定义异样

当 Java 异样体系中没有任何规范异样类可能充沛地形容问题的时候,能够自定义一个异样类来形容。Java 自定义异样只须要派生于 Exception 类及其子类即可。如下所示:

public class ErrorDetail {public ErrorDetail(ErrorCode errorCode, String errorMsg) {
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }
    private final ErrorCode errorCode;
    private final String errorMsg;
    @Override
    public String toString() {return "code:" + errorCode + ", msg:" + errorMsg;}
}
public class CustomException extends Exception {
    private ErrorDetail detail;
    public CustomException(ErrorDetail detail) {this.detail = detail;}
    @Override
    public String toString() { return "CustomException [" + detail + "]"; }
}

能够在自定义的异样类中定义本人的变量和办法来传递额定的异样信息。尽管能够本人定义异样类,但还是优先应用规范库中提供的异样类。应用规范的异样有许多益处:

  1. 使得 API 更易于学习和应用。
  2. 使得 API 可读性会更好。
  3. 异样类越少,意味着内存占用就越小,装载这些类的工夫开销也越少。

当所抛出的谬误与规范库中的异样类的语义相当,就能够应用规范的异样。然而也不要间接重用 ExceptionRuntimeExceptionThrowable 或者 Error。因为这些异样类是其余异样类的基类,相当于抽象类,无奈牢靠地测试这些异样。

捕捉异样

申明异样和抛出异样的过程非常简单,只有将其抛出就不必理会,因为申明异样的办法曾经管制异样的权限交给了调用的办法。当然,有时不须要申明异样,而是对异样进行解决。

捕捉异样

在 Java 中,须要应用 try-catch 语句块捕捉异样。如下是 try-catch 语句块的执行形式:

  1. try 语句块没有抛出异样时,执行完 try 语句块后执行步骤 3;而 try 语句块中抛出异样时,跳过 try 语句其余代码执行步骤 2;try 语句块抛出 catch 子句中没有申明的异样类型,try-catch语句块所在的办法会立即退出。
  2. 执行 catch 子句中的处理器代码。
  3. try-catch语句块执行实现。

上面给出一个读取数据的例子:

public void read(File file) {
    try {InputStream in = new FileInputStream(file);
        int len;
        byte[] buf = new byte[4096];
        while ((len = in.read(buf)) != -1) {System.out.println(new String(buf, 0, len));
        }
    } catch (IOException ex) {// Process IOException} 
}

如上所示,当 in.read() 办法会抛出一个 IOException 异样时,会跳出 while 循环,进入 catch 子句,并生成一个栈轨迹。

捕捉多个异样

try 语句块中抛出多个异样时,就须要应用 catch 子句对不同类型的异样做不同的解决。catch 子句会依照程序执行,且只有一个被执行。上面给出一个例子:

public Object readObject(InputStream in) {
    try {ObjectInputStream ois = new ObjectInputStream(in);
        return ois.readObject();} catch (IOException e) {e.printStackTrace();
    } catch (ClassNotFoundException e) {e.printStackTrace();
    }
}

下面多个 catch 子句中的异样类型彼此之间不存在子类关系时,能够应用 Java SE 7 推出的同一个 catch 子句捕捉多个异样类型的性能。如下所示:

public Object readObject(InputStream in) {
    try {ObjectInputStream ois = new ObjectInputStream(in);
        return ois.readObject();} catch (IOException | ClassNotFoundException e) {e.printStackTrace();
    }
}

也能够将下面 catch 子句解决的多个异样,应用异样类型的基类 Exception 代替。如下所示:

public Object readObject(InputStream in) {
    try {ObjectInputStream ois = new ObjectInputStream(in);
        return ois.readObject();} catch (Exception e) {e.printStackTrace();
    }
    return null;
}

当捕捉异样后,不要通过空的 catch 子句疏忽它。如下所示:

try {...} catch (CustomException e) {// Ignore}

疏忽异样后,程序在呈现谬误的状况下会悄悄地执行上来,而不是提醒谬误。兴许会在未来的程序埋下隐患。最好是正确地解决异样,防止失败,或者将异样抛出,导致程序失败,从而保留当前调试该失败的有用信息。

异样转译

如果抛出的异样与执行的工作没有显著的分割时,会令人困惑,也会“净化”具备实现细节的更高层的 API。因而,高层实现应该捕捉低层的异样,并抛出依照高层形象实现的异样。这种做法称为 异样转译。上面的例子是应用 catch 子句将捕捉到的异样包装为须要的异样并从新抛出:

try {ObjectInputStream ois = new ObjectInputStream(in);
    return ois.readObject();} catch (IOException | ClassNotFoundException e) {throw new SerializationException(e);
} 

异样链

异样链就是一种非凡的异样转译。如果低层的异样对高层的异样调试有帮忙,就能够应用异样链。所有实现 Throwable 的子类都能够应用 getCause() 办法获取原始异样并当成参数结构新的异样。

public class CustomException extends Exception {public CustomException(Throwable cause) {super(cause);
    }
}

大多数规范的异样都有反对链的结构器。对于没有反对链的异样,能够利用 ThrowableinitCause(Throwable cause) 办法设置cause。异样链不仅让你能够通过程序拜访起因,还能够将起因的堆栈轨迹集成到更高层的异样中。

异样转译与间接抛出异样相比有所改进,但也不能滥用。最好是确保低层办法能胜利执行,防止抛出异样。如果不能阻止或者解决来自更底层的异样,个别应用异样转译;其次,低层办法抛出的异样合乎更高层的话,将异样流传到更高层,两头能够应用异样转译抛出适当的高层异样。

堆栈轨迹

当程序因为异样未被捕捉而失败时,零碎会主动打印该异样的堆栈轨迹。在堆栈轨迹中蕴含该异样的形容信息,即异样的 toString() 办法。通常蕴含异样的类名,以及紧随其后的细节音讯(detail message)。

能够应用 Throwable 类提供的 printStackTrace() 办法拜访堆栈轨迹的信息。

Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String desc = out.toString();

也能够通过 getStackTrace() 办法获取一个形容堆栈轨迹的 StackTraceElement 数组对象。如下所示:

public class PrintStackTrace {static void first() throws CustomException {second();
    }
    static void second() throws CustomException {third();
    }
    static void third() throws CustomException {throw new CustomException(new ErrorDetail(ErrorDetail.ErrorCode.PASSWORDERROR, "明码谬误"));
    }
    public static void main(String[] args) {
        try {first();
        } catch (CustomException e) {for (StackTraceElement stackTraceElement : e.getStackTrace()) {System.out.println(stackTraceElement);
            }
        }
    }
}

输入后果如下:

xxx.xxx.xxx.PrintStackTrace.third(PrintStackTrace.java:23)
xxx.xxx.xxx.PrintStackTrace.second(PrintStackTrace.java:18)
xxx.xxx.xxx.PrintStackTrace.first(PrintStackTrace.java:13)
xxx.xxx.xxx.PrintStackTrace.main(PrintStackTrace.java:30)

因为堆栈信息是考察程序失败起因时必须查看的信息。失败的情景不容易重现的话,想取得更多的信息会十分艰难,甚至不可能。因而,要在 toString() 办法中尽可能多地返回无关失败起因的信息,以便进行剖析。但 千万不要在堆栈音讯中蕴含明码、密钥以及相似的信息

finally 子句

当执行一个 try-catch 语句块时,代码抛出异样会执行相应的 catch 子句中的异样解决,并退出这个办法的执行。然而,咱们心愿一些有些代码会在解决完 try-catch 语句块后被调用。Java 提供了 finally 子句来解决这个问题。try-catch-finally 语句块无论是否有异样产生,finally 子句中的代码都会执行。

try-catch-finally 语句块的执行程序如下:

  1. 失常执行 try 语句块,当 try 语句块中代码执行完,执行步骤 3;如果 try 抛出异样,执行步骤 2。
  2. 执行与步骤 1 抛出的异样相匹配的 catch 子句,异样解决完,执行步骤 3。
  3. 执行 finally 子句的代码。

上面给出 try-catch-finally 的例子:

public void read(File file) {
    InputStream in = null;
    try {in = new FileInputStream(file);
        int len;
        byte[] buf = new byte[4096];
        while ((len = in.read(buf)) != -1) {System.out.println(new String(buf, 0, len));
        }
    } catch (IOException ex) {// Exception Handler} finally {
        try {Objects.requireNonNull(in).close();} catch (IOException e) {// Exception Handler}
    }
}

在下面的代码中,无论是否会调用 catch 子句解决异样,最初都会调用 finally 子句将 InputStream 资源敞开。也有非凡状况,会使 finally 子句不会被执行:

  1. finally 子句中产生异样。
  2. 在后面代码中应用 System.exit(int status) 退出程序。
  3. 程序所在线程死亡。
  4. 敞开cpu

因而,应用 try-catch-finally 语句块时,将可能会抛出异样类型的代码块放在 try 语句块中执行;将无论 try 语句块怎么退出,始终能被执行的代码放在 finally 子句中。

事实上,应用 try-finally 语句确保资源会被适时敞开是比拟不错的抉择,但还是倡议将下面的例子解耦合 try-catchtry-finally 语句块,用于进步代码的清晰度。

public void read(File file) {
    InputStream in = null;
    try {
        try {in = new FileInputStream(file);
            int len;
            byte[] buf = new byte[4096];
            while ((len = in.read(buf)) != -1) {System.out.println(new String(buf, 0, len));
            }
        } finally {in.close();
        }
    } catch (IOException ex) {ex.printStackTrace();
    } 
}

try 子句和 finally 子句中都有 return 语句时,会执行 finally 子句中的代码段,并且 finally 子句的返回值会笼罩 try 子句中的返回值。如下所示:

public static int process(int value) {
    try {return value * value;} finally {if (value == 2) {return 0;}
    }
}

留神:trycatchfinally 三个子句不能独自应用,三者能够组成try-catchtry-finallytry-catch-finally 三种语句块。

try-with-resources

下面的例子应用 try-finally 语句块用来敞开资源,将须要敞开的资源放在 finally 子句中确保资源敞开,但这种语法会显得太繁琐,但这种状况在 Java SE 7 中被改良。

Java SE 7 引入了 try-with-resources 语法用来敞开资源,应用这种语法须要实现 java.lang.AutoCloseable 接口:

public interface AutoCloseable {void close() throws Exception;
}

Java 5 中的 Closeable 曾经被批改,批改之后的接口继承了 AutoCloseable 接口。所有实现了 Closeable 接口的对象,都反对 try-with-resources 个性。

另外,Java SE 5 中的 Closeable 接口也继承了 AutoCloseable 接口,与 AutoCloseable 的差别处就是抛出的异样类型不同:

public interface Closeable extends AutoCloseable {public void close() throws IOException;
}

如果编写一个类,它代表的是必须被敞开的资源,那么也须要实现 AutoCloseable 接口。

上面是应用 try-with-resources 语法来简化下面的代码:

public void read(File file) {
    InputStream in = null;
    try (InputStream in = new FileInputStream()) {in = new FileInputStream(file);
        int len;
        byte[] buf = new byte[4096];
        while ((len = in.read(buf)) != -1) {System.out.println(new String(buf, 0, len));
        }
    } catch (IOException ex) {// Exception Handler} 
}

应用 try-with-resources 使得代码变得更简洁易懂,因而,优先思考用 try-with-resources,而不是 try-finally

总结

异样是 Java 不可分割的一部分,因而要学习如何正确的应用异样,好的异样解决机制会确保程序的健壮性及进步零碎的可用率。然而过多且不正确的异样解决也会连累程序的运行效率。因而,只有在谬误不可避免的状况下应用异样,尽量避免对异样进行解决。

更多内容请关注公众号「海人为记

正文完
 0