Effective Java(一)

时间:2022-07-22
本文章向大家介绍Effective Java(一),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

创建和销毁对象

本章的主题是创建和销毁对象:何时以及如何创建对象,何时以及如何避免创建对象,如何确保它们能够适时地销毁,以及如何管理对象销毁之前必须进行的各种清理动作。

用静态工厂方式代替构造器

对于类而言,为了让客户端获取它自身的一个实例,最传统的方法就是提供一个公有的构造器。还有一种方法,也应该在每个程序员的工具箱中占有一席之地。类可以提供一个公有的静态工厂方法( static factory method ),它只是一个返回类的实例的静态方法。下面是一个来自 Boolean(基本类型 boolean 装箱类)的简单示例。这个方法将 boolean 基本类型值转换成了 Boolean 对象引用:

public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);

   public static Boolean valueOf(boolean b) {
       return (b ? TRUE : FALSE);
   }

注意:静态工厂方法与设计模式中的工厂方法(Factory Method)模式不同。本条目中所指的静态工厂方法并不直接对应于设计模式中的工厂方法。

优势

  1. 它们有名称,代码也更易于阅读
  2. 不必再每次调用它们的时候都创建一个新对象,可极大地提升性能
  3. 它们可以返回原返回类型的任何子类型的对象,有了更大的灵活性
  4. 返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值
  5. 方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在

缺点

  1. 类如果不含公有的或者受保护的构造器,就不能被子类化
  2. 程序员很难发现它们,API文档没有明确标识出来

总结

简而言之,静态工厂方法和公有构造器都各有用处,我们需要理解它们各自的长处。静态工厂经常更加合适,因此切忌第一反应就是提供公有的构造器,而不先考虑静态工厂

遇到多个构造器参数时要考虑使用构建器

静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。

方式一:重叠构造器模式

程序员一向习惯采用重叠构造器(telescoping constructor)模式,在这种模式下,提供的第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有两个可选参数,以此类推,最后一个构造器包含所有可选的参数。

/**
 * 描述:重叠构造器
 *
 * @author Ray
 * @create 2020/2/8
 * @since 1.0.0
 */
public class NutritionFactsConstructor {

    private final int servingSize;   // required
    private final int servings;      // required
    private final int calories;      // optional
    private final int fat;           // optional
    private final int sodium;        // optional
    private final int carbohydrate;  // optional


    public NutritionFactsConstructor(int servingSize, int servings){
        this(servingSize, servings, 0);
    }
    public NutritionFactsConstructor(int servingSize, int servings, int calories){
        this(servingSize, servings, calories, 0);
    }
    public NutritionFactsConstructor(int servingSize, int servings, int calories, int fat){
        this(servingSize, servings, calories, fat, 0);
    }
    public NutritionFactsConstructor(int servingSize, int servings, int calories, int fat, int sodium){
        this(servingSize, servings, calories, fat, sodium, 0);
    }
    public NutritionFactsConstructor(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate){
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }

    @Override
    public String toString() {
        return "NutritionFactsConstructor{" +
                "servingSize=" + servingSize +
                ", servings=" + servings +
                ", calories=" + calories +
                ", fat=" + fat +
                ", sodium=" + sodium +
                ", carbohydrate=" + carbohydrate +
                '}';
    }

    public static void main(String[] args) {
        NutritionFactsConstructor n1 = new NutritionFactsConstructor(1, 2);
        NutritionFactsConstructor n2 = new NutritionFactsConstructor(1, 2 ,3);
        NutritionFactsConstructor n3 = new NutritionFactsConstructor(1, 2, 0 , 4);

        System.out.println("n1 = " + n1);
        System.out.println("n2 = " + n2);
        System.out.println("n3 = " + n3);


        //n1 = NutritionFactsConstructor{servingSize=1, servings=2, calories=0, fat=0, sodium=0, carbohydrate=0}
        //n2 = NutritionFactsConstructor{servingSize=1, servings=2, calories=3, fat=0, sodium=0, carbohydrate=0}
        //n3 = NutritionFactsConstructor{servingSize=1, servings=2, calories=0, fat=4, sodium=0, carbohydrate=0}
    }
}

