《Effective Java Third》第二章总结:创建和销毁对象

https://github.com/clxering/Effective-Java-3rd-edition-Chinese-English-bilingual/blob/master/Chapter-5/Chapter-5-Item-33-Consider-typesafe-heterogeneous-containers.md

https://sjsdfg.github.io/effective-java-3rd-chinese/#/notes/33.%20%E4%BC%98%E5%85%88%E8%80%83%E8%99%91%E7%B1%BB%E5%9E%8B%E5%AE%89%E5%85%A8%E7%9A%84%E5%BC%82%E6%9E%84%E5%AE%B9%E5%99%A8

第二章 创建和销毁对象

1 考虑使用静态工厂方法替代构造方法

一个类可以提供一个返回类实例的公共的静态工厂方法来替代构造方法,如Boolean#valueOf:

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

1.1 与构造方法相比的优点

1.静态工厂方法是有名字的,可以更加符合业务逻辑,易于使用

2.静态工厂方法不需要每次调用时都创建一个新对象

如果经常请求等价对象,那么可以提高性能

重复调用返回相同实例让类在任何时候都能对实例保持严格的控制(实例控制类)

3.可以返回此方法返回类型的任何子类型的对象

4.返回对象的类可以根据输入参数的不同而不同,只要为返回类型的子类即可,而构造方法只能返回本类

5.在编写包含该方法的类时,返回的对象的类不需要存在

在服务提供者框架中使用:客户端只需要有父类即可,不需要有具体子类,具体子类由服务提供者返回:
https://www.cnblogs.com/tabCtrlShift/p/9417111.html

1.2 只提供静态工厂方法的主要限制

1.没有公共或受保护构造方法的类不能被子类化,当然,可以使用组合来替代

2.很难找到此方法,对于提供了静态工厂方法而不是构造器的类来说,想要查明如何实例化一个类是十分困难的

2 当构造方法参数过多时使用 builder 模式

静态工厂和构造方法都有一个限制:不能很好地扩展到很多可选参数的情景。

1.这种情景下,可以使用可伸缩构造方法模式:
首先提供一个只有必需参数的构造方法,接着提供增加了一个可选参数的构造函数,然后提供增加了两个可选参数的构造函数,等等,逐个提供可选参数,最终在构造函数中包含所有必需和可选参数

  • 评价

比较安全;
但是当有很多参数时,很难编写客户端代码,而且很难读懂它;也很容易不小心写反参数的位置

2.也可以使用JavaBeans模式
调用一个无参的构造方法来创建对象,然后调用 setter 方法来设置每个必需的参数和可选参数

  • 评价

没有伸缩构造方法模式的缺点;虽然有点冗长,但是创建实例很容易,并且易于阅读所生成的代码;
但是由于构造方法被分割成了多次setter,所以在构造过程中 JavaBean 可能处于不一致的状态;
该类没有通过检查构造参数的有效性来进行强制一致性的选项;
在不一致的状态下尝试使用对象可能会导致一些错误,这些错误与平常代码的BUG很是不同,因此很难调试;
一个相关的例子是,JavaBeans 模式排除了让类不可变的可能性(提供了set方法,外界可以调用此方法来修改属性,具体见第 17 ),并且需要程序员增加工作以确保线程安全。

  • 缺点的解决方案

通过在对象构建完成时手动”冻结“对象,并且禁止此对象在解冻之前使用,可以减少这些缺点
但是这种方案在实践中很难使用并且很少使用;
而且,在运行时会导致错误,因为编译器无法确保程序员会在使用对象之前调用 freeze 方法。

3.使用Builder模式:
是 Builder 模式的一种形式,将可伸缩构造方法模式的安全性和 JavaBean 模式的可读性结合起来;
客户端(即使用者)不直接构造所需的对象,而是调用一个包含所有必需参数的构造方法 (或静态工厂)得到获得一个 builder 对象;
然后,客户端调用 builder 对象的与 setter 相似方法来设置你想设置的可选参数;
最后,客户端调用builder对象的一个无参的 build 方法来生成所需的对象,该对象的属性通常被制作成不可变的

Builder 通常是它所构建的类的一个静态成员类,其 setter 方法返回 builder 本身,这样就可以进行链式调用,从而生成一个流畅的 API。

可以在build方法调用的构造方法中检查包含多个参数的不变性;
如果检查失败,则抛出 IllegalArgumentException 异常(具体见第 72 ),其详细消息指示哪些参数无效(具体见第 75 )。

  • 评价

非常灵活,非常适合类层次结构,即使用平行层次的 builder,每个builder嵌套在相应的类中: 抽象类有抽象的 builder;具体的类有具体的 builder;
每个子类 builder 中的 build 方法被声明为返回正确的子类,这样可以进行灵活的多态(协变返回类型技术,即父类引用指向子类对象)

