Effective Java--读书笔记(二)【第一版已完成】

第二章 创建和销毁对象

第一条:用静态工厂方法代替构造器

类可以提供一个公有的静态工厂方法,他只是一个返回类的实例的静态方法。

下例为Boolean的简单示例。这个方法将boolean基本类型值转换成了一个Boolean对象的引用。

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

注意:静态工厂方法与设计模式中的工厂方法模式不同

1.1 静态工厂方法的优势

  • 它们有名称:例如构造器BigInteger返回的BigInteger可能为素数,如果用BigInteger.probablePrime的静态工厂来表示。因为一个类只能有一个带有指定签名的构造器。由于静态工厂方法有名称,所以它们不受上述限制。当一个类需要带有相同签名的构造器时,就用静态工厂方法代替构造器,并且仔细的选择名称以便突出静态工厂方法之间的区别。
  • 不必再每次调用它们的时候都创建一个新对象。这使得不可变类可以使用预先构建好的实例,或者将实例缓存起来,进行重复利用。这种方法类似于享元模式(Flyweight)。静态工厂方法能够为重复的调用返回相同对象,这样有助于类总能严格控制在某个时刻哪些实例应该存在。这种类被称作实例受控制的类这种类可以确保它是一个单例或者不可实例化的。它还实得不可变的值类可以确保不会存在两个不可相同的实例,既当且仅当a==b时,a.equals(b)才为true。这是享元模式的基础。枚举类型可以保证这一点。
  • 可以返回原返回类型的任何子类型的对象。这里注意的是:Java8中仍要求接口的所有静态成员都必须是公有的。在java9中允许接口有private的静态方法,但是静态域和静态成员仍然需要public
  • 所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。只要是已声明的返回类型在子类型,都是允许的。返回对象的类可能随着发行版本的不同而不同。

EnumSet没有公有的构造器,只有静态工厂方法。在openJDK实现中,他们返回2种子类之一的实例,具体取决于底层枚举类型的大小,如果它的元素少于等于64个,那么会返回一个RegalarEnumSet示例,用单个long进行支持;如果超过65个元素,工厂就会返回JumboEnumSet实例,用一个long类型数组进行支持。

  • 方法返回的对象所属的类,在编写包含改静态工厂方法的类时可以不存在。这种灵活的静态工厂方法构成了服务提供者框架的基础。例如JDBC。服务提供者框架是指一个系统:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把他们从多个实现中解耦出来。它具有三个重要的组件:服务接口:这是服务提供者提供的;提供者注册API:这是提供者用来注册实现的;服务访问API:这是客户端用来获取服务的实例。还有一个可选的组件:服务提供者接口:获取服务提供者的工厂对象。如果没有此组件,通常是通过反射的方式进行实例化。例如JDBC中的Connection就是其服务接口的一部分,DriverManager.registerDriver是提供者注册API,DriverManager.getConnection是服务访问API,Driver是服务提供者接口

1.2 静态工厂方法的劣势

  • 类如果不含公有的或者受保护的构造器,就不能被子类化
  • 程序员很难发现它们

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

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

2.1 解决办法

  1. 习惯采用重叠构造器模式,换言之:就是创建多个构造方法,每个构造方法里面依靠方法的重载写不同的参数。但是这样客户端代码很难编写。
  2. JavaBeans模式,即只有一个无参构造,通过setter赋值。缺点:在构造过程中JavaBean可能处于不一致的状态。无法保证数据的一致性。即JavaBeans模式会有数据可变的可能性。这需要程序员自己来确保它的线程安全。
  3. 构造者模式是一种既能保证安全性,还能保证可读性的一种模式。它不能直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或静态工厂),得到一个builder对象。然后客户端在builder对象上调用类似于setter的方法,来设置每个相关的可选参数。最后客户端调用无参的build方法来生成通常是不可变的对象。这个builder通常是它构建的类的静态成员类。

2.2 构造者模式【待补全】

package project1.builderpattern;

/**
 * @author yuxingang
 * date 2020-12-21  17:35
 */
