有些类也需计划生育--单例模式

1.1 类也需要计划生育

"今天我在公司写一个窗体程序,当中有一个是'工具箱'窗体,问题就是,我希望工具箱要么不出现,出现也只出现一个,可实际上却是我每单击菜单,实例化'工具箱',就会出来一个,这样单击多次就会出现多个'工具箱',怎么办?"

"代码是这样的,首先我建立了一个Java的swing窗体应用程序,默认的窗体为JFrame,左上角有一个'打开工具箱'按钮。我希望单击此按钮后,可以另创建一个窗体,也就是'工具箱'窗体,里面可以有一些相关工具按钮。"

package code.chapter21.singleton1;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class Test {
    public static void main(String[] args) {
        new SingletonWindow();
    }
}

//窗体类
class SingletonWindow{
    public SingletonWindow(){
        JFrame frame = new JFrame("单例模式");
        frame.setSize(1024,768);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JPanel panel = new JPanel();    
        frame.add(panel);
        panel.setLayout(null);
        JButton button = new JButton("打开工具箱");
        button.setBounds(10, 10, 120, 25);
        button.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e) {
                JFrame toolkit = new JFrame("工具箱");
                toolkit.setSize(150,300);
                toolkit.setLocation(100,100);
                toolkit.setResizable(false);
                toolkit.setAlwaysOnTop(true); //置顶
                toolkit.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
                toolkit.setVisible(true);
            }
        });
        panel.add(button);
        frame.setVisible(true);
    }
}

代码执行后的样子如右图所示。

"我每单击一次'打开工具箱'按钮,就产生一个新的'工具箱'窗体,但实际上,我只希望它出现一次之后就不再出现第二次,除非关闭后单击再出现。"

1.2 判断对象是否是null

"这个其实不难办呀,你判断一下,这个工具箱的JFrame有没有实例化过不就行了。"

"什么叫JFrame有没有实例化过?我是在单击了按钮时,才去JFrame toolkit = new JFrame("工具箱");那当然是新实例化了。"

"问题就在于此,为什么要在单击按钮时才声明JFrame对象呢?你完全可以把声明的工作放到类的全局变量中完成。这样就可以去判断这个变量是否被实例化过了。"

package code.chapter21.singleton2;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class Test {
    public static void main(String[] args) {
        new SingletonWindow();
    }
}

//窗体类
class SingletonWindow{
    public SingletonWindow(){
        JFrame frame = new JFrame("单例模式");
        frame.setSize(1024,768);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JPanel panel = new JPanel();    
        frame.add(panel);
        panel.setLayout(null);
        JButton button = new JButton("打开工具箱");
        button.setBounds(10, 10, 120, 25);
        button.addActionListener(new ActionListener(){
            JFrame toolkit; //JFrame类变量声明

            public void actionPerformed(ActionEvent e) {
                if (toolkit == null || !toolkit.isVisible()){
                    
                    toolkit = new JFrame("工具箱");
                    toolkit.setSize(150,300);
                    toolkit.setLocation(100,100);
                    toolkit.setResizable(false);
                    toolkit.setAlwaysOnTop(true); //置顶
                    toolkit.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
                    toolkit.setVisible(true);
                }
            }
        });
        panel.add(button);
        frame.setVisible(true);
    }
}

如果做任何事情不求完美,只求简单达成目标,那你又如何能有提高?

打个比方,你现在不但要在菜单里启动'工具箱',还需要'工具栏'上有一个按钮来启动'工具箱',如何做?也就是有两个按钮都要能打开这个工具箱。

"这个不难。增加一个工具栏按钮的事件处理,将刚才那段代码复制过去。

复制粘贴是最容易的编程,但也是最没有价值的编程。你现在将两个地方的代码复制在一起,这就是重复。这要是需求变化或有Bug时就需要改多个地方。"

package code.chapter21.singleton3;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class Test {
    public static void main(String[] args) {
        new SingletonWindow();
    }
}

//窗体类
class SingletonWindow{
    public SingletonWindow(){
        JFrame frame = new JFrame("单例模式");
        frame.setSize(1024,768);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JPanel panel = new JPanel();    
        frame.add(panel);
        panel.setLayout(null);
        JButton button = new JButton("打开工具箱");
        button.setBounds(10, 10, 120, 25);
        button.addActionListener(new ToolkitListener());
        panel.add(button);

        JButton button2 = new JButton("打开工具箱2");
        button2.setBounds(130, 10, 120, 25);
        button2.addActionListener(new ToolkitListener());
        panel.add(button2);
        
        frame.setVisible(true);
    }
}

//工具箱事件类
class ToolkitListener implements ActionListener{
    private JFrame toolkit;
    