简而言之,重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难缩写,并且仍然较难以阅读。如果客户端不小心颠倒了其中两个参数的顺序,编译器也不会出错,但是程序在运行时会出现错误的行为。

方式二:JavaBeans模式

在这种模式下,先调用一个无参构造器来创建对象,然后再调用 setter 方法来设置每个必要的参数,以及每个相关的可选参数

/**
 * 描述:JavaBeans 模式
 *
 * @author Ray
 * @create 2020/2/8
 * @since 1.0.0
 */
public class NutritionFactsJavaBeans {

    private int servingSize = -1;  // Required; no default value
    private int servings = -1;     // Required; no default value
    private int calories = 0;
    private int fat = 0;
    private int sodium = 0;
    private int carbohydrate = 0;

    public NutritionFactsJavaBeans() {
    }

    public void setServingSize(int servingSize) {
        this.servingSize = servingSize;
    }

    public void setServings(int servings) {
        this.servings = servings;
    }

    public void setCalories(int calories) {
        this.calories = calories;
    }

    public void setFat(int fat) {
        this.fat = fat;
    }

    public void setSodium(int sodium) {
        this.sodium = sodium;
    }

    public void setCarbohydrate(int carbohydrate) {
        this.carbohydrate = carbohydrate;
    }

    @Override
    public String toString() {
        return "NutritionFactsJavaBeans{" +
                "servingSize=" + servingSize +
                ", servings=" + servings +
                ", calories=" + calories +
                ", fat=" + fat +
                ", sodium=" + sodium +
                ", carbohydrate=" + carbohydrate +
                '}';
    }

    public static void main(String[] args) {
        NutritionFactsJavaBeans n = new NutritionFactsJavaBeans();
        n.setServingSize(240);
        n.setServings(8);
        n.setCalories(100);
        n.setSodium(35);
        n.setCarbohydrate(27);

        System.out.println("n = " + n);

        //n = NutritionFactsJavaBeans{servingSize=240, servings=8, calories=100, fat=0, sodium=35, carbohydrate=27}
    }
}

这种模式弥补了重叠构造器模式的不足。创建实例很容易,读起来也很容易。

遗憾的是,JavaBeans 模式自身有着很严重的缺点 因为构造过程被分到了几个调用中,在构造过程中 Java Bean 可能处于不一致的状态。 类无法仅仅通过检验构造器参数的有效性来保证一致性。试图使用处于不一致状态的对象将会导致失败,这种失败与包含错误的代码大相径庭,因此调试起来十分困难。与此相关的另一点不足在于, Java Beans 模式使得把类做成不可变的可能性不复存在 (详见第 17 条),这就需要程序员付出额外的努力来确保的线程安全

方式三:建造者(Build)模式

它既能保证像重叠构造器模式那样的安全性,也能保证像 JavaBean 模式那么好的可读性 这就是建造者(Builder)模式的一种形式。它不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个 builder 对象。然后客户端在 bulder 对象上调用类 setter 的方法,来设置每个相关的可选参数 最后,客户端调用无参的 build 方法来生成通常是不可变的对象。这个 builder 通常是它构建的类的静态成员类

/**
 * 描述:Builder 模式
 *
 * @author Ray
 * @create 2020/2/8
 * @since 1.0.0
 */
public class NutritionFactsBuilder {

    private final int servingSize;   // required
    private final int servings;      // required
    private final int calories;      // optional
    private final int fat;           // optional
    private final int sodium;        // optional
    private final int carbohydrate;  // optional

    private NutritionFactsBuilder(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }

    @Override
    public String toString() {
        return "NutritionFactsBuilder{" +
                "servingSize=" + servingSize +
                ", servings=" + servings +
                ", calories=" + calories +
                ", fat=" + fat +
                ", sodium=" + sodium +
                ", carbohydrate=" + carbohydrate +
                '}';
    }

    public static class Builder {
        // required
        private final int servingSize;
        private final int servings;

        // optional
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;

        public NutritionFactsBuilder build() {
            return new NutritionFactsBuilder(this);
        }

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int value) {
            this.calories = value;
            return this;
        }

        public Builder fat(int value) {
            this.fat = fat;
            return this;
        }

        public Builder sodium(int value) {
            this.sodium = value;
            return this;
        }

