《java8学习笔记》读书笔记(九)

96
默然说话_牟勇
2018.04.28 12:23* 字数 8763

摘要:花了差不多一个月的时间,写完了第8章异常处理,这章讲述了try-catch-finally的Java异常处理机制,讲述了throw和throws的抛出异常机制。还介绍了assert语法,JDK 7开始出现的一些变化,比如try-with-resource语法蜜糖。对于Java异常的介绍还是比较全面的,希望大家喜欢。欢迎留言提问,欢迎分享,欢迎关注。

第8章 异常处理

学习目标

  • 使用try、catch处理异常
  • 认识异常继承架构
  • 认识throw、throws的使用时机
  • 运用finally关闭资源
  • 使用自动关闭资源语法
  • 认识AutoCloseable接口

8.1 语法与继承结构

程序正如生活,人间之事,不如意十之八九。总有意想不到的事情引发错误,有时即使你的代码全对了,一点语法错误也没有,也会在运行时出现错误。比如你写了一个保存文件的程序,可是在保存的时候,磁盘满了……程序也就崩溃了,然而你的程序真的没有错呀。Java中提供了很好错误处理机制,就叫异常处理,它能够使你的程序即使遇上天灾人祸(比如磁盘满了,网络断了)都不会崩溃,而是给出正确的处理提示,让用户在使用时可以想办法挽回损失,这也是我们常常所说的“健壮性”。
Java中的异常也以对象方式呈现,它们均有一个父接口java.lang.Throwable(默然说话:换个说法,如果你要自己写个异常类——当然,刚开始,我们只用现成的异常类——你只要实现Throwable接口,Java就会正确的识别你写的异常了)的各种子类实例。只要你能捕捉包装异常的对象,就可以针对该异常做一些处理,比如尝试恢复正常流程,进行日志记录,以某种形式提醒用户等。

8.1.1 使用try、catch

来看一个简单的程序,用户可以连续输入整数,每个整数之间用空格分隔,输入0表示结束,程序会计算平均分并显示出来:

package cn.speakermore.ch08;

import java.util.Scanner;

/**
 * 对异常讲解的引入,从输入的错误说起
 * @author mouyong
 */
public class Average {
    public static void main(String[] args){
        Scanner input=new Scanner(System.in);
        double sum=0;
        int count=0;
        System.out.println("请输入一组整数,每个数之间用空格分隔,0为结束:");
        while(true){
            //这个输入存在不可控制的风险,因为用户什么都会输入
            int number=input.nextInt();
            if(number==0){
                break;
            }
            sum+=number;
            count++;
        }
        System.out.println("平均分:"+(sum/count));
    }
}

在乖乖妹看来,自然是要听老师的话,只输入整数,最后还要输个0,程序当然也如预期的反应:



图8.1 程序真好使
在调皮哥看来,规则就是拿来破坏的。你说用空格我就用空格,我不是很没面子?而且,为何不能用逗号呢?逗号不是看上去更好些?



图8.2 程序很无奈
嗯,出错了不是?我们可以看到一段错误信息出现在了控制台。

这段错误信息对我们发现错误,找到错误原因并改正是很有用的。我们来看下第一行。

Exception in thread "main" java.util.InputMismatchException

我们的代码中,在接收输入的时候使用了nextInt(),这表示我们只能接收整型数,如果出现了InputMismatchException就说明,用户输入的不是数字。当程序遇到了这样的情况,它是不知道如何处理的,这时,它就挂了,停止运行,退出。我们称之为:程序崩溃。
异常处理最重要的一个目的,就是希望在程序遇到了不期望的情况时,不会崩溃,而是以一种恰当的方式进行处理,并能继续正常运行。Java中已经做好了一步,就是它会将所有的错误都打包为异常对象(默然说话:如上面的java.util.InputMismatchException就是一个异常对象的类型),你需要做的,就是去捕获这些错误的对象,以便提供后继的异常处理。如何做?我们看示例:

package cn.speakermore.ch08;

import java.util.InputMismatchException;
import java.util.Scanner;

/**
 * 带了try-catch的平均计算
 * @author mouyong
 */
public class AverageWithTryCatch {
    public static void main(String[] args){
        Scanner input=new Scanner(System.in);
        double sum=0;
        int count=0;
        System.out.println("请输入一组整数,每个数之间用空格分隔,0为结束:");
        //try块:放置有运行时错误风险的代码
        try{
            while(true){
                //有了try-catch,即使出现输入风险,我们也能捕获它,并进行适当的处理
                int number=input.nextInt();
                if(number==0){
                    break;
                }
                sum+=number;
                count++;
            }
            System.out.println("平均分:"+(sum/count));
        }catch(InputMismatchException e){
            //catch块:捕获指定的异常对象并进行错误处理
            //进行错误处理:再次错误消息输出
            System.out.println("必须输入正确的格式,亲。以空格分隔的整数,0为结束");
        }
    }
}

