一篇文章教你使用枚举来实现java单例模式

目录
  • 传统的单例写法解决了什么问题
    • 仍然存在的问题
    • 为什么枚举就没有问题
  • 总结

传统的单例写法解决了什么问题

首先,在大多数情况下(不包含面试),传统的单例写法已经完全够用了。通过 synchronized 关键字解决了多线程并发使用。

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

考虑到每次获取单例对象都需要加锁,解锁。又有人发明了双重锁校验 + volatile 关键字模式:

    private static volatile SingleClassV2 instance;
    public static SingletonV2 getInstance() {
         if(instance == null){
             synchronized (SingletonV2.class){
                 if(instance == null){
                     instance = new SingletonV2();
                 }
             }
         }
         return instance;
     }

另外一种为了解决单例被重复初始化的写法:利用类只会被初始化一次的特性,又有人发明出来一种内部类单例的写法。

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

仍然存在的问题

由于 java 中有反射 API 这种变态的存在,以上所有的私有构造方法在反射面前都是毛毛雨。

    Class<?> clazzV2 = Class.forName(SingleClassV2.class.getName());
    Constructor<?> constructor = clazzV2.getDeclaredConstructors()[0];
    constructor.setAccessible(true);
    Object o = constructor.newInstance();

看来私有方法是防君子不防小人

为什么枚举就没有问题

我们来先看一下基于枚举的单例是什么样的。

public enum SingleClassV4 {
    INSTANCE;
    public String doSomeThing(){
        return "hello world";
    }
}

当然,从 java 代码是看不出来任何端倪的。再使用 javap 看一下字节码。

public final class git.frank.SingleClassV4 extends java.lang.Enum<git.frank.SingleClassV4>
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM

可以发现,枚举类型会帮我们自动继承 java.lang.Enum 类。并且,在 flags 中该类被添加了 ACC_ENUM 标识。然后,再看一下枚举类的构造方法:

  private git.frank.SingleClassV4();
    descriptor: (Ljava/lang/String;I)V
    flags: ACC_PRIVATE
    Code:
      stack=3, locals=3, args_size=3
         0: aload_0
         1: aload_1
         2: iload_2
         3: invokespecial #6
         // Method java/lang/Enum."<init>":(Ljava/lang/String;I)V
         6: return
      LineNumberTable:
        line 3: 0//加入Java开发交流君样:756584822一起吹水聊天
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lgit/frank/SingleClassV4;
    Signature: #29                          // ()V

枚举类也是要有构造方法的,而且也和普通的类没什么不同,也一样可以通过反射获取到:

接下来,让我们通过反射 invoke 一下他的构造方法看看会发生什么:

constructor.newInstance();

结果如下:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.lang.reflect.Constructor.newInstance(Constructor.java:417)

