方式一:饿汉式 介绍 所谓饿汉式单例模式,就是在第一次获取单例对象之前(而不是获取时),就创建好了这个对象 ,比如下图的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Singtelon1 { private static final Singtelon1 INSTANCE = new Singtelon1 (); private Singtelon1 () { System.out.println("Singleton1 is constructed" ); } public static Singtelon1 getInstance () { return INSTANCE; } public static void doSomething () { System.out.println("Singleton1 is doing something" ); } }
将构造方法私有就是为了防止其他类调用,从而在一定程度上保证了单例。现在我们可以做一下测试,看下如果我尝试通过getInstance
获取两次对象,对象是否是同一个:
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) throws Exception{ Singtelon1.doSomething(); System.out.println("-------------------------------------------" ); System.out.println(Singtelon1.getInstance()); System.out.println(Singtelon1.getInstance()); } }
运行出的结果:
1 2 3 4 5 6 7 Singleton1 is constructed Singleton1 is doing something ------------------------------------------- Singtelon1@3b07d329 Singtelon1@3b07d329 Process finished with exit code 0
可以看出,两次调用getInstance
获取到的对象的哈希值是一致的,我们可以认为他们是同一个对象。这里的结果还说明了,对象是在类加载(第一次被使用)后就创建出的,因为Singleton1 is constructed
在Singleton1 is doing something
之前,而Singleton1 is doing something
与获取对象和使用对象都没关系。
破坏饿汉式单例模式的方法 方法一:反射 众所周知,我们可以通过反射获取构造方法,并给予可执行的权限,这样我们就可以在别的类中调用构造函数 了!单例模式就这么被破坏掉了!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;public class Main { public static void main (String[] args) throws Exception{ Singtelon1.doSomething(); System.out.println("-------------------------------------------" ); System.out.println(Singtelon1.getInstance()); System.out.println(Singtelon1.getInstance()); reflection(Singtelon1.class); } private static void reflection (Class<?> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { Constructor<?> constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true ); System.out.println("反射创建对象:" + constructor.newInstance()); } }
结果为:
1 2 3 4 5 6 7 8 Singleton1 is constructed Singleton1 is doing something ------------------------------------------- Singtelon1@3b07d329 Singtelon1@3b07d329 反射创建对象:Singtelon1@3d075dc0 Process finished with exit code 0
从结果可以看出,我们的确构造了两个不同的Singleton1
对象,这就破坏了单例模式。但是反射破坏的单例模式并非是无解的 !我们可以这么修改代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Singtelon1 { private static final Singtelon1 INSTANCE = new Singtelon1 (); private Singtelon1 () { if (INSTANCE == null ) System.out.println("Singleton1 is constructed" ); else throw new RuntimeException ("Singleton1 was constructed!!!" ); } public static Singtelon1 getInstance () { return INSTANCE; } public static void doSomething () { System.out.println("Singleton1 is doing something" ); } }
再来测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Singleton1 is constructed Singleton1 is doing something ------------------------------------------- Singtelon1@3b07d329 Singtelon1@3b07d329 Exception in thread "main" java.lang.reflect.InvocationTargetException at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499) at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480) at Main.reflection(Main.java:17) at Main.main(Main.java:11) Caused by: java.lang.RuntimeException: Singleton1 was constructed!!! at Singtelon1.<init>(Singtelon1.java:8) ... 7 more Process finished with exit code 1
可以看出,当我们尝试使用反射来构造Singtelon1
的时候,代码是报错了的!这就解决了问题。
方法二:反序列化 如果Singtelon1
实现了Serializable
接口,那么这个Singtelon1
就可以通过反序列化的方式得到一个新的对象,甚至不需要构造方法 !反序列化有很多应用,在这里不进行说明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.io.Serializable;public class Singtelon1 implements Serializable { private static final Singtelon1 INSTANCE = new Singtelon1 (); private Singtelon1 () { System.out.println("Singleton1 is constructed" ); } public static Singtelon1 getInstance () { return INSTANCE; } public static void doSomething () { System.out.println("Singleton1 is doing something" ); } }
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Main { public static void main (String[] args) throws Exception{ Singtelon1.doSomething(); System.out.println("-------------------------------------------" ); System.out.println(Singtelon1.getInstance()); System.out.println(Singtelon1.getInstance()); serialization(Singtelon1.getInstance()); } private static void serialization (Object instance) throws IOException, ClassNotFoundException { ByteArrayOutputStream bos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (bos); oos.writeObject(instance); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (bos.toByteArray())); System.out.println("反序列化创建对象:" + ois.readObject()); } }
结果:
1 2 3 4 5 6 7 8 Singleton1 is constructed Singleton1 is doing something ------------------------------------------- Singtelon1@3b07d329 Singtelon1@3b07d329 反序列化创建对象:Singtelon1@2957fcb0 Process finished with exit code 0
可以看出,反序列化不仅可以创建一个新的对象,而且还不需要调用构造方法!(因为我们得到了两个Singleton1
对象,但是
Singleton1 is constructed
却只输出了一次)。幸运的是,这种破坏单例模式的方法也是可以解决的 !
只需要我们在Singleton1
类中增加一个readResolve
方法:
1 2 3 private Object readResolve () { return INSTANCE; }
然后问题就被解决了:
1 2 3 4 5 6 7 8 Singleton1 is constructed Singleton1 is doing something ------------------------------------------- Singtelon1@3b07d329 Singtelon1@3b07d329 反序列化创建对象:Singtelon1@3b07d329 Process finished with exit code 0
原因是这样会使ois.readObject()
执行的返回结果为INSTANCE
方法三:unsafe unsafe是jdk中内置的一个类,我们并不能直接访问,但我们可以利用反射来访问 。而unsafe也可以通过不调用构造方法的方式得到对应类的对象 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import sun.misc.Unsafe;import java.lang.reflect.Field;public class Main { public static void main (String[] args) throws Exception{ Singtelon1.doSomething(); System.out.println("-------------------------------------------" ); System.out.println(Singtelon1.getInstance()); System.out.println(Singtelon1.getInstance()); unsafe(Singtelon1.class); } private static void unsafe (Class<?> clazz) throws NoSuchFieldException, IllegalAccessException, InstantiationException { Field field = Unsafe.class.getDeclaredField("theUnsafe" ); field.setAccessible(true ); Unsafe unsafe = (Unsafe) field.get(null ); Object instance = unsafe.allocateInstance(clazz); System.out.println("unsafe创建对象: " + instance); } }
结果为:
1 2 3 4 5 6 7 8 Singleton1 is constructed Singleton1 is doing something ------------------------------------------- Singtelon1@3b07d329 Singtelon1@3b07d329 unsafe创建对象: Singtelon1@3b9a45b3 Process finished with exit code 0
可惜的是,这种方法目前还没找到比较好的解决办法。
方式二:枚举(饿汉式) 介绍 枚举类在编译后的字节码中,对每个选项都进行了饿汉式的创建,并在获取选项的时候是单例的,即不会再次创建。那么,假如我们的枚举类中只有一个选项,那么不就实现了单例模式吗?让我们看下下边的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public enum Singtelon2 { INSTANCE; Singtelon2(){ System.out.println("Singleton2 is constructed" ); } @Override public String toString () { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } public static Singtelon2 getInstance () { return INSTANCE; } public static void doSomething () { System.out.println("Singleton2 is doing something" ); } }public class Main { public static void main (String[] args) { Singtelon2.doSomething(); System.out.println("-------------------------------------------" ); System.out.println(Singtelon2.getInstance()); System.out.println(Singtelon2.getInstance()); } }
结果为:
1 2 3 4 5 6 7 Singleton2 is constructed Singleton2 is doing something ------------------------------------------- Singtelon2@3b07d329 Singtelon2@3b07d329 Process finished with exit code 0
显然,两次获取的对象是同一个,并且对象是在类加载(第一次被使用)后就创建出的,也就是用饿汉式创建出的。
破坏枚举单例模式的方法 因为都是饿汉式的,所以破坏方式也和方式一中的一样:反射、反序列化、unsafe,是这样的吗?
方法一:反射(不可行) 我们先仿照方式一中的代码试一试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;public class Main { public static void main (String[] args) throws Exception { Singtelon2.doSomething(); System.out.println("-------------------------------------------" ); System.out.println(Singtelon2.getInstance()); System.out.println(Singtelon2.getInstance()); reflection(Singtelon2.class); } private static void reflection (Class<?> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { Constructor<?> constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true ); System.out.println("反射创建对象:" + constructor.newInstance()); } }
结果:
1 2 3 4 5 6 7 8 9 10 11 12 Singleton2 is constructed Singleton2 is doing something ------------------------------------------- Singtelon2@3b07d329 Singtelon2@3b07d329 Exception in thread "main" java.lang.NoSuchMethodException: Singtelon2.<init>() at java.base/java.lang.Class.getConstructor0(Class.java:3585) at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2754) at Main.reflection(Main.java:15) at Main.main(Main.java:11) Process finished with exit code 1
可以看到,代码报错了,这是因为我们在获取构造方法时除了问题。枚举类的构造方法,实际上是有两个参数的:枚举选项的名字(String类型)和选项的编号(int类型),所以我们修改代码:
1 2 3 4 5 private static void reflection (Class<?> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { Constructor<?> constructor = clazz.getDeclaredConstructor(String.class,int .class); constructor.setAccessible(true ); System.out.println("反射创建对象:" + constructor.newInstance("OTHER" ,1 )); }
然而结果为:
1 2 3 4 5 6 7 8 9 10 11 12 Singleton2 is constructed Singleton2 is doing something ------------------------------------------- Singtelon2@3b07d329 Singtelon2@3b07d329 Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492) at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480) at Main.reflection(Main.java:17) at Main.main(Main.java:11) Process finished with exit code 1
可以看出,用反射来构造枚举类中的对象时,代码会报错,所以反射破坏不了枚举的单例模式。
方法二:反序列化(不可行) 直接上测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.io.*;public class Main { public static void main (String[] args) throws Exception{ Singtelon2.doSomething(); System.out.println("-------------------------------------------" ); System.out.println(Singtelon2.getInstance()); System.out.println(Singtelon2.getInstance()); serialization(Singtelon2.getInstance()); } private static void serialization (Object instance) throws IOException, ClassNotFoundException { ByteArrayOutputStream bos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (bos); oos.writeObject(instance); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (bos.toByteArray())); System.out.println("反序列化创建对象:" + ois.readObject()); } }
结果:
1 2 3 4 5 6 7 8 Singleton2 is constructed Singleton2 is doing something ------------------------------------------- Singtelon2@3b07d329 Singtelon2@3b07d329 反序列化创建对象:Singtelon2@3b07d329 Process finished with exit code 0
可以看出,这里的反序列化没有破坏单例模式!!!
方法三:unsafe 测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import sun.misc.Unsafe;import java.lang.reflect.Field;public class Main { public static void main (String[] args) throws Exception{ Singtelon2.doSomething(); System.out.println("-------------------------------------------" ); System.out.println(Singtelon2.getInstance()); System.out.println(Singtelon2.getInstance()); unsafe(Singtelon2.class); } private static void unsafe (Class<?> clazz) throws NoSuchFieldException, IllegalAccessException, InstantiationException { Field field = Unsafe.class.getDeclaredField("theUnsafe" ); field.setAccessible(true ); Unsafe unsafe = (Unsafe) field.get(null ); Object instance = unsafe.allocateInstance(clazz); System.out.println("unsafe创建对象: " + instance); } }
结果为:
1 2 3 4 5 6 7 8 Singleton2 is constructed Singleton2 is doing something ------------------------------------------- Singtelon2@3b07d329 Singtelon2@3b07d329 unsafe创建对象: Singtelon2@5f184fc6 Process finished with exit code 0
同样的,这种方法不仅可以破坏单例,还没有找到解决办法。
方式三:懒汉式 介绍 饿汉式单例模式在类的在第一次获取单例对象之前(而不是获取时),就创建好了这个对象。而如果我们一直没有使用获取这个对象的方法,那就会造成内存的浪费,为了减少内存的浪费,我们可以采用懒汉式的单例模式,即当第一次获取单例对象时,才去创建对象 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Singtelon3 { private static Singtelon3 INSTANCE; private Singtelon3 () { System.out.println("Singleton is constructed" ); } public static Singtelon3 getInstance () { if (INSTANCE == null ) INSTANCE = new Singtelon3 (); return INSTANCE; } public static void doSomething () { System.out.println("Singleton3 is doing something" ); } }
做一个测试:
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) { Singtelon3.doSomething(); System.out.println("-------------------------------------------" ); System.out.println(Singtelon3.getInstance()); System.out.println(Singtelon3.getInstance()); } }
结果:
1 2 3 4 5 6 7 Singleton3 is doing something ------------------------------------------- Singleton3 is constructed Singtelon3@3b07d329 Singtelon3@3b07d329 Process finished with exit code 0
可以看出,这种方式的确可以保证单例(两次调用getInstance
获取对象的哈希值一致),并且,类加载后并没有立即创建对象,而是在第一次获取对象的时候才对对象进行创建。这就是懒汉式的单例模式。
懒汉式在多线程中的问题 但是,在多线程的环境中,这种模式就有很大的问题了:
1 2 3 4 5 6 7 8 9 10 11 12 public class Main { public static void main (String[] args) { Thread t1 = new Thread (() -> { System.out.println(Singtelon3.getInstance()); }); Thread t2 = new Thread (() -> { System.out.println(Singtelon3.getInstance()); }); t1.start(); t2.start(); } }
执行上述代码,得到的结果是:
1 2 3 4 5 6 Singleton3 is constructed Singtelon3@63236bfa Singleton3 is constructed Singtelon3@24224129 Process finished with exit code 0
可以看到,单例模式被破坏了!
原因可能有很多,总体来说就是下列代码中,两个线程都进入了if为true的代码中,然后各自创建了对象。而导致两个线程都进入了if为true的代码中的原因可能有很多,比如一个刚进去,就切换到另一个,这时还没创建好对象,判断结果还是true,另一个又进去了;或者是一个线程刚创建好对象,还没赋值给INSTANCE
,就切换到另一个线程了,另一个线程判断结果还是true,然后又能执行new Singtelon3()
操作了,等等。
1 2 3 4 5 public static Singtelon3 getInstance () { if (INSTANCE == null ) INSTANCE = new Singtelon3 (); return INSTANCE; }
解决方法:双重检查锁 问题出在两个线程都进入了if为true的代码中,然后各自创建了对象
,因此我们只要使得,两个线程里只能有一个线程创建对象就好了,因此就得到了如下代码:
1 2 3 4 5 6 7 8 9 public static Singtelon3 getInstance () { if (INSTANCE == null ){ synchronized (Singtelon3.class){ if (INSTANCE == null ) INSTANCE = new Singtelon3 (); } } return INSTANCE; }
当INSTANCE不为空:直接返回;当INSTANCE为空:先去尝试获得锁,在成功获得锁之前,可能已经有其他线程进入同步代码块创建对象了,因此进入同步代码块后,需要再次判断是否为空。
为什么要加volatile 但这里需要注意的是,这里的INSTANCE一定得是加了volatile
关键字的原因如下:
为了保证共享变量INSTANCE的可见性,在为INSTANCE赋值后能让其他线程立即看到。这个原因并不是主要原因,因为INSTANCE == null
的判断会去获取INSTANCE的值,所以其实不加volatile
并不会出现可见性的问题。(做测试发现确实没有出现问题)
1 2 3 4 5 Singleton3 is constructed Singtelon3@7a092293 Singtelon3@7a092293 Process finished with exit code 0
为了保证编译代码的有序性 。这是最核心的原因。如果不加volatile
,则INSTANCE = new Singtelon3();
的编译结果未必是我们想的那样。使用javap -c -v -p Singtelon3.class
查看一下Singtelon3.class
的反编译:
1 2 3 4 17: new #22 // class Singtelon3 分配内存空间 20: dup 21: invokespecial #27 // Method "<init>":()V 对象内初始化赋值 24: putstatic #21 // Field INSTANCE:LSingtelon3; 把对象赋值给INSTANCE
可以看到以上三步,分别是分配空间,给对象初始化赋值,和把对象地址赋值给INSTANCE。分配空间肯定是第一步做的,但是后两步都是赋值,因此编译的代码可能会改变两者的顺序。 这就有问题了:如果先执行了putstatic
,然后另一个线程就去进行INSTANCE == null
的检测,发现不为null(因为已经赋值给INSTANCE了),但此时还没有对对象进行初始化,那么拿到的INSTANCE其实是有问题的,这就很危险了。所以,我们需要保证 invokespecial
一定在putstatic
之前执行。而因为volatile有内存屏障,所以可以保证 invokespecial
一定在putstatic
之前执行。这才是为什么我们要给INSTANCE加volatile。
方式四:内部类(懒汉式) 我们知道,private static final Singtelon1 INSTANCE = new Singtelon1();
这一步是线程安全的,这是因为这一步是在类的初始化时给静态变量赋值,是在静态代码块中执行的,而静态代码块会维护线程安全,因此只要我们也能通过静态代码块懒汉式地构建对象,那么不就不需要双重检查锁了吗?要实现这件事,就需要我们执行private static final Singtelon1 INSTANCE = new Singtelon1()
,但我们又不希望在这之前执行类的方法时就对类初始化,所以我们可以通过内部类的方式来实现这一点。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Singtelon4 { private Singtelon4 () { System.out.println("Singleton4 is constructed" ); } private static class Holder { static Singtelon4 INSTANCE = new Singtelon4 (); } public static Singtelon4 getInstance () { return Holder.INSTANCE; } public static void doSomething () { System.out.println("Singleton4 is doing something" ); } }
只要我们不去使用getInstance
方法,Holder
就不会执行类初始化,也就不会创建Singtelon4
对象,而其创建Singtelon4
对象并复制的过程是在静态代码块中的,是线程安全的,因此我们轻松地实现了懒汉式单例模式。
测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Main { public static void main (String[] args) { Singtelon4.doSomething(); System.out.println("----------------------------------" ); Thread t1 = new Thread (() -> { System.out.println(Singtelon4.getInstance()); }); Thread t2 = new Thread (() -> { System.out.println(Singtelon4.getInstance()); }); t1.start(); t2.start(); } }
结果如下:
1 2 3 4 5 6 7 Singleton4 is doing something ---------------------------------- Singleton4 is constructed Singtelon4@5635b27f Singtelon4@5635b27f Process finished with exit code 0
可以看出,当我执行doSomething()
时,并不会引发单例对象的构建,而当我多线程地获取单例对象时,单例对象也只被创建了一次,也就是说,我们用这种方式,实现了懒汉式单例模式。
单例模式在JDK中的体现 项目中使用单例模式是危险的,因为可能会用错。但是有道这样的面试题:
在哪里使用了单例模式 ?
既然我们没能在项目使用单例模式,我们就不能举项目中的例子了。我们可以把目光投向JDK和框架上,在这里,我会介绍单例模式在JDK中的一些体现。
例一:Runtime类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package java.lang;public class Runtime { private static final Runtime currentRuntime = new Runtime (); private static Version version; public static Runtime getRuntime () { return currentRuntime; } private Runtime () {}
不难看出,JDK里的Runtime类是用了单例模式的,而且是我们提到的饿汉式单例模式。
当我们要退出JVM时,会调用exit方法,这个方法是在System类中的,这个方法的代码如下:
1 2 3 public static void exit (int status) { Runtime.getRuntime().exit(status); }
可以看出,这里是通过Runtime.getRuntime()获取到的Runtime单例对象。
除了exit之外,System类中还有很多别的方法也是通过Runtime.getRuntime()获取单例对象来调用Runtime类中的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package java.lang;public final class System { ...... public static void exit (int status) { Runtime.getRuntime().exit(status); } public static void gc () { Runtime.getRuntime().gc(); } public static void runFinalization () { Runtime.getRuntime().runFinalization(); } @CallerSensitive public static void load (String filename) { Runtime.getRuntime().load0(Reflection.getCallerClass(), filename); }
例二:Console类 刚刚提到了System类,这个类有个Console对象,也实现了单例模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private static volatile Console cons; public static Console console () { Console c; if ((c = cons) == null ) { synchronized (System.class) { if ((c = cons) == null ) { cons = c = SharedSecrets.getJavaIOAccess().console(); } } } return c; }
这是我能找到的JDK中的唯一一个懒汉式单例模式了。
需要注意的是,这里的单例对象是Console的对象,但是是写在System中的,这也是为什么对cons的赋值并不是直接new,而是一定要用SharedSecrets.getJavaIOAccess().console()
。
在Console类中,Console对象也是单例的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 private static final Charset CHARSET;static { String csname = encoding(); Charset cs = null ; if (csname == null ) { csname = GetPropertyAction.privilegedGetProperty("sun.stdout.encoding" ); } if (csname != null ) { try { cs = Charset.forName(csname); } catch (Exception ignored) { } } CHARSET = cs == null ? Charset.defaultCharset() : cs; SharedSecrets.setJavaIOAccess(new JavaIOAccess () { public Console console () { if (istty()) { if (cons == null ) cons = new Console (); return cons; } return null ; } public Charset charset () { return CHARSET; } }); }private static Console cons;private static native boolean istty () ;private Console () {
例三:Collections类 Conllections类是一个集合工具类,我们可以通过这个类来获取空的集合,这时的空集合就用到了单例模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private static class EmptyNavigableSet <E> extends UnmodifiableNavigableSet <E> implements Serializable { @java .io.Serial private static final long serialVersionUID = -6291252904449939134L ; public EmptyNavigableSet () { super (new TreeSet <>()); } @java .io.Serial private Object readResolve () { return EMPTY_NAVIGABLE_SET; } }@SuppressWarnings("rawtypes") private static final NavigableSet<?> EMPTY_NAVIGABLE_SET = new EmptyNavigableSet <>();
而且可以看到,为了保护类的单例,还实现了Serializable,从而避免了反序列化破坏单例。这种获取空集合的方式,在Conllections类中还有很多,在此不一一列举了。
除了直接把对象赋值给Conllections类的静态成员变量,Collections类内部还有很多内部类,这些内部类也运用了单例模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static <T> Iterator<T> emptyIterator () { return (Iterator<T>) EmptyIterator.EMPTY_ITERATOR; }private static class EmptyIterator <E> implements Iterator <E> { static final EmptyIterator<Object> EMPTY_ITERATOR = new EmptyIterator <>(); public boolean hasNext () { return false ; } public E next () { throw new NoSuchElementException (); } public void remove () { throw new IllegalStateException (); } @Override public void forEachRemaining (Consumer<? super E> action) { Objects.requireNonNull(action); } }
顺藤摸瓜,我们还可以找到枚举饿汉式的例子:
1 2 3 public static <T> Comparator<T> reverseOrder () { return (Comparator<T>) ReverseComparator.REVERSE_ORDER; }
Conllections类中的reverseOrder()方法,返回值是ReverseComparator类的REVERSE_ORDER,这是一个单例对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private static class ReverseComparator implements Comparator <Comparable<Object>>, Serializable { @java .io.Serial private static final long serialVersionUID = 7207038068494060240L ; static final ReverseComparator REVERSE_ORDER = new ReverseComparator (); public int compare (Comparable<Object> c1, Comparable<Object> c2) { return c2.compareTo(c1); } @java .io.Serial private Object readResolve () { return Collections.reverseOrder(); } @Override public Comparator<Comparable<Object>> reversed () { return Comparator.naturalOrder(); } }
而Comparator的naturalOrder方法的返回值,也是单例的:
1 2 3 public static <T extends Comparable <? super T>> Comparator<T> naturalOrder () { return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Comparators { private Comparators () { throw new AssertionError ("no instances" ); } enum NaturalOrderComparator implements Comparator <Comparable<Object>> { INSTANCE; @Override public int compare (Comparable<Object> c1, Comparable<Object> c2) { return c1.compareTo(c2); } @Override public Comparator<Comparable<Object>> reversed () { return Comparator.reverseOrder(); } }
可以看出,这里的INSTANCE是枚举类NaturalOrderComparator的唯一一个选项,这就是通过枚举实现饿汉式单例模式的体现。