JAVA SE(二十二)—— Effective Java 之你真的会创建和销毁对象吗?

一、创建和销毁对象

1.1 用静态工厂方法代替构造器

1.1.1 静态工厂方法说明
对于类而言,为了让客户端获取它自身的一个实例,最常用的方法就是提供一个公有的构造器。除此之外,还可以考虑用类提供一个公有的静态工厂方法(static factory method),这个方法只是一个返回类的实例的静态方法。示例如下

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

这个方法将 boolean 基本类型值转换成了一个 Boolean 对象引用。

1.1.2 使用静态工厂方法的优点
(1)静态工厂方法具有名称
如果构造器的参数本身没有确切地描述正被返回的对象,那么具有适当名称的静态工厂会更容易使用,产生的客户端代码也更易于阅读。例如,构造器 BigInteger(int, int, Random) 返回的 BigInteger 可能为素数,如果用名为 BigInteger.probablePrime 的静态工厂方法来表示,显然更为清楚。

一个类只能有一个带有指定签名的构造器,所以一般会通过提供两个构造器,它们的参数列表只在参数类型的顺序上有所不同,但是在实际中当构造器比较多的时候就相当不方便,往往还会造成不必要的错误。

(2)静态工厂方法不必在每次调用它们的时候都创建一个新对象
静态工厂方法能够为重复的调用返回相同的对象,这样有助于类总能严格控制在某个时刻哪些实例应该存在,使得不可变类以使用预先构建好的实例,或者将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。

(3)静态工厂方法可以返回原返回类型的任何子类型的对象
API可以返回对象,同时又不会使对象的类变成公有的,以这种方式隐藏实现类会时API变得非常简洁,也使得我们在选择返回对象的类时就有了更大的灵活性。

(4)静态工厂方法所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。
只要是已声明的返回类型的子类型,都是允许的。为了提升软件的可维护性和性能,返回对象的类也可能随着发行版本的不同而不同。

(5)静态工程方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不必存在。
这种灵活的静态工厂方法构成了服务提供者框架(Service Provider Framework)的基础,例如JDBC(Java数据库连接, Java Databse Connectivity)API。服务提供者框架是指这样一个系统:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把他们从多个实现中解耦出来。

服务提供者框架中有三个重要的组件:服务接口(Service Interface),这是提供者实现的;提供者注册API(Provider Registration API),这是系统用来注册实现,让客户端访问它们的;服务访问API(Service Access API),是客户端用来获取服务的实例的。服务访问API一般允许但是不要求客户端指定某种选择提供者的条件。如果没有这样的规定,API就会返回默认实现的一个实例。服务访问API是“灵活的静态工厂”,它构成了服务提供者框架的基础。

服务提供者框架模式有着无数种变体,下面是一个简单的实现示例,包含一个服务提供者接口和一个默认提供者:
Service接口

public interface Service {

}

Provider接口

public interface Provider {
	Service newService();
}

Services类

public class Services {
	private Services() {} // Prevents instantiation (Item 4)
	
	private static final Map<String, Provider> providers = new ConcurrentHashMap<String, Provider>();
	public static final String DEFAULT_PROVIDER_NAME = "<def>";
	
	public static void registerDefaultProvider(Provider p) {
		registerProvider(DEFAULT_PROVIDER_NAME, p);
	}
	public static void registerProvider(String name, Provider p) {
		providers.put(name, p);
	}
	
	public static Service newInstance() {
		return newInstance(DEFAULT_PROVIDER_NAME);
	}
	public static Service newInstance(String name) {
		Provider p = providers.get(name);
		if (p == null)
			throw new IllegalArgumentException("No provider registered with name: " + name);
		return p.newService();
	}
}

1.1.3 使用静态工厂方法的缺点
(1)类如果不含公有的或者受受保护的构造器,就不能被子类化。
例如,想要将Collection Framework中的任何便利的实现类子类化都市不可能的。

(2)静态工程方法很难被发现
在开发API文档中,构造器有明确的标识和使用方法,但是静态工厂方法没有,所以对于提供了静态工厂方法而不是构造器的类来说,想要查明如何实例化一个类就比较困难。

下面时静态工厂方法的一些惯用名称:
from:类型转换方法,它只有单个参数,返回该类型的一个相对应的实例。

Date d = Date.from(instant);

of:聚合方法,带有多个参数,返回该类型的一个实例,并把它们合并起来

Set<Name> names = EnumSet.of(Jack,Bob,Frank);

valueOf:比from和of更繁琐的一种替代方法,该方法返回的实例与它的参数具有相同的值,这种静态工厂方法实际上是类型转换方法。

BigInteger param = BigInteger.valueOf(Integer.MAX_VALUE)

instance/getInstance:返回的实例是通过方法的参数来描述的,但是不能够说与参数具有同样的值。对于 Singleton 来说,该方法没有参数,并返回唯一的实例。

StackWalker sw = StackWalker.getInstance(options);

create/newInstance:像 instance 或者 getInstance 一样,但 create 或者 newInstance 能够确保每次调用返回的实例都是一个新的实例。

