Java 单例模式:类里面为什么可以有自己类型的字段?

Java 单例设计模式:为什么类里面可以有一个自己类型的字段?

最近学习 Java 设计模式时,看到了一个很常见的写法:

java 复制代码
public class Singleton {

    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

刚开始看到这段代码时,我最困惑的是这一行:

java 复制代码
private static final Singleton INSTANCE = new Singleton();

Singleton 类里面,怎么又有一个 Singleton 类型的字段?

这不就是"自己里面套自己"吗?

如果 Singleton 里面有一个 Singleton,那这个里面是不是又有一个 Singleton,然后无限套娃?

后来理解之后发现,问题的关键在于:Java 里的对象和引用不是一回事。


1. 什么是单例模式?

单例模式的核心思想是:

一个类在整个程序运行期间,只创建一个对象,所有地方都使用这同一个对象。

也就是说,不能让外部代码随便这样写:

java 复制代码
Singleton s1 = new Singleton();
Singleton s2 = new Singleton();
Singleton s3 = new Singleton();

如果可以随便 new,那就会创建多个对象,就不是单例了。

所以单例模式一般会做三件事:

java 复制代码
public class Singleton {

    // 1. 类内部自己保存唯一对象
    private static final Singleton INSTANCE = new Singleton();

    // 2. 构造方法私有,禁止外部 new
    private Singleton() {
    }

    // 3. 提供公共方法,让外部拿到这个唯一对象
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

总结起来就是:

text 复制代码
构造方法私有
类内部自己创建对象
外部只能通过 getInstance() 获取对象

2. 写一个带字段的单例例子

为了更好理解,我们写一个带业务字段的单例类。

比如有一个优惠券配置类:

java 复制代码
public class CouponConfig {

    private static final CouponConfig INSTANCE = new CouponConfig();

    private int maxReceiveCount;
    private boolean enableReminder;

    private CouponConfig() {
        this.maxReceiveCount = 3;
        this.enableReminder = true;
    }

    public static CouponConfig getInstance() {
        return INSTANCE;
    }

    public int getMaxReceiveCount() {
        return maxReceiveCount;
    }

    public void setMaxReceiveCount(int maxReceiveCount) {
        this.maxReceiveCount = maxReceiveCount;
    }

    public boolean isEnableReminder() {
        return enableReminder;
    }