        public Builder carbohydrate(int value) {
            this.carbohydrate = value;
            return this;
        }
    }

    public static void main(String[] args) {
        NutritionFactsBuilder n = new NutritionFactsBuilder.Builder(240, 8)
                .calories(100)
                .sodium(35)
                .carbohydrate(27)
                .build();

        System.out.println("n = " + n);
    }
}

注意:NutritionFactsBuilder 是不可变的,所有的默认参数值都单独放在一个地方。builder 的设值方法返回 builder 本身,以便把调用链接起来,得到一个流式的API。

这样的客户端代码很容易编写,更为重要的是易于阅读。 Builder 模式模拟了具名的可选参数。

为了简洁起见,示例中省略了有效性检查。要想进口侦测到无效的参数,可以在 builder 的构造器和方法中检查参数的有效性。查看不可变量,包括 build 方法调用的构造器中的多个参数。

Builder模式的确也有它自身的不足。为了创建对象,必须先创建它的构建器。虽然创建这个构建器的开销在实践中可能不那么明显,但是在某些十分注重性能的情况下,可能就成问题了。Builder模式还比重叠构造器模式更加冗长,因此它只在有很多参数的时候才使用,比如4个或者更多个参数。但是记住,将来你可能需要添加参数。如果一开始就使用构造器或者静态工厂,等到类需要多个参数时才添加构造器,就会无法控制,那些过时的构造器或者静态工厂显得十分不协调。因此,通常最好一开始就使用构建器。

简而言之,如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是一种不错的选择,特别是当大多数参数都是可选或者类型相同的时候。与使用重叠构造器模式相比,使用Builder模式的客户端代码将更易于阅读和编写,构建器也比JavaBeans更加安全。

用私有构造器或者枚举类型强化 Singleton 属性

Singleton 是指仅仅被实例化一次的类。Singleton 通常被用来代表一个无状态的对象,如函数,或者那些本质上唯一的系统组件。使类成为 Singleton 会使它的客户端测试变得十分困难,因为不可能给 Singleton 替换模式实现,除非实现一个充当其类型的接口。

实现 Singleton 有两种常见的方法。这两种方法都要保持构造器为私有的,并导出公有的静态成员,以便允许客户端能够访问该类的唯一实例。

公有静态成员是 final 域

public class Elvis {

    // 公有静态成员是个 final 域
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    }

    public void leaveTheBuilding() {
        
    }
}

私有构造器仅被调用一次,用来实例化公有的静态 final 域 Elvis.INSTANCE。由于缺少公有的或者受保护的构造器,所以保证了 Elvis 的全局唯一性:一旦 Elvis 类被实例化,将只会存在一个 Elvis 实例,不多也不少。客户端的任何行为都不会改变这一点,但要提醒一点:享有特权的客户端可以借助Accessibleobject.setAccessible方法,通过反射机制调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。

公有的成员是个静态工厂方法

public class Elvis {

    // 公有的成员是个静态工厂方法
    private static final Elvis INSTANCE = new Elvis();

    public Elvis() {
    }

    public static Elvis getInstance() {
        return INSTANCE;
    }

    public void leaveTheBuilding() {

    }
}

对于静态方法 Elvis.getInstance() 的所有调用,都会返回同一个对象引用,所以,永远不会创建其他的 Elvis 实例。

公有域方法的主要优势在于,API很清楚地表明了这个类是一个Singleton:公有的静态域是final的,所以该域总是包含相同的对象引用。第二个优势在于它更简单。

声明一个包含单个元素的枚举类型

单元素的枚举类型已经成为实现Singleton的最佳方法                      – 出自 《effective java》

/**
 * 描述:枚举单例示例
 *
 * @author Ray
 * @create 2020/2/9
 * @since 1.0.0
 */
public class User {

    // 私有化构造函数
    private User() {
    }

    // 定义一个静态枚举类
    static enum SingletomEnum {
        // 创建一个枚举对象,该对象天生为单例
        INSTANCE;

        private User user;

        // 私有化枚举的构造函数
        private SingletomEnum() {
            user = new User();
        }

        public User getInstance() {
            return user;
        }
    }