Object newArray = Array.newInstance(classObject,arrayLen);

getType:像 getInstance 一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型。

FileStore fs = Files.getFileStore(path);

newType:像 newInstance 一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型。

BufferedReader br = Files.newBufferedReader(path);

type:getType和newType的简洁版。

List<Complaint> list = Collections.list(legacyLitany);

1.2 遇到多个构造器参数时考虑是应用构建器

1.2.1 重叠构造器模式
静态工厂和构造器有个共同的局限性,即它们都不能很好地扩展到大量的可选参数。对于这种情况,一般可以采用重叠构造器(telescoping constructor)模式,在这种模式下,你提供第一个只有必要参数的构造器,第二个构造器有一个可选参数,第三个有两个可选参数,以此类推,最后一个构造器包含所有可选参数。

重叠构造器模式示例

/**
 * 重叠构造器模式
 */
public class TCEntity {
    private int id;         //必选参数
    private String name;    //必选参数
    private int age;
    private String sex;
    private String city;
    private long phone;

    /* 只有两个必选参数 */
    public TCEntity (int id, String name) {
        this.id = id;
        this.name = name;
    }

    /* 包含一个可选参数 */
    public TCEntity (int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    /* 包含两个可选参数 */
    public TCEntity (int id, String name, int age, String sex) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    /* 包含三个可选参数 */
    public TCEntity (int id, String name, int age, String sex, String city) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.city = city;
    }

    /* 包含四个可选参数 */
    public TCEntity (int id, String name, int age, String sex, String city, long phone) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.city = city;
        this.phone = phone;
    }
}

当想要创建对象时,就直接调用列表中的构造器,示例如下

public class CreateObject {
    public static void main(String[] args) {
        TCEntity tcEntity = new TCEntity (1,"JACK",0,"MAN","",1212123);
    }
}

当我们在调用这个构造器的时候,这个构造器包含一些我们并不想设置的参数如age、city,但还是不得不为他们传递值。在这个例子中,我们给 age 传递了一个值为0,给 city 传递了一个空值,这就使得创建对象变得麻烦和消耗资源,当参数较少的时候还能清楚的知道每个参数应该如何传递,当参数很多的时候,创建对象就会变得难以控制。

重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写,并且仍然较难以阅读。如果使用者想知道那些值是什么意思,必须很仔细地数着这些参数来探个究竟。一长串类型相同的参数会导致一些微妙的错误。如果客户端不小心颠倒了其中两个参数的顺序,编译器也不会出错,但是程序在运行时会出现错误的行为。

1.2.2 JavaBeans模式
遇到许多构造参数的时候,还有第二种代替办法,即JavaBeans模式,在这种模式下,调用一个无参构造器来创建队形,然后调用setter方法来设置每个必要的参数,以及每个相关的可选参数。

JavaBeans模式示例

/**
 * JavaBeans模式
 */
public class JBEntity {
    private int id;         //必选参数
    private String name;    //必选参数
    private int age;
    private String sex;
    private String city;
    private long phone;

    /* 无参构造 */
    public JBEntity() {
    }

    /* Setters*/
    public void setId(int id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public void setPhone(long phone) {
        this.phone = phone;
    }
}

创建对象示例

public class CreateObject {
    public static void main(String[] args) {
        JBEntity jbEntity = new JBEntity();
        jbEntity.setId(1);
        jbEntity.setName("ROSE");
        jbEntity.setAge(18);
        jbEntity.setSex("WOMAN");
        jbEntity.setCity("LONDON");
        jbEntity.setPhone(1212122);
    }
}

从上述的示例中可以看出,JavaBeans模式有效的弥补了重叠构造器模式的不足,使创建对象变容易,传递参数也简单易读。

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

当对象的构造完成,并且不允许在解冻之前使用时,通过手工“冻结”对象,可以弥补这些不足,但是这种方式十分笨拙,在实践很少使用。此外,它甚至会在运行时导致错误,因为编译器无法确保程序员会在使用之前先在对象上调用freeze方法。

1.2.3 Builder模式
上述两种方法创建对象都有较大的弊端,于是我们可以考虑第三种方法,这种方法既能保证像重叠构造器模式那样的安全性,也能保证像JavaBeans模式那么好的可读性。这就是Builder模式的一种形式。不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个builder对象,然后客户端调用无参的 build 方法来生成不可变的对象,这个builder是它构建的类的静态成员类

Builder模式示例

/**
 * Builder模式
 */
public class BrEntity {
    private int id;         //必选参数
    private String name;    //必选参数
    private int age;
    private String sex;
    private String city;
    private long phone;

    public BrEntity(Builder builder) {
        id = builder.id;
        name = builder.name;
        age = builder.age;
        sex = builder.sex;
        city = builder.city;
        phone = builder.phone;
    }

    public static class Builder {
        private int id;         //必选参数
        private String name;    //必选参数
        private int age;
        private String sex;
        private String city;
        private long phone;

        /* 必选参数 */
        public Builder(int id, String name) {
            this.id = id;
            this.name = name;
        }
        