缺点:为了创建对象,首先必须创建它的 builder;
虽然创建这个 builder 的成本在实践中不太可能被注意到,但在看中性能的场合下这可能就是一个问题;
而且,builder 模式比伸缩构造方法模式更冗长,因此只有在有足够的参数时使用它好处才大,比如四个或更多。但是可能在以后会想要添加更多的参数,如果一开始是使用的构造方法或静态工厂,当类的参数数量失控的时候再转到Builder模式,过时的构造方法或静态工厂就会变成鸡肋,很尴尬了;
因此在实践中,通常最好从一开始就创建一个 builder。

3 使用私有构造方法或枚举类实现Singleton属性

3.1 第一种:公共属性方法

public class Singleton {
    public static final Singleton INSTANCE = new Singleton();
    private Singleton() {。。。}
}

问题:
客户端可以使用 AccessibleObject.setAccessible 方法,以反射方式调用私有构造方法(详见第 65条);
解决:
可以修改构造函数,使其在请求创建第二个实例时抛出异常。

3.2 第二种:使公有域变为私有,然后使用静态工厂方法返回实例

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {。。。}
    public static Singleton getInstance() { return INSTANCE; }
}

静态工厂方法的好处1
提供了灵活性:在不改变其 API 的前提下,可以改变该类是否应该为单例的想法;
工厂方法返回该类的唯一实例,但是,它很容易被修改,比如,改为每个调用该方法的线程返回一个唯一的实例;
好处2
如果应用程序需要它,可以编写一个泛型单例工厂(generic singleton factory )(具体见第30 );
好处3
可以通过方法引用(method reference)作为提供者,例如 Singleton::instance 等同于 Supplier
除非满足以上任意一种优势,否则还是优先考虑公有域(public-field)的方法。

  • 关于序列化

为了将上述方法中实现的单例类变成是可序列化的 (第 12 章),仅仅将 implements Serializable 添加到声明中是不够的;
为了保证单例模式不被破坏,必须声明所有的实例字段为 transient,并提供一个 readResolve 方法,此方法返回INSTANCE即可(具体见第 89 );
否则,每当序列化的实例被反序列化时,就会创建一个新的实例,在我们的例子中,导致出现新的 Singleton实例。

3.3 第三种:声明单一元素的枚举类

public enum Singleton {
    INSTANCE;
}

这种方式类似于第一种,但更简洁,无偿地提供了序列化机制,并提供了防止多个实例化的坚固保证,即使是在复杂的序列化或反射攻击的情况下;
原因:https://blog.csdn.net/qq_36387730/article/details/82146799)

单一元素枚举类通常是实现单例的最佳方式

注意,如果单例必须继承 Enum 以外的父类 (虽然可以声明一个 Enum 来实现接口),那么就不能使用这种方法。

4 使用私有构造方法执行非实例化

只有当类不包含显式构造方法时,才会生成一个默认构造方法,因此可以通过包含一个私有构造方法来实现类的非实例化,这样也阻止了类的子类化:

public class UtilityClass {
    // AssertionError 异常不是严格要求的
    //但是它能以防在类中意外地调用构造方法,保证了类在任何情况下都不会被实例化。
    private UtilityClass() {
        throw new AssertionError();
    }
    ... // Remainder omitted
}

使用场景:Math类

5 依赖注入优于硬连接资源(hardwiring resources)

对于那些行为被底层资源参数化的类来说,静态实用类和单例的实现是不合适的,因为这些资源的行为会影响类的行为,并且不让类直接创建这些资源

所需要的是:能够支持类的多个实例 ,每个实例都使用客户端所期望的资源。
满足这一需求的简单模式是在创建新实例时将资源传递到构造方法中;
这是依赖项注入(dependency injection)的一种形式,极大地增强类的灵活性、可重用性和可测试性。

6 避免创建不必要的对象

在每次需要时重用一个对象而不是创建一个新的相同功能对象通常是恰当的。重用可以更快更流行。
如果对象是不可变的,它总是可以被重用。

如String:

String s = new String("pdc"); //错误
String s = "pdc";//正确

通过使用静态工厂方法(static factory methods, 条目 1),可以避免创建不需要的对象。

某一些对象的创建很昂贵,如果要重复使用一个「昂贵的对象」,可以将其缓存起来以便重复使用,如Pattern

优先使用基本类型而不是包装类,也要注意无意识的自动装箱,如:

private static long sum() {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        sum += i;//构造了大约2^31个不必要的 Long 实例,如果sum改为long,则不会
    return sum;
}