    // 对外暴露一个获取 User 对象的静态方法
    public static User getInstance() {
        return SingletomEnum.INSTANCE.getInstance();
    }


    // 测试
    public static void main(String[] args) {
        // true
        System.out.println(User.getInstance() == User.getInstance());
    }
}

这种方法在功能上与公有域方法相似,但更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型经常成为实现 Singleton 的最佳方法。注意,如果 Singleton 必须扩展一个超类,而不是扩展 Enum 的时候,则不宜使用这种方法(虽然可以声明枚举去实现接口)。

通过私有构造器强化不可实例化的能力

有些工具类(utility class)不希望被实例化,因为实例化对它没有任何意义。然而,在缺少显式构造器的情况下,编译器会自动提供一个公有的、无参的缺省构造器(default constructor)。对于用户而言,这个构造器与其他的构造器没有任何区别。

企图通过将类做成抽象类来强制该类不可被实例化是行不通的。

由于只有当类不包含显式的构造器时,编译器才会生成缺省的构造器,因此只要让这个类包含一个私有构造器,它就不能被实例化

/**
 * 描述:不能被实例化的工具类
 *
 * @author Ray
 * @create 2020/2/9
 * @since 1.0.0
 */
public class UtilityClass {

    /**
     * 为了不能被实例化改为私有构造器
     */
    private UtilityClass() {
    }

}

这种习惯用法也有副作用,它使得一个类不能被子类化。所有的构造器都必须显式或者隐式地调用超类(superclass)构造器,在这种情形下,子类就没有可访问的超类构造器可调用了。

优先考虑依赖注入来引用资源

这里需要的是能够支持类的多个实例(在本例中是指SpellChecker),每一个实例都使用客户端指定的资源(在本例中是指词典)。满足该需求的最简单的模式是,当创建一个新的实例时,就将该资源传到构造器中。这是依赖注入(dependency injection)的一种形式:词典(dictionary)是拼写检查器的一个依赖(dependency),在创建拼写检查器时就将词典注入(injected)其中。

public class SpellChecker {

    private final Lexicon dictionary;

    public SpellChecker(Lexicon dictionary) {
        this.dictionary = Objects.requireNonNull(dictionary);
    }

}

class Lexicon {

}

依赖注入模式就是这么简单,因此许多程序员使用多年,却不知道它还有名字呢。

总而言之,不要用Singleton和静态工具类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为;也不要直接用这个类来创建这些资源。而应该将这些资源或者工厂传给构造器(或者静态工厂,或者构建器),通过它们来创建类。这个实践就被称作依赖注入,它极大地提升了类的灵活性、可重用性和可测试性。

避免创建不必要的对象

一般来说,最好能重用单个对象,而不是在每次需要的时候就创建一个相同功能的新对象。重用方式既快速,又流行。如果对象是不可变的(immutable),它就始终可以被重用。

作为一个极端的反面例子,看看下面的语句:

// DON'T DO THIS!
String s = new String("bikini");

该语句每次被执行的时候都创建一个新的String实例,但是这些创建对象的动作全都是不必要的。传递给String构造器的参数("bikini")本身就是一个String实例,功能方面等同于构造器创建的所有对象。如果这种用法是在一个循环中,或者是在一个被频繁调用的方法中,就会创建出成千上万不必要的String实例。

改进后的版本如下所示:

String s = "bikini";

这个版本只用了一个String实例,而不是每次执行的时候都创建一个新的实例。而且,它可以保证,对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用。

静态工厂方法(static factory method)

对于同时提供了静态工厂方法(static factory method)和构造器的不可变类,通常优先使用静态工厂方法而不是构造器,以避免创建不必要的对象。

有些对象创建的成本比其他对象要高得多。如果重复地需要这类“昂贵的对象”,建议将它缓存下来重用。遗憾的是,在创建这种对象的时候,并非总是那么显而易见。假设想要编写一个方法,用它确定一个字符串是否为一个有效的罗马数字。下面介绍一种最容易的方法,使用一个正则表达式:

static boolean isRomanNumeral(String s) {
    return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
            + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}

