EffectiveJava第二章内容总结

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

       一个类允许客户端获取其实例的传统方式是提供一个公共构造方法。 其实还有另一种技术应该成为每个程序员工具箱的一部分。 一个类可以提供一个公共静态工厂方法,它只是一个返回类实例的静态方法。

下面是一个 Boolean 简单的例子( boolean 基本类型的包装类)。 此方法将 boolean 基本类型转换为 Boolean 对象引用:

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

1. 静态方法对比构造器的优势

1.1 他们有名字

如果构造方法的参数本身并不描述被返回的对象,则具有精心选择名称的静态工厂更易于使用,并且生成的客户端代码更易于阅读。 例如,返回一个可能为素数的 BigInteger 的构造方法 BigInteger(int,int,Random) 可以更好地表示为名为BigInteger.probablePrime 的静态工厂方法。 (这个方法是在 Java 1.4 中添加的。)

1.2 不用在每次调用的时候创建新对象

这允许不可变的类使用预先构建的实例,或者在构造时缓存实例,并反复分配它们以避免创建不必要的重复对象。Boolean.valueof(boolean) 方法说明了这种方法:它从不创建对象。这种技术类似于 Flyweight 模式。如果经常请求等价对象,那么它可以极大地提高性能,特别是如果在创建它们非常昂贵的情况下。

1.3 可以返回原返回类型的任意子类型对象

这为你在选择返回对象的类时提供了很大的灵活性,这种灵活性的一个应用是 API 可以返回对象而不需要公开它的类。 以这种方式隐藏实现类会使 API 非常紧凑。这种技术适用于基于接口的框架,其中接口为静态工厂方法提供自然返回类型。
在 Java 8 之前,接口不能有静态方法。根据约定,一个名为 Type 的接口的静态工厂方法被放入一个非实例化的伙伴类 (companion class) Types 类中。例如,Java 集合框架有 45 个接口的实用工具实现,提供不可修改的集合、同步集合等等。几乎所有这些实现都是通过静态工厂方法在一个非实例类 ( java .util. collections )中导出的。返回对象的类都是非公开的。

1.4 返回对象的类可以随着每次调用发生变化(取决于静态工厂方法的参数值)

声明的返回类型的任何子类都是允许的。 返回对象的类也可以随每次发布而不同。
EnumSet 类(条目 36)没有公共构造方法,只有静态工厂。 在 OpenJDK 实现中,它们根据底层枚举类型的大小返回两个子类中的一个的实例:如果大多数枚举类型具有 64 个或更少的元素,静态工厂将返回一个RegularEnumSet 实例, 返回一个 long 类型;如果枚举类型具有六十五个或更多元素,则工厂将返回一个JumboEnumSet 实例,返回一个 long 类型的数组。
这两个实现类的存在对于客户是不可见的。 如果 RegularEnumSet 不再为小枚举类型提供性能优势,则可以在未来版本中将其淘汰,而不会产生任何不良影响。 同样,未来的版本可能会添加 EnumSet 的第三个或第四个实现,如果它证明有利于性能。 客户既不知道也不关心他们从工厂返回的对象的类别; 他们只关心它是 EnumSet 的一些子类。

1.5 方法返回的对象所属的类,在编写包含该静态工厂方法时可以不存在

这种灵活的静态工厂方法构成了服务提供者框架的基础,比如 Java 数据库连接 API(JDBC)。服务提供者框架是提供者实现服务的系统,并且系统使得实现对客户端可用,从而将客户端从实现中分离出来。

服务提供者框架中有三个基本组:服务接口,它表示实现;提供者注册 API,提供者用来注册实现;以及服务访问 API,客户端使用该 API 获取服务的实例。服务访问 API 允许客户指定选择实现的标准。在缺少这样的标准的情况下,API 返回一个默认实现的实例,或者允许客户通过所有可用的实现进行遍历。服务访问 API 是灵活的静态工厂,它构成了服务提供者框架的基础。

服务提供者框架的一个可选的第四个组件是一个服务提供者接口,它描述了一个生成服务接口实例的工厂对象。在没有服务提供者接口的情况下,必须对实现进行反射实例化 。在 JDBC 的情况下, Connection 扮演服务接口的一部分, DriverManager.registerDriver 提供程序注册 API、 DriverManager.getConnection 是服务访问 API, Driver 是服务提供者接口。

