Singleton

单例器(Singleton)是实例控制的极端情况。但也非常常见。这本书列举了3中常见的单例器的惯用实现方法。但构建单例器的基本思路是不变的:

隐藏构造器,只保留一个实例,置于类的静态域中。

直接通过静态域访问

最朴素的Singleton实现就是 直接公开那个静态域中的唯一实例

public class OurPlanet {
    public static final OurPlanet EARTH = new OurPlanet("The Earth"); // 公有访问权限
    private String name;
    private OurPlanet(String name) {
        this.name = name;
    }
}

通过公有静态工厂方法访问

前面已经讲过,用静态工厂方法替代构造器,能提供灵活性。

public class OurPlanet {
    private static final OurPlanet EARTH = new OurPlanet("The Earth"); // 私有化静态域中的实例
    private String name;
    private OurPlanet(String name) {
        this.name = name;
    }
    public OurPlanet getInstance() { // 公有的静态工厂方法成为唯一的访问途径
        return EARTH; // 总是返回唯一的实例
    }
}

这样可以在不改变API的情况下(用户还是访问newInstance()方法),改变是否应该是Singleton的想法。比如,若干年后,人类可以居住在火星。

public class OurPlanet {
    private static final OurPlanet EARTH = new OurPlanet("The Earth"); // 私有化静态域中的实例
    private static final OurPlanet MARS = new OurPlanet("The Mars"); // 私有化静态域中的实例
    private static boolean switch = true;
    private String name;
    private OurPlanet(String name) {
        this.name = name;
    }
    public OurPlanet getInstance() { // 公有的静态工厂方法还是唯一的访问途径
        switch = !switch;
        return switch? EARTH : MARS; // 人类已经有两个家园,地球和火星
    }
}

包含单个元素的枚举是实现Singleton的最佳方法

枚举型是个大大的语法糖,它其实是一个实实在在的类。只需编写一个只包含单个元素的枚举型,我们就有了一个质量可靠的Singleton。

public enum OurPlanet { EARTH }

上面EARTH的实际身份就是一个用static final修饰过的公有域。这都和Singleton的模式一模一样。

记住,单元素的枚举型已经成为实现Singleton的最佳方法。和传统的Singleton比,枚举明显的优势有两个,

  1. 抵御反序列化攻击。为了让Singleton成为可序列化的,光实现Serializable接口是不够的,而且所有实例域必须是transient的,而且必须重写readResolve()方法,否则反序列化的过程会产生一个假冒的实例。防御这些问题的工作,枚举型做的很好,编译器无偿替我们做了。
  2. 抵御反射攻击。AccessibleObject.setAccessable()方法可以改变私有构造器的访问权限。这方面枚举型的构造器能够抵御这样的攻击。在接到生产额外实例的请求时,枚举型的构造器会抛出异常。关于抵御反射攻击,在下一节会讲到。

隐藏构造器

为了控制系统中存在的实例数量,就必须隐藏类的构造器。禁止用户访问它。

抽象类不能强化不可实例化的能力

最简单的设置成抽象类,并不能禁止用户将它实例化。因为虽然不能实例化抽象类本身,但用户可以实例化抽象类的子类

最好是私有化构造器

像前面的Singleton的三个实现,构造器都被设为了private权限。

但反射攻击能够改变私有构造器的访问权限

AccessibleObject.setAccessable()方法可以私有构造器的访问权限改为公有。

终极防御是让构造器有条件地抛出异常

最简单的比如增加一个计数器,在创造了足够数量的实例之后,构造器再接到实例化请求就抛出异常。

public class TenUnits {
    private static int max = 10;
    private TenUnits() {
        if(max++ >= 10) { // 超出10个实例,抛出异常
            throw new RuntimeException("Only 10 Objects allowed!");
        }
        // some code
    }
}

避免创建不必要的对象

记住,一般来说最好能重用对象,而不是在每次需要的时候就创建一个相同功能的新对象。最简单的,当类的某个方法总是重复创建某些相同的对象时,设置一个域来储存这些对象,能防止每次调用这个方法都重复创建对象。

下面的代码片段用来判断一个人是否出生于1946-1965年间的“婴儿潮”。

public class Person {
    private final Date birthDate;
    // other fields, methods ... ...
    public boolean isBabyBoomer() {
        Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946,Calendar.JANUARY,1,0,0,0);
        Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        Date boomStart = gmtCal.getTime();
        gmtCal.set(1965,Calendar.JANUARY,1,0,0,0);
        Date boomEnd = gmtCal.getTime();
        return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
    }
}

Date对象设置成静态域以后,每次调用isBabyBoomer()方法都不会再创建这么多对象了。

public class Person {
    public boolean isBabyBoomer() {
        private static final Date BOOM_START;
        private static final Date BOOM_END;
        private final Date birthDate;
        // other fields, methods ... ...
        static {
            Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
            gmtCal.set(1946,Calendar.JANUARY,1,0,0,0);
            Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
            Date boomStart = gmtCal.getTime();
            gmtCal.set(1965,Calendar.JANUARY,1,0,0,0);
            Date boomEnd = gmtCal.getTime();
        }
        public boolean isBabyBoomer() {
            return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
        }    
    }
}

小心”不可变“类特别容易产生多余的对象

当尝试修改一个不可变对象,获得的会是一个拥有不同值的全新的对象,而不是在原对象上修修补补。一个极端的例子就是String类。下面的代码其实是返回了字面量为abc的另一个String对象,虽然变量名还是s,但却指向了不同的对象。因为String是不可变的,声明的时候是ABC就一直是ABC,要abc只能重新创建一个新对象。

String s = "ABC";
s.toLowerCase();

下面的代码,实际产生了两个String对象。字面量ABC本身已经是一个完整的String对象,最后的变量s又是一个拥有不同内存地址的新对象。

String s = new String("ABC"); // 不要这样做

如果直接使用字面量,就不会产生多余的对象。

String s = "ABC"; // 这样比较好

小心“自动装箱”类型

下面这个例子,计算所有int正值的总和,

public static void main(String[] args) {
    Long sum = 0L;
    for (long i = 0; i < Integer.MAX_VALUE; i++) {
        sum += i;
    }
    System.out.println(sum);
}

也不是对象越少就越好,适得其反

避免创建不必要的类,不等于说就一定对象越少越好。小对象的创建开销相当廉价,因此有意识地添加一些附加对象,提升程序的可读性,功能性还是很好的。而且在创建“对象池”以重用以后对象的时候,也要想清楚,因此带来的代码混乱度是不是值得这么做。