public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder {
        //必要参数
        private final int servingSize;
        private final int servings;
        //可选参数--初始化默认值
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;

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

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

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

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

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

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

    public NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }

}

注意:NutritionFacts是不可变的,所有默认参数值都单独放在一个地方。builder的设置方法返回builder本身,以便把调用连接起来,得到一个流式API,下面是客户端代码:

NutritionFacts nutritionFacts = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();

上例中没有进行有效性检查。有时为了确保这些不变量免受攻击,从builder复制完参数后,要检查对象域,如果检查失败抛出IllegalArgumentException。

Builder模式也适用与类层次结构

  • p13~p14

builder也可以自动填充某些域,例如每次创建对象时自动增加序列号。

2.2.1 Builder模式的应用场景
  • Builder模式还比重叠构造器更加冗长,性能不高。只有在很多个参数的时候才使用,例如4个参数以上。
  • 在项目开发中如果需要添加参数,使用构造器和静态工厂就会无法控制。这个时候最好使用Builder模式
2.2.2 总结

如果类的构造器或者静态工厂中具有多个参数,使用Builder模式。它比重叠构造器可读性好,比JavaBean安全性高,但是性能与前两个优点不可兼得。

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

Singleton是指仅仅被实例化一次的类。
Singleton通常被用来代表一个无状态的对象,如函数,或者那些本质上唯一的系统组件。Singleton会使客户端测试变的十分困难。

3.1 两种实现方法

第一种:公有静态成员是个final域:

/**
 * @author yuxingang
 * date 2020-12-22  10:59
 */
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis(){
        //...
    }
    public void leaveTheBuilding(){
        //...
    }
}

上例,私有的构造器仅被调用一次,用来实例化公有静态final域Elvis.INSTANCE。Elvis具有全局唯一性

注意:如果享有特权的客户端借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。可以创建出第二个Elvis实例。可以在构造器增加一个判断,如果创建第二个实例的时候抛出异常。

第二种:公有的成员是个静态工厂方法:

/**
 * @author yuxingang
 * date 2020-12-22  11:11
 */
public class ElvisStaticFactory {
    private static final ElvisStaticFactory INSTANCE = new ElvisStaticFactory();
    private ElvisStaticFactory(){
        //...
    }
    public static ElvisStaticFactory getInstance(){
        return INSTANCE;
    }
    public void leaveTheBuilding(){
        //...
    }
}

应用场景:

  1. 在不改变其API的情况下,可以改变该类是否是单例的。
  2. 可以编写一个泛型Singleton工厂(第30条)
  3. 可以通过方法引用作为提供者,比如Elvis::instance就是一个Supplier< Elvis >

除非满足以上任意一点,否则优先考虑公有域的方法。

第三种:声明一个包含单个元素的枚举类型。

/**
 * @author yuxingang
 * date 2020-12-22  11:30
 */
public enum ElvisEnum {
    INSTANCE;
    public void leaveTheBuilding(){
        //...
    }
}

应用场景:

  • 实现Singleton的最佳方法。
  • 如果Singleton必须扩展一个超类,而不是扩展Enum的时候,则不宜使用这个方法(虽然可以声明枚举去实现接口)
3.1.1 Singleton的序列化问题

在第一种和第二种仅仅在声明中加上implement Serializable是不够的。为了维护并保证Singleton,必须声明所有的实例域都是瞬时(transient)的,并提供readResolve方法(第89条),否则,每次反序列化一个序列化的实例时,都会创建一个新的实例,上面第一,第二种的例子就会导致有“假冒的Elvis”。要加入readResolve方法:

    //readResolve method tp preserve singleton property
    private Object readResolve(){
        //Return the one true Elvis and let the garbage collector
        //take care of the Elvis impersonator
        return  INSTANCE;
    }

第三种 在功能上与公有域方法相似,但它无偿提供了序列化机制,就算面对复杂的序列化或者反射攻击的时候,依然可以房子多次实例化。

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

企图通过将类做成抽象类来强制该类不可实例化是行不通的。因为该类可以被子类继承,并将子类实例化。
由于只有不包含显示构造器时,编译器才会生成缺省构造器,因此只要让这个类包含一个私有构造器,他就不能被实例化