        public Builder age(int param){
            age = param;
            return this;
        }

        public Builder sex(String param){
            sex = param;
            return this;
        }

        public Builder city(String param){
            city = param;
            return this;
        }

        public Builder phone(long param){
            phone = param;
            return this;
        }
        
        public BrEntity build(){
            return new BrEntity(this);
        }
    }
}

BrEntity是不可变的,所有的默认参数值都单独放在一个地方,builder的 setter 方法返回 builder 本身,以便可以把调用链接起来,下面是创建对象的示例

public class CreateObject {
    public static void main(String[] args) {
        BrEntity brEntity =new BrEntity
                .Builder(1,"JACK")
                .age(18).sex("MAN")
                .city("LONDON")
                .phone(1212123)
                .build();
    }
}

builder像个构造器一样,可以对其参数强加约束条件, build 方法可以检验这些约束条件。将参数从builder拷贝到对象中之后,并在对象域而不是builder域中对它们进行检验,如果违反了任何约束条件, build 方法就应该抛出 IllegalStateException。

对多个参数强加约束条件的另一种方法是,用多个setter方法对某个约束条件必须持有的所有参数进行检查。如果该约束条件没有得到满足,setter方法就会抛出 IllegalArgumentsException 。这有个好处,就是一旦传递了无效的参数,立即就会发现约束条件失败,而不是等着调用 build 方法。

Builder模式也适用于类层次结构,使用平行层次结构的builder时,各自嵌套在相应的类中。抽象类有抽象的builder,具体类有具体类的builder。示例如下
① Annimon类:表示各种的动物

/**
 * Annimon类:表示各种的动物
 */
public abstract class Annimal {
    /* 颜色 */
    public enum Color {
        RED, BLACK, WHITE, YELLO
    }
    final Set<Color> colors;

    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Color> colors = EnumSet.noneOf(Color.class);

        protected abstract T self();

        /* 设置颜色 */
        public T setColor(Color color) {
            colors.add(Objects.requireNonNull(color));
            return self();
        }

        abstract Annimal build();
    }

    /* 构造器 */
    Annimal(Builder<?> builder) {
        colors = builder.colors.clone();
    }
}

Annimal.Builder 的类型时泛型,带有一个递归类型参数,它和抽象的self方法一样,允许在子类种适当地进行方法链接,不需要转换类型。这种针对Java缺乏self类型的解决方法被称作走模拟的self类型。

② Dogs 类继承 Annimal类
这个类种,可以设置品种和颜色

/**
 * Dogs 类继承 Annimal类
 */
public class Dogs extends Annimal {
    /* Dogs 的品种 */
    public enum Kinds {
        KEJI, JINMAO, HASHIQI, BAGE, TAIDI
    }

    public final Kinds kinds;

    public static class Builder extends Annimal.Builder<Builder> {

        private final Kinds kinds;

        /* 设置 Dogs 的品种 */
        public Builder(Kinds kinds) {
            this.kinds = Objects.requireNonNull(kinds);
        }

        @Override
        protected Builder self() {
            return this;
        }

        @Override
        public Dogs build() {
            return new Dogs(this);
        }
    }

    /* 构造器*/
    private Dogs(Builder builder) {
        super(builder);
        kinds = builder.kinds;
    }

    @Override
    public String toString() {
        return "Dogs{" +
                "kinds=" + kinds +
                ", colors=" + colors +
                '}';
    }
}

③ Cats 类继承 Annimal类
这个类可以设置性别和颜色

/**
 * Cats 类继承 Annimal类
 */
public class Cats extends Annimal {
    /* Cats 是不是母猫*/
    private final boolean female;

    public static class Builder extends Annimal.Builder<Builder> {

        private boolean female = false;

        /* 更改Cats的female */
        public Builder femaleOrMale() {
            female = true;
            return this;
        }

        @Override
        protected Builder self() {
            return this;
        }

        @Override
        public Cats build() {
            return new Cats(this);
        }
    }

    /* 构造器*/
    private Cats(Builder builder) {
        super(builder);
        female = builder.female;
    }

    @Override
    public String toString() {
        return "Cats{" +
                "female=" + female +
                ", colors=" + colors +
                '}';
    }
}

每个子类的构建器种的build方法都声明返回正确的子类:Dogs.Builder的build方法返回Dogs,Cats.Builder的build返回Cats。在该方法种,子类方法声明返回超类种声明的返回类型的子类型,这种叫做协变返回类型,它允许客户端无需转换类型就能使用这些构建器。

④ 测试类

public class CreateObject {
    public static void main(String[] args) {
        /* 创建 Dog 对象:品种柯基、颜色YELLO*/
        Dogs dog = new Dogs.Builder(KEJI)
                .setColor(YELLO)
                .build();

        /* 创建 Cat 对象:白色、female*/
        Cats cat = new Cats.Builder()
                .setColor(WHITE)
                .femaleOrMale()
                .build();

        System.out.println(dog);
        System.out.println(cat);
    }
}

⑤ 结果

Dogs{kinds=KEJI, colors=[YELLO]}
Cats{female=true, colors=[WHITE]}

