单例模式:为何继承无法保证子类的单例特性

news2024/12/24 0:40:49

这里写目录标题

  • 一、引言
  • 二、背景描述
  • 三、单例模式的规范边界
    • 全局访问点与静态工厂方法
    • 代码示例与注意事项
  • 四、单例实现继承遇到的问题
  • 五、结论与替代方案
    • 结论
    • 替代方案
    • 特殊的想法🌸🌸
      • 源码阶段验证
      • 编译阶段验证
      • 运行阶段验证
      • 总结

一、引言

    在软件设计中,单例模式是一种常见的设计模式,它确保一个类在程序运行期间只有一个实例,并提供一个全局访问点来获取这个实例。单例模式在需要控制资源访问、实现全局状态管理或确保某个操作具有唯一性的场景中尤为有用。然而,当涉及到继承时,单例模式的行为可能会变得复杂,特别是当希望子类也保持单例特性时。本文将深入探讨单例模式在实现过程中必须遵循的规范边界,以及为何通过继承无法保证子类的单例特性。同时,还将探讨是否有替代方案,以实现代码复用和单例特性的结合。

二、背景描述

    我想说一下为什么要研究单例的继承,设计模式是开发者们经过长期实践总结出的解决常见问题的有效方案。这些模式不仅提升了代码的可读性和可维护性,还促进了代码的复用与扩充。在探索23个经典设计模式的过程中,我逐渐发现复用与扩充是它们共同的显著特点。复用意味着代码片段可以在不同场景下重复使用,而扩充则是指在不修改原有代码的基础上,通过扩展功能来满足新的需求。

    为了深入理解这些模式的共性,我尝试从父类和子类的关系出发,将复用与扩充的概念具体化。最初,我认为通过父类实现代码的复用,而子类则在此基础上进行功能的扩充,这是一个直观且有效的思路。然而,当我尝试将这一思路应用于单例模式时,却发现遇到了挑战—我并不能通过继承,保证子类还是个单例。面对这一困境,我开始思考是否有其他方式,既能保持单例模式的特性,又能实现子类功能的扩充。这一探索过程不仅让我对单例模式有了更深入的理解,也让我意识到设计模式的应用并非一成不变,而是需要根据具体场景进行灵活调整和创新。

三、单例模式的规范边界

    确定一个类是否是单例,关键在于确保这个类在整个程序运行期间只存在一个实例,并提供一个全局访问点来获取这个实例。以下是实现单例模式时需要遵循的一些规范边界:
在这里插入图片描述

  1. 静态变量

    • 类的实例应该存储在一个静态私有变量中,这个变量会在类第一次加载时被初始化。
  2. 私有构造函数

    • 类的构造函数应该是私有的,以防止外部代码通过 new 关键字创建实例。
  3. 静态工厂方法

    • 提供一个公共的静态方法(如 getInstance),用于返回类的唯一实例。这个方法会检查静态变量是否已经持有实例,如果没有,则创建一个新的实例并返回;如果已经存在实例,则直接返回这个实例。

全局访问点与静态工厂方法

  • 全局访问点:是指在整个程序或应用程序域中,可以从任何地方访问到的代码或数据的位置。在单例模式中,全局访问点通常是一个公共的静态方法,它允许类的外部获取到该类的唯一实例。
  • 静态工厂方法:用于创建对象,而不需要直接调用构造函数。在单例模式中,静态工厂方法(通常命名为 getInstance 或类似名称)用于返回类的唯一实例,它提供了一种灵活的方式来创建对象。这个方法首先检查是否已经存在一个实例,如果不存在,则创建一个新的实例并返回它;如果已存在实例,则直接返回该实例。静态工厂方法提供了一种灵活的方式来创建对象,因为它们可以返回现有实例(在单例情况下),也可以根据需要返回新实例(在非单例情况下)。此外,静态工厂方法还可以用于实现其他设计模式,如原型模式、工厂方法模式和抽象工厂模式等。
    在单例模式中,静态工厂方法是实现单例的关键部分之一,它确保了类的唯一实例在整个程序运行期间只被创建一次,并提供了全局访问点来访问这个实例。