/**
 * @author yuxingang
 * date 2020-12-22  14:01
 */
//Non Instantiable utility class
public class UtilityClass {
    //Suppress default constructor for noninstantiablility
    private UtilityClass(){
        throw new AssertionError();
    }
    //..Remainder omitted
}

由于显式的构造器时私有的,所以不可以在该类的外部访问它。AssertionError不是必需的,但是它可以避免不小心在类的内部调用构造器。
副作用:它使得一个类不能被子类化。所有的构造器都必须显式或隐式地调用超类构造器,在这种情况下,子类就没有课访问的超类构造可用了。
应用场景:比如一些工具类,我们提供了静态方法,不希望这个类被私有化

第五条:优先考虑依赖注入来引用资源

许多类会依赖一个或多个底层的资源。通常使用静态工具类或者Singleton的方式。但是这两种不理想,因为假设只有一本词典可用。实际上,每一种语言都有自己的词典,特殊词汇还要使用特殊的词典。此外,可能特殊的词典还要进行测试。所以一本词典是不可能满足所有的需求的。

5.1 依赖注入模式

当创建一个新的实例时,就将该资源传到构造器中
示例:词典(dictionary)是拼写检查器的一个依赖(dependency),在创建拼写检查器时就将词典注入(injected)其中。

/**
 * @author yuxingang
 * date 2020-12-22  14:24
 */
public class SpellChecker {
    private final Lexicon dictionary;

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

    public boolean isValid(String word) {
        return false;//...
    }

    public List<String> suggestions(String typo) {
        return null;//...
    }
}

上例中,虽然拼写检查器的范例只有一个资源(词典),但是依赖注入却适用于任意数量的资源,以及任意的依赖形式。依赖注入的对象资源具有不可变性,因此多个客户端可以共享依赖对象。

5.2 工厂方法模式

将资源工厂(factory)传给构造器。工厂是可以被重复调用来创建类型实例的一个对象。在java8中新增了Supplier< T >接口(第31条),适合用于表示工厂。例如:生产马赛克的方法,利用客户端提供的工厂来生产每一片马赛克:

Mosaic create(Supplier<? extends Tile> tileFactory){//...} 

5.3总结

虽然依赖注入模式极大的提升了灵活性和可测试性,但是它会导致大型项目凌乱不堪,因为它通常包含上千个依赖,所以催生出了spring框架。
不要用Singleton和静态工具类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为;也不要直接用这个类来创建这些资源。而应该将这些资源或者工厂传给构造器(或者静态工厂,或者构建器),通过它们来创建类。这个实践就被称作依赖注入,它极大地提升了类的灵活性、可重用性和可测试性。

第六条:避免创建不必要的对象

6.1 不可变对象

对于同时提供了静态工厂方法和构造器的不可变类,通常优先使用静态工厂方法而不是构造器,以避免创建不必要的对象。因为根据静态工厂方法的优势:能够为重复的调用返回相同对象,这种类被称为作实例受控的类,它可以确保是一个Singleton或者不可实例化的。并且它除了重用不可变的对象之外,也可以重用那些一直不会修改的可变对象。而构造器会在每次调用的时候创建一个新的对象。
反例:

    static boolean isRomanNumeral(String s){
        return s.matches("...");
    }

这个实现的问题在于它依赖String.matches方法。虽然String.matches方法最易于查看一个字符串是否与正则表达式相匹配,但并不适合在注重性能的情形中反复使用。因为它内部为正则表达式创建了一个pattern实例,却只用了一次,之后就会进行垃圾回收。
为了提升性能,应该显式的将正则表达式编译成一个Pattern实例(不可变),让它成为类初始化的一部分,并将它缓存起来,每当调用isRomanNumeral方法的时候就重用同一实例:
正例:

/**
 * @author yuxingang
 * date 2020-12-22  16:09
 */
public class Demo {
    private static final Pattern ROMAN = Pattern.compile("...");
    static boolean isRomanNumeral(String s){
        return ROMAN.matcher(s).matches();
    }
}

上例不仅优化了性能,还将不可见的Pattern实例做成了final静态域,可读性大大提高。
备注:
如果上例的类被初始化了,但是该方法没有被调用,那么就没必要初始化ROMAN域。通过在isRomanNumeral方法第一次被调用的时候延迟初始化(lazily initializing)这个域,有可能消除这个不必要的初始化工作,但是不建议这样做。因为这会是方法的实现更加复杂,从而无法将性能显著提高到超过已经达到的水平。

6.2 可变对象

如果这个对象是可变的。考虑适配器(adapter),有时也叫作视图(view)。适配器:它把功能委托给一个后备对象(backing object),从而为后备对象提供一个可以代替的接口。由于适配器除了后备对象之外,没有其它的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例。

6.3 装箱类型与基本类型

自动装箱是的基本数据类型和装箱数据类型之间的差别变得模糊起来,但是并没有完全消除。
示例:

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

这里注意:变量声明称Long而不是long,以为这程序构造了大量的Long实例,从而导致运行效率很低,所以应该改回long类型。
结论:要优先使用基本类型而不是装箱类型,要当心无意识的自动装箱。

6.4 总结

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

第七条:消除过期的对象引用

7.1 内存泄露

7.1.1 过期引用

程序中哪里发生了内存泄漏呢?
如果一个栈先是增长,然后在收缩,那么,从栈中弹出来的对象将不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,他们也不会被当做垃圾回收。这是因为 栈内部维护着对这些对象的过期引用(obsolete reference),所谓的过期引用,是指永远也不会再被解除的引用。
在支持垃圾回收机制的语言里,内存泄露是很隐蔽的(称这类内存泄露是“无意识的对象保持(unintentional object retention)”)。如果一个对象的引用被无意识的保留起来了,那么垃圾回收机制不仅不会处理这个对象,而且也不会处理这个对象的所引用的其它对象。即使只有少数的几个对象引用被无意识的保留下来,也会有许许多多的对象被排除在垃圾回收机制之外,从而对性能造成潜在的重大影响。
修复方法:一个对象的引用只要已过期,清空即可。对于模拟栈而言,只要一个单元被弹出栈,只想它的引用就过期了。

/**
 * @author yuxingang
 * date 2020-12-22  17:41
 */
public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

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

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

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        /*↓清空*/
        elements[size] = null;//Eliminate obsolete reference
        return result;
    }

    /**
     * Ensure space for at least one more element,roughly
     * doubling the capacity each time the array needs to grow
     */
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

清空过期引用的另一个好处是:如果他们以后又被错误的引用了,程序就会立即抛出NullPointerExcept异常,而不会悄悄的运行下去。尽快的检测出程序中的错误是有益的。

7.1.1.1 示例中的问题

何时应该清空引用?Stack类的哪些方面特性使得它可能发生内存泄露?

问题在于:Stack类自己管理内存。存储池(storage pool)包含了elements数组(对象引用但愿你,而不是对象本身)的元素。数组活动区域中的元素是已分配的(allocated),而数组其余部分的元素则是自由的。但是垃圾回收器并不知道这一点;对于垃圾回收器而言,elements数组中的所有对象的引用都同等有效。只是程序员知道数组的非活动部分是不重要的。程序员可以把这个情况告知垃圾回收器,做法很简单:一旦数组元素变成了非活动部分,就收工清空这些数组元素。

建言:清空对象引用只是一种例外,而不是一种规范只要是类自己管理内存,程序员就应该警惕内存泄露问题

7.1.2 缓存导致内存泄露

导致原因:一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间任然留在缓存中。
解决方案:

  • 如果你正好要实现这样的缓存:只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存;当缓存中的项过期后,它们就会被自动删除。注意:只有所要的缓存项的生命周期是有该键的外部引用而不是由值决定时,WeakHashMap才有用。