与构造器想比,builder模式的略微优势在于,builder可以有多个可变参数。构造器就像方法一样,只能有一个可变参数。因为builder利用单独的方法来设置每个参数,你想要多少个可变参数,它们就可以有多少个,知道每个setter方法都有一个可变参数。

Builder模式十分灵活,可以利用单个builder构建多个对象。builder的参数可以在创建对象期间进行调整,也可以随着不同的对象而改变。builder可以自动填充某些域,例如每次创建对象时自动增加序列号。

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

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

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

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

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

1.3.2 公共静态成员是个final域

public class Elvis {
	public static final Elvis INSTANCE = new Elvis();
	
	private Elvis() { ... }
	
	public void leaveTheBuilding() { ... }
}

私有构造器仅被调用一次,用来实例化公有的静态 final 域 Elvis.INSTANCE 。由于缺少公有的或者受保护的构造器,所以保证了 Elvis 的全局唯一性:一旦 Elvis 类被实例化,只会存在一个 Elvis 实例,不多也不少。

客户端的任何行为都不会改变这一点,但要注意的是,享有特权的客户端可以借助 AccessibleObject.setAccessible 方法,通过反射机制调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候创建异常。

优点:
(1)API很清楚地表明了这个类是一个Singleton:共有的静态域是final的,所以该域总是包含相同的对象引用。。

(2)使用更简单

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

public class Elvis {
	private static final Elvis INSTANCE = new Elvis();
	
	private Elvis() { ... }
	
	public static Elvis getInstance() { return INSTANCE }
	
	public void leaveTheBuilding() { ... }
}

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

优点:
(1)使用更具有灵活性:在不改变其API的前提下,我们可以改变该类是否应该为Singleton的想法。工厂方法返回该类的唯一实例,但是它可以很容易被修改,比如改成为每个调用该方法的线程返回一个唯一的实例。

(2)如果硬要程序需要,可以编写一个泛型Singleton工厂。

(3)可以通过方法引用作为提供者。

为了将利用上述方法实现的Singleton类变成是可序列化的( Serializable ),仅仅在声明中加上implements Serializable是不够的。为了维护并保证Singleton,必须声明所有实例域都是瞬时的,并提供一个 readResolve 方法。否则,每次反序列化一个序列化的实例时,都会创建一个新的实例,比如说,在我们的例子中,会导致“假冒的Elvis”。为了防止这种情况,要在 Elvis 类中加入下面这个 readResolve 方法:

private Object readResolve() {
	return INSTANCE;
}

1.3.4 声明一个包含单个元素的枚举类型
实现Singleton的第三种方法使声明一个包含单个元素的枚举类型:

public enum Elvis {
	INSTANCE;
	public void leaveTheBuilding() { ... }
}

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

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

有时候,你可能需要编写只包含静态方法和静态域的类。这些类的名声很不好,因为有些人在面向对象的语言中滥用这样的类来编写过程化的程序。尽管如此,它们也确实有它们特有的用处。我们可以利用这种类,以 java.lang.Math 或者 java.util.Arrays 的方式,把基本类型的值或数组类型上的相关方法组织起来。我们也可以通过 java.util.Collections 的方式,把实现特定接口的对象上的静态方法组织起来。最后,还可以利用这种类把 final 类上的方法组织起来,以取代扩展该类的做法。

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

企图通过将类做成抽象类来强制该类不可被实例化,这是行不通的。该类可以被子类化,并且该子类也可以被实例化。这样做甚至会误导用户,以为这种类是专门为了继承而设计的。然而,有一些简单习惯用法可以确保类不可被实例化。由于只有当类不包含显式的构造器时,编译器才会生成缺省的构造器,因此我们只要让这个类包含私有构造器,它就不能被实例化了:

public class UtilityClass {
	// 私有化构造器,使其不能被实例化
	private UtilityClass() {
		throw new AssertionError();
	}
}

由于显式的构造器是私有的,所以不可以在该类的外部访问它。 AssertionError 不是必需的,但是它可以避免不小心在类的内部调用构造器。它保证该类在任何情况下都不会被实例化。这种习惯用法有点违背直觉,好像构造器就是专门设计成不能被调用一样。因此,明智的做法就是在代码中增加一条注释,如上所示。

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

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

有许多类会依赖一个或多个底层的资源,以词典为例,拼写检查器需要依赖词典,于是这个示例会有下面的两种做法。
(1)把类实现为静态工具类

public class SpellChecker {
    private static final Lexicon dictionary = ...;
    
    private SpellChecker() {
        
    }
    
    public static boolean isValid(String word) {
        
    }
    
    public static List<String> suggestions(String typo) {
        
    }
}

(2)把类实现为Singleton

public class SpellChecker {
    private final Lexicon dictionary = ...;

    private SpellChecker() {

    }

    public  static INSTANCE = new SpellChecker();
    
    public static boolean isValid(String word) {

    }

    public static List<String> suggestions(String typo) {

    }
}

