鸣谢:《大话设计模式》
建议结合文章看com.cf.sqlTest.api.designPatterns.singletonMode;包下的源码,源码位置
全部源码github.com/xs-alpha/designPattern转载请注明出处:小生听雨园 http://blog.devilwst.top/xiaosheng/2265.html
恶意转载会采用法律途径维护权益。
简介
是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点以访问该实例。单例模式的主要目的是限制一个类的实例化次数,以确保在整个应用程序中只存在一个实例。
有点像一个实用类的静态方法,比如Java框架里的Math类,有很多数学计算方法.它们之间的确很类似,实用类通常也会采用私有化的构造方法来避免其有实例。但它们还是有很多不同的,比如实用类不保存状态,仅提供一些静态方法或静态属性让你使用,而单例类是有状态的。实用类不能用于继承多态,而单例虽然实例唯一,却是可以有子类来继承。实用类只不过是一些方法属性的集合,而单例却是有着唯一的对象实例。
要素:
- 私有构造函数(Private Constructor):单例类的构造函数应该是私有的,这阻止了外部代码直接实例化该类。
-
私有静态变量(Private Static Variable):单例类通常包含一个私有的静态变量,用于存储该类的唯一实例。
-
静态方法(Static Method):单例类提供一个静态方法,通常命名为getInstance(),以允许外部代码获取唯一实例。
-
延迟实例化(Lazy Initialization):在首次调用getInstance()方法时,单例类会创建并返回唯一实例。这种延迟实例化确保了实例在需要时才被创建。
工作流程
直接getInstance
例子
代码
懒汉式
/**
* @author: lpy
* @Date: 2023/10/30
*/
public class SimpleLazySingleton {
private static SimpleLazySingleton simpleLazySingleton;
private SimpleLazySingleton(){
}
public static SimpleLazySingleton getInstance(){
if (null==simpleLazySingleton){
simpleLazySingleton = new SimpleLazySingleton();
}
return simpleLazySingleton;
}
}
饿汉式
/**
* @author: lpy
* @Date: 2023/10/30
*/
public class HungrySingleton {
// 在类加载的时候就创建出来了,属于饿汉式
private static HungrySingleton hungrySingleton = new HungrySingleton();
/** 私有构造函数*/
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
DCL
即使有synchronized,但如图所示,线程1可能比线程0更早的访问,访问到了未初始化完成的对象,会出现问题,
下面的解决方案是防止指令重排,还有一种解决方法是让线程1看不到线程0的数据,在下面的例子讲
/**
* @author: lpy
* @Date: 2023/10/30
*/
public class DCLSingleton {
private static DCLSingleton dclSingleton;
private DCLSingleton(){
}
public static DCLSingleton getInstance(){
if (null == dclSingleton){
synchronized (DCLSingleton.class){
if (null == dclSingleton){
dclSingleton = new DCLSingleton();
}
}
}
return dclSingleton;
}
}
静态内部类
例子来源于慕课视频《Java设计模式精讲 Debug方式+内存分析》的第八章《单例设计模式-静态内部类-基于类初始化的延迟加载解决方案及原理解析 》
/**
* @author: lpy
* @Date: 2023/10/31
* @desc: 线程安全的
*/
public class StaticInnerClassSingleton {
/**静态内部类,私有的: 静态内部类是基于类初始化的延迟加载解决方案![](https://image.devilwst.top/imgs/2023/10/0abc48a73ff32eee.png)*/
/**InnerClass的初始化锁看哪个线程拿到,就先初始化他*/
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
// 私有构造器必须要有
private StaticInnerClassSingleton(){
}
// jvm在类的初始化阶段,也就是class被加载后,被线程使用之前,都是类的初始化阶段,在这个阶段会执行类的初始化,在执行类的初始化期间,jvm会去获取一个锁,这个锁可以同步多个线程对一个类的初始化,——Class对象的初始化锁
// 基于这个特性,我们可以实现基于静态内部类的并且线程安全的延迟初始化方案,图中线程0的 2和3指令重排对于线程1并不会看到,非构造线程是不允许看到这个重排序的,/
// java语言特性,出现下面五种情况,这个类将会被立刻初始化,
// 1.有一个A类型的实例被创建,2.A类中声明的一个静态方法被调用,3.A类中声明的一个静态成员被赋值 ,4.A类中声明的一个静态成员被使用,并且这个成员不是一个常量成员,5.A类是一个顶级类,并且在这个类中有嵌套的断言语句
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
}
静态内部类和Double Check都是为了延迟初始化, 来降低创建单例造成的性能开销
test测试类
/**
* @author: lpy
* @Date: 2023/10/31
*/
public class SingletonTest {
public static void main(String[] args) {
DCLSingleton instance = DCLSingleton.getInstance();
HungrySingleton instance1 = HungrySingleton.getInstance();
SimpleLazySingleton instance2 = SimpleLazySingleton.getInstance();
DCLSingleton dd = DCLSingleton.getInstance();
System.out.println(dd.hashCode()==instance.hashCode());
}
}
容器单例
这里的容器单例和spring的容器单例不一样
/**
* @author: lpy
* @Date: 2023/10/31
* @desc: 容器单例
* @scene: 系统中单例对象非常多的
*/
public class ContainerSingleton {
private ContainerSingleton(){
}
private static Map<String, Object> singletonMap = new HashMap<String, Object>();
// private static Map<String, Object> singletonMap = new ConcurrentHashMap<>();
public static void putInstance(String key, Object object){
// 这里线程不安全,多线程debug会发现,如果两个线程同时到达put那里,就会出现getInstance到对象不一样 这样的情况
if (StringUtils.isNotBlank(key) &&
null!=object){
if (!singletonMap.containsKey(key)){
singletonMap.put(key,object);
}
}
}
public static Object getInstance(String key){
return singletonMap.get(key);
}
}
Threadlocal 单例
是单线程的单例
线程隔离的
/**
* @author: lpy
* @Date: 2023/10/31
*/
public class ThreadlocalSingleton {
private static final ThreadLocal<ThreadlocalSingleton> ts = new ThreadLocal<ThreadlocalSingleton>(){
@Override
protected ThreadlocalSingleton initialValue() {
return new ThreadlocalSingleton();
}
};
public static ThreadlocalSingleton getInstance(){
return ts.get();
}
public static void main(String[] args) {
Thread thread = new Thread(new TS());
Thread thread2 = new Thread(new TS());
thread.start();
thread2.start();
System.out.println(getInstance().hashCode());
System.out.println(getInstance().hashCode());
System.out.println(getInstance().hashCode());
System.out.println(getInstance().hashCode());
}
}
class TS implements Runnable{
@Override
public void run() {
System.out.println(ThreadlocalSingleton.getInstance().hashCode());
}
}
重要补充
序列化破坏
如果业务中涉及序列化和反序列化的情况,一定要注意对单例的破坏,下面在 obj = desc.isInstantiable() ? desc.newInstance() : null;创建对象了,只是没有返回
public class HungrySingleton implements Serializable {
private static HungrySingleton hungrySingleton = new HungrySingleton();
//...省略部分代码
// 序列化破坏的对抗
public Object readResolve(){
return hungrySingleton;
}
}
原因
入口ObjectInputStream的readObject方法,第373行的readObject0(),中第1353行的
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
点击readOrdinaryObject方法
ObjectInputStream的1815行
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}
进到方法里面执行这个反射方法
Object rep = desc.invokeReadResolve(obj);
单例模式反射攻击及解决方案
/** 演示攻击*/
/**
* @author: lpy
* @Date: 2023/10/31
* @desc: 反射攻击,演示,有一个前提,在类加载就把对象创建好了这种情况是ok的,静态内部类的单例也可以用下面的方法避免反射攻击
*/
public class ReflectAttackTest {
public static void main(String[] args) throws Exception {
// 正常方式获取instance
//反射攻击,演示,
HungrySingleton instance = HungrySingleton.getInstance();
StaticInnerClassSingleton instance1 = StaticInnerClassSingleton.getInstance();
// 骚操作开始
Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class;
Constructor<HungrySingleton> declaredConstructor = hungrySingletonClass.getDeclaredConstructor();
// 由于是私有化的,这样直接访问是不可以的,需要设置访问权限
declaredConstructor.setAccessible(true);
HungrySingleton hungrySingleton = declaredConstructor.newInstance();
System.out.println(hungrySingleton.hashCode()==instance.hashCode()); //false
}
}
/** 解决方法*/
// 有一个前提,在类加载就把对象创建好了这种情况是ok的,静态内部类的单例也可以用下面的方法避免反射攻击
/**
* @author: lpy
* @Date: 2023/10/30
*/
public class HungrySingleton {
private static HungrySingleton hungrySingleton = new HungrySingleton();
/** 私有构造函数*/
private HungrySingleton(){
if (null!=hungrySingleton){
throw new RuntimeException("单例构造器禁止反射");
}
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
懒加载方式的反射攻击
/**
* @author: lpy
* @Date: 2023/10/31
*/
public class LazySingletonPreventReflectAttack {
private static LazySingletonPreventReflectAttack lazySingletonPreventReflectAttack;
// 注意,一定是static
private static boolean flag = true;
private LazySingletonPreventReflectAttack(){
if (flag) {
flag = false;
}else {
throw new RuntimeException("禁止反射创建单例");
}
}
public static LazySingletonPreventReflectAttack getInstance(){
if (null==lazySingletonPreventReflectAttack){
lazySingletonPreventReflectAttack = new LazySingletonPreventReflectAttack();
}
return lazySingletonPreventReflectAttack;
}
public static void main(String[] args) throws Exception{
LazySingletonPreventReflectAttack instance = LazySingletonPreventReflectAttack.getInstance();
// 下面的通过flag可以解决反射创建单例的问题,
Class<LazySingletonPreventReflectAttack> attackClass = LazySingletonPreventReflectAttack.class;
Constructor<LazySingletonPreventReflectAttack> declaredConstructor = attackClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
declaredConstructor.newInstance();
System.out.println(reflectAttack.hashCode()==instance.hashCode());
}
}
// 上述看似用一个flag解决了反射攻击的问题,但可以通过反射修改flag的值呀,所以没用
public static void main(String[] args) throws Exception{
LazySingletonPreventReflectAttack instance = LazySingletonPreventReflectAttack.getInstance();
// 下面的通过flag可以解决反射创建单例的问题,
Class<LazySingletonPreventReflectAttack> attackClass = LazySingletonPreventReflectAttack.class;
Constructor<LazySingletonPreventReflectAttack> declaredConstructor = attackClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
// declaredConstructor.newInstance();
// 但是也可以通过反射改flag字段突破,所以这种懒汉式加载无论逻辑多复杂,都能通过反射轻易突破
Field flag = attackClass.getDeclaredField("flag");
flag.setAccessible(true);
flag.set(attackClass, true);
LazySingletonPreventReflectAttack reflectAttack = declaredConstructor.newInstance();
System.out.println(reflectAttack.hashCode()==instance.hashCode());
}
所以上面这种懒汉式加载无论逻辑多复杂,都能通过反射轻易突破
effective java推荐的单例
代码
/**
* @author: lpy
* @Date: 2023/10/31
*/
public enum EnumInstance {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
public static void main(String[] args) throws Exception {
EnumInstance instance = EnumInstance.getInstance();
instance.setData(new Object());
String filename = "singleton_file";
// 即使是序列化,依然是同一个对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename));
oos.writeObject(instance);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File(filename)));
EnumInstance enumInstance = (EnumInstance) ois.readObject();
System.out.println(enumInstance.getData().hashCode() == instance.getData().hashCode());
// 既然序列化没问题,那通过反射呢
Class<EnumInstance> enumInstanceClass = EnumInstance.class;
// 看java.lang.Enum类是一个抽象类,构造器没有无参构造器,public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
// Constructor<EnumInstance> declaredConstructor = enumInstanceClass.getDeclaredConstructor();
Constructor<EnumInstance> declaredConstructor = enumInstanceClass.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
//在Constructor类的 if ((clazz.getModifiers() & Modifier.ENUM) != 0)
// throw new IllegalArgumentException("Cannot reflectively create enum objects");会找到无法通过反射创建的原因
declaredConstructor.newInstance("xiaosheng",666);
// io相关的类和反射相关的类来为枚举类保驾护航。
}
}
jad反编译
地址
使用 jad xxx.class
反编译发现几个特点:
- final 不能被集成
- 构造器私有 不能被外部实例化
- INSTANCE 是静态的,没有延迟初始化,通过静态代码块,在类加载的时候就把对象初始化完成,所以他也是线程安全的
- 此外,io相关的类,反射相关的类也为枚举类保驾护航,适当的抛出异常
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumInstance.java
package com.cf.sqlTest.api.designPatterns.singletonMode;
import java.io.*;
import java.lang.reflect.Constructor;
public final class EnumInstance extends Enum
{
public static EnumInstance[] values()
{
return (EnumInstance[])$VALUES.clone();
}
public static EnumInstance valueOf(String name)
{
return (EnumInstance)Enum.valueOf(com/cf/sqlTest/api/designPatterns/singletonMode/EnumInstance, name);
}
private EnumInstance(String s, int i)
{
super(s, i);
}
public Object getData()
{
return data;
}
public void setData(Object data)
{
this.data = data;
}
public static EnumInstance getInstance()
{
return INSTANCE;
}
public static void main(String args[])
throws Exception
{
EnumInstance instance = getInstance();
instance.setData(new Object());
String filename = "singleton_file";
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename));
oos.writeObject(instance);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File(filename)));
EnumInstance enumInstance = (EnumInstance)ois.readObject();
System.out.println(enumInstance.getData().hashCode() == instance.getData().hashCode());
Class enumInstanceClass = com/cf/sqlTest/api/designPatterns/singletonMode/EnumInstance;
Constructor declaredConstructor = enumInstanceClass.getDeclaredConstructor(new Class[] {
java/lang/String, Integer.TYPE
});
declaredConstructor.setAccessible(true);
declaredConstructor.newInstance(new Object[] {
"xiaosheng", Integer.valueOf(666)
});
}
public static final EnumInstance INSTANCE;
private Object data;
private static final EnumInstance $VALUES[];
static
{
INSTANCE = new EnumInstance("INSTANCE", 0);
$VALUES = (new EnumInstance[] {
INSTANCE
});
}
}
ObjectInputStream类的readEnum方法中的Enum<?> en = Enum.valueOf((Class)cl, name); 通过类型cl和name获取枚举常量,由于enum的name是唯一的,并且对应一个枚举常量,所以这里拿到的肯定是唯一的常量对象,这样就没有创建新的对象,维持了这个对象的单例属性
在Constructor类的 if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException(“Cannot reflectively create enum objects”);会找到无法通过反射创建的原因
应用
- 配置管理器:在应用程序中,通常只需要一个全局的配置管理器来存储和管理配置信息,例如数据库连接参数、日志级别等。
-
日志记录器:单例日志记录器确保应用程序中的所有部分都可以使用相同的日志实例,以便在不同组件中记录日志。
-
数据库连接池:数据库连接是一种昂贵的资源,因此数据库连接池管理一个全局唯一的连接池实例,以提供可复用的数据库连接。
-
线程池:线程池是一种常见的并发模式,单例模式可以用来管理应用程序中的线程池,确保只有一个实例。
-
缓存管理器:缓存管理器可以用单例模式来实现,以确保在应用程序中只有一个全局的缓存管理器,用于缓存数据和提高性能。
-
窗口管理器:在图形用户界面(GUI)应用程序中,窗口管理器可以使用单例模式来管理所有窗口的打开和关闭,以维护窗口状态。
-
打印队列:打印队列可以使用单例模式来管理,以确保打印任务按顺序排队执行。
-
应用程序状态:在游戏开发中,单例模式可以用来管理游戏的全局状态,例如游戏配置、分数、关卡等。
-
计数器:在某些情况下,需要计数器来跟踪某个全局变量的值,例如应用程序的访问计数器。
-
日历或时钟:日历或时钟应用程序可以使用单例模式来确保只有一个全局的时间管理实例。
在源码的应用
-
Runtime的getRuntime方法
-
awt包下的Desktop的getDesktop方法
-
spring的AbstractFactoryBean的getObject方法
单例模式优缺点
-
优点:
- 全局唯一实例:单例模式确保了整个应用程序中只存在一个实例,提供了一个全局唯一访问点。
-
节省资源:懒汉式单例通过延迟实例化可以节省资源,只有在需要时才创建实例。
-
线程安全:饿汉式单例保证了线程安全,不需要额外的同步机制。
-
控制实例化:单例模式允许对实例化过程进行控制,确保只有一个实例。
-
缺点:
- 全局状态:单例模式引入全局状态,因为它提供了一个全局唯一访问点,这可能导致全局变量的滥用,使代码更难测试和维护。全局状态可能会导致意外的副作用和依赖关系。
-
单一职责原则:单例模式通常违反了单一职责原则(Single Responsibility Principle),因为单例类不仅负责自己的核心功能,还充当了实例管理者的角色。这可能导致类变得过于臃肿。
-
扩展困难:在某些情况下,需要扩展单例类的功能可能会变得复杂,因为单例类通常不允许继承,而修改现有的单例类可能会破坏全局唯一性。
-
隐藏依赖关系:单例模式可能隐藏了对象之间的依赖关系,因为其他类直接访问单例实例。这可能会导致紧耦合的设计,使代码更难以维护和扩展。
-
线程安全的复杂性:在多线程环境下,实现线程安全的单例模式可能需要使用同步机制,这会增加代码的复杂性并可能引入性能开销。
-
难以进行单元测试:由于全局状态和紧耦合的设计,单例模式的代码通常难以进行单元测试,因为无法轻松地模拟单例实例的行为。
7 大设计原则
不能全部遵守 , 也不能不遵守 , 注意平衡 功能 和 系统复杂度 , 找到最合适的一个点 ;
- 单一职责原则(Single Responsibility Principle - SRP):
这个原则规定一个类应该只有一个改变的理由,也就是说,一个类应该只有一个单一的职责。这有助于保持类的简单性和可维护性,因为每个类只需关注一个特定的功能。
- 开放-封闭原则(Open-Closed Principle - OCP):
开放-封闭原则要求系统中的软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这意味着当需要添加新功能时,应该通过扩展现有代码来实现,而不是修改原有代码。
- 里氏替换原则(Liskov Substitution Principle - LSP):
里氏替换原则规定,子类应该能够替换其父类,而不会影响程序的正确性。也就是说,子类应该继承父类的行为,但可以扩展或修改该行为,但不应该改变父类的行为。
- 接口隔离原则(Interface Segregation Principle - ISP):
接口隔离原则要求一个类不应该强迫其客户端依赖于它们不需要的接口。应该根据客户端的需求定义小而精确的接口,而不是大而笨重的接口。
- 依赖倒置原则(Dependency Inversion Principle - DIP):
依赖倒置原则要求高级模块不应该依赖于低级模块,它们都应该依赖于抽象。具体来说,这意味着应该通过抽象接口或抽象类来定义模块之间的依赖关系,而不是直接依赖于具体实现。
- 迪米特法则(Law of Demeter - LoD):
如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
- 合成复用原则(Composite Reuse Principle - CRP):
合成复用原则鼓励通过组合(合成)已有的类来实现新功能,而不是通过继承现有的类。这样可以降低系统的耦合度,并且更加灵活。
源码位置
全部源码github.com/xs-alpha/designPattern
参考文献
- 《大话设计模式》
-
慕课视频《Java设计模式精讲 Debug方式+内存分析》
如有内容侵权,麻烦联系博主删除
补充
类将会被立刻初始化的情况
- 1.有一个A类型的实例被创建,
-
2.A类中声明的一个静态方法被调用,
-
3.A类中声明的一个静态成员被赋值 ,
-
4.A类中声明的一个静态成员被使用,并且这个成员不是一个常量成员,
-
5.A类是一个顶级类,并且在这个类中有嵌套的断言语句
枚举类声明方法
/**
* @author: lpy
* @Date: 2023/10/31
* @desc: 枚举类中可以声明方法
*/
public enum EnumInstance2 {
INSTANCE{
protected void testPrintFunc(){
System.out.println("打印测试");
}
};
private Object data;
// 为了外部可以调用到testPrintFunc方法,否则不写下面的抽象方法,外部调用不到
// protected abstract void testPrintFunc();
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance2 getInstance(){
return INSTANCE;
}
public static void main(String[] args) throws Exception {
EnumInstance2 instance = EnumInstance2.getInstance();
// 必须加testPrintFunc这个抽象方法
// instance.testPrintFunc();
}
}
下面是反编译的源码
public class EnumInstance2 extends Enum
{
public static EnumInstance2[] values()
{
return (EnumInstance2[])$VALUES.clone();
}
public static EnumInstance2 valueOf(String name)
{
return (EnumInstance2)Enum.valueOf(com/cf/sqlTest/api/designPatterns/singletonMode/EnumInstance2, name);
}
private EnumInstance2(String s, int i)
{
super(s, i);
}
public Object getData()
{
return data;
}
public void setData(Object data)
{
this.data = data;
}
public static EnumInstance2 getInstance()
{
return INSTANCE;
}
public static void main(String args[])
throws Exception
{
EnumInstance2 instance = getInstance();
}
public static final EnumInstance2 INSTANCE;
private Object data;
private static final EnumInstance2 $VALUES[];
static
{
INSTANCE = new EnumInstance2("INSTANCE", 0) {
protected void testPrintFunc()
{
System.out.println("\u6253\u5370\u6D4B\u8BD5");
}
}
;
$VALUES = (new EnumInstance2[] {
INSTANCE
});
}
}
在Java中,枚举类型是一种特殊的类,它们可以包含常量值。枚举类通常继承自java.lang.Enum类。在你的代码中,EnumInstance2 继承自 Enum 类。
具体分析代码:
- INSTANCE 是 EnumInstance2 的一个枚举实例。
-
枚举实例是在静态代码块中初始化的,这是枚举类特有的。在这个静态代码块中,你为 INSTANCE 实例提供了一个匿名内部类,这个内部类覆盖了 testPrintFunc 方法,以提供具体的实现,即在内部类中打印 “打印测试”。
-
INSTANCE 是 EnumInstance2 的一个实例,但它的 testPrintFunc 方法的具体实现是在匿名内部类中提供的。
-
由于 testPrintFunc 方法在匿名内部类中实现,它不会被直接继承到 EnumInstance2 类。因此,外部代码无法直接访问 EnumInstance2 类上的 testPrintFunc 方法,除非你在 EnumInstance2 类中定义一个抽象方法 testPrintFunc,使其成为外部可见的方法。这就是为什么需要添加 protected abstract void testPrintFunc(); 这个抽象方法,以允许外部代码访问 testPrintFunc。
总结一下,testPrintFunc 方法是在枚举实例的匿名内部类中实现的,但要使外部代码能够访问该方法,你需要在枚举类中定义一个抽象方法 testPrintFunc,以确保外部代码能够正确覆盖和调用它。这是为了维护Java的多态性和方法覆盖规则。