代码示例与注意事项

以下是一个简单的单例模式实现示例,并附带了注意事项:

public class Singleton {
    // 静态私有变量,持有唯一实例
    private static Singleton instance;

    // 私有构造函数,防止外部实例化
    private Singleton() {
        // 防止通过反射创建实例(可选)
        if (instance != null) {
            throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
        }
    }

    // 公共静态工厂方法,返回类的唯一实例
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    // 示例方法
    public void doSomething() {
        System.out.println("Doing something...");
    }

    // 防止反序列化重新创建对象
    protected Object readResolve() {
        return getInstance();
    }
}

注意事项

  • 线程安全:上面的 getInstance 方法使用了同步关键字 synchronized,虽然保证了线程安全,但可能会带来性能问题。可以使用双重检查锁定来优化性能。
  • 饿汉式与懒汉式:上面的例子属于懒汉式单例(延迟加载),即实例在第一次使用时才创建。饿汉式单例(立即加载)在类加载时就创建实例,更简单但可能浪费资源。
  • 枚举单例:在 Java 中,使用枚举来实现单例是最简单且线程安全的方式,同时可以防止反序列化和反射攻击。

四、单例实现继承遇到的问题

在尝试通过继承来实现单例时,会遇到以下问题:
1、不能直接实例化-抽象类
在这里插入图片描述

2、抽象方法由子类实现-非法的修饰符组合 ‘abstract’ 和 ‘static’
在这里插入图片描述
abstract 关键字
● 用途:abstract关键字用于声明一个抽象方法。这意味着该方法没有具体的实现(即没有方法体)。
● 存在场景:abstract方法只能存在于抽象类中。
● 子类责任:继承抽象类的子类必须提供这些抽象方法的具体实现,除非子类本身也是抽象的。
static 关键字
● 用途:static关键字用于声明一个静态方法。静态方法属于类本身,而不是类的实例。
● 调用方式:静态方法可以通过类名直接调用,而不需要创建类的实例。
● 访问静态变量:静态方法只能直接访问静态变量和静态方法,因为它们与类的实例无关。

为什么不能同时使用
a. 定义冲突:
○ abstract方法要求子类提供实现。
○ static方法属于类本身,不提供实例相关的行为,且不允许子类重写(可以被隐藏,但这不同于重写)。
b. 实现矛盾:
○ 如果一个方法是abstract的,那么它必须被子类实现。但静态方法属于类级别,无法被子类实例级别的方法覆盖或实现。
○ 抽象方法意味着每个子类可以有不同的实现,而静态方法在所有子类中是共享的,这违反了多态性的原则。
c. 语法规则:
○ Java的语法规则不允许在同一个方法声明中同时使用abstract和static关键字。编译器会报错。

3、去掉abstract–缺少方法体
在这里插入图片描述
4、添加方法体-不是抽象的不能保证子类必须实现
在这里插入图片描述
5、去掉static- ‘createInstance()’
在这里插入图片描述
6、不能保证子类必须实现,留着无用,删除
在这里插入图片描述
7、那属性也没用了,删除
在这里插入图片描述
8、父类构造函数私有,影响子类
在这里插入图片描述
9、父类构造函数访问修饰符改为protect
在这里插入图片描述
10、父类又不能创建对象,构造函数共有私有没什么影响-直接去掉了
在这里插入图片描述
    在单例设计模式的上下文中,当子类实现单例时,父类的构造函数是否为public确实不是决定性的因素,至少不是从防止外部直接创建实例的角度来看。关键在于子类如何控制自己的实例化过程,以及它如何确保自己只被实例化一次。
    ● 无论父类的构造函数如何设置,实现单例的子类都需要确保自己只能被实例化一次。这通常通过私有静态变量来持有唯一实例的引用,以及一个公有的静态方法来提供对该实例的访问。
    ● 子类还需要一个私有的构造函数来防止外部代码通过new关键字直接创建实例。

五、结论与替代方案

结论