WeakHashMap 继承于AbstractMap,实现了Map接口。
和HashMap一样,WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。
不过WeakHashMap的键是“弱键”。在 WeakHashMap 中,当某个键不再正常使用时,会被从WeakHashMap中被自动移除。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。某个键被终止时,它对应的键值对也就从映射中有效地移除了。
这个“弱键”的原理呢?大致上就是,通过WeakReference和ReferenceQueue实现的。 WeakHashMap的key是“弱键”,即是WeakReference类型的;ReferenceQueue是一个队列,它会保存被GC回收的“弱键”。实现步骤是:
(01) 新建WeakHashMap,将“键值对”添加到WeakHashMap中。
实际上,WeakHashMap是通过数组table保存Entry(键值对);每一个Entry实际上是一个单向链表,即Entry是键值对链表。
(02) 当某“弱键”不再被其它对象引用,并被GC回收时。在GC回收该“弱键”时,这个“弱键”也同时会被添加到ReferenceQueue(queue)队列中。
(03) 当下一次我们需要操作WeakHashMap时,会先同步table和queue。table中保存了全部的键值对,而queue中保存被GC回收的键值对;同步它们,就是删除table中被GC回收的键值对。

  • 如果随着时间的推移,你要清除无用项。这种清除工作可以由一个后台线程(ScheduledThreadPoolExecutor)来完成。
  • 或者也可以在缓存添加新条目的时候顺便清理。LinkedHashMap类利用它的removeEldestEntry方法可以容易实现后一种方案。
  • 对于更加复杂的缓存,必须直接使用java.lang.ref
7.1.3 监听器和回调导致的内存泄露

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

7.2 总结

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

第八条:避免使用终结方法和清除方法

8.0 finalize简介

一般来说,需要自己close的东西,都是用了虚拟机之外的资源,例如端口,显存,文件等,虚拟机无法通过垃圾回收释放这些资源,只能显式调用close方法来释放。比如释放占用的端口,文件句柄,网络操作数据库应用等。

8.0.1 什么是finalize

finalize-方法名。Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

8.0.2 什么时候执行finalize

有三种情况
1.正常情况下,所有对象被Garbage Collection时自动调用,比如运行System.gc()的时候.
2.程序退出时为每个对象调用一次finalize方法。
3.显式的调用finalize方法
注意:GC只与内存有关。

8.0.3 重要知识点

终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。它会导致不稳定、性能降低,以及可移植问题。
在java9中用清除方法(cleaner)代替了终结方法。但是一般情况下,清除方法也是不必要的

不能把终结方法当做是C++中的析构器(destructors)的对应物。
在C++中,析构器是回收一个对象所占用资源的常用方法。
在C++中也可以使用析构器开回收其他的非内存资源,在Java中,一般用try-finally来完成类似的工作。

8.1 缺点

8.1.1 不能保证被及时执行

注重时间的(time-critical)的任务不应该由终结方法或者清除方法来完成。例如:用它们来关闭一个已经打开的文件。
及时的执行终结方法和清除方法是垃圾回收算法的一个主要功能,这种算法在不同的JVM实现会大相径庭。
Java语言规范不仅不能保证终结方法和清除方法会被及时的执行,而且根本就不能保证他们会被执行。结论是:永远不应该依赖终结方法和清除方法来更新重要的持久状态。

8.1.1.1 注意点

虽然System.gcSystem.runFinalization增加了终结方法和清除方法被执行的机会,但是不能保证其一定会被执行
唯一声称保证它们会被执行的两个方法是System.runFinalizersOnExitRuntime.runFinalizersOnExit。但是它们都被废弃了。

8.1.2 非常严重的性能损失

终结方法阻止了有效的垃圾回收。如果用清除方法,会快点。一般情况下会把清除方法作为一道安全网,这种情况下,创建,清除和销毁对象所消耗的时间比普通的创建对象,try-with-resource,在垃圾回收要花费5倍的时间。

8.1.3 严重的安全问题

它们为终结方法攻击(finalizer attack)打开了类的大门。思想:如果构造器或者它的序列化对等体抛出异常,恶意子类的终结方法就可以在构造了一半的对象上运行。这个终结方法会将该对象的引用记录在一个静态域中,阻止它会被垃圾回收。因为从构造器抛出的异常,应该防止对象继续存在;有了终结方法的存在,这一点就做不到了。final类不会受到终结方法攻击,因为编写不出final类的子类。为了防止非final类受到终结方法攻击,要编写一个空的final的finalize方法。这样还可以防止在释放资源的时候,忘记了在子类写super.finalize()方法可能导致的父类资源无法释放的问题。