服务提供者框架模式有许多变种。 例如,服务访问 API 可以向客户端返回比提供者提供的更丰富的服务接口。这是桥接模式。 依赖注入框架可以被看作是强大的服务提供者。 从 Java 6 开始,平台包含一个通用的服务提供者框架 java.util.ServiceLoader ,所以你不需要,一般也不应该自己编写。JDBC 不使用 ServiceLoader ,因为前者早于后者

2. 静态方法对比构造器的缺点

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

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

当想要创建一个实例时,可以使用包含要设置参数的构造方法:

public class Test {
    
    
    // 实例化对象
    UserInfo userInfo=  new UserInfo(1,"haiyang",21,"济南");
}

class UserInfo {
    
    
    private long userid;
    private String name;
    private int age;
    private String area;

    public UserInfo(long userid, String name, int age, String area) {
    
    
        this.userid = userid;
        this.name = name;
        this.age = age;
        this.area = area;
    }
}

通常情况下,这个构造方法的调用需要许多你不想设置的参数,但是你不得不为它们传递一个值。 在这种情况下,我们为 fat 属性传递了 0 值。「只有」六个参数可能看起来并不那么糟糕,但随着参数数量的增加,它会很快失控。

简而言之,可伸缩构造方法模式是有效的,但是当有很多参数时,很难编写客户端代码,而且很难读懂它。

1. JavaBeans模式

当在构造方法中遇到许多可选参数时,另一种选择是 JavaBeans 模式,在这种模式中,调用一个无参数的构造函数来创建对象,然后调用 setter 方法来设置每个必需的参数和可选参数:

public class Test {
    
    
    public static void main(String[] args) {
    
    
        // 实例化对象
        UserInfo userInfo =  new UserInfo();
        userInfo.setUserid(1);
        userInfo.setName("haiyang");
        userInfo.setAge(21);
        userInfo.setArea("济南");
    }
}

class UserInfo {
    
    
    private long userid;
    private String name;
    private int age;
    private String area;

    public void setUserid(long userid) {
    
     this.userid = userid; }

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

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

    public void setArea(String area) {
    
     this.area = area; }
}

这种模式没有伸缩构造方法模式的缺点。有点冗长,但创建实例很容易,并且易于阅读所生成的代码:

但是由于构造方法在多次调用中被分割,所以在构造过程中 JavaBean可能处于不一致的状态,JavaBeans 模式排除了让类 不可变的可能性

2. Builder模式

builder 的 setter 方法返回 builder 本身,这样调用就可以被链接起来,从而生成一个流畅的 API。下面是客户端代码的示例

public class Test {
    
    
    public static void main(String[] args) {
    
    
        // 实例化对象
        UserInfo userInfo = new UserInfo.Builder("haiyang")
                .userid(1)
                .age(21)
                .area("济南")
                .build();
    }
}

class UserInfo {
    
    
    private long userid;
    private String name;
    private int age;
    private String area;

    private UserInfo(Builder builder) {
    
    
        this.userid = builder.userid;
        this.name = builder.name;
        this.age = builder.age;
        this.area = builder.area;

    }
    public static class Builder {
    
    
        private long userid = 0L;
        private String name;
        private int age = 0;
        private String area;

        public Builder (String name) {
    
    
            this.name = name;
        }

        public Builder userid(long userid) {
    
    
            this.userid = userid;
            return this;
        }

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

        public Builder area(String area) {
    
    
            this.area = area;
            return this;
        }
        public UserInfo build() {
    
    
            return new UserInfo(this);
        }
    }
}

三. 单例模式(省略)

四. 强化私有构造器不可实例化能力

4.1 通过反射获取私有构造器

public class Test {
    
    
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    
    
        // 实例化对象
        Class userClass = Class.forName("com.haiyang.onestep.UserInfo");
        Constructor cons = userClass.getDeclaredConstructor(null);
        //set accessble to access private constructor
        cons.setAccessible(true);
        UserInfo userInfo = (UserInfo)cons.newInstance();
    }
}