单例父类–不能保证子类必然是个单例
    在单例模式中,通常不是父类本身实现单例,而是子类(或具体的实现类)实现单例。父类通常不包含与单例模式直接相关的逻辑,而是提供了一些通用的行为或属性。
    ● 在单例模式中,子类实现单例的关键在于它如何控制自己的实例化过程,以及它如何确保自己只被实例化一次。
    ● 父类的构造函数是否为public通常不会影响子类的单例属性,因为外部代码通常不会直接实例化父类,而是会尝试实例化子类。
    ● 如果父类本身不应该被外部直接实例化,那么它的构造函数应该被设置为适当的访问级别(通常是protected或private),但这主要是出于封装和设计的考虑,而不是出于单例模式的需要。

替代方案

如果父类不能保证子类肯定是个单例,是否有其他的替代方案,比如:

  1. 使用组合而非继承:通过组合关系将单例行为注入到需要它的类中,而不是通过继承关系。
  2. 工厂模式与单例模式结合:创建一个工厂类来管理单例实例的创建和访问,同时提供额外的灵活性来支持代码复用。
  3. 使用依赖注入框架:依赖注入框架(如 Spring)提供了强大的功能来管理对象的生命周期和依赖关系,可以轻松地实现单例模式并促进代码复用。

特殊的想法🌸🌸

    最后,想说一点其他的想法,就是是否可以从代码的生命周期思考验证办法,去实现复用?(这点还有待继续研究)

源码阶段验证

    在源码阶段,可以编写一个静态代码分析工具(或者利用现有的工具如 Checkstyle、PMD 等)来检查单例模式的实现是否符合规范。具体验证点包括:

  1. 私有构造函数:确保单例类有一个私有的构造函数,防止外部通过 new 关键字创建实例。
  2. 静态实例变量:检查类中是否有一个私有的静态实例变量,用于存储单例的唯一实例。
  3. 公共的静态获取方法:确保有一个公共的静态方法,用于返回单例的唯一实例。这个方法应该是线程安全的(例如,使用双重检查锁定或静态内部类等方式)。

示例代码(使用Checkstyle自定义规则):

// 自定义Checkstyle规则示例(需要编写XML配置文件)
<module name="CustomCheck">
    <property name="message" value="Singleton class should have a private constructor"/>
    <property name="format" value="SingletonCheck"/>
    <property name="fileExtensions" value="java"/>
    <property name="class" value="com.example.SingletonCheck"/>
</module>

// SingletonCheck.java(自定义Checkstyle检查器)
public class SingletonCheck extends Check {
    @Override
    public int[] getDefaultTokens() {
        return new int[]{TokenTypes.CTOR_DEF};
    }

    @Override
    public void visitToken(DetailAST ast) {
        DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
        if (modifiers != null && !modifiers.branchContains(TokenTypes.LITERAL_PRIVATE)) {
            log(ast, "Singleton class should have a private constructor");
        }
    }
}

编译阶段验证

    在编译阶段,可以编写一个自定义的注解处理器(Annotation Processor)来验证生成的 .class 文件是否符合单例模式的要求。注解处理器可以在编译时检查类结构,并生成警告或错误。

示例代码(自定义注解和处理器):

// Singleton.java(自定义注解)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Singleton {
}

// SingletonProcessor.java(注解处理器)
@SupportedAnnotationTypes("com.example.Singleton")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class SingletonProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(Singleton.class)) {
            TypeElement typeElement = (TypeElement) element;

            // 检查构造函数是否为私有
            List<? extends Element> constructors = element.getEnclosedElements().stream()
                    .filter(e -> e.getKind() == ElementKind.CONSTRUCTOR)
                    .collect(Collectors.toList());
            if (constructors.stream().noneMatch(c -> c.getModifiers().contains(Modifier.PRIVATE))) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Singleton class should have a private constructor", typeElement);
            }

            // 检查是否有一个静态的实例变量
            // ...(这里需要更复杂的逻辑来检查静态变量)

            // 检查是否有一个公共的静态获取方法
            // ...(同样需要更复杂的逻辑来检查方法)
        }
        return true;
    }
}

运行阶段验证

    在运行阶段,可以通过编写单元测试来验证单例模式的正确性。具体方法是创建两个对象,并比较它们的哈希码或使用 == 操作符来验证它们是否是同一个实例。