/**
 * T1为子类
 **/
public class T1 extends T2 {
    @Override
    protected void finalize() throws Throwable {
        // 当子类忘记显示调用父类finalize
//                super.finalize();

        System.out.println("T1资源被清理");

    }

}
/**
 * T2为父类
 **/
public class T2 {

    // 假设是需要清理的资源
    private Integer i = new Integer(0);

    // 在T2中定义一个对象,仅仅用来除法T2中的finalize
    private final Object finalizer = new Object() {
        @Override
        protected void finalize() throws Throwable {
            // 在此处清理外层类的资源
            i = null;
            System.out.println("T2资源被清理");
        }
    };
}
8.1.3.1 AutoCloseable

如果类的对象中封装的资源(文件或者线程)需要终止,只要让类实现AutoCloseable,并要求其客户端在每个实例不再需要的时候调用close方法,一般是利用try-with-resource确保终止,即使遇到异常也是如此。

8.2 优点

8.2.1 以终结方法充当安全网

当资源的所有者忘记调用它的close时,终结方法或者清除方法可以充当安全网。虽然不能保证及时运行这两个方法,但是可以保证一定会被执行,只不过不知道什么时候执行。就是以性能换安全。有些Java类(如FileInputStream、FileOutStream、ThreadPoolExecutor和java.sql.Connection)都具有能充当安全网的终结方法
FileInputStream源码示例:

        /**
     * Closes this file input stream and releases any system resources
     * associated with the stream.
     *
     * <p> If this stream has an associated channel then the channel is closed
     * as well.
     *
     * @exception  IOException  if an I/O error occurs.
     *
     * @revised 1.4
     * @spec JSR-51
     */
    public void close() throws IOException {
        synchronized (closeLock) {
            if (closed) {
                return;
            }
            closed = true;
        }
        if (channel != null) {
           channel.close();
        }

        fd.closeAll(new Closeable() {
            public void close() throws IOException {
               close0();
           }
        });
    }

    /**
     * Ensures that the <code>close</code> method of this file input stream is
     * called when there are no more references to it.
     *
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.FileInputStream#close()
     */
    protected void finalize() throws IOException {
        if ((fd != null) &&  (fd != FileDescriptor.in)) {
            /* if fd is shared, the references in FileDescriptor
             * will ensure that finalizer is only called when
             * safe to do so. All references using the fd have
             * become unreachable. We can call close()
             */
            close();
        }
    }

可以看到FileInputStream还是有覆盖finalize方法的,而里面做的就是调用close方法,这是为了当对象持有者忘记调用close方法,在finalize方法中为它做调用close的事,这就是“安全网”的意思。
下例是BufferedInputStream的close方法。

    /**
     * Closes this input stream and releases any system resources
     * associated with the stream.
     * Once the stream has been closed, further read(), available(), reset(),
     * or skip() invocations will throw an IOException.
     * Closing a previously closed stream has no effect.
     *
     * @exception  IOException  if an I/O error occurs.
     */
    public void close() throws IOException {
        byte[] buffer;
        while ( (buffer = buf) != null) {
            if (bufUpdater.compareAndSet(this, buffer, null)) {
                InputStream input = in;
                in = null;
                if (input != null)
                    input.close();
                return;
            }
            // Else retry in case a new buf was CASed in fill()
        }
    }
8.2.2 清除方法与本地对等体【待写】

8.3 总结

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

第九条:try-with-resource优先于try-finally【待补全】

9.1 前言

Java类库中包含许多必须通过调用close方法来收工关闭的资源。例如InputSteam、java.sql.Connection。虽然这其中大部分都是用终结方法做安全网,但是效果并不理想。根据经验try-finally是确保资源会被适当关闭的最佳方法。但是这并不是最优解。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值