class UserInfo {
    
    
    private long userid;
    private String name;
    private int age;
    private String area;
    private UserInfo () {
    
    
        
    }
}

4.2 强化私有构造器

    private UserInfo () {
    
    
        throw new AssertionError();
    }

因为显式构造方法是私有的,所以在类之外是不可访问的。 AssertionError 异常不是严格要求的,但是它提供了一种保证,以防在类中意外地调用构造方法。它保证类在任何情况下都不会被实例化。这个习惯用法有点违反直觉,好像构造方法就是设计成不能调用的一样。

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

许多类依赖于一个或多个底层资源。例如,拼写检查器依赖于字典。将此类类实现为静态实用工具类并不少见:

// Inappropriate use of static utility - inflexible & untestable!
public class SpellChecker {
    
    
	private static final Lexicon dictionary = ...;
	private SpellChecker() {
    
    } // Noninstantiable
	public static boolean isValid(String word) {
    
     ... }
	public static List<String> suggestions(String typo) {
    
     ... }
}

单例实现

// Inappropriate use of singleton - inflexible & untestable!
public class SpellChecker {
    
    
	private final Lexicon dictionary = ...;
	private SpellChecker(...) {
    
    }
	public static INSTANCE = new SpellChecker(...);
	public boolean isValid(String word) {
    
     ... }
	public List<String> suggestions(String typo) {
    
     ... }
}

这两种方法都不令人满意,因为他们假设只有一本字典值得使用。在实际中,每种语言都有自己的字典,特殊的字典被用于特殊的词汇表。另外,使用专门的字典来进行测试也是可取的。

依赖注入实现

// Dependency injection provides flexibility and testability
public class SpellChecker {
    
    
	private final Lexicon dictionary;
	public SpellChecker(Lexicon dictionary) {
    
    
	this.dictionary = Objects.requireNonNull(dictionary);
	}
	public boolean isValid(String word) {
    
     ... }
	public List<String> suggestions(String typo) {
    
     ... }
}

该模式的一个有用的变体是将资源工厂传递给构造方法。 工厂是可以重复调用以创建类型实例的对象。 这种工厂体现了工厂方法模式


总之,不要使用单例或静态的实用类来实现一个类,该类依赖于一个或多个底层资源,这些资源的行为会影响类的行为,并且不让类直接创建这些资源。相反,将资源或工厂传递给构造方法(或静态工厂或 builder 模式)。这种称为依赖注入的实践将极大地增强类的灵活性、可重用性和可测试性。


六. 避免创建没必要的对象

一般来说,最好重用单个对象,而不是在每次需要的时候创建一个功能相同的新对象。

作为一个不应该这样做的极端例子,请考虑以下语句:

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

语句每次执行时都会创建一个新的 String 实例,而这些对象的创建都不是必需的。String 构造方法("bikini") 的参数本身就是一个 bikini 实例,它与构造方法创建的所有对象的功能相同。如果这种用法发生在循环中,或者在频繁调用的方法中,就可以毫无必要地创建数百万个 String 实例。

改进后的版本如下:

String s = "bikini";

该版本使用单个 String 实例,而不是每次执行时创建一个新实例。此外,它可以保证对象运行在同一虚拟机上的任何其他代码重用,而这些代码恰好包含相同的字符串字面量。

通过使用静态工厂方法,可以避免创建不需要的对象。例如,工厂方法Boolean.valueOf(String) 比构造方法 Boolean(String) 更可取,后者在 Java 9 中被弃用。构造方法每次调用时都必须创建一个新对象,而工厂方法永远不需要这样做,在实践中也不需要。除了重用不可变对象,如果知道它们不会被修改,还可以重用可变对象。

6.1 【案例1】验证一个字符串是否是一个有效的罗马数字

// Performance can be greatly improved!
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 实例是昂贵的,因为它需要将正则表达式编译成有限状态机。

为了提高性能,作为类初始化的一部分,将正则表达式显式编译为一个 Pattern 实例(不可变),缓存它,并在 isRomanNumeral 方法的每个调用中重复使用相同的实例:

// Reusing expensive object for improved performance
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 方法的改进版本的类被初始化,但该方法从未被调用,可以考虑将