    public void actionPerformed(ActionEvent e) {
        if (toolkit == null || !toolkit.isVisible()){

            toolkit = new JFrame("工具箱");
            
            toolkit.setSize(150,300);
            toolkit.setLocation(100,100);
            toolkit.setResizable(false);
            toolkit.setAlwaysOnTop(true); //置顶
            toolkit.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            toolkit.setVisible(true);
        }
    }
}

把程序运行后,分别单击'打开工具箱'和'打开工具箱2'按钮看看,有没有问题?

"啊!好像两个按钮分别打开了一个工具箱窗体。唉!这依然不符合我们'只能打开一个工具箱'的需求呀。那我没有办法了。"

1.3 生还是不生是自己的责任

"办法当然是有。我问你,夫妻已经有了一个小孩子,下面是否生第二胎,这是谁来负责呀?"

"当然是他们自己负责。"

"说得好,你再想想看这种场景:领导问下属,报告交了没有?下属可能说'早交了'。于是领导满意地点点头,下属也可能说'还剩下一点内容没写,很快上交',领导皱起眉头说'要抓紧'。此时这份报告交还是没交,由谁来判断?"

"当然是下属自己的判断,因为下属最清楚报告交了没有,领导只需要问问就行了。"

"好了,同样地,现在'工具箱'JFrame是否实例化都由外部的代码决定,你不觉得这不合逻辑吗?"

//工具箱事件类
class ToolkitListener implements ActionListener{
    private JFrame toolkit;
    
    public void actionPerformed(ActionEvent e) {
        if (toolkit == null || !toolkit.isVisible()){

            toolkit = new JFrame("工具箱");
            
            toolkit.setSize(150,300);
            toolkit.setLocation(100,100);
            toolkit.setResizable(false);
            toolkit.setAlwaysOnTop(true); //置顶
            toolkit.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            toolkit.setVisible(true);
        }
    }
}

"你的意思是说,主窗体里应该只是通知启动'工具箱',至于'工具箱'窗体是否实例化过,应该由'工具箱'自己的类来判断?"

"哈,当然,实例化与否的过程其实就和报告交了与否的过程一样,应该由自己来判断,这是它自己的责任,而不是别人的责任。别人应该只是使用它就可以了。"

"我想想看,实例化其实就是new的过程,但问题是怎么让人家不用new呢?"

"是的,如果你不对构造方法做改动的话,是不可能阻止他人不去用new的。所以我们完全可以直接就把这个类的构造方法改成私有(private),你应该知道,所有类都有构造方法,不编码则系统默认生成空的构造方法,若有显式定义的构造方法,默认的构造方法就会失效。 于是只要你将'工具箱'类的构造方法写成是private的,那么外部程序就不能用new来实例化它了。"

"哈,私有的方法外界不能访问,这是对的,但是这样一来,这个类如何能有实例呢?"

"哈,我们的目的是什么?"

"让这个类只能实例化一次。没有new,我现在连一次也不能实例化了。"

"错,只能说,对于外部代码,不能用new来实例化它,但是我们完全可以再写一个public方法,叫作getInstance(),这个方法的目的就是返回一个类实例,而此方法中,去做是否有实例化的判断。如果没有实例化过,由调用private的构造方法new出这个实例,之所以它可以调用,是因为它们在同一个类中,private方法可以被调用的。"

有了上面的代码,我们在写工具箱事件类时,就可以改造如下。

package code.chapter21.singleton4;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

class Test extends JFrame {

    public static void main(String[] args) {
        new SingletonWindow();
    }
}

//窗体类
class SingletonWindow{
    public SingletonWindow(){
        JFrame frame = new JFrame("单例模式");
        frame.setSize(1024,768);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JPanel panel = new JPanel();    
        // 添加面板
        frame.add(panel);
        
        panel.setLayout(null);

        JButton button = new JButton("打开工具箱");
        button.setBounds(10, 10, 120, 25);
        button.addActionListener(new ToolkitListener());
        panel.add(button);

        JButton button2 = new JButton("打开工具箱2");
        button2.setBounds(130, 10, 120, 25);
        button2.addActionListener(new ToolkitListener());
        panel.add(button2);

        frame.setVisible(true);
    }
}

//工具箱事件类
class ToolkitListener implements ActionListener{
    public void actionPerformed(ActionEvent e) {

        //Toolkit toolkit = new Toolkit("工具箱");
        
        Toolkit.getInstance();

    }
}

//工具箱类
class Toolkit extends JFrame {

    private static Toolkit toolkit;

    private Toolkit(String title){
        super(title);
    }
    
    public static Toolkit getInstance(){
        //若toolkit不存在或隐藏时,可以实例化
        if (toolkit==null || !toolkit.isVisible()){
            toolkit = new Toolkit("工具箱");
            toolkit.setSize(150,300);
            toolkit.setLocation(100,100);
            toolkit.setResizable(false);
            toolkit.setAlwaysOnTop(true); //置顶
            toolkit.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            toolkit.setVisible(true);
        }
        return toolkit;
    }
}