以上两种方法实现都不是理想的方法,因为这两种做法都假定只有一本词典可以用,但在实际中每一种语言都会对应有自己的词典,特殊语言或词汇还有特殊的词典。所以这种假定只有一本词典的做法不能满足需求。

于是,我们可以考虑将dictionary域设为nonfinal,并且添加一个方法来用于修改词典,不过这么做也不是很好的方式,因为这样显得很笨拙,而且容易出错,还不能并行工作,静态工具类和Singleton类不适合于需要引用底层资源的类。

根据上述实现方法的弊端,于是我们可以有一下的实现方式:为了实现能够支持多个实例,让每一个实例都使用客户端指定的资源,可以使用依赖注入的一种形式,当创建一个新的实例时,就将该资源传到构造器中。

public class SpellChecker {
    private final Lexicon dictionary;

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

    public static boolean isValid(String word) {

    }

    public static List<String> suggestions(String typo) {

    }
}

dictionary是SpellChecker 的一个依赖,在创建SpellChecker 时就会将dictionary注入其中。

虽然这个SpellChecker 的范例只有一个资源(dictionary),但是依赖注入却适用于任意数量的资源,以及任意的依赖形式。依赖注入的对象资源具有不可变性,因此多个客户端可以共享对象,依赖注入也同样适用于构造器、静态工厂和构建器。

这种模式还有另外一种变体,即将资源工厂传给构造器。工厂是可以重复调用来创建类型实例的一个对象,这类工厂具体表现为工厂方法模式。

虽然依赖注入极大的提升了灵活性和可测试性,但它会导致大型项目凌乱不堪,因为它通常包含上千个依赖。不过这种凌乱可以用一个依赖注入框架来改善,如Dagger、Guice、Spring。

1.6 避免创建不必要的对象

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

例如下面的例子