这里使用了try-catch的语法,大家要注意的是,try部分是必须要用大括号括起来的部分,我们称为“try块”,里面的代码就是尝试(try翻译过来就是尝试)运行的部分。如果没有错误发生,代码就继续try-catch后面的代码(默然说话:额,我们这里try-catch后面没有代码了,所以程序就会结束运行。)。如果try块内的代码发生了错误,JVM就会打包这个错误为一个异常对象(默然说话:对的,异常的类型是很多的,我们甚至可以自行声明我们自己的异常类型),离开错误发生点(默然说话:这又是一个细节。当try块发生了错误时,从错误点开始之后的代码就不会被运行了)并抛出这个异常对象,程序执行将转到catch块。JVM会核对catch后的小括号中写的异常对象类型,找到对应异常类型的catch块,并执行这个catch块的代码(默然说话:也就是对应此异常对象的错误处理代码)。执行完毕之后,继续执行try-catch后面的代码。
所以,如果nextInt()发生了InputMismatchException错误,流程就会离开try块,跳到捕获InputMismatchException对象的catch块中,输出错误消息提示,然后正常结束,而不是程序崩溃。


图8.3 即使程序输入不正确,也能正常结束
这时会存在一个小问题。如果抛出的异常并没有对应的catch块呢?在实际代码中这样的情况是会存在的。这时JVM会直接把出这种情况的程序给毙了(默然说话:也就是我们的程序崩溃了,JVM把它清除出了内存),然后自己来处理这个异常,处理的办法大部分情况就和图8.1一样,把异常对象包装的错误信息打印出来。

8.1.2 异常继承架构

关于前面的平均分例子,在学了异常之后几年,突然有一天我发现了一个问题。nextInt()是会抛出一个异常的。可是,编译器居然不提示你需要使用try-catch。而很多的其他异常,编译都会给出报错的提示,如果你不写try-catch,就不给你运行的。当时的我对这个问题还是困扰了一段时间的,直到学习了异常的继承架构,才明白了是怎么回事情。


图8.4 Throwable继承架构
从上面的继承架构图可以看出,顶层是一个可抛出类Throwable(默然说话:Throwable的意思就是“可抛出”。Java规定了所有的错误类都要继承自Throwable,否则try-catch将不理会。),它的下面扩展了两个类,一个是错误类Error,另一个是异常类Exception。
Error及其子类代表严重的系统错误,如硬件层面的错误、JVM错误等。这类错误发生时,我们的程序通常是做不了什么事情来挽回的,所以我们都不会对这些问题进行处理,而是交给JVM自裁。
Exception及其子类则代表我们写的代码本身包含的错误。这也就是为何我们把这章叫异常(Exception)处理的原因。也是我们主要研究的部分。
如果某个方法抛出了RuntimeException或者其子类对象(默然说话:如前面平均分那个例子中的InputMismatchException,它就是RuntimeException的子类),编译器认为这类异常应该是由我们自己事前预防并处理好,所以,编译器并不会对可能抛出此类异常的代码进行编译时报错处理。也就是,你写不写try-catch由你自己决定,你可以使用try-catch来处理此类异常,也可以使用别的方式来处理此类异常。所以,我们又把RuntimeException及其子类称为非受检异常(unchecked exception)。
在前面平均分的例子中,由于用户的输入是不可控的,所以我们可以在从控制台取用户输入的数据时,并不用nextInt(),而是使用next(),以字符串的方式获得,然后在代码中对数据进行判断之后,再进行相应的处理。

package cn.speakermore.ch08;

import java.util.Scanner;

/**
 * 不使用try-catch的平均分计算
 * @author mouyong
 */
public class AverageWithoutTryCatch {
    
     public static void main(String[] args){
        
        double sum=0;
        int count=0;
        System.out.println("请输入一组整数,每个数之间用空格分隔,0为结束:");
        while(true){
            //myNextInt()对用户的输入进行了处理,妈妈再也不担心淘气哥的破坏了
            int number=myNextInt();
            if(number==0){
                break;
            }
            sum+=number;
            count++;
        }
        System.out.println("平均分:"+(sum/count));
    }
     private static int myNextInt(){
         Scanner input=new Scanner(System.in);
         String number=input.next();
         //matches()方法使用正则表达式比较字符串,\d*的意思表达纯整数
         //使用循环的目的是为了让用户在输入错误的情况下,可以重复输入,直到输入正确
         while(!number.matches("\\d*")){
             System.out.println("请输入数字,亲,并以空隔分隔每个数字,0为结束");
             number=input.next();
         }
         //将字符串转化为整数
         return Integer.parseInt(number);
     }
}

一个可能的执行结果如下图:



图8.5 淘气哥再也无孔可入了
我们可以看到,上例中我们写了一个叫myNextInt()的方法,以next()方法,以String类型来接收用户的输入,之后调用了String的matches()方法来比较输入是否为纯数字。这里的“\d*”是一个正则表达式,关于正则表达式我们会在第15章说明。
如果某个方法抛出的是除RuntimeException及其子类的异常对象,这样的异常对象被称为受检对象,如果你没有使用try-catch,编译器就会提醒你必须使用。
除了了解Error与Exception的区别,以及Exception与RuntimeException的区别之外,我们还要知道,如果父类异常对象在子类异常对象前,由于父类会捕获子类异常,导致catch子类异常将永远不会被执行。编译器会报错。如下:



图8.6 父类会捕获子类的对象
所以,在进行catch时,要注意子类在前,父类在后。
        try{
            int result=input.nextInt();
        }catch(InputMismatchException e){
            e.printStackTrace();
        }catch(RuntimeException e){
            e.printStackTrace();
        }catch(Exception e){
            e.printStackTrace();
        }

如果多个catch块对异常的处理都是相同的,那么上面的写法其实是很苦逼的,而且造成代码的严重重复(默然说话:当然,你可以写个方法在各catch中调用,以此解决代码重复的问题。)。JDK7针对这个问题推出了多重捕获(Multi-catch)语法:

try{
    //可能出错的代码
} (InputMismatchException |IndexOutOfBoundsException|ClassCastException e){
    e.printStackTrace();
}