如果Toolkit.getInstance()改回成Toolkit toolkit=new Toolkit("工具箱");则编译时会报错。

上面的代码可以达到一个效果,只有在实例不存在时,才会去new新的实例,而当存在时,可以直接返回存在的同一个实例。

"其实也就是把你之前写的代码搬到了'工具箱'Toolkit类中,由于构造方法私有,就只能从内部去调用。然后当访问静态的公有方法getInstance()时,它会先去查看内存中有没有这个类的实例,若有就直接返回,也就是不会超生了。"

"哦,我知道了。就拿计划生育的例子来说,刚解放时,国家需要人,人多力量大嘛,于是老百姓生!生!生!于是人口爆炸了。后来实行了计划生育,规定了一对夫妇最多只能生育一胎,并把判断的责任交给了夫妇,于是刚结婚时,想要孩子就生一个,而生好一个后,无论谁来要求,都不生了,因为有一个孩子,不可以再生了,否则无论对家庭还是国家都将是沉重的负担。"

"有点偏激,但也可以这么理解吧,现在国家已经在鼓励生二胎三胎了,这也是根据实际情况发生的改变吧。"

"这样一来,客户端不再考虑是否需要去实例化的问题,而把责任都给了应该负责的类去处理。其实这就是一个很基本的设计模式:单例模式。"

1.4 单例模式

单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。[DP]
"通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。一个最好的办法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法。[DP]"

单例模式(Singleton)结构图

package code.chapter21.singleton0;

public class Test {

    public static void main(String[] args) {

        System.out.println("**********************************************");       
        System.out.println("《大话设计模式》代码样例");
        System.out.println();       

        //Singleton s0 = new Singleton();
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

        if (s1 == s2) {
            System.out.println("两个对象是相同的实例。");
        }

        System.out.println();
        System.out.println("**********************************************");
    }
}

//单例模式类
class Singleton {

    private static Singleton instance;

    //构造方法private化
    private Singleton() {
    }

    //得到Singleton的实例(唯一途径)
    public static Singleton getInstance() {

        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }
}

Singleton类,定义一个GetInstance操作,允许客户访问它的唯一实例。GetInstance是一个静态方法,主要负责创建自己的唯一实例。

"单例模式除了可以保证唯一的实例外,还有什么好处呢?"

"好处还有呀,比如单例模式因为Singleton类封装它的唯一实例,这样它可以严格地控制客户怎样访问它以及何时访问它。简单地说就是对唯一实例的受控访问。"

"我怎么感觉单例有点像一个实用类的静态方法,比如Java框架里的Math类,有很多数学计算方法,这两者有什么区别呢?"

"你说得没错,它们之间的确很类似,实用类通常也会采用私有化的构造方法来避免其有实例。但它们还是有很多不同的,比如实用类不保存状态,仅提供一些静态方法或静态属性让你使用,而单例类是有状态的。实用类不能用于继承多态,而单例虽然实例唯一,却是可以有子类来继承。实用类只不过是一些方法属性的集合,而单例却是有着唯一的对象实例。在运用中还得仔细分析再做决定用哪一种方式。"

1.5 多线程的单例

"另外,你还需要注意一些细节,比如说,多线程的程序中,多个线程同时,注意是同时访问Singleton类,调用getInstance()方法,会有可能造成创建多个实例。"

"啊,是呀,这应该怎么办呢?"

"可以给进程一把锁来处理。这里需要解释一下synchronized语句的含义。synchronized是Java中的关键字,是一种同步锁。意思就是当一个线程没有退出之前,先锁住这段代码不被其他线程代码调用执行,以保证同一时间只有一个线程在执行此段代码。"

package code.chapter21.singleton01;

public class Test {

    public static void main(String[] args) {

        System.out.println("**********************************************");       
        System.out.println("《大话设计模式》代码样例");
        System.out.println();       

        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

        if (s1 == s2) {
            System.out.println("两个对象是相同的实例。");
        }

        System.out.println();
        System.out.println("**********************************************");
    }
}

//单例模式类
class Singleton {

    private static Singleton instance;

    //构造方法private化
    private Singleton() {
    }

    //得到Singleton的实例(唯一途径)
    public static synchronized Singleton getInstance() {

        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }
}

"这段代码使得对象实例由最先进入的那个线程创建,以后的线程在进入时不会再去创建对象实例了。由于有了synchronized,就保证了多线程环境下的同时访问也不会造成多个实例的生成。"

"为什么不直接锁实例,而是用synchronized锁呢?"

"加锁时,instance实例有没有被创建过实例都还不知道,怎么对它加锁呢?"

"我知道了,原来是这样。但这样就得每次调用getInstance方法时都需要锁,好像不太好吧。"

