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

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

    单例模式(Singleton):当系统中只需要的某个类的唯一对象时,可以使用该模式。Singleton指仅仅被实例化一次的类。Singleton通常被用来代表那些本质上唯一的系统组件。

    为什么会用到该模式?因为有时候某些对象的创建需要耗费大量的资源、使用单一(唯一)的对象实例来维护某些共享数据等,在这些场景下即可采用单例模式进行设计,可以适当地渐少内存开销,因为此时该唯一对象不会(被限制了)频繁地创建。

    再Java 1.5发行版本之前,实现Singleton有两种方法,为了保证单例,所有的构造方法都需要被声明成private方法,并在该类中都持有自己的一个静态实例。第一种是:

public class A {

    //在正常情况下构造器只会被调用这一次,创建一个公用的实例变量
    public static final A INSTANCE = new A();

    private A() { ... }

    public void leaveTheBuilding() { ... }
}

    私有构造器会被调用一次,实例化一个A类型的对象,再也没有其他的途径来实例化A类,除了一些使用反射机制的客户端,可以借助AccessibleObject.setAccessible()方法来调用private方法。例如下面:

公有域方法

class A {

    public static final A INSTANCE = new A();

    private A() {}
}

public class Main {

    public static void main(String[] args) {

        //A has a private access in A
        //A a = new A();

        //use reflect to invoke the private method
        Class clz = A.class;
        try {
            Constructor<A> constructor = clz.getDeclaredConstructor();
            constructor.setAccessible(true);
            A a = constructor.newInstance();
            System.out.println(a.getClass());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

    = =当时看到这里的时候简直了,当时就觉得反射真无耻,还好有相应的解决办法,可以通过修改构造器来防止反射的无耻调用,例如:

class A {

    private static boolean flag = false;
    public static final A INSTANCE = new A();
    private A() {
        if (!flag) {
            System.out.println("=========invoke==========");
            flag = !flag;
        } else {
            try {
                if (null != INSTANCE) throw new Exception("duplicate instance create error!" + A.class.getName());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

public class Main {

    public static void main(String[] args) {

        //A has a private access in A
        //A a = new A();

        //use reflect to invoke the private method
        Class clz = A.class;
        try {
            Constructor<A> constructor = clz.getDeclaredConstructor();
            constructor.setAccessible(true);
            A a = constructor.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

    这样在使用反射调用private构造器企图做坏事的时候,就会抛出异常,阻止反射的进行。

    第二种方法就是类中有一个公有的静态工厂方法:

静态工厂方法

public class A {
    private static final A INSTANCE =  new A();
    private A() { ... }
    public static A getInstance() {return INSTANCE;}
}

    在这里的INSTANCE被声明成了private的,对外的接口暴露成了静态工厂方法getInstance()

    公有域方法的好处主要在于,组成类的成员的声明很清楚的表明了这个类是一个Singleton:公有的静态域是final的,因此该域总是持有的一个相同的对象引用。而再性能上不再有任何的优势了:现在改进过的JVM几乎都能实现将静态工厂方法的调用内联化。

    方法内联:在C++中,可以明确定义内联函数,使用inline关键字。在Java中不能定义内联函数,但是方法的内联在JIT编译中还是存在的,只不过是JIT自动优化的,我们无法在写代码的时候指定。所谓内联函数就是指函数在被调用的地方直接展开,编译器在调用时不用像一般函数那样,参数压栈,返回时参数出栈以及资源释放等,这样提高了程序执行速度。 一般函数的调用时,JVM会自动新建一个堆栈框架来处理参数和下一条指令的地址,当执行完函数调用后再撤销该堆栈。


    传送门

        JVM的方法内联

        JVM中的步骤内联


    说简单点就是下面这个例子:

private int add4(int x1, int x2, int x3, int x4) {
  return add2(x1, x2) + add2(x3, x4);
}

private int add2(int x1, int x2) {
  return x1 + x2;
} 

    运行一段时间之后,JVM会将add2方法去掉,并且直接将add4方法中的add2方法去掉,并将代码翻译成:

private int add4(int x1, int x2, int x3, int x4) {
    return x1 + x2 + x3 + x4;
}

    工厂方法的优势之一在于,提供了灵活性:在不改变其他API的前提下,可以改变该类是否应该为Singleton的想法,只需要将getInstance()方法中的具体实现改掉就可以实现。

    为了使上面两种方法创建的Singleton类变成是可序列化的(Serializable),仅仅在声明上加上”implements Serializable”是不够的,例如下面的这个例子:

class A implements Serializable {

    public static final A INSTANCE = new A();

    private int a = 1;

    private A() {
    }


}

public class Main {

    public static void main(String[] args) throws IOException,
        ClassNotFoundException  {

        FileOutputStream fos = new FileOutputStream("11.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        try {
            oos.writeObject(A.INSTANCE);
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        A a ;

        FileInputStream fis = new FileInputStream("11.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);
        try {
            a = (A) ois.readObject();
            ois.close();
            System.out.println(A.INSTANCE == a);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}
输出:false

    可以在电脑上跑一跑这个例子,会得到false的结果,也就是说我们将INSTANCE序列化到文件中后,再读取出来的时候,已经不是之前的那个INSTANCE了。每次反序列化一个序列化的实例的时候,都创建了一个新的实例,这样就不能保证Singleton的唯一性了,为了防止这种情况发生,需要再A类里面加入下面这个方法:

@Transient
private Object readResolve() {
    return INSTANCE;
}

    至于这个方法为什么可以行,追踪了一段时间的源码,发现ObjectInputStream.java类中存在checkResolve()等方法,会检测类是否存在readResolve()等方法,来确定变量enableResolve的值,然后根据这个变量的值来判断返回给反序列化调用者之前是否需要用readResolve()中提供的变量来进行替换。代码跟踪有点多,就不在这里给出了,可以自己去跟进,入口为a = (A) ois.readObject();

    实际上从Java 1.5发行版本开始,实现Singleton还有第三种方法。只需要编写一个包含单个元素的枚举类型:
public enum A {
INSTANCE;
}

    虽然这种方法能够很有效的保证了Singleton,更加的简洁,无偿的提供了序列化机制,可以有效的防止多次实例化,面对反射攻击也可以应付自如,虽然这种方法没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值