Effective java如何高效创建和销毁对象-中

接上一篇文章:
Effective java如何高效创建和销毁对象-上(https://www.jianshu.com/p/30d0fa1b930c)

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

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

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

实例一: 使用final的方式,由于缺少了公有或者受保护的构造器,所以保证了他的全局唯一性。一旦被实例化,只会保证只有一个实例。但是如果客户端借助反射去修改AccessibleObject.setAccessible();,就可以通过反射机制调用私有构造器,此时我们可以通过判断第二次创建实例的时候抛出异常。

public class Elvis {

    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
        if(INSTANCE != null) {
            throw new NullPointerException("创建第二个实例异常");
        }
    }
}

实例二:

    private static final Elvis INSTANCE2 = new Elvis();

    private Elvis() {
        ....
    }
    
    public Elvis getInstance(){
        return INSTANCE2;
    }

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

公有域方法的优势

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

使用静态工厂方法创建singleton的优势总结

如果使用静态工厂方法创建的话,会有更多的优势,比如灵活性: 在不改变api的前提下,我们可以改变该类是否应该为Singleton的想法。比如改为每个调用的线程都返回一个唯一的实例。

第二个优势: 如果应用程序需要,可以编写一个泛型Singleton工厂。就是说,我们可以运用泛型,去控制返回的具体的类的singleton实例。

最后一个优势:可以作为方法引用作为提供者,Elvis::instance就是一个Supplier.

但是为了简单化,如果不是为了满足以上三种优势,书中还是建议使用第一种共有域的方式。我个人是建议使用第二种,做拓展的时候也非常方便。

关于序列化

如果创建的类需要序列化,我们通过上面的方式创建的类,如果仅仅使用实现Serializable接口是不够了,为了对singleton类进行序列化,保证唯一,我们还需要对其所有的
实例域都声明为(transient),同时需要提供一个readSolve方法。 不然在每次反序列化之后都会创建一个新的实例,比如在我们的例子中,回复导致“假冒的Elvis”。为了防止发生这种情况,要在Elvis类中加入readResolve方法。
(可能有些java初学者不太清楚为什么要重写这个函数的作用,这里说明一下: 我们类在序列化后,再虚拟机中,就要进行反序列化才能把该对象实例从二进制编码转化回来,虚拟机在反序列化的时候会调用该readResolve方法。)


最后一种方法是声明一个包含单个元素的枚举类型:

public enum ElvisEnum {
    INSTANCE;

    public void build(){}
}

这种方法在功能上与公有域方法相似,但是会更加简洁,而且无偿胡地提供了序列化机制,绝对可以防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。 单元素的枚举类型经常成为Singleton的最佳方法。

注意,如果singleton必须拓展词汇表超类,而不是拓展Enum的时候,则不宜使用这个方法(李章洙我们可以声明枚举匀实现接口)。

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

我们有时经常需要去编写只包含静态方法和静态域的类,这些类经常名声很不好,容易被初级程序员在面向对象的语言中去滥用这种类,来去编写过程化的程序。

除此之外,我们就是普遍用来构建工具类,或者用来把基本数据类型Maths、Arrays或者数组类型上的相关方法组织起来,我们也会通过java.util.Collections的方式,把集合对象上的静态方法跟工厂方法组织起来,最后我们还可以利用这种方法把final类上的方法组织起来,因为不能把他们放在子类中。。。。。等等还有很多

这时候我们是不希望这种工具类被实例化的,但是我们编译器会因为其缺少显示构造器而自动提供一个默认、公有的无参缺省构造器。

为了不让这样的类被实例化,我们通常会提供一个私有构造器,这样来保证他不会被实例化。

public class UtilityClass {
    // Suppress default constructor for noninstantiability
    private UtilityClass(){
        throw new AssertionError();
    }

}

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

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

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

有许多的类会依赖一个或者多个底层的资源,例如拼写检查器需要依赖词典。因此像下面这样把类实现为静态工具类的做法并不少见或者实现为单例。



上面两种方法都不理想,因为我们可以根据实际请客思考下,拼写检查器依赖词典没错,但是可能不止依赖一种词典,实际上,每一种语言都需要自己的词典,这里使用一种词典来满足所有需求,简直是不可能的。

在大多数情况中,就是静态工具类以及单例不适合与需要引用底层资源的类。

这里需要的是能够支持类的多个实例,每一个实例都可以使用客户指定的资源(也就是词典)。满足该需求的模式就是:

当创建一个新的实例时,就将该资源传到构造器中。这是依赖注入的一种形式:词典是拼写检查器的一个依赖,我们可以在创建的时候就将其注入其中。

这就是很多程序员使用多年却还不知道该名字的,依赖注入模式。 虽然这个例子中,只依赖了一个资源,但是我们依赖注入是可以适用于任意数量的资源以及任何的依赖形式。

依赖注入的对象具有不可变性,因此多个客户端可以共享依赖对象(这里说的场景是假设我们客户端想要的是同一个底层的资源)。扩展一下: 依赖注入同样可以适用我们的构造器、静态工厂、构建器。