这个实现的问题在于它依赖string.matches方法。虽然String.matches方法最易于查看一个字符串是否与正则表达式相匹配,但并不适合在注重性能的情形中重复使用。问题在于,它在内部为正则表达式创建了一个Pattern实例,却只用了一次,之后就可以进行垃圾回收了。创建Pattern实例的成本很高,因为需要将正则表达式编译成一个有限状态机(finite state machine)

public boolean matches(String regex) {
    return Pattern.matches(regex, this);
}

为了提升性能,应该显式地将正则表达式编译成一个Pattern实例(不可变),让它成为类初始化的一部分,并将它缓存起来,每当调用isRomanNumera1方法的时候就重用同一个实例:

public class RomanNumerals {

    private static final Pattern ROMAN = Pattern.compile(
            "^(?=.)M*(C[MD]|D?C{0,3})"
            + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
    
    static boolean isRomanNumeral(String s) {
        return ROMAN.matcher(s).matches();
    }
}

改进后的isRomanNumeral方法如果被频繁地调用,会显示出明显的性能优势。在我的机器上,原来的版本在一个8字符的输入字符串上花了1.1us,而改进后的版本只花了0.17us,速度快了6.5倍。除了提高性能之外,可以说代码也更清晰了。将不可见的Pattern实例做成final静态域时,可以给它起个名字,这样会比正则表达式本身更有可读性。

延迟初始化(lazily initializing)

如果包含改进后的isRomanNumeral方法的类被初始化了,但是该方法没有被调用,那就没必要初始化ROMAN域。通过在isRomanNumeral方法第一次被调用的时候延迟初始化(lazily initializing)这个域,有可能消除这个不必要的初始化工作,但是不建议这样做。正如延迟初始化中常见的情况一样,这样做会使方法的实现更加复杂,从而无法将性能显著提高到超过已经达到的水平。

自动装箱(autoboxing)

另一种创建多余对象的方法,称作自动装箱(autoboxing),它允许程序员将基本类型和装箱基本类型(Boxed Primitive Type)混用,按需要自动装箱和拆箱。自动装箱使得基本类型和装箱基本类型之间的差别变得模糊起来,但是并没有完全消除。它们在语义上还有着微妙的差别,在性能上也有着比较明显的差别(详见第61条)。请看下面的程序,它计算所有int正整数值的总和。为此,程序必须使用1ong算法,因为int不够大,无法容纳所有int正整数值的总和:

private static long sum() {

    //Long sum = 0L;  // 11756ms
    long sum = 0L;    // 2044ms

    for (long i = 0; i <= Integer.MAX_VALUE; i++) {
        sum += i;
    }

    return sum;
}

这段程序算出来的答案是正确的,但是比实际情况要更慢一些,只因为打错了一个字符(L)。变量sum被声明为Long而不是long,意味着程序构造了大约2的31次方个多余的Long实例(大约每次往Long sum中添加long时构造一个实例)。将sum的声明从Long改成long,速度明显提高。

结论很明显:要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱

消除过期的对象引用

当你第一次经历对象回收功能的时候,会觉得这简直有点不可思议。它很容易给你留下这样的印象,认为自己不再需要考虑内存管理的事情了,其实不然。

清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用最好的方法是让包含该引用的变量结束其生命周期。如果你是在最紧凑的作用域范围内定义每一个变量,这种情形就会自然而然地发生。

一般来说,只要类是自己管理内存,程序员就应该警惕内存泄漏问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。

内存泄漏的另一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间内仍然留在缓存中。

内存泄漏的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却没有显式地取消注册,那么除非你采取某些动作,否则它们就会不断地堆积起来。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用(weakreference),例如,只将它们保存成WeakHashMap中的键。

由于内存泄漏通常不会表现成明显的失败,所以它们可以在一个系统中存在很多年。往往只有通过仔细检查代码,或者借助于Heap剖析工具(Heap Profiler)才能发现内存泄漏问题。因此,如果能够在内存泄漏发生之前就知道如何预测此类问题,并阻止它们发生,那是最好不过的了。

避免使用终结方法和清除方法

总而言之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用清除方法,对于在 Java 9 之前的发行版本,则尽量不要使用终结方法。若使用了终结方法或者清除方法,则要注意它的不确定性和性能后果

try-with-resources 优先于 try-finally

Java类库中包括许多必须通过调用 close 方法来手工关闭的资源。例如 InputStream、OutputStream 和 java.sql.Connection。客户端经常会忽略资源的关闭,造成严重的性能后果也就可想而知了。

根据经验,try-finally 语句是确保资源会被适时关闭的最佳方法,就算发生异常或者返回也一样。

/**
 * try-finally - 1
 */
public String demo1(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    } finally {
        br.close();
    }
}