此item不应该被被误解为暗示对象创建是昂贵的,应该避免创建对象;
相反,使用构造方法创建和回收小的对象是非常廉价,构造方法只会做很少的显示工作,尤其是在现代 JVM 实现上;
创建额外的对象以增强程序的清晰度,简单性或功能性通常是件好事;
除非池中的对象非常重量级,否则通过维护自己的对象池来避免对象创建是一个坏主意;
对象池的典型例子就是数据库连接:建立连接的成本非常高,因此重用这些对象是有意义的;
但是,一般来说,维护自己的对象池会使代码混乱,增加内存占用,并损害性能;
现代 JVM 实现具有高度优化的垃圾收集器,它们在轻量级对象上的回收效率轻松胜过此类对象池。

7 消除过期的对象引用

内存泄漏(更适当地称为无意的对象保留 unintentional object retentions)是一种隐蔽行为;
如果无意中保留了对象引用,那么不仅这个对象排除在垃圾回收之外,而且该对象引用的任何对象也是如此。 即使只有少数对象引用被无意地保留下来,也可以阻止垃圾回收机制对许多对象的回收,这对性能产生很大的影响。

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

这类问题为第一个常见的内存泄漏来源,其解决方法很简单:一旦对象引用过期,将它们设置为 null。

如栈,只要元素从栈中弹出,就将其设置为null即可,这样就能避免内存泄漏

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

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

第二个常见的内存泄漏来源是缓存。

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

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

第三个常见的内存泄漏来源是监听器和其他回调

如果你实现了一个 API,其客户端注册回调,但是没有显式地撤销注册回调,那么除非采取一些操作,否则它们将会累积;
确保回调是垃圾收集的一种方法是只存储弱引用(weak references);
例如,仅将它们保存在 WeakHashMap 的键(key)中。

  • 观点

因为内存泄漏通常不会表现为明显的故障,所以它们可能会在系统中保持多年;
通常仅在仔细的代码检查或借助堆分析器( heap profiler)的调试工具才会被发现;
因此,需要预见这些问题,并防止这些问题发生

8 避免使用Finalizer和Cleaner机制

Finalizer 和 Cleaner 机制的一个缺点是不能保证他们能够及时执行[JLS,12.6];
在一个对象变得无法访问时,到 Finalizer 和 Cleaner 机制开始运行时,这期间的时间是任意长的;
这意味着你永远不应该 Finalizer 和 Cleaner 机制做任何时间敏感(time-critical)的事情。

Java 规范不能保证 Finalizer 和 Cleaner 机制能及时运行;
它甚至不能能保证它们是否会运行;
当一个程序结束后,一些不可达对象上的 Finalizer 和 Cleaner 机制可能仍然没有运行;
因此,不应该依赖于 Finalizer 和 Cleaner 机制来更新持久化状态

使用 finalizer 和 cleaner 机制会导致严重的性能损失;
finalizer 机制有一个严重的安全问题:它们会打开你的类来进行 finalizer 机制攻击

为对象封装需要结束的资源(如文件或线程)时,可以让类实现AutoCloseable 接口,并要求使用者在不再需要时调用每个实例 close 方法,而不是为该类编写 Finalizer 和 Cleaner 机制

使用try-with-resources 或 try-finally 块来回收非内存资源

  • 合理用法

第一种合理用法是作为一个安全网(safety net),以防资源的拥有者忽略了它的 close 方法;
虽然不能保证 Finalizer 和 Cleaner 机制会迅速运行 (或者根本就没有运行),最好是把资源释放晚点出来,也要好过客户端没有这样做;

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

即使是这两种情况,也要当心不确定性和性能影响。

9 使用try-with-resources语句替代try-finally语句

finally 块中的代码可以抛出异常,如果try发生异常,finally也发生异常,则导致finally中的异常完全冲掉了第一个异常,在异常堆栈跟踪中没有第一个异常的记录,这可能使实际系统中的调试非常复杂——通常这是你想要诊断问题的第一个异常;
虽然可以编写代码来抑制第二个异常,但是实际上没有人这样做,因为它太冗长了。

所以使用不会出错的try-with-resources语句:要使用这个构造,类必须实现 AutoCloseable 接口,该接口由一个返回为 voidclose 方法组成

案例:

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);
    }
}

也可以添加catch:

static String firstLineOfFile(String path, String defaultVal) {
    try (BufferedReader br = new BufferedReader(
           new FileReader(path))) {
        return br.readLine();
    } catch (IOException e) {
        return defaultVal;//不会抛出异常,但是如果它不能打开或读取文件,则返回默认值
    }
}

参考

https://sjsdfg.github.io/effective-java-3rd-chinese

https://www.cnblogs.com/tabCtrlShift/p/9417111.html

https://blog.csdn.net/qq_36387730/article/details/82146799

你可能感兴趣的:(Effective,Java)