这个写法看上去要简洁很多。不过这个写法要注意一个问题,就是在同一个catch块中同时捕获的异常类不能有直接的继承关系,否则会报错。(默然说话:个人很不喜欢这样的写法,觉得这种写法在实际开发中并没有什么卵用。当然,也可能是因为自己十多年都是写的多个catch吧,你们自己可以选择。

8.1.3 要抓还是要抛

假设你今天受命开发一个中间库,其中有个功能是读取txt文本文件,并以字符串返回文档中的所有字符,你也许会这样:
虽然还没有正式讲到Java如何存取文本文件,不过前面也说过,Scanner在创建的时候,可以给构造函数传入InputStream的对象,而FileInputStream就是用来读取指定名称的文件的。它是InputStream的子类,所以也可以把它传递给Scanner。而在创建FileInputStream对象时会抛出FileNotFoundException,根据目前学过的异常处理语法,于是你catch了FileNotFoundException,并在控制台中显示错误信息。

package cn.speakermore.ch08;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * try-catch的误用
 * @author mouyong
 */
public class MiddleLib {
    public String readFile(String filename){
        StringBuffer sb=new StringBuffer();
        try {
            Scanner input=new Scanner(new FileInputStream(filename));
            while(input.hasNext()){
                sb.append(input.nextLine())
                        .append('\n');
                
            }
        } catch (FileNotFoundException ex) {
            //在控制台输出错误真的合适吗?
            ex.printStackTrace();
        }
        return sb.toString();
    }
}

不过,你要考虑到一个问题,你做的中间库。中间库的意思就是,它可能用于任何可以用它的地方。也许会在一个网站中用来读取用户上传的配置信息,可是控制台却在服务器端,你在控制台输出,用户不是就会无法知道他上传的配置有没有读取成功了么?说到这儿,你会发现,在这个假设的场景中,你的catch做任何事情,都是不符需求的,换个说法,如果你的方法出了抛出了异常,你根本不知道应该如何处理这个异常。
当我们上学的时候,如果做作业遇到了困难,我们首先会尝试自己解决,如果我们自己解决不了,我们会怎么做呢?对,我们会把问题抛出,去让能解决这个问题的对象去解决。异常处理也做了同样的语法结构。try就是给我们尝试执行代码的地方,catch就是让我们捕获异常作对应处理的地方。如果异常我们无法处理时,Java设计了throws来声明这个方法有异常,让调用这个方法的程序去处理,象这样。

package cn.speakermore.ch08;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;


/**
 * 不使用try-catch,而使用throws
 * @author mouyong
 */
public class MiddleLibWithThrows {
    public String readFile(String filename) throws FileNotFoundException{
        StringBuffer sb=new StringBuffer();
        
        Scanner input=new Scanner(new FileInputStream(filename));
        while(input.hasNext()){
            sb.append(input.nextLine())
                    .append('\n');  
        }
        return sb.toString();
    }
}

默然说话:throws是一个声明,它在说“这里可能会有异常发生,请注意!”。就好象我们路过豪宅,看到门口挂着的“小心!内有恶狗”一样,throws就是那块挂在那里的牌子,“恶狗”并不一定会让你遇上,但是你必须准备好进行异常处理。例如:换上一双可以让你比同伴跑得更快一些的跑鞋。
当我们声明抛出的是一个受检异常时(例如前面所说的FileNotFoundException),调用我们方法的程序就会得到提示:如果不把我们的方法放到try-catch中,就会得到一个编译错误信息,提示他们使用try-catch,或者象我们一样继续使用throws声明抛出这个受检异常,让别的程序调用去处理这个异常。
声明抛出受检异常,表示你认为调用方法的程序有能力且应该处理此异常。因为throws关键字使用于方法声明的头部,所以在用户查看API文档时可以直接就看到它,而无需去查看你的源代码。


图8.7 受检异常不捕获,会出现编译错误

如果你认为程序在调用你的方法应该先准备好前置条件,而不应该是蛮撞的调用,你可以声明抛出一个非受检异常(RuntimeException及其子类),这样编译器将不会强制调用程序必须使用try-catch来处理你的方法抛出的异常,如果一旦抛出,将会向上传导,正确的处理方法是使用正确的代码来完成逻辑,避免这样的异常出现,而不是用try-catch把它消灭。

默然说话:对的,异常处理机制是一个很好的工具,但好工具也不能滥用。有的同学会在什么地方都加try-catch,这其实是一件很危险的行为。这会导致在最后项目上线后出现错误却看不到任何错误消息的输出,让人无从排除错误的原因是什么。所以,大家在学习的时候,还是要注意慎用try-catch。不是只要有异常抛出的地方,就来个try-catch把错误处理掉,而是要做全局的考虑,内部的,简单直接的,影响范围小的异常,直接try-catch处理,而复杂的,影响范围大的,无法处理的异常,最好不要处理,而是使用throws去声明抛出,让别的能处理的程序去try-catch。

其实我们有些时候还可能是这样的,出现的异常我们可以处理一部分,但是并不能处理全部。比如,日常生活中我们也会遇到这样的问题,交通事故如果是小事呢,车主相互商量一下就解决了(try-catch),更多的情况是事故本身商量的差不多,但对于责任认定双方起了争执,这时还是要让交警出面。但这时的情况就是当事双方处理了一部分,但不能处理全部的情况。这时就得打电话来让交警处理剩下的问题。这里的“打电话”,就是把没法处理的部分“抛(throw)”给交警。
程序里也会存在这样的情况,这时我们会在异常处理之后,继续将异常对象抛出,关键字throw。

package cn.speakermore.ch08;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;


/**
 * 使用try-catch,同时继续抛出异常
 * @author mouyong
 */
public class MiddleLibWithThrow {
    public String readFile(String filename) throws FileNotFoundException{
        StringBuffer sb=new StringBuffer();
        try {
            Scanner input=new Scanner(new FileInputStream(filename));
            while(input.hasNext()){
                sb.append(input.nextLine())
                        .append('\n');
                
            }
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
            //继续抛出异常
            throw ex;
        }
        return sb.toString();
    }
}

在catch处理完异常之后,可以使用throw关键字把异常对象继续抛出。当然,因为有这个异常对象的抛出,所以你的方法声明处也要加上throws关键字来说明可能抛出的异常。

默然说话:要注意throws和throw的区别。throws是声明抛出,是一个名词,写在方法声明的最后,大括号前面。throws关键字后面跟着的是异常类的名称,可以跟多个异常类,每个之间用逗号(,)分隔。而throw是抛出,是一个动词,理论上可以写在方法体的任何位置。throw关键字后面跟着的是异常的对象名,一次也只能抛一个异常对象。当代码执行到throw时,将会强制停止执行throw之后的所有代码,从方法中跳出,回到方法的调用位置,而调用位置将得到跟在throw后面的那个异常对象。从这一点来说,它很象方法的return关键字。

如果说,throws是“小心!内有恶狗!”那块牌子的话,那throw就是把“恶狗”对象抛在你的面前了。这也解释了throws后跟着的是异常类的名称(仅只告诉你有某种类型的异常会抛出),而throw后要跟异常对象(已实例化的异常类)的原因。

8.1.4 贴心还是造成麻烦

异常处理的本意是,在程序错误发生时,能够有明确的方式通知API客户端,让客户端采取进一步的动作修正错误。目前Java是唯一采用受检异常(Checked Exception)的语言.这有两个目的:一个是受检异常通常会在方法声明部分使用throws声明可能会抛出的异常,这样在用户读API文档的时候就可以明确了解这个方法的调用需要处理哪些异常,二是受检异常有助于编译器在编译阶段就提前发现未被处理的异常,这样可以大大提高效率。
但是有些异常其实你是无法处理的,比如使用JDBC的数据库连接代码,经常要处理java.sql.SQLException,它是一个受检异常,可是如果数据库一旦因为物理联接连不上而造成的异常,我们的程序真的无能为力处理。这时,除了通知用户网络连接有问题,别无办法。
这时也许有的同学会说,前面不是说过了么?无法处理就抛嘛。的确如此,可是在大规模应用程序中,数据库处理的代码通常位于最底层(默然说话:嗯,我见过的最复杂的程序分层,从最低一直到界面有18层之多),你一层层往上抛不是一件很麻烦的事情么?特别是如果一开始其实没抛,后来才发现要抛,你写底层的抛一个,好嘛,因为SQLException是一个受检异常,只要调了的方法都必须处理,如果都采用抛,后面有18个人都要改自己的代码,一直改到界面层。这实在是一个很大的麻烦呀。
受检异常的本意是很好的,有助于程序设计人员注意到异常的可能性并加以处理,但在应用程序规模增大时,会逐渐对维护造成困难。因为可能底层的维护引入一个受检异常就会扩散到整个架构的相关方面都要来捕获并抛出这个异常,直到可以处理的地方进行处理。

8.1.5 认识堆栈追踪

在程序中,如果想看到异常发生的根源,以及方法反复调用过程中的栈传播,可以利用异常对象自动收集的栈轨迹(Stack Trace)来取得相关信息。
查看栈轨迹的最简单方法,就是调用每个异常对象都有的printStackTrace()。例如:

package cn.speakermore.ch08;

/**
 * 异常栈轨迹跟踪测试
 * @author mouyong
 */
public class ExceptionStackTraceTest {
    /**
     * 异常的根源,methodA
     * @return 字符串,其实这里会抛异常NullPointerException,所以它并没有返回
     */
    static String methodA(){
        String text=null;
        return text.toLowerCase();
    }
    /**
     * 调用methodA,以制造调用栈
     */
    static void methodB(){
        methodA();
    }
    /**
     * 调用methodB,以制造二重调用栈
     */
    static void methodC(){
        methodB();
    }
    /**
     * 主方法,制造三重调用栈
     * @param args 字符串数组,在这里没有什么用
     */
    public static void main(String[] args) {
        try {
            methodC();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
    }
}

在methodA()中故意制造了一个NullPointerException,用methodB()来调用了methodA(),又用methodC()来调用methodB(),最后在main()中调用了methodC()。运行之后的结果如下:



图8.8 异常栈轨迹打印
上面的第一行红色字体是一个异常类的类型名称:NullPointerException。从第二行开始的部分为栈的轨迹,第二行就是异常抛出来的位置,如果是在IDE中,则用鼠标单击蓝色的链接就会跳转到该行代码,以便让程序员快速定位错误位置并进行修改。这是程序员快速定位并调整程序,修补bug的工具,所以一定一定一定不要不输出这样的信息,象下面这样。

try{
//做一些事
}catch(Exception e){
//什么都不做,绝对绝对绝对不要这样做。
}

如果你这样做,很可能不仅仅是影响到你的代码,甚至会影响到整个项目因为这里的无任何异常的输出而造成大家都不知道异常去哪里了,从而无法定位错误位置,找不到错误原因,浪费相当多的时间和精力。
我建议所有的初学者都养成使用e.printStackTrace()这样的输出异常栈的形式,而不是自己输出一条连自己都弄不明白的错误信息,因为这样会误导自己及别人,造成找不到真正的错误原因。比如象这样:

try{
}catch(Exception e){
    System.out.println("找不到文件");
}

这样没有营养的错误信息太过模糊,给使用软件的人看影响不大,但是如果是程序员,提供的信息就太少太少,无法去定位错误的位置,甚至不知道错误的真正原因(默然说话:异常的类型就在代表着异常的原因,初学者看不懂,高手可是都懂的!

8.1.6 关于assert

很多时候,我们可以在需求或设计时就能确认,程序执行的某个时点或情况下,一定处于或不处于某种状态,如果它不是这样的,那就是严重的错误,开发过程中如果发现在这种情况,必须重新确认需求与设计。
程序在某个时段一定处于或不处于某个状态,我们称为“断言”(assert)。比如:某个变量的值在这里一定是几。断言的特点就是成立或不成立,预期结果与实际结果相同时,断言成立,否则断言就不成立。
Java在JDK1.4之后就加入了断言,它的语法如下:

assert 布尔表达式;
assert 布尔表达式 : 不成立时的文字提示;

其中,布尔表达式如果为true,则什么都不会发生。如果为false,则在第一个式子中会抛出java.lang.AssertionError,在第二个式子中则会将不成立时的文字提示显示出来。这个提示如果是个对象,则会自动调用其toString()方法。
assert作为关键字的使用并不是默认的,你需要启动断言检查。要在执行时启动断言检查,可以在使用java指令时,使用-ea参数。
那么何时使用断言呢?

  • 断言客户端调用方法前,已经准备好某些前置条件。
  • 断言客商调用方法后,具有方法承诺的结果。
  • 断言对象某个时间点下的状态
  • 使用断言取代批注
  • 断言程序中绝对不会执行到的程序代码

默然说话:这小节内容其实挺尴尬,因为目前公认的理论认为assert是一个很重要的单元测试概念,但是在实际项目代码中却不应该让它成为信息处理、判断和输出的一部分,assert应该使用在单独的测试环境中。所以思来想去,最后我决定只是简单介绍一下概念,而不再对其进行代码示例了。如果想要做更多的了解,可以关注单元测试的相关文章。

8.2 异常与资源管理

程序因为抛出异常会导致原来的程序执行流程被中断,抛出点之后的代码都会不被执行,如果程序开启了相关资源(例如:打开了一个文件,或者连接上了数据库等等),很可能会导致后继工作无法进行,甚至会导致别的程序都出现无法使用相关资源的情况(默然说话:我就遇到过,在Java代码里打开一个文件之后忘记关闭,导致这个文件再用word或记事本程序打开时报错的情况。这一错误直到我重启电脑之后才解决)。所以,在抛出异常之后,你的设计是否还能正确地关闭资源呢?

8.2.1 使用finally

前面8.1.3的写的代码其实并不正确,因为我们在打开一个文件之后,并没有关闭它。
那要何时关闭资源呢?我们可以使用下面的代码来完成:

…
public String readFile(String filename){
        StringBuffer sb=new StringBuffer();
        try {
            Scanner input=new Scanner(new FileInputStream(filename));
            while(input.hasNext()){
                sb.append(input.nextLine()).append('\n');
            }
            input.close();
        } catch (FileNotFoundException ex) {
            //在控制台输出错误真的合适吗?
            ex.printStackTrace();
        }
        
        return sb.toString();
    }
…

可是通过前面的学习,我们知道上面的代码存在隐患,因为一旦input.close()之前的代码出现了异常,那input.close()这句代码就会被跳过,文件就会不被关闭。
也许有同学会想到,在catch块再写一次。可是这却违反了我们的“代码不重复”的原则。倒底应该怎么办呢?我们需要新的帮助。
其实如果你想要的是无论如何,最后一定要执行到关闭资源的代码,try/catch还有一个关键字可以用,就是finally。写在finally里的代码,无论任何情况(try正常执行完,还是try里抛出了异常),都会被执行到,它的优先级非常高。(默然说话:我曾经尝试过在try里写了return语句。return就是让方法调用结束,可是当程序执行到return时并不会马上停止方法代码的执行,而是先去执行完finally里的代码,然后才停止方法代码的执行,回到调用位置。
经过改造后的代码如下:

package cn.speakermore.ch08;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;


/**
 * try-catch-finally形式
 * @author mouyong
 */
public class MiddleLib {
    public String readFile(String filename){
        StringBuffer sb=new StringBuffer();
        Scanner input=null;
        try {
            input=new Scanner(new FileInputStream(filename));
            while(input.hasNext()){
                sb.append(input.nextLine()).append('\n');
            }
        } catch (FileNotFoundException ex) {
           
            ex.printStackTrace();
        }finally{//无论如何都必须执行的代码放这儿
            if(input!=null){
                input.close();//在不为null的情况下关闭文件
            }
        }
        return sb.toString();
    }
}

细心的同学可能注意到了finally语句中,除了写input.close(),还给这条语句加了一个非空判断,这是由于Java变量作用域的限制造成的。在Java中,声明在try块中的变量只能在try的大括号范围有效,在finally块里是引用不到try里的变量的,所以,我们其实首先改变了input这个变量的声明,将它声明到了方法的大括号中。这样一来,必须在声明时设置input变量为空(默然说话:方法中声明的变量必须手动初始化,对象型通常我们都设置为null)。这会带来一个隐患,如果input在new的代码上出了错,那进入到finally时input就会为null,而一个null对象调用方法会抛出NullPointerException异常。所以,为了避免NullPointerException的抛出,我们加上了判断语句,保证只有在input不为null的时候才调用close()方法来关闭文件。(默然说话:这也是说得过去的,因为如果input为null,它的含义就是文件并没有打开,文件没打开,也是不需要关闭的。

8.2.2 Try With Resources

在使用try-catch-finally块或try-finally块来关闭资源时,我们都会注意到,其实又是重复代码,先检查是否为空,然后调用close()方法。在JDK7后,新增了一个方便的语法蜜糖:Try-With-Resources。(***默然说话:实在不知道如何翻译成中文呀,尝试资源?!好奇怪的英语)看关键代码:

…
try(Scanner input=new Scanner(new FileInputStream(filename))) {
            
            while(input.hasNext()){
                sb.append(input.nextLine()).append('\n');
            }
        } catch (FileNotFoundException ex) {
           
            ex.printStackTrace();
        }
…

在try的后面加上小括号,把无论有没有抛异常都需要关闭的资源声明放进小括号就可以。你不需要自己再写finally块,编译器会自动的帮你加上合适的finally。
需要注意的一点是,try with resources仅尝试帮你关闭资源对象,并不会帮你catch异常,所以该写的catch块还是得自己写的。

8.2.3 java.lang.AutoCloseable

如果你想自已实现一个能使用try with resources机制的类,只要继承JDK7新增的接口java.lang.AutoCloseable就可以了。如下:

package cn.speakermore.ch08;

/**
 * 尝试关闭资源的类示例
 * 继承AutoCloseable,实现了close方法
 * @author mouyong
 */
public class ResourceDemo implements AutoCloseable {
     private final String resName;
     /**
      * 为了便于识别,加入了一个资源名称
      * @param resName 字符串,资源的标识名
      */
     public ResourceDemo(String resName){
         this.resName=resName;
     }
     /**
      * 重写父接口的方法,模拟关闭的过程
      * Thread.sleep(800)的意思,就是让程序暂停800毫秒
      * @throws Exception 
      */
    @Override
    public void close() throws Exception {
        System.out.println(resName+":现在模拟关闭资源!");
        Thread.sleep(800);//让程序暂停800毫秒
        System.out.println(resName+":3....");
        Thread.sleep(800);//让程序暂停800毫秒
        System.out.println(resName+":2....");
        Thread.sleep(800);//让程序暂停800毫秒
        System.out.println(resName+":1....");
        Thread.sleep(200);//让程序暂停200毫秒
        System.out.println(resName+":关闭资源成功!");
    }    
}

这里写了一个叫ResourceDemo的资源类,它继承AutoCloseable接口,重写了close()方法。由它其实并没有要关闭的内容,所以在close()方法我模拟了一段动画,就是在每一行文字输出之后让程序暂停0.8秒(800毫秒),再输出下一句话。
完成这个程序之后,我们再写一个类,完成测试:

package cn.speakermore.ch08;

/**
 * 对try with resources机制的测试,单个资源资源关闭
 * @author mouyong
 */
public class CloseResourceTest {
    public static void main(String[] args){
        try(ResourceDemo rd1=new ResourceDemo("demo1");){
            //因为只是测试,所以try块内什么都没写
        }catch(Exception e){
            //catch是必须要写的,因为close()方法会抛出Exception对象
            e.printStackTrace();
        }
    }
}

执行结果如下:



图8.9 单个资源try with resources的测试结果
很可惜书不能贴动画,大家只能看到最后的结果。
Try with resources机制也可以关闭多个资源,只要在try的小括号中写成多条语句,每条语句用分号分隔就可以了。关键代码如下:

…
        try(ResourceDemo rd1=new ResourceDemo("demo1");
                ResourceDemo rd2=new ResourceDemo("demo2");){
            //因为只是测试,所以try块内什么都没写
        }catch(Exception e){
            //catch是必须要写的,因为close()方法会抛出Exception对象
            e.printStackTrace();
        }
…

如果这多个资源的关闭有顺序要求,那么要注意了,写在前面的资源总是靠后关闭的,写在后面的资源总是靠前关闭的,比如前面的例子的执行结果就能明显看出这一点。



图8.10 先写的资源后关闭,后写的资源先关闭

8.3 重点复习

Java中所有的错误都被封装为对象,我们可以try执行程序并catch那些代表错误的对象后做一起处理。try-catch机制中,JVM会首先尝试执行try中的代码,如果有错误对象抛出,就立即离开try转到catch去匹配抛出的错误对象类型,找到对应的类型之后,执行对应的catch块。需要注意的是,父类可以匹配所有的子类,所以如果要分开catch,子类一定要写在父类的前面,否则报错。
所有错误都要是可抛出的,所以Java设计了Throwable这个根错误类。Throwable定义了我们常用的printStackTrace()方法,以帮助程序员快速了解异常的原因及错误的发生位置。它下面有两个子类:Error和Exception。
Error类代表所有严重的系统性错误,并不建议进行catch。
Exception类代表所有我们程序本身可处理的异常,这也是Java把这个机制称为异常处理机制的原因。Java也推荐我们自定义的异常类继承自Exception或它的其他子类。
如果我们自己写的catch没有与抛出的异常相匹配的部分,JVM就会捕获这个异常对象,并让我们的程序立即停止执行,并将捕获到的异常栈轨迹输出给我们。
在语法上,如果声明抛出(throws)异常类是继承自除RuntimeException以外的异常父类,则编译器会强制我们使用try-catch对这个异常进行处理或使用throws进行声明抛出,以便调用该方法的程序知道有异常可能会抛出,以便进行相应的处理,我们称之为“受检异常(Checked Exception)”。如果是继承自RutimeException,编译器则不会强制我们使用try-catch,同样也不会强制进行声明抛出。我们称之为“非受检异常(Unchecked Exception)”。
在catch块中处理部分错误之后,如果需要,还可以使用throw将异常对象继续抛出。
要善用栈追踪,要把自己处理不了的异常抛出来,不要catch之后什么都不做(默然说话:如果你的确不知道应该做什么,那就在catch中throw并在方法上throws,让知道应该如何处理的人去处理),这样会给程序开发带来灾难性的后果,严重影响开发进度。
无论try块有没有发生异常,只要有finally块,就一定会执行finally块。
JDK 7之后引入了一个很方便的语法蜜糖----try with resource,鼓励大家使用。JDK 7引用的这个语法中,可以使用自动关闭的资源必须继承java.lang.AutoCloseable接口。
try with resource可以同时关闭多个资源,只要用分号分隔它们就可以了,关闭的顺序是按栈的方式来关闭,先new的资源后关闭,后new的资源先关闭。

8.4 课后练习

8.4.1 选择题

1.如果有以下的程序片段:

public class Main{
    public static void main(String[] args){
        try{
            int number=Integer.parseInt(args[0]);
            System.out.println(number++);
        }catch(NumberFormatException ex){
            System.out.println(“必须输入数字”);
        }
    }
}

执行时若没有指定命令行参数,以下描述正确的是()

A. 编译错误
B.显示“必须输入数字”
C.显示 ArrayIndexOutOfBoundException栈追踪
D.不显示任何信息

2.如果有以下的程序片段:

public class Main{
    public static void main(String[] args){
        Object[] objs={“java”,”7”};
        Integer number=(Integer) objs[1];
        System.out.println(number);
    }
}

以下描述正确的是()

A. 编译错误
B.显示7
C.显示 ClassCastException栈追踪
D.不显示任何信息

3.如果有以下的程序片段:

public class Main{
    public static void main(String[] args){
        try{
            int number=Integer.parseInt(args[0]);
            System.out.println(number++);
        }catch(NumberFormatException ex){
            System.out.println(“必须输入数字”);
        }
    }
}

执行时若指定命令行参数one,以下描述正确的是()

A. 编译错误
B.显示“必须输入数字”
C.显示 ArrayIndexOutOfBoundException栈追踪
D.不显示任何信息

4.如果有以下的程序片段:

public class FileUtil{
    public static String readFile(String name) throws ____________{
        FileInputStream input=new FileInputStream(name);
        …
    }
}

请问下划线处填入以下()选项可以通过编译

A. Throwable
B.Error
C.IOException
D.FileNotFoundException

5.FileInputStream的构造方法使用throws声明了FileNotFoundException,如果有以下的程序片段:

public class FileUtil{
    public static String readFile(String name) {
        FileInputStream input=null
    try{
            input=new FileInputStream(name);
            …
        }catch(______________ ex){
            …
        }
    }
}

请问下划线处填入以下()选项可以通过编译

A. Throwable
B.Error
C.IOException
D.FileNotFoundException

6.如果有以下的程序片段:

public class Resource{
    void doService() throws IOException{
        …
    }
}
class Some extends Resource{
    @Override
    void doService() throws ________ {
        …
    }
}

请问下划线处填入以下()选项可以通过编译

A. Throwable
B.Error
C.IOException
D.FileNotFoundException

7.如果有以下的程序片段:

public class Main{
    public static void main(String[] args){
        try{
            int number=Integer.parseInt(args[0]);
            System.out.println(number++);
        }catch(ArrayIndexOutOfBoundException | NumberFormatException ex){
            System.out.println(“必须输入数字”);
        }
    }
}

执行时若没有指定命令行参数,以下描述正确的是()

A. 编译错误
B.显示“必须输入数字”
C.显示 ArrayIndexOutOfBoundException栈追踪
D.不显示任何信息

8.如果有以下的程序片段:

public class Main{
    public static void main(String[] args){
        try{
            int number=Integer.parseInt(args[0]);
            System.out.println(number++);
        }catch(RuntimeException | NumberFormatException ex){
            System.out.println(“必须输入数字”);
        }
    }
}

执行时若没有指定命令行参数,以下描述正确的是()

A. 编译错误
B.显示“必须输入数字”
C.显示 ArrayIndexOutOfBoundException栈追踪
D.不显示任何信息

9.FileInputStream的构造方法使用throws声明了FileNotFoundException,如果有以下的程序片段:

public class FileUtil{
    public static String readFile(String name) {
        try(FileInputStream input= new FileInputStream(name)){
            …
        }
    }
}

以下描述正确的是()

A. 编译失败
B.编译成功
C.调用readFile()时必须处理FileNotFoundException
D.调用readFile()时不一定要处理FileNotFoundException

9.如果ResourceSome与ResourceOther都继承了AutoCloseable接口

public class Main{
    public static void main (String[] args) {
        try(ResourceSome some=new ResourceSome();
            ResourceOther other=new ResourceOther()){
            …
        }
    }
}

以下描述正确的是()

A. 执行完try后会先关闭ResourceSome
B.执行完try后会先关闭ResourceOther
C.执行完main()之后才关闭ResourceSome与ResourceOther
D.编译失败

8.4.2 操作题
默然说话编程工作坊