String s = new String("biki”);

这是一个极端的反面例子,该语句每次被执行的时候都创建一个新的String 实例,但是这些创建对象的动作全都是不必要的。传递给Sting 构造器的参数(“biki”)本身就是一个String 实例,功能方面等同于构造器创建的所有对象。如果这种用法是在一个循环中,或者是在一个被频繁调用的方法中,就会创建出成千上万不必要的String 实例。于是我们可以改进为下面的方式:

String s = "biki";

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

对于同时提供了静态工厂方法和构造器的不可变类,通常优先使用静态工厂方法而不是构造器,以避免创建不必要的对象。构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则不会。除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。

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

static boolean isRomanNumearal(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 方法最易于查看一个字符串是否与正则表达式相匹配, 但并不适合在注重性能的情形中重复使用。问题在于, 它在内部为正则表达式创建了一个Pattern 实例,却只用了一次,之后就可以进行垃圾回收了。创建Pattern 口实例的戚本很高,因为需要将正则表达式编译成一个有限状态机 。

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

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 方法如果被频繁地调用,会显示出明显的性能优势。

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

如果一个对象是不变的,那么它显然能够被安全地重用,但其他有些情形则并不总是这么明显。考虑适配器的情形,有时也叫作视图 。适配器是指这样一个对象: 它把功能委托给一个后备对象,从而为后备对象提供一个可以替代的接口。由于适配器除了后备对象之外, 没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例。

例如, Map 接口的keySet 方法返回该Map 对象的Set 视图,其中包含该Map 中所有的键 。乍看之下,好像每次调用keySet 都应该创建一个新的Set 实例,但是,对于一个给定的Map 对象,实际上每次调用keySet 都返回同样的Set 实例。虽然被返回的Set 实例一般是可改变的,但是所有返回的对象在功能上是等同的: 当其中一个返回对象发生变化的时候,所有其他的返回对象也要发生变化,因为它们是由同一个Map 实例支撑的。虽然创建keySet 视图对象的多个实例并无害处, 却是没有必要,也没有好处的。

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

private static long num(){
	Long sum = 0L;
	for(long i = 0; i <= Integer.MAX_VALUE; i++){
		sum += i;
	}
	return sum;
}

使用这段代码计算,运行效率比较慢,因为变量sum 被声明成Lo 口q 而不是long ,意味着程序构造了大约231 个多余的Long 实例(大约每次往Long sum 中增加long时构造一个实例) 。将sum 的声明从Long 改成long,就可以有效的提升效率。所以,要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。

通过维护自己的对象池( object pool )来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。正确使用对象池的典型对象示例就是数据库连接池。建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义。而且,数据库的许可可能限制你只能使用一定数量的连接。但是,一般而言,维护自己的对象池必定会把代码弄得很乱,同时增加内存占用( footprint ),并且还会损害性能。现代的JVM 实现具有高度优化的垃圾回收器,其性能很容易就会超过轻量级对象池的性能。

1.7 消除过期的对象引用

1.7.1 自动回收对象
Java语言具有自动回收机制,当我们使用完一个对象后,其会被自动回收。对象自当回收让我们的工作变得更加容易,但是也会给我们一个错觉,让我们觉得不需要考虑内存管理的事情。

示例

public class Stack {
    private Object[] elements;
    
    private int size = 0;
    
    private static final int CAP = 16;
    
    public Stack() {
        elements = new Object[CAP];
    }
    
    public void push(Object object) {
        ensureCapacity();
        elements[size++] = object;
    }
    
    public Object pop() {
        if(size == 0) {
            throw new EmptyStackException();
        }
        return elements[--size];
    }
    
    private void ensureCapacity() {
        if (elements.length == size){
            elements = Arrays.copyOf(elements,2 * size + 1);
        }
    }
}

这段程序中并没有很明显的错误,运行也不会有什么错误,但是这个程序中隐藏着一个问题,即“内存泄漏”,随着垃圾回收器活动的增加,或者由于内存占用的不断增加,程序性能的降低会逐渐表现出来。在极端的情况下,这种内存泄漏会导致磁盘交换,甚至导致程序失败( OutOfMemoryError 错误),但是这种失败情形相对比较少见。

如果一个栈先是增长,然后再收缩, 那么从栈中弹出来的对象将不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。这是因为栈内部维护着对这些对象的过期引用 。所谓的过期引用,是指永远也不会再被解除的引用。在本例中,凡是在elements 数组的“活动部分”之外的任何引用都是过期的,而活动部分是指elements 中下标小于size 的那些元素。

在支持垃圾回收的语言中,内存泄漏是很隐蔽的。如果一个对象引用被无意识地保留起来了,那么垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其他对象。即使只有少量的几个对象引用被无意识地保留下来,也会有许许多多的对象被排除在垃圾回收机制之外,从而对性能造成潜在的重大影响。

对于这类问题,当对象引用过期时即清空这些引用即可。修改后的示例如下

public class Stack {
    private Object[] elements;

    private int size = 0;

    private static final int CAP = 16;

    public Stack() {
        elements = new Object[CAP];
    }

    public void push(Object object) {
        ensureCapacity();
        elements[size++] = object;
    }

	/* 修改 */
    public Object pop() {
        if(size == 0) {
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

    private void ensureCapacity() {
        if (elements.length == size){
            elements = Arrays.copyOf(elements,2 * size + 1);
        }
    }
}

清空过期引用的另一个好处是,如果它们以后又被错误地解除引用,程序就会立即抛出NullPointerException 异常,而不是悄悄地错误运行下去。

对于每一个对象引用,一旦程序不再用到它,就把它清空。但实际情况张,这样做会把程序代码弄得很乱。消除过期引用最好的方法是让包含该引用的变量结束其生命周期。

存储池包含了elements 数组(对象引用单元,而不是对象本身)的元素。数组活动区域(同前面的定义)中的元素是己分配的,而数组其余部分的元素则是自由的。但是垃圾回收器并不知道这一点,对于垃圾回收器而言, elements 数组中的所有对象引用都同等有效。于是我们可以主动将失效对象的告诉垃圾回收器,一旦数组元素变成了非活动部分的一部分,就手工清空这些数组元素。一般来说, 只要类是自己管理内存,我们就应该警惕内存泄漏问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。

内存泄漏的另一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间内仍然留在缓存中。对于这个问题,有几种可能的解决方案:
(1)只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap 代表缓存;
(2)当缓存中的项过期之后,它们就会自动被删除。
只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时, WeakHashMap 才有用处.

更为常见的情形则是,“缓存项的生命周期是否有意义”并不是很容易确定,随着时间的推移,其中的项会变得越来越没有价值。在这种情况下,缓存应该时不时地清除掉没用的项。这项清除工作可以由一个后台线程(可能是ScheduledThreadPoolExecutor)来完成,或者也可以在给缓存添加新条目的时候顺便进行清理。LinkedHashMap 类利用它的removeEldestEntry 方法可以很容易地实现后一种方案。对于更加复杂的缓存,必须直接使用java.lang.ref 。

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

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

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

1.8.1 终结方法
终结方法( finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。使用终结方法会导致行为不稳定、性能降低,以及可移植性问题。当然,终结方法也有其可用之处,但是根据经验,一般还是应该避免使用终结方法。在Java 9中用清除方法( cleaner )代替了终结方法。清除方法没有终结方法那么危险,但仍然是不可预测、运行缓慢,一般情况下也是不必要的。

C++的程序员被告知“不要把终结方法当作是C++中析构器的对应物” 。在C++中,析构器是回收一个对象所占用资源的常规方法,是构造器所必需的对应物。在Java 中,当一个对象变得不可到达的时候,垃圾回收器会回收与该对象相关联的存储空间,并不需要程序员做专门的工作。C++的析构器也可以被用来回收其他的非内存资源。而在Java 中,一般用try-finally 块来完成类似的工作。

终结方法和清除方法的缺点在于不能保证会被及时执行。从一个对象变得不可到达开始,到它的终结方法被执行,所花费的这段时间是任意长的。这意味着, 注重时间的任务不应该由终结方法或者清除方法来完成。例如,用终结方法或者清除方法来关闭已经打开的文件,就是一个严重的错误,因为打开文件的描述符是一种很有限的资源。如果系统无法及时运行终结方法或者清除方法就会导致大量的文件仍然保留在打开状态,于是当一个程序再也不能打开文件的时候,它可能会运行失败。

及时地执行终结方法和清除方法正是垃圾回收算法的一个主要功能,这种算法在不同的JVM 实现中会大相径庭。如果程序依赖于终结方法或者清除方法被执行的时间点,那么这个程序的行为在不同的JVM 中运行的表现可能就会截然不同。一个程序在你测试用的JVM 平台上运行得非常好,而在你最重要顾客的JVM 平台上却根本无法运行,这是完全有可能的。

在很少见的情况下,为类提供终结方法,可能会随意地延迟其实例的回收过程。Java 语言规范并不保证哪个线程将会执行终结方法,所以,除了不使用终结方法之外,并没有很轻便的办法能够避免这样的问题。在这方面,清除方法比终结方法稍好一些,因为类的设计者可以控制自己的清除线程, 但清除方法仍然在后台运行,处于垃圾回收器的控制之下,因此不能确保及时清除。

Java 语言规范不仅不保证终结方法或者清除方法会被及时地执行,而且根本就不保证它们会被执行。当一个程序终止的时候,某些已经无法访问的对象上的终结方法却根本没有被执行,这是完全有可能的。结论是: 永远不应该依赖终结方法或者清除方法来更新重要的持久状态。例如,依赖终结方法或者清除方法来释放共享资源(比如数据库)上的永久锁,这很容易让整个分布式系统垮掉。

不要被System.gc 和System.runFinalization 这两个方法所诱惑,它们确实增加了终结方法或者清除方法被执行的机会,但是它们并不保证终结方法或者清除方法一定会被执行。唯一声称保证它们会被执行的两个方法是System.runFinalizersOnExit,及其Runtime.runFinalizersOnExit 。这两个方法都有致命的缺陷,井且已经被废弃很久了。

使用终结方法的另一个问题是:如果忽略在终结过程中被抛出来的未被捕获的异常,该对象的终结过程也会终止[ JLS, 12 . 6 ] 。未被捕获的异常会使对象处于破坏的状态( corruptstate ),如果另一个线程企图使用这种被破坏的对象,则可能发生任何不确定的行为。正常情况下,未被捕获的异常将会使线程终止,并打印出战轨迹( Stack Trace ),但是,如果异常发生在终结方法之中,则不会如此,甚至连警告都不会打印出来。清除方法没有这个问题,因为使用清除方法的一个类库在控制它的线程。

终结方法有一个严重的安全问题: 它们为终结方法攻击打开了类的大门。终结方法攻击背后的思想很简单:如果从构造器或者它的序列化对等体抛出异常,恶意子类的终结方法就可以在构造了一部分的应该已经半途夭折的对象上运行。这个终结方法会将对该对象的引用记录在一个静态域中,阻止它被垃圾回收。一旦记录到异常的对象,就可以轻松地在这个对象上调用任何原本永远不允许在这里出现的方法。从构造器抛出的异常,应该足以防止对象继续存在;有了终结方法的存在,这一点就做不到了。这种攻击可能造成致命的后果,final 类不会受到终结方法攻击,因为没有人能够编写出final 类的恶意子类。为了防止非final 类受到终结方法攻击, 要编写一个空的final 的finalize 方法。

如果类的对象中封装的资源(例如文件或者线程)确实需要终止,只需让类实现AutoCloseable,并要求其客户端在每个实例不再需要的时候调用close 方法,一般是利用try-with-resources 确保终止,即使遇到异常也是如此。值得提及的一个细节是,该实例必须记录下自己是否已经被关闭了: close 方法必须在一个私有域中记录下“该对象已经不再有效” 。如果这些方法是在对象已经终止之后被调用,其他的方法就必须检查这个域,并抛出IllegalStateException 异常。

终结方法和清除方法的用途:
(1)当资源的所有者忘记调用它的close 方法时,终结方法或者清除方法可以充当”安全网“,虽然这样做并不能保证终结方法或者清除方法会被及时地运行,但是在客户端无法正常结束操作的情况下,迟一点释放资源总比永远不释放要好。如果考虑编写这样的安全网终结方法,就要认真考虑清楚,这种保护是否值得付出这样的代价。有些Java 类(如FileinputStream 、FileOutputStream 、ThreadPoolExecutor 和j ava.sql.Connection )都具有能充当安全网的终结方法。

(2)第二种用途与对象的本地对等体有关。本地对等体是一个本地(非Java 的)对象,普通对象通过本地方法委托给一个本地对象。因为本地对等体不是一个普通对象,所以垃圾回收器不会知道它,当它的Java 对等体被回收的时候,它不会被回收。如果本地对等体没有关键资源,并且性能也可以接受的话,那么清除方法或者终结方法正是执行这项任务最合适的工具。如果本地对等体拥有必须被及时终止的资源,或者性能无法接受,那么该类就应该具有一个close 方法。

1.8.2 清除方法
清除方法的使用有一定的技巧。下面以一个简单的Room 类为例。假设房间在收回之前必须进行清除。Room 类实现了AutoCloseable ;它利用清除方法自动清除安全网的过程只不过是一个实现细节。与终结方法不同的是,清除方法不会污染类的公有API:

public class Room implements AutoCloseable{

    private static final Cleaner cleaner = Cleaner.create();
    
    private static class State implements Runnable {

        int numJuckPiles;
        
        State(int numJuckPiles) {
            this.numJuckPiles = numJuckPiles;
        }
        
        @Override
        public void run() {
            System.out.println("Cleaning Room");
            numJuckPiles = 0;
        }
    }

    private final State state;

    private final Cleaner.Cleanable cleanable;

    public Room(int numJuckPiles) {
        state = new State(numJuckPiles);
        cleanable = cleaner.register(this,state);
    }
    
    @Override
    public void close() throws Exception {
        cleanable.clean();
    }
}

内嵌的静态类State 保存清除方法清除房间所需的资源。在这个例子中,就是numJunkPiles域,表示房间的杂乱度。更现实地说,它可以是final 的long, 包含一个指向本地对等体的指针。State 实现了Runnable 接口,它的run 方法最多被Cleanable调用一次,后者是我们在Room 构造器中用清除器注册State 实例时获得的。以下两种情况之一会触发run 方法的调用:通常是通过调用Room 的close 方法触发的,后者又调用了Cleanable 的清除方法。如果到了Room 实例应该被垃圾回收时,客户端还没有调用close 方法,清除方法就会调用State 的run 方法。

关键是State 实例没有引用它的Room 实例。如果它引用了,会造成循环,阻止Room实例被垃圾回收(以及防止被自动清除) 。因此State 必须是一个静态的嵌套类,因为非静态的嵌套类包含了对其外围实例的引用 。同样地,也不建议使用lambda,因为它们很容易捕捉到对外围对象的引用。如前所述, Room 的清除方法只用作安全网。如果客户端将所有的Room 实例化都包在try-with-resource 块中,将永远不会请求到自动清除。如下示例:

public class Adult {
    public static void main(String[] args) {
        try(Room room = new Room(7)) {
            System.out.println("Clean");
        }
    }
}

正如所期待的一样,运行Adult 程序会打印出Goodbye ,接着是Cleaning room 。再看下面的示例

public class Adult {
    public static void main(String[] args) {
        new Room(99);
        System.out.println("Clean");
    }
}

这个示例可能并不会先打印打印出Goodbye ,接着是Cleaning room。Cleaner 规范指出:“清除方法在System.exit 期间的行为是与实现相关的。不确保清除动作是否会被调用。”虽然规范没有指明,其实对于正常的程序退出也是如此。

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

1.9 try-with-resources优先于try-finally

Java 类库中包括许多必须通过调用close 方法来手工关闭的资源。例如InputStreamOutputStreamjava.sql.Connection 。客户端经常会忽略资源的关闭,造成严重的性能后果也就可想而知了。虽然这其中的许多资源都是用终结方法作为安全网,但是效果并不理想 。

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

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

这个示例目前看起来还没有不好的地方,但是如果再添加第二个资源,就会变得比较糟:

static void copy(String str, String str1) throws IOException {
    InputStream in = new FileInputStream(str);
    try {
        OutputStream os = new FileOutputStream(str1);
        try {
            byte[] bytes = new byte[1024];
            int n;
            while ((n = in.read(bytes)) >= 0){
                os.write(bytes,0,n);
            }
        }finally {
            os.close();
        }
    }finally {
        in.close();
    }
}

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

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

以下就是使用try-with-resources 的第一个范例:

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

以下是使用try-with -resources 的第二个范例:

static void copy(String str, String str1) throws IOException {
    try {
        InputStream in = new FileInputStream(str);
        OutputStream os = new FileOutputStream(str1);
        byte[] bytes = new byte[1024];
        int n;
        while ((n = in.read(bytes)) >= 0){
            os.write(bytes,0,n);
        }
    }
}

使用try-with-resources 不仅使代码变得更简洁易懂, 也更容易进行诊断。以first LineOfFile方法为例,如果调用readLine 和close 方法都抛出异常,后一个异常就会被禁止,以保留第一个异常。事实上,为了保留你想要看到的那个异常,即便多个异常都可以被禁止。这些被禁止的异常并不是简单地被抛弃了,而是会被打印在堆栈轨迹中,并注明它们是被禁止的异常。通过编程调用getSuppressed 方法还可以访问到它们。

在try-with-resources 语句中还可以使用catch 子句,就像在平时的try-finally 语句中一样。这样既可以处理异常,又不需要再套用一层代码。如下示例,这个firstLineOfFile 方法没有抛出异常但是如果它无法打开文件,或者无法从中读取,就会返回一个默认值:

static String firstLineOfFile(String path, String val) throws IOException {
    try {
        BufferedReader br = new BufferedReader(new FileReader(path));
        return br.readLine();
    }catch (IOException e){
        return val;
    }
}

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

参考文献
[1] Effective Java 中文版(原书第3版),Joshua Bloch[美],俞黎敏 译,机械工业出版社。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TwoYellowGuns

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值