通过看 newInstance 方法代码的话,就很容易知道原因了:

    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {//加入Java开发交流君样:756584822一起吹水聊天
        ...
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ...
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

java 的反射 API 在创建对象实例是判断了当前类是否是枚举类型,否则就抛异常出来。

总结

在传统的单例写法中,由于私有构造方法并不能完全杜绝从外部创建实例,所以严格来说那些单例的实现方式是存在漏洞的。

由于 java 的反射 API 已经通过写死的方式限制了不能为枚举类型创建实例,所以… 也算了解决了吧。哎呀,这个东西是也是面试被问到的,正常人谁会用反射这种外挂去突破单例。

本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注我们的更多内容!

时间: 2021-07-17

java面试常见模式问题---单例模式

1.简介 单例模式使⽤场景: 业务系统全局只需要⼀个对象实例,⽐如发号器. redis 连接对象等. Spring IOC容器中的 Bean 默认就是单例. Spring Boot 中的 Controller.Service.Dao 层中通过 @Autowire的依赖注⼊对象默认都是单例的. 单例模式分类: 懒汉:就是所谓的懒加载,延迟创建对象,需要用的时候再创建对象. 饿汉:与懒汉相反,提前创建对象. 单例模式实现步骤: 私有化构造函数. 提供获取单例的方法. 2.单例模式--懒汉式 单例模式

教你java面试时如何聊单例模式

目录 NO.1 单例模式的应用场景 NO.2 饿汉式单例 NO.3 懒汉式单例 NO.4 反射破坏单例 NO.5 序列化破坏单例 NO.6 注册式单例 NO.7 ThreadLocal 线程单例 总结 NO.1 单例模式的应用场景 单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点.单例模式是创建型模式.单例模式在现实生活中应用也非常广泛.例如公司 CEO.部门经理等.在 J2EE 标准中,ServletContext.Servlet

Java枚举类与注解,新手一篇搞定它

一.枚举类 类的对象只有有限个, 确定的. 我们称此类为枚举类. 说明: 1.类的对象只有有限个,确定的.如: 星期:Monday(星期一).-.Sunday(星期天) 性别:Man(男).Woman(女) Ø 季节:Spring(春节)-Winter(冬天) 支付方式:Cash(现金).WeChatPay(微信).Alipay(支付宝).BankCard(银 行卡).CreditCard(信用卡) 就职状态:Busy.Free.Vocation.Dimission订单状态:Nonpayment

分析java中全面的单例模式多种实现方式

一.单例模式的思想 想整理一些 java 并发相关的知识,不知道从哪开始,想起了单例模式中要考虑的线程安全,就从单例模式开始吧.以前写过单例模式,这里再重新汇总补充整理一下,单例模式的多种实现. 单例模式的主要思想是: 将构造方法私有化( 声明为 private ),这样外界不能随意 new 出新的实例对象: 声明一个私有的静态的实例对象,供外界使用: 提供一个公开的方法,让外界获得该类的实例对象 这种说法看上去没错,但也好像不太准确.其实,就算外界能随意 new 出新的实例对象,但只要我们保证

Java基础之枚举Enum类案例详解

一.文章序言 Java中引用类型:数组.类.接口.枚举.注解 枚举这个既熟悉又陌生的东西具体再哪里可以使用呢? 什么是枚举? 枚举是一个引用类型,枚举就是一个规定了取值范围的变量类型. 枚举变量不能使用其他的数据,只能使用枚举中常量赋值.提高程序安全性: //格式: public enum 枚举名{ //枚举的取值范围 //枚举中可以生命方法 } 枚举的使用场景介绍? 1.最常见的情况如星期,相关变量我们会在Java里面重复使用,在这里我们就可以来定义一个叫做"星期"的枚举. publ

java基础(System.err和System.out)详解

今天有位同事在使用System.err和System.out遇上了一些小问题. 看了些资料总结下: 1.JDK文档对两者的解释: out:"标准"输出流.此流已打开并准备接受输出数据.通常,此流对应于显示器输出或者由主机环境或用户指定的另一个输出目标. err:"标准"错误输出流.此流已打开并准备接受输出数据.通常,此流对应于显示器输出或者由主机环境或用户指定的另一个输出目标.按照惯例,此输出流用于显示错误消息,或者显示那些即使用户输出流(变量 out 的值)已经重

Java中IO流 RandomAccessFile类实例详解

Java中IO流 RandomAccessFile类实例详解 RandomAccessFile java提供的对文件内容的访问,既可以读文件,也可以写文件. 支持随机访问文件,可以访问文件的任意位置. java文件模型,在硬盘上的文件是byte byte byte存储的,是数据的集合 打开文件,有两种模式,"rw"读写."r"只读:RandomAccessFile raf = new RandomAccessFile(file, "rw");,文

JAVA基础之继承(inheritance)详解

继承(inheritance)是Java OOP中一个非常重要的概念.继承是在复用已存在的类的方法和域的基础上,还可以添加新的方法和域.Java用extends关键字来表示继承关系(is-a).被继承的类称为超类(superclass).基类(base class).父类(parent class),而新类被称为子类(subclass).派生类(derived class)或孩子类(child class). 1.class:编程语言中的基本单位.将数据和功能封装到了一起. 2.基类包含其所有导

Java中JSON处理工具类使用详解

本文实例为大家分享了JSON处理工具类的具体代码,供大家参考,具体内容如下 import java.io.IOException; import java.util.Date; import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpServletResponse; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.seri

Java基础之代理原理与用法详解

本文实例讲述了Java基础之代理原理与用法.分享给大家供大家参考,具体如下: 1.什么是代理 动态代理技术是整个java技术中最重要的一个技术,它是学习java框架的基础,不会动态代理技术,那么在学习Spring这些框架时是学不明白的. 动态代理技术就是用来产生一个对象的代理对象的.在开发中为什么需要为一个对象产生代理对象呢? 举一个现实生活中的例子:歌星或者明星都有一个自己的经纪人,这个经纪人就是他们的代理人,当我们需要找明星表演时,不能直接找到该明星,只能是找明星的代理人.比如刘德华在现实生

在java poi导入Excel通用工具类示例详解

前言 本文主要给大家介绍了关于java poi导入Excel通用工具类的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 问题引入和分析 提示:如果不想看罗嗦的文章,可以直接到最后点击源码下载运行即可 最近在做一个导入Excel的功能,在做之前在百度上面查找"java通用导入Excel工具类",没有查到,大多数都是java通用导出Excel.后来仔细想想,导出可以利用java的反射,做成通用的,放进相应的实体成员变量中,导入为什么不可以呢?也是可以的,不过在做

java基础知识I/O流使用详解

"流"概念源于UNIX中的管道(pipe)的概念.在UNIX中,管道是一条不间断的字节流,用来实现程序或进程间的通信,或读写外围设备.外部文件等,它屏蔽了实际的I/O设备中处理数据的细节.   一个流,必有源端和目的端,它们可以是计算机内存的某些区域,也可以是磁盘文件,甚至可以是Internet上的某个URL. 流的方向是重要的,根据流的方向,流可以分为两类:输入流和输出流.其实输入/输出是想对于内存来说的.实际上,流的源端和目的端可简单地看成是字节的生产者和消费者,对于输入流,可不必

Java动态代理语法Proxy类原理详解

1.前言 写动态代理的代码涉及了一个非常重要的类 Proxy,通过Proxy的静态方法newProxyInstance才会动态创建代理对象. 2.newProxyInstance方法 public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 三个参数分别表示: loader表示类加载器, interfaces表示代码要用来代理的接口 , h表示一个

Java枚举的使用方法详解

 Java枚举的使用方法详解 前言  你代码中的flag和status,都应该用枚举来替代 很多人都说,枚举在实际开发中很少用到,甚至就没用到.因为,他们的代码往往是这样子的: public class Constant { /* * 以下几个变量表示英雄的状态 */ public final static int STATUS_WALKING = 0;//走 public final static int STATUS_RUNNINGING = 1;//跑 public final stati

java日期相关类实例详解

一.Date类 类 Date 表示特定的瞬间,精确到毫秒.用距离1970年1月1日00:00:00的毫秒数(long)类型来表示一个特定的时间点,该值可正可负. Date类中很多方法已经过时,使用Calendar来替代. 常用方法有 long getTime() void setTime(long time) public class T01Date { public static void main(String[] args) { //getTime():返回的时间是距离1970年的毫秒数.