示例代码(JUnit测试):

public class SingletonTest {
    @Test
    public void testSingleton() {
        SingletonClass instance1 = SingletonClass.getInstance();
        SingletonClass instance2 = SingletonClass.getInstance();
        
        assertTrue(instance1 == instance2);
    }
}

总结

    是否可以从代码的生命周期思考,通过源码阶段、编译阶段和运行阶段的验证,来确保单例模式的正确性和可靠性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2217371.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

实时语音转文字(基于NAudio+Whisper+VOSP+Websocket)

今天花了大半天时间研究一个实时语音转文字的程序&#xff0c;目的还包括能够唤醒服务&#xff0c;并把命令提供给第三方。 由于这方面的材料已经很多&#xff0c;我就只把过程中遇到的和解决方案简单说下。源代码开源在AudioWhisper: 实时语音转文字(基于NAudioWhisperVOSPWe…

基于SSM的个性化商铺系统【附源码】

基于SSM的个性化商铺系统 效果如下&#xff1a; 用户登录界面 app首页界面 商品信息界面 店铺信息界面 用户功能界面 我的订单界面 后台登录界面 管理员功能界面 用户管理界面 商家管理界面 店铺信息管理界面 商家功能界面 个人中心界面 研究背景 研究背景 科学技术日新月异…

Leetcode 每日温度