    public void setEnableReminder(boolean enableReminder) {
        this.enableReminder = enableReminder;
    }
}

使用方式如下:

java 复制代码
public class Test {
    public static void main(String[] args) {
        CouponConfig config1 = CouponConfig.getInstance();
        CouponConfig config2 = CouponConfig.getInstance();

        System.out.println(config1 == config2); 
        // true

        System.out.println(config1.getMaxReceiveCount());
        // 3

        config1.setMaxReceiveCount(5);

        System.out.println(config2.getMaxReceiveCount());
        // 5
    }
}

为什么 config1 修改了 maxReceiveCountconfig2 也能看到变化?

因为:

java 复制代码
CouponConfig config1 = CouponConfig.getInstance();
CouponConfig config2 = CouponConfig.getInstance();

这两行拿到的是同一个对象。

可以理解成:

text 复制代码
config1 ----+
            |
            v
       CouponConfig 对象
            ^
            |
config2 ----+

config1config2 只是两个引用变量,它们都指向堆内存里的同一个 CouponConfig 对象。


3. 为什么类里面有自己类型的字段不是"无限套娃"?

真正容易误解的是这句:

java 复制代码
private static final CouponConfig INSTANCE = new CouponConfig();

初学时很容易把它理解成:

text 复制代码
CouponConfig 对象里面又放了一个 CouponConfig 对象
这个 CouponConfig 对象里面又放了一个 CouponConfig 对象
然后无限递归

但这其实是不对的。

因为 INSTANCE 是一个引用变量,不是直接把整个对象塞进类里面。

这句话可以拆开理解:

java 复制代码
private static final CouponConfig INSTANCE;

这表示声明了一个 CouponConfig 类型的静态引用变量。

然后:

java 复制代码
new CouponConfig();

这才是真正创建对象。

完整写法:

java 复制代码
private static final CouponConfig INSTANCE = new CouponConfig();

意思是:

创建一个 CouponConfig 对象,然后让 INSTANCE 这个引用指向它。

所以它不是无限套娃,而是:

text 复制代码
CouponConfig 类本身
    |
    |-- static INSTANCE
            |
            v
       堆内存中的唯一 CouponConfig 对象
            |
            |-- maxReceiveCount = 3
            |-- enableReminder = true

注意,INSTANCEstatic 的,它属于类本身,不属于某一个对象。


4. static 是理解单例的关键

在 Java 里,字段可以分成两类:

text 复制代码
普通成员变量:属于对象
static 静态变量:属于类

比如:

java 复制代码
private int maxReceiveCount;
private boolean enableReminder;

这两个字段属于具体的 CouponConfig 对象。

而:

java 复制代码
private static final CouponConfig INSTANCE = new CouponConfig();

这个字段属于 CouponConfig 类本身。

所以内存结构不是这样:

text 复制代码
错误理解:

CouponConfig 对象
    |
    |-- INSTANCE
            |
            v
       CouponConfig 对象
            |
            |-- INSTANCE
                    |
                    v
               CouponConfig 对象
                    |
                    |-- INSTANCE
                            ...

正确理解是这样:

text 复制代码
CouponConfig 类
    |
    |-- static INSTANCE
            |
            v
       CouponConfig 对象
            |
            |-- maxReceiveCount
            |-- enableReminder

也就是说:

类自己保存了一个指向唯一对象的引用。

不是对象里面又完整嵌套了一个对象。


5. 引用变量和对象不是一回事

这是理解单例最重要的点。

看这行代码:

java 复制代码
CouponConfig config;

这里只是声明了一个变量。

此时没有创建对象。

它大概是:

text 复制代码
config = null

只有写了:

java 复制代码
new CouponConfig();

才是真正创建对象。

比如:

java 复制代码
CouponConfig config = new CouponConfig();

可以拆成三步理解:

text 复制代码
1. 声明一个 CouponConfig 类型的引用变量 config
2. new CouponConfig() 在堆内存中创建一个对象
3. 让 config 指向这个对象

所以单例里的:

java 复制代码
private static final CouponConfig INSTANCE = new CouponConfig();

本质也是一样:

text 复制代码
1. 声明一个 static 引用变量 INSTANCE
2. 创建一个 CouponConfig 对象
3. 让 INSTANCE 指向这个对象

它并不是"对象里面塞对象"。


6. 类里面有自己类型的字段很常见

其实不只是单例,Java 里很多结构都会出现"类里面有自己类型字段"的情况。

比如链表节点:

java 复制代码
public class ListNode {
    int value;
    ListNode next;
}

这里的:

java 复制代码
ListNode next;

也是当前类自己的类型。

但它也不会无限套娃。

因为 next 只是一个引用,默认是 null

比如:

java 复制代码
ListNode node1 = new ListNode();
ListNode node2 = new ListNode();

node1.next = node2;
node2.next = null;

对应结构是:

text 复制代码
node1 -> node2 -> null

不是:

text 复制代码
node1 里面自动 new node2
node2 里面自动 new node3
node3 里面自动 new node4
...

字段只是"可以指向某个对象",并不代表它会自动创建对象。


7. 为什么构造方法要 private?

单例模式里通常会这样写:

java 复制代码
private CouponConfig() {
}

原因是:不让外部随便创建对象。

如果构造方法是 public:

java 复制代码
public CouponConfig() {
}

外部就可以这样写:

java 复制代码
CouponConfig c1 = new CouponConfig();
CouponConfig c2 = new CouponConfig();

这样就创建了两个对象,单例就被破坏了。

所以单例必须控制对象创建过程:

text 复制代码
外部不能 new
类内部自己 new
外部只能 getInstance()

8. 饿汉式单例

前面的写法属于饿汉式单例:

java 复制代码
public class CouponConfig {

    private static final CouponConfig INSTANCE = new CouponConfig();

    private CouponConfig() {
    }

    public static CouponConfig getInstance() {
        return INSTANCE;
    }
}

所谓饿汉式,就是:

类一加载,就马上创建对象。

优点:

text 复制代码
写法简单
线程安全

缺点:

text 复制代码
即使暂时用不到,也会提前创建对象

9. 懒汉式单例

懒汉式的思想是:

先不创建对象,等真正用到时再创建。

代码如下:

java 复制代码
public class CouponConfig {

    private static CouponConfig instance;

    private CouponConfig() {
    }

    public static CouponConfig getInstance() {
        if (instance == null) {
            instance = new CouponConfig();
        }
        return instance;
    }
}

这段代码在单线程下没问题,但在多线程下可能有问题。

假设两个线程同时执行:

java 复制代码
if (instance == null) {
    instance = new CouponConfig();
}

可能出现这种情况:

text 复制代码
线程 A 判断 instance == null,准备创建对象
线程 B 也判断 instance == null,也准备创建对象
线程 A 创建了一个对象
线程 B 又创建了一个对象

这样就不是单例了。

所以普通懒汉式是线程不安全的。


10. 双重检查锁单例

为了兼顾懒加载和线程安全,可以使用双重检查锁:

java 复制代码
public class CouponConfig {

    private static volatile CouponConfig instance;

    private int maxReceiveCount;
    private boolean enableReminder;

    private CouponConfig() {
        this.maxReceiveCount = 3;
        this.enableReminder = true;
    }

    public static CouponConfig getInstance() {
        if (instance == null) {
            synchronized (CouponConfig.class) {
                if (instance == null) {
                    instance = new CouponConfig();
                }
            }
        }
        return instance;
    }
}

这里有两个关键点。

第一个是 synchronized

java 复制代码
synchronized (CouponConfig.class) {
}

它保证同一时间只有一个线程能进入创建对象的代码。

第二个是 volatile

java 复制代码
private static volatile CouponConfig instance;

它主要是防止指令重排序,避免其他线程拿到一个"还没初始化完成"的对象。


11. 静态内部类单例

还有一种更推荐的写法:静态内部类。

java 复制代码
public class CouponConfig {