但是如果再添加第二个资源,就会一团糟了。

/**
 * try-finally - 2
 */
public void demo2(String src, String dst) throws IOException {
    InputStream in = new FileInputStream(src);
    try {
        OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[1024];
            int n;
            while ((n = in.read(buf)) >= 0) {
                out.write(buf, 0, n);
            }
        } finally {
            out.close();
        }
    } finally {
        in.close();
    }
}

这可能令人有些难以置信,不过就算优秀的程序员也会经常犯这样的错误。

即使用 try-finally 语句正确地关闭了资源,如前两段代码范例所示,它也存在着些许不足。因为在 try 块和 finally 块中的代码,都会抛出异常。例如在 firstLineOfFile方法中,如果底层的物理设备异常,那么调用 readLine 就会抛出异常,基于同样的原因,调用 close 也会出现异常 在这种情况下,第二个异常完全抹除了第一个异常 在异常堆枝轨迹中,完全没有关于第一个异常的记录,这在现实的系统中会导致调试变得非常复杂,因为通常需要看到第一个异常才能诊断出问题何在 虽然可以通过编写代码来禁止第二个异常,保留第一个异常,但事实上没有人会这么做,因为实现起来太烦琐了

当 Java 7 引入 try-with-resources 语句时,所有这些问题一下子就全部解决了。要使用这个构造的资源,必须先实现 AutoCloseable 接口,其中包含了单个返回 void 的close 方法。Java类库与第三方类库中的许多类和接口,现在都实现或扩展了 AutoCloseable 接口。如果编写了一个类,它代表的是必须被关闭的资源,那么这个类也应该实现 AutoCloseable。

public static void main(String[] args) {
    try (FileInputStream inputStream = new FileInputStream(new File("test"))) {
        System.out.println(inputStream.read());
    } catch (IOException e) {
        throw new RuntimeException(e.getMessage(), e);
    }
}

将外部资源的句柄对象的创建放在try关键字后面的括号中,当这个try-catch代码块执行完毕后,Java会确保外部资源的close方法被调用。代码是不是瞬间简洁许多!

try-with-resource并不是JVM虚拟机的新增功能,只是JDK实现了一个语法糖,当你将上面代码反编译后会发现,其实对JVM虚拟机而言,它看到的依然是之前的写法:

public static void main(String[] args) {
    try {
        FileInputStream inputStream = new FileInputStream(new File("test"));
        Throwable var2 = null;

        try {
            System.out.println(inputStream.read());
        } catch (Throwable var12) {
            var2 = var12;
            throw var12;
        } finally {
            if (inputStream != null) {
                if (var2 != null) {
                    try {
                        inputStream.close();
                    } catch (Throwable var11) {
                        var2.addSuppressed(var11);
                    }
                } else {
                    inputStream.close();
                }
            }

        }

    } catch (IOException var14) {
        throw new RuntimeException(var14.getMessage(), var14);
    }
}

通过反编译的代码,大家可能注意到代码中有一处对异常的特殊处理:

var2.addSuppressed(var11);

这是try-with-resource语法涉及的另外一个知识点,叫做异常抑制。当对外部资源进行处理(例如读或写)时,如果遭遇了异常,且在随后的关闭外部资源过程中,又遭遇了异常,那么你catch到的将会是对外部资源进行处理时遭遇的异常,关闭资源时遭遇的异常将被“抑制”但不是丢弃,通过异常的getSuppressed方法,可以提取出被抑制的异常。

结论很明显 在处理必须关闭的资源时,始终要优先考虑用 try-with-resources,而不是try-finally。这样得到的代码将更加简洁、清晰,产生的异常也更有价值。有了 try-with-resources 语句,在使用必须关闭的资源时,就能更轻松地正确编写代码了。实践证明,这个用 try-finally 是不可能做到的。