变体:

这个程序模式的另一种有用的变体是将资源工厂传给构造器,工厂是可被重复调用来创建类型实例的一个对象 这类工厂具体表现为工厂方法( Factory Method) 模式。我们在java8中增加的接口Supplier ,最适合用于表示工厂,带有Supplier的方法, 通常应该限制输入工厂的类型参数,使用有限制的通配符类型。以便客户能够传入一个工厂,来创建指定类型的任意子类型。

例如,下面是一个生产马赛克的方法,它利用客户端提供的工厂来生产每一片

马赛克:

虽然依赖注人极大地提升了灵活性和可测试性,但它会导致大型项目凌乱不堪,因为它通常包含上千个依赖 不过这种凌乱用一个依赖、注入框架( dep ndency injection framework ) 便可以终结,如 Dagger [Dagger Guice Guice]或者 Spring [Spring 这些框架的用法超出了本书的讨论范畴,但是,请注意:设计成手动依赖注入的 凹,一般都适用于这些框架。

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

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

一般来说,最好能重用单个对象,而不是每次需要都要去创建一个新对象,这时候我们会想到,如果对象是不可变的,它就始终可以被重用。

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

String s = new String("abc");

该语句每次被执行的时候都创建1个新的 String 实例,但是这些创建对象的动作全都是不必要的 传递给 Stri 呵构造器的参数(飞ikini ”)本身就是 String 实例,功能方面等同于构造器创建的所有对象 如果这种用法是在一个循环中,或者是在 一个被频繁调用的方法中,就会创建出成千上万不必要的 String 实例.改进后的版本如下所示:
String s = "abc";
这个版本只用了一个 String 实例,而不是每次执行的时候都创建一个新的实例且,它可以保证,对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用.

示例二: 对于同时提供了静态工厂方法和构造器的不可变类,我们需要引用的时候??? 怎么创建呢?

这时候我们需要尽量引用静态工厂类而不是构造器,可以避免创建不必要的对象。例如: Boolean.value(String)总是优先于构造器(该api在java9已经被废弃了).
构造器基本上都是创建一个新的对象,而静态工厂方法,基本上不会。

有些对象,创建的成本很重,如果重复地需要这类“昂贵的对象”,建议将它缓存起来重用。

假设想要编写一个方法,用它确定一个字符串是否为一个有效的罗马数字 下面介绍一种最容易的方法,使用一个正则表达式:


这个实现的问题在于它依赖 String.matches 方法。虽然String.matches 方法最容易查看一个字符串是否与正则表达式相匹配 但并不适合在注重性能的情形中重复的使用。

问题在于它在内部为正则表达式创建了一个 Pattern 实例,却只用了一 次,之后就可以进行垃圾回收了 创建 Patter 口实例的成本很大 ,因为需要将正则表达式编译成 一个有限状态机( finite state machine)。这时候为了提升性能,应该显式地将正则表达式编译成一个 Patter 口实例(不可变),让它成为类初始化的一部分,并将它缓存起来,每当调用 isRomanNumeral 方法的时候就重用同一个实例:



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

有点经验的程序员可能也会考虑到书中说的情况,如果代码中的ROMAN实例初始化后没有得到调用,是不是可以实现延迟初始化?? 但是书中并不建议这样实现,因为这样可能会使其实现更复杂,同时导致消除这个初始化·的动作,无法将性能提升到超过已经达到的水平。

另一种创建多余对象的方法,叫做自动装箱,它允许程序员将基本类型和装箱基本类型( Boxed Primitive Type 混用,按需要自动装箱和拆箱 。自动装箱使得基本类型和装箱 本类型之间的差别变得模糊起来, 但是并没有完全消除 。在语义上还有着微妙的差别,在性能上也有着比较明显的差别。

书中列举了一个代码例子:看下面的程序,它计算所有int正整数值的总和。 为此,程序必须使用 long 算法,因为 int 不够大,无法容纳所有int 正整数值的总和:



上述代码执行时,因为使用的是Long包装类型,所以计算中,会产生2的31次方个实例,如果改成long,则不会。
书中作者将Long改成long去计算,运行时间从6.3秒降低到了0.59秒。

得出的结论: 要优先使用基本类型,而不是装箱类型,要当心无意识的装箱。主要大家不要根据这个示例误解创建对象的代价很昂贵,要避免创建。其实不然,我们主要避免的是大对象的创建,小对象的构造基本非常廉价的。

反之,通过维护自己的对象池来避免创建对象,其实也并非是一种好的做法,除非对象池红的对象是重量级的,说到对象池,正确使用的典型方法还是使用数据库连接池,建立数据库连接的代价很昂贵,因此重用这些对象才非常有意义。

这一条规范主要是说,当你应该重用现有对象的时候,请不要创建新对象,在后面第50条规范(保护性拷贝)也提及了,当你需要创建新对象时,不要重用现有对象。

待续

你可能感兴趣的:(Effective java如何高效创建和销毁对象-中)