class Solution {public int[] dailyTemperatures(int[] temperatures) {int n temperatures.length;Stack<Integer> stack new Stack<>();//默认将数组中的所有元素初始化为 0int[] results new int[n];for(int i 0; i < n; i) {while(!stack.isEmpty() &a…

leaflet前端JS实现高德地图POI兴趣点批量分类下载(附源码下载)

前言 leaflet 入门开发系列环境知识点了解&#xff1a; leaflet api文档介绍&#xff0c;详细介绍 leaflet 每个类的函数以及属性等等leaflet 在线例子leaflet 插件&#xff0c;leaflet 的插件库&#xff0c;非常有用 内容概览 leaflet前端JS实现高德地图POI兴趣点批量分类下载…

小猿口算炸鱼脚本

目录 写在前面&#xff1a; 一、关于小猿口算&#xff1a; 二、代码逻辑 1.数字识别 2.答题部分 三、代码分享&#xff1a; 补充&#xff1a;软件包下载 写在前面&#xff1a; 最近小猿口算已经被不少大学生攻占&#xff0c;小学生直呼有挂。原本是以为大学生都打着本…

【Python爬虫】看电影还在用VIP?一个python代码让你实现电影自由!附源码

今日主题 如何用Python解析vip电影。 什么是vip电影&#xff1f; 这些vip电影啊&#xff0c;想要观看的话&#xff0c;必须充值会员&#xff0c;否则没法看。 比如这个&#xff1a; 这些vip电影解析后呢&#xff1f; 不需要会员&#xff0c;不需要登录&#xff0c;可以直接…

Java-类与对象

一、面向对象 在了解类与对象前&#xff0c;我们需要先知道"面向对象"这个词的概念&#xff1a; 在Java语言中&#xff0c;我们的主要思想就是"面向对象"&#xff0c;而在之前我们所学习的C语言中大部分时候的思想是"面向过程"。 那么什么是&…

MySQL-10.DML-添加数据insert

一.DML(INSERT) -- DDL&#xff1a;数据操作语言 -- DML&#xff1a;插入数据 - insert -- 1.为tb_emp表的username&#xff0c;name&#xff0c;gender字段插入值 insert into tb_emp (username,name,gender) values (wuji,无忌,1); -- 这样会报错&#xff0c;因为create_ti…

DS堆的实际应用(10)

文章目录 前言一、堆排序建堆排序 二、TopK问题原理实战创建一个有一万个数的文件读取文件并将前k个数据创建小堆用剩余的N-K个元素依次与堆顶元素来比较将前k个数据打印出来并关闭文件 测试 三、堆的相关习题总结 前言 学完了堆这个数据结构的概念和特性后&#xff0c;我们来看…

限时设计ui

ctrl-------放大缩小 空格-----画面移动 alt------复制 页面<画板<图层 添加交互事件 原型 点击蓝色的圆&#xff0c;从1跳转到2 点击绿色的圆&#xff0c;从2跳转到1

基于SSM+Vue+MySQL的健身房管理系统

系统展示 系统背景 随着人们生活水平的提高和健康意识的增强&#xff0c;越来越多的人选择去健身房锻炼。传统的健身房管理方式往往依赖于纸质记录和人工操作&#xff0c;这种方式不仅效率低下&#xff0c;而且容易出错。为了提高健身房的管理效率和服务质量&#xff0c;开发一…

python项目实战——下载美女图片

python项目实战——下载美女图片 文章目录 python项目实战——下载美女图片完整代码思路整理实现过程使用xpath语法找图片的链接检查链接是否正确下载图片创建文件夹获取一组图片的链接获取页数 获取目录页的链接 完善代码注意事项 完整代码 import requests import re import…

图文检索综述(2):Deep Multimodal Data Fusion

Deep Multimodal Data Fusion 摘要1 引言2 基于编码器-解码器融合2.1 数据级别融合2.2 分层特征融合2.3 决策级别融合 3 基于注意力融合3.1 模态内的自注意力3.2 模态间的交叉注意力3.3 基于transformer的方法 4 基于图神经网络融合4.1 单个模态的表示学习4.2 融合数据的表示学…

【数据结构】宜宾大学-计院-实验三

线性表的应用——实现两多项式的相加 课前准备&#xff1a;实验学时&#xff1a;2实验目的&#xff1a;实验内容&#xff1a;实验结果&#xff1a;实验报告:&#xff08;及时撰写实验报告&#xff09;实验测试结果&#xff1a;代码实现&#xff1a;&#xff08;C/C&#xff09;…

Java 小游戏《超级马里奥》

文章目录 一、效果展示二、代码编写1. 素材准备2. 创建窗口类3. 创建常量类4. 创建动作类5. 创建关卡类6. 创建障碍物类7. 创建马里奥类8. 编写程序入口 一、效果展示 二、代码编写 1. 素材准备 首先创建一个基本的 java 项目&#xff0c;并将本游戏需要用到的图片素材 image…

华为 HCIP-Datacom H12-821 题库 (38)

&#x1f423;博客最下方微信公众号回复题库,领取题库和教学资源 &#x1f424;诚挚欢迎IT交流有兴趣的公众号回复交流群 &#x1f998;公众号会持续更新网络小知识&#x1f63c; 1.请对 2001:0DB8:0000:C030:0000:0000:09A0:CDEF 地址进行压缩。&#xff08; &#xff09;&…

阻塞I/O与非阻塞I/O

目录 一、基本概念 二、阻塞I/O的实现机制 —— 等待队列 一、基本概念 阻塞&#xff1a;在执行单元进行操作时&#xff0c;如果不能获得申请的资源&#xff0c;则执行单元挂起直至资源可用后再进行操作。 非阻塞&#xff1a;在执行单元进行操作时&#xff0c;如果不能获得申…

UDP反射放大攻击防范手册

UDP反射放大攻击是一种极具破坏力的恶意攻击手段。 一、UDP反射放大攻击的原理 UDP反射放大攻击主要利用了UDP协议的特性。攻击者会向互联网上大量的开放UDP服务的服务器发送伪造的请求数据包。这些请求数据包的源IP地址被篡改为目标受害者的IP地址。当服务器收到这些请求后&…

爬虫实战(黑马论坛)

1.定位爬取位置内容&#xff1a; # -*- coding: utf-8 -*- import requests import time import re# 请求的 URL 和头信息 url https://bbs.itheima.com/forum-425-1.html headers {user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like…

DBSwitch和Seatunel

一、DBSwitch 什么是DBSwitch?它主要用在什么场景&#xff1f; 通过步骤分析可以看到这个是通过配置数据源&#xff0c;采用一次性或定时方案&#xff0c;同步到数据仓库的指定表&#xff0c;并且指定映射关系的工具。有点类似于flinkcdc的增量同步。 参考&#xff1a; dbs…