6.2【案例2】计算所有正整数的总和

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

这个程序的结果是正确的,但由于写错了一个字符,运行的结果要比实际慢很多。变量 sum 被声明成了Long 而不是 long ,这意味着程序构造了大约 2^31不必要的 Long 实例(大约每次往 Long 类型的 sum 变量中增加一个 long 类型构造的实例),把 sum 变量的类型由 Long 改为 long ,在我的机器上运行时间从 6.3秒降低到 0.59 秒。这个教训很明显:优先使用基本类型而不是装箱的基本类型,也要注意无意识的自动装箱

这个案例不应该被误解为暗示对象创建是昂贵的,我们应该避免创建对象。 相反,使用构造方法创建和回收小的对象是非常廉价,构造方法只会做很少的显示工作,尤其是在现代 JVM 实现上。 创建额外的对象以增强程序的清晰度,简单性或功能性通常是件好事。

七. 消除过期的对象引用

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();
        return elements[--size];
    }
    /**
     * 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);
    }
}

这个程序没有什么明显的错误, 无论如何测试,它都会成功地通过每一项测试,但有一个潜在的问题。 笼统地说,程序有一个**“内存泄漏”**,由于垃圾回收器的活动的增加,或内存占用的增加,静默地表现为性能下降。 在极端的情况下,这样的内存泄漏可能会导致磁盘分页( disk paging),甚至导致内存溢出(OutOfMemoryError)的失败,但是这样的故障相对较少。

那么哪里发生了内存泄漏? 如果一个栈增长后收缩,收缩时,从栈弹出的对象不会被垃圾收集,即使使用栈的程序不再引用这些对象。 这是因为栈维护对这些对象的过期引用( obsolete references)。 过期引用简单来说就是永远不会解除的引用。 在这种情况下,元素数组“活动部分(active portion)”之外的任何引用都是过期的。 活动部分是由索引下标小于 size 的元素组成。

这类问题的解决方法很简单:一旦对象引用过期,将它们设置为 null。 在我们的 Stack 类的情景下,只要从栈中弹出,元素的引用就设置为过期。 pop 方法的修正版本如下所示:

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

取消过期引用的另一个好处是,如果它们随后被错误地引用,程序立即抛出 NullPointerException 异常,而不是悄悄地做继续做错误的事情。尽可能快地发现程序中的错误是有好处的

当程序员第一次被这个问题困扰时,他们可能会在程序结束后立即清空所有对象引用。这既不是必要的,也不是可取的,这样会把程序弄得很乱。清空对象引用应该是例外而不是规范。消除过期引用的最好方法是让包含引用的变量超出范围。如果在最近的作用域范围内定义每个变量 (条目 57),这种自然就会出现这种情况。

那么什么时候应该清空一个引用呢? Stack 类的哪个方面使它容易受到内存泄漏的影响?简单地说,它管理自己的内存。存储池由 elements 数组的元素组成 (对象引用单元,而不是对象本身)。数组中活动部分的元素 (如前面定义的) 被分配,垃圾收集器没有办法知道其余的元素都是空闲的。对于垃圾收集器来说,elements 数组中的所有对象引用都同样有效。只有程序员知道数组的非活动部分不重要。程序员可以向垃圾收集器传达这样一个事实,一旦数组中的元素变成非活动的一部分,就可以手动清空这些元素的引用。(意思是垃圾收集器只能检测这个数组对象,如果对象本身不为空,那么垃圾收集器就不会处理这个对象,所以这个对象的存储内存只能通过对象本身来控制)

一般来说,当一个类自己管理内存时,程序员应该警惕内存泄漏问题。 每当一个元素被释放时,元素中包含的任何对象引用都应该被清除。

另一个常见的内存泄漏来源是缓存。 一旦将对象引用放入缓存中,很容易忘记它的存在,并且在它变得无关紧要之后,仍然保留在缓存中。对于这个问题有几种解决方案。如果你正好想实现了一个缓存:只要在缓存之外存在对某个项(entry)的键(key)引用,那么这项就是明确有关联的,就可以用WeakHashMap 来表示缓存;这些项在过期之后自动删除。记住,只有当缓存中某个项的生命周期是由外部引用到键(key)而不是值(value)决定时,WeakHashMap 才有用。

更常见的情况是,缓存项有用的生命周期不太明确,随着时间的推移一些项变得越来越没有价值。在这种情况下,缓存应该偶尔清理掉已经废弃的项。这可以通过一个后台线程 (也许是ScheduledThreadPoolExecutor ) 或将新的项添加到缓存时顺便清理。 LinkedHashMap 类使用它的 removeEldestEntry 方法实现了后一种方案。对于更复杂的缓存,可能直接需要使用 java.lang.ref 。

第三个常见的内存泄漏来源是监听器和其他回调。如果你实现了一个 API,其客户端注册回调,但是没有显式地撤销注册回调,除非采取一些操作,否则它们将会累积。确保回调是垃圾收集的一种方法是只存储弱引用(weak references),例如,仅将它们保存在 WeakHashMap 的键(key)中。
因为内存泄漏通常不会表现为明显的故障,所以它们可能会在系统中保持多年。 通常仅在仔细的代码检查或借助堆分析器( heap profiler)的调试工具才会被发现。 因此,学习如何预见这些问题,并防止这些问题发生,是非常值得的。

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

Finalizer 机制是不可预知的,往往是危险的,而且通常是不必要的。 它们的使用会导致不稳定的行为,糟糕的性能和移植性问题。 Finalizer 机制有一些特殊的用途,我们稍后会在这个条目中介绍,但是通常应该避免它们。 从Java 9 开始,Finalizer 机制已被弃用,但仍被 Java 类库所使用。
Java 9 中 Cleaner 机制代替了 Finalizer 机制,我在这篇jvm文章里写过关于finalize,有兴趣可以看一下 深入理解Java虚拟机—垃圾收集算法
Cleaner 机制不如 Finalizer 机制那样危险,但仍然是不可预测,运行缓慢并且通常是不必要的

Finalizer 和 Cleaner 机制的一个缺点是不能保证他们能够及时执行。 在一个对象变得无法访问时,到Finalizer 和 Cleaner 机制开始运行时,这期间的时间是任意长的。 这意味着你永远不应该 Finalizer 和 Cleaner 机制做任何时间敏感(time-critical)的事情。 例如,依赖于 Finalizer 和 Cleaner 机制来关闭文件是严重的错误,因为打开的文件描述符是有限的资源。 如果由于系统迟迟没有运行 Finalizer 和 Cleaner 机制而导致许多文件被打开,程序可能会失败,因为它不能再打开文件了。

及时执行 Finalizer 和 Cleaner 机制是垃圾收集算法的一个功能,这种算法在不同的实现中有很大的不同。程序的行为依赖于 Finalizer 和 Cleaner 机制的及时执行,其行为也可能大不不同。 这样的程序完全可以在你测试的 JVM 上完美运行,然而在你最重要的客户的机器上可能运行就会失败。

延迟终结(finalization)不只是一个理论问题。为一个类提供一个 Finalizer 机制可以任意拖延它的实例的回收。

一位同事调试了一个长时间运行的 GUI 应用程序,这个应用程序正在被一个 OutOfMemoryError 错误神秘地死掉。分析显示,在它死亡的时候,应用程序的 Finalizer 机制队列上有成千上万的图形对象正在等待被终结和回收。不幸的是,Finalizer 机制线程的运行优先级低于其他应用程序线程,所以对象被回收的速度低于进入队列的速度。语言规范并不保证哪个线程执行 Finalizer 机制,因此除了避免使用 Finalizer 机制之外,没有轻便的方法来防止这类问题。在这方面, Cleaner 机制比 Finalizer 机制要好一些,因为 Java 类的创建者可以控制自己 cleaner 机制的线程,但 cleaner机制仍然在后台运行,在垃圾回收器的控制下运行,但不能保证及时清理。

Java 规范不能保证 Finalizer 和 Cleaner 机制能及时运行;它甚至不能能保证它们是否会运行。当一个程序结束
后,一些不可达对象上的 Finalizer 和 Cleaner 机制仍然没有运行。因此,不应该依赖于 Finalizer 和 Cleaner 机制来更新持久化状态。例如,依赖于 Finalizer 和 Cleaner 机制来释放对共享资源 (如数据库) 的持久锁,这样会使整个分布式系统陷入停滞

不要相信 System.gcSystem.runFinalization 方法。 他们可能会增加 Finalizer 和 Cleaner 机制被执行的几率,但不能保证一定会执行。 曾经声称做出这种保证的两个方法: System.runFinalizersOnExit 和它的孪生兄弟 Runtime.runFinalizersOnExit ,包含致命的缺陷,并已被弃用了几十年

Finalizer 机制的另一个问题是在执行 Finalizer 机制过程中,未捕获的异常会被忽略,并且该对象的 Finalizer 机制也会终止。未捕获的异常会使其他对象陷入一种损坏的状态(corrupt state)。如果另一个线程试图使用这样一个损坏的对象,可能会导致任意不确定的行为。通常情况下,未捕获的异常将终止线程并打印堆栈跟踪(stacktrace),但如果发生在 Finalizer 机制中,则不会发出警告。Cleaner 机制没有这个问题,因为使用 Cleaner 机制的类库可以控制其线程

使用 finalizer 和 cleaner 机制会导致严重的性能损失。 在我的机器上,创建一个简单的 AutoCloseable 对象,使用 try-with-resources 关闭它,并让垃圾回收器回收它的时间大约是 12 纳秒。 使用 finalizer 机制,而时间增加到 550 纳秒。 换句话说,使用 finalizer 机制创建和销毁对象的速度要慢 50 倍。 这主要是因为 finalizer 机制会阻碍有效的垃圾收集。 如果使用它们来清理类的所有实例 (在我的机器上的每个实例大约是 500 纳秒),那么 cleaner 机制的速度与 finalizer 机制的速度相当,但是如果仅将它们用作安全网( safety net),则 cleaner 机制要快得多,如下所述。 在这种环境下,创建,清理和销毁一个对象在我的机器上需要大约 66 纳秒,这意味着如果你不使用安全网的话,需要支付 5 倍 (而不是 50 倍) 的保险。

finalizer 机制有一个严重的安全问题:它们会打开你的类来进行 finalizer 机制攻击。finalizer 机制攻击的想法很简单:如果一个异常是从构造方法或它的序列化中抛出的—— readObjectreadResolve 方法 (第 12 章)——恶意子类的 finalizer 机制可以运行在本应该“中途夭折(died on the vine)”的部分构造对象上。finalizer 机制可以在静态字属性记录对对象的引用,防止其被垃圾收集。一旦记录了有缺陷的对象,就可以简单地调用该对象上的任意方法,而这些方法本来就不应该允许存在。从构造方法中抛出异常应该足以防止对象出现;而在 finalizer 机制存在下,则不是。这样的攻击会带来可怕的后果。Final 类不受 finalizer 机制攻击的影响,因为没有人可以编写一个 final 类的恶意子类。为了保护非 final 类不受 finalizer 机制攻击,编写一个 final 的 finalize 方法,它什么都不做。
那么,你应该怎样做呢?为对象封装需要结束的资源 (如文件或线程),而不是为该类编写 Finalizer 和 Cleaner 机
制?让你的类实现 AutoCloseable 接口即可,并要求客户在在不再需要时调用每个实例 close 方法,通常使用 trywith-resources 确保终止,即使面对有异常抛出情况(条目 9)。一个值得一提的细节是实例必须跟踪是否已经关闭:close 方法必须记录在对象里不再有效的属性,其他方法必须检查该属性,如果在对象关闭后调用它们,则抛出IllegalStateException 异常。

那么,Finalizer 和 Cleaner 机制有什么好处呢?它们可能有两个合法用途。一个是作为一个安全网(safetynet),以防资源的拥有者忽略了它的 close 方法。虽然不能保证 Finalizer 和 Cleaner 机制会迅速运行 (或者根本就没有运行),最好是把资源释放晚点出来,也要好过客户端没有这样做。如果你正在考虑编写这样的安全网 Finalizer机制,请仔细考虑一下这样保护是否值得付出对应的代价。一些 Java 库类,如 FileInputStream 、FileOutputStream 、 ThreadPoolExecutor 和 java.sql.Connection ,都有作为安全网的 Finalizer 机制。

第二种合理使用 Cleaner 机制的方法与本地对等类(native peers)有关。本地对等类是一个由普通对象委托的本地 (非 Java) 对象。由于本地对等类不是普通的 Java 对象,所以垃圾收集器并不知道它,当它的 Java 对等对象被回收时,本地对等类也不会回收。假设性能是可以接受的,并且本地对等类没有关键的资源,那么 Finalizer 和 Cleaner机制可能是这项任务的合适的工具。但如果性能是不可接受的,或者本地对等类持有必须迅速回收的资源,那么类应该有一个 close 方法,正如前面所述。

Cleaner 机制使用起来有点棘手。下面是演示该功能的一个简单的 Room 类。假设 Room 对象必须在被回收前
清理干净。 Room 类实现 AutoCloseable 接口;它的自动清理安全网使用的是一个 Cleaner 机制,这仅仅是一个
实现细节。与 Finalizer 机制不同,Cleaner 机制不污染一个类的公共 API:

public class Room implements AutoCloseable {
    
    
    private static final Cleaner cleaner = Cleaner.create();
    // Resource that requires cleaning. Must not refer to Room!
    private static class State implements Runnable {
    
    
        int numJunkPiles; // Number of junk piles in this room
        State(int numJunkPiles) {
    
    
            this.numJunkPiles = numJunkPiles;
        }
        // Invoked by close method or cleaner
        @Override
        public void run() {
    
    
            System.out.println("Cleaning room");
            numJunkPiles = 0;
        }
    }
    // The state of this room, shared with our cleanable
    private final State state;
    // Our cleanable. Cleans the room when it’s eligible for gc
    private final Cleaner.Cleanable cleanable;
    public Room(int numJunkPiles) {
    
    
        state = new State(numJunkPiles);
        cleanable = cleaner.register(this, state);
    }
    @Override
    public void close() {
    
    
        cleanable.clean();
    }
}

静态内部 State 类拥有 Cleaner 机制清理房间所需的资源。 在这里,它仅仅包含 numJunkPiles 属性,它代表混乱房间的数量。 更实际地说,它可能是一个 final 修饰的 long 类型的指向本地对等类的指针。 State 类实现了 Runnable 接口,其 run 方法最多只能调用一次,只能被我们在 Room 构造方法中用 Cleaner 机制注册 State 实例时得到的 Cleanable 调用。 对 run 方法的调用通过以下两种方法触发:通常,通过调用Room 的 close 方法内调用 Cleanable 的 clean 方法来触发。 如果在 Room 实例有资格进行垃圾回收的时候客户端没有调用 close 方法,那么 Cleaner 机制将(希望)调用 State 的 run 方法。

一个 State 实例不引用它的 Room 实例是非常重要的。如果它引用了,则创建了一个循环,阻止了 Room实例成为垃圾收集的资格 (以及自动清除)。因此, State 必须是静态的嵌内部类,因为非静态内部类包含对其宿主类的实例的引用 (条目 24)。同样,使用 lambda 表达式也是不明智的,因为它们很容易获取对宿主类对象的引用。

就像我们之前说的, Room 的 Cleaner 机制仅仅被用作一个安全网。如果客户将所有 Room 的实例放在 trywith-resource 块中,则永远不需要自动清理。行为良好的客户端如下所示:

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

正如你所预料的,运行 Adult 程序会打印 Goodbye 字符串,随后打印 Cleaning room 字符串。但是如果时不合规矩的程序,它从来不清理它的房间会是什么样的?

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

你可能期望它打印出 Peace out ,然后打印 Cleaning room 字符串,但在我的机器上,它从不打印Cleaning room 字符串;仅仅是程序退出了。 这是我们之前谈到的不可预见性。 Cleaner 机制的规范说:“ System.exit 方法期间的清理行为是特定于实现的。 不保证清理行为是否被调用。”虽然规范没有说明,但对于正常的程序退出也是如此。 在我的机器上,将 System.gc() 方法添加到 Teenager 类的 main 方法足以让程序退出之前打印 Cleaning room ,但不能保证在你的机器上会看到相同的行为。

总之,除了作为一个安全网或者终止非关键的本地资源,不要使用 Cleaner 机制,或者是在 Java 9 发布之前的
finalizers 机制。即使是这样,也要当心不确定性和性能影响。

九. try-with-resources 优于try-finally

Java 类库中包含许多必须通过调用 close 方法手动关闭的资源。 比如 InputStreamOutputStreamjava.sql.Connection 。 客户经常忽视关闭资源,其性能结果可想而知。 尽管这些资源中有很多使用 finalizer 机制作为安全网,但 finalizer 机制却不能很好地工作。

从以往来看,try-finally 语句是保证资源正确关闭的最佳方式,即使是在程序抛出异常或返回的情况下:

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

这可能看起来并不坏,但是当添加第二个资源时,情况会变得更糟:

    // try-finally is ugly when used with more than one resource!
    static void copy(String src, String dst) throws IOException {
    
    
        InputStream in = new FileInputStream(src);
        try {
    
    
            OutputStream out = new FileOutputStream(dst);
            try {
    
    
                byte[] buf = new byte[BUFFER_SIZE];
                int n;
                while ((n = in.read(buf)) >= 0)
                    out.write(buf, 0, n);
            } finally {
    
    
                out.close();
            }
        } finally {
    
    
            in.close();
        }
    }

即使是用 try-finally 语句关闭资源的正确代码,如前面两个代码示例所示,也有一个微妙的缺陷。 try-withresources 块和 finally 块中的代码都可以抛出异常。 例如,在 firstLineOfFile 方法中,由于底层物理设备发生故障,对 readLine 方法的调用可能会引发异常,并且由于相同的原因,调用 close 方法可能会失败。 在这种情况下,因第一个异常引发的第二个异常,第二个异常完全冲掉了第一个异常。 在异常堆栈跟踪中没有第一个异常的记录,这可能使实际系统中的调
试非常复杂——通常这是你想要诊断问题的第一个异常。 虽然可以编写代码来抑制第二个异常,但是实际上没有人这样做,因为它太冗长了。

当 Java 7 引入了 try-with-resources 语句时,所有这些问题一下子都得到了解决[JLS,14.20.3]。要使用这个构造,资源必须实现 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 的示例:

    // try-with-resources on multiple resources - short and sweet
    static void copy(String src, String dst) throws IOException {
    
    
        try (InputStream in = new FileInputStream(src);
             OutputStream out = new FileOutputStream(dst)) {
    
    
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = in.read(buf)) >= 0)
                out.write(buf, 0, n);
        }
    }

不仅 try-with-resources 版本比原始版本更精简,更好的可读性,而且它们提供了更好的诊断。 考虑firstLineOfFile 方法。 如果调用 readLine 和(不可见) close 方法都抛出异常,则后一个异常将被抑制(suppressed),而不是前者。 事实上,为了保留你真正想看到的异常,可能会抑制多个异常。 这些抑制的异常没有被抛弃, 而是打印在堆栈跟踪中,并标注为被抑制了。 你也可以使用 getSuppressed 方法以编程方式访问它们,该方法在 Java 7 中已添加到的 Throwable 中。

可以在 try-with-resources 语句中添加 catch 子句,就像在常规的 try-finally 语句中一样。这允许你处理异常,而不会在另一层嵌套中污染代码。作为一个稍微有些做作的例子,这里有一个版本的 firstLineOfFile 方法,它不会抛出异常,但是如果它不能打开或读取文件,则返回默认值:

   // try-with-resources with a catch clause
    static String firstLineOfFile(String path, String defaultVal) {
    
    
        try (BufferedReader br = new BufferedReader(
                new FileReader(path))) {
    
    
            return br.readLine();
        } catch (IOException e) {
    
    
            return defaultVal;
        }
    }

结论很明确:在处理必须关闭的资源时,使用 try-with-resources 语句替代 try-finally 语句。 生成的代码更简洁,
更清晰,并且生成的异常更有用。 try-with-resources 语句在编写必须关闭资源的代码时会更容易,也不会出错,而使用 try-finally 语句实际上是不可能的

猜你喜欢

转载自blog.csdn.net/haiyanghan/article/details/112434713