    private int maxReceiveCount;
    private boolean enableReminder;

    private CouponConfig() {
        this.maxReceiveCount = 3;
        this.enableReminder = true;
    }

    private static class Holder {
        private static final CouponConfig INSTANCE = new CouponConfig();
    }

    public static CouponConfig getInstance() {
        return Holder.INSTANCE;
    }

    public int getMaxReceiveCount() {
        return maxReceiveCount;
    }

    public void setMaxReceiveCount(int maxReceiveCount) {
        this.maxReceiveCount = maxReceiveCount;
    }

    public boolean isEnableReminder() {
        return enableReminder;
    }

    public void setEnableReminder(boolean enableReminder) {
        this.enableReminder = enableReminder;
    }
}

这种写法的优点是:

text 复制代码
懒加载
线程安全
代码相对简单

为什么它是懒加载?

因为 Holder 这个内部类只有在调用:

java 复制代码
CouponConfig.getInstance();

的时候才会被加载。

为什么它线程安全?

因为 Java 的类加载机制保证:一个类只会被初始化一次。


12. Spring 里的 Bean 默认也是单例

在 Spring Boot 里,我们经常写:

java 复制代码
@Service
public class UserService {
}

然后在别的地方注入:

java 复制代码
@Autowired
private UserService userService;

默认情况下,Spring 容器里的 Bean 也是单例的。

也就是说,整个 Spring 容器中默认只有一个 UserService 对象。

这也是为什么实际项目里,我们很少自己手写单例类,而是把对象交给 Spring 容器管理。

比如:

java 复制代码
@Component
public class CouponConfig {
}

默认就是单例 Bean。


13. Hutool Singleton.get 的理解

有时候也会看到 Hutool 的写法:

java 复制代码
DefaultRedisScript<Long> buildLuaScript = Singleton.get(
    STOCK_DECREMENT_AND_SAVE_USER_RECEIVE_LUA_PATH,
    () -> {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(
            new ResourceScriptSource(
                new ClassPathResource(STOCK_DECREMENT_AND_SAVE_USER_RECEIVE_LUA_PATH)
            )
        );
        redisScript.setResultType(Long.class);
        return redisScript;
    }
);

可以这样理解:

text 复制代码
Hutool 内部有一个单例容器
key 是 STOCK_DECREMENT_AND_SAVE_USER_RECEIVE_LUA_PATH
value 是 DefaultRedisScript 对象

第一次调用时:

text 复制代码
容器里没有这个 key
执行 lambda 表达式
创建 DefaultRedisScript 对象
放入容器
返回对象

第二次调用时:

text 复制代码
容器里已经有这个 key
不会再执行 lambda
直接返回之前创建好的对象

所以它的思想也是:

有就直接拿,没有就创建并缓存起来。


14. 最后总结

单例模式的本质是:

text 复制代码
一个类只创建一个对象,并提供一个全局访问点。

它最经典的结构是:

java 复制代码
public class Singleton {

    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

初学时最容易误解的是:

java 复制代码
private static final Singleton INSTANCE = new Singleton();

它不是无限套娃。

因为:

text 复制代码
INSTANCE 是一个引用变量
new Singleton() 才是真正创建对象
static INSTANCE 属于类本身,不属于某一个对象

所以正确理解应该是:

text 复制代码
类自己保存了一个指向唯一对象的引用。

不是:

text 复制代码
对象里面无限嵌套对象。

可以用一句话记住:

单例模式就是:不让别人随便 new,我自己在类里面创建唯一对象,别人只能通过 getInstance() 拿这个唯一对象。

相关推荐
8Qi81 小时前
LeetCode 32:最长有效括号 —— 栈 + 标记法 题解
java·数据结构·算法·leetcode·职场和发展··括号匹配
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【73】两步 RAG
java·人工智能·spring
_Evan_Yao1 小时前
面向对象实战:用 Java/Python 设计一个简单的“怪物战斗”小游戏
java·开发语言
asdfg12589631 小时前
一文通俗理解JDBC中的核心概念+案例
java·数据库·oracle·jdbc
布朗克1681 小时前
26 多线程基础——Thread、Runnable与线程安全
java·安全·多线程
c++之路1 小时前
CMake 系列教程(一):CMake 基础知识
c语言·开发语言·c++
AI行业学习1 小时前
CC‑Switch v3.16.1-下载、配置、安装(2026‑06‑01 最新官方版)
开发语言·人工智能·windows·python
赵庆明老师1 小时前
JS检查提交的文件是否合规
开发语言·前端·javascript
轮子飞了1 小时前
Spring Ai 集成 DashScope 多模态模型实现身份证信息识别
java·人工智能·spring