"说得非常好,的确是这样,这种做法是会影响性能的,所以对这个类还需要改造。"

1.6 双重锁定

package code.chapter21.singleton02;

public class Test {

    public static void main(String[] args) {

        System.out.println("**********************************************");       
        System.out.println("《大话设计模式》代码样例");
        System.out.println();       

        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

        if (s1 == s2) {
            System.out.println("两个对象是相同的实例。");
        }

        System.out.println();
        System.out.println("**********************************************");
    }
}

//单例模式类
class Singleton {

    private volatile static Singleton instance;

    //构造方法private化
    private Singleton() {
    }

    //得到Singleton的实例(唯一途径)
    public static Singleton getInstance() {

        if (instance == null){

            synchronized(Singleton.class){

                if (instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

"现在这样,我们不用让线程每次都加锁,而只是在实例未被创建的时候再加锁处理。同时也能保证多线程的安全。这种做法被称为Double-Check Locking(双重锁定)。"

"我有问题,我在外面已经判断了instance实例是否存在,为什么在synchronized里面还需要做一次instance实例是否存在的判断呢?"

"那是因为你没有仔细分析。对于instance存在的情况,就直接返回,这没有问题。当instance为null并且同时有两个线程调用getInstance()方法时,它们将都可以通过第一重instance==null的判断。然后由于'锁'机制,这两个线程则只有一个进入,另一个在外排队等候,必须要其中的一个进入并出来后,另一个才能进入。而此时如果没有了第二重的instance是否为null的判断,则第一个线程创建了实例,而第二个线程还是可以继续再创建新的实例,这就没有达到单例的目的。你明白了吗?"

"如果单例类的性能是你关注的重点,上面的这个做法可以大大减少getInstance()方法在时间上的耗费。"

1.7 静态初始化

"在实际应用当中,上面的做法一般都能应付自如。不过为了确保实例唯一,还是会带来很大的性能代价。对于那些性能要求特别高的程序来说,传统单例代码实现或许还不是最好的方法。"

"哦,还有更好的办法?"

"有一种更简单的实现。我们来看代码。"

package code.chapter21.singleton03;

public class Test {

    public static void main(String[] args) {

        System.out.println("**********************************************");       
        System.out.println("《大话设计模式》代码样例");
        System.out.println();       

        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

        if (s1 == s2) {
            System.out.println("两个对象是相同的实例。");
        }

        System.out.println();
        System.out.println("**********************************************");
    }
}

//单例模式类
class Singleton {

    private static Singleton instance = new Singleton();

    //构造方法private化
    private Singleton() {
    }

    //得到Singleton的实例(唯一途径)
    public static Singleton getInstance() {
        return instance;
    }
}

"这样的实现与前面的示例类似,也是解决了单例模式试图解决的两个基本问题:全局访问和实例化控制,公共静态属性为访问实例提供了一个全局访问点。不同之处在于它依赖公共语言运行库来初始化变量。由于构造方法是私有的,因此不能在类本身以外实例化Singleton类;因此,变量引用的是可以在系统中存在的唯一的实例。由于这种静态初始化的方式是在自己被加载时就将自己实例化,所以被形象地称之为饿汉式单例类, 原先的单例模式处理方式是要在第一次被引用时,才会将自己实例化,所以就被称为懒汉式单例类。 [J&DP]"

"懒汉饿汉,哈,很形象的比喻。它们主要有什么区别呢?"

"由于饿汉式,即静态初始化的方式,它是类一加载就实例化的对象,所以要提前占用系统资源。然而懒汉式,又会面临着多线程访问的安全性问题,需要做双重锁定这样的处理才可以保证安全。所以到底使用哪一种方式,取决于实际的需求。从Java语言角度来讲,饿汉式的单例类已经足够满足我们的需求了。"

"没想到小小的单例模式也有这么多需要考虑的问题。

相关推荐
尘浮生2 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
闲人一枚(学习中)9 分钟前
设计模式-创建型-抽象工厂模式
设计模式·抽象工厂模式
不是二师兄的八戒25 分钟前
本地 PHP 和 Java 开发环境 Docker 化与配置开机自启
java·docker·php
爱编程的小生37 分钟前
Easyexcel(2-文件读取)
java·excel
带多刺的玫瑰1 小时前
Leecode刷题C语言之统计不是特殊数字的数字数量
java·c语言·算法
计算机毕设指导62 小时前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
Gu Gu Study2 小时前
枚举与lambda表达式,枚举实现单例模式为什么是安全的,lambda表达式与函数式接口的小九九~
java·开发语言
Chris _data2 小时前
二叉树oj题解析
java·数据结构
牙牙7052 小时前
Centos7安装Jenkins脚本一键部署
java·servlet·jenkins
paopaokaka_luck2 小时前
[371]基于springboot的高校实习管理系统
java·spring boot·后端