本周首先紧接上周没有写完的内部类,详情点击《第19次文章:类加载器的加密解密+内部类》,再加单例模式的实现。
-方法内部类的地位和方法内的局部变量 的地位类似
因此不能修饰局部变量的修饰符也不能修饰局部内部类,譬如public、private、protected、static、transient等。
-方法内部类只能在声明的方法内是可见的
因此定义局部内部类之后,想用的话就要在此方法内直接实例化,记住这里顺序不能反了,一定是要先声明后使用,否则编译器就会找不到。
-方法内部类不能访问定义它的方法内的局部变量,除非这个变量被定义为final
本质原因:局部变量和内部类生命周期不一致所致!
-方法内部类只能包含非静态成员!
-匿名内部类的实例化方式:new SomeInterfaceOrClass(){.........};
意思是创造了一个实现(继承)了SomeInterfaceOrClass的类的对象;
-根据声明的位置,判断匿名内部类是成员内部类还是方法内部类。注:一般是方法内部类,这就具备方法内部类的特性。
-三种使用方法:
(1)接口式
(2)继承式
(3)参数式
对于匿名类,我们利用一段实例进行详解:
/** * 测试匿名内部类的几种方式 */public class Demo05 { public static void main(String[] args) { Outer05 o5 = new Outer05(); o5.test(); }}
class Outer05{ //参数式匿名内部类 public void test02(Car car) { car.run(); } public void test() { //匿名内部类(接口式),由于本内部类定义在方法中,也是方法内部类 Runnable runnable = new Runnable() { @Override public void run() { } }; //匿名内部类,继承式 Car car = new Car() { @Override public void run() { System.out.println("继承式子类的汽车跑"); } }; car.run(); //利用参数式 test02(new Car() { @Override public void run() { System.out.println("参数式匿名内部类,车在跑"); } }); }}
class Car{ public void run () { System.out.println("汽车跑"); }}
查看一下结果:
tips:
(1)在这段代码中,我们分别介绍了匿名内部类的三种模式,对于第一种接口式,我们使用Runnable进行创建,然后重新定义其中的run方法,可以在里面增添我们需要的相关操作。
(2)对于继承式,我们首先需要创建一个父类Car,然后我们再来利用继承关系,创建一个匿名的继承式匿名内部类,在使用继承式匿名类的时候,我们同样可以更改父类的原有方法。
(3)利用参数式的匿名内部类,需要根据方法的要求,在传递参数的时候,直接创建一个匿名类,并且可以根据我们自己的需求修改其原有方法。
(4)匿名内部类的最大特点就是,我们创建的对象没有名称,这也就代表着我们对于此类只能在其创建的时候使用一次。使用结束之后,我们就无法再次利用匿名类了。在实际情况中,也的确有很多对象我们只会使用一次。此时使用匿名类就是一个很好的选择。
-单列模式,工厂模式,抽象工厂模式,建造者模式,原型模式
-适配器模式,桥接模式,装饰模式,组合模式,外观模式,享元模式,代理模式
-模板方法模式,命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。
保证一个类只有一个实例,并且提供一个访问实例的全局访问点。比如说我们在打开Windows下的资源管理器的时候,无论我们打开多少次,每次打开的对象都会指向同一个资源管理器,但是QQ就不一样了啊,如果你不断的点击QQ的快捷方式,它会不断的产生新的QQ登录界面,这就不属于单例模式。
-由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置文件、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
-单例模式可以在系统设计全局的访问点,优化环共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
(1)主要:
-饿汉式(线程安全,调用效率高。但是,不能延时加载。)
-懒汉式(线程安全,调用效率不高。但是,可以延时加载。)
(2)其他:
-双重检测锁式(由于JVM底层内部模型原因,偶尔会出问题,不建议使用)
-静态内部类式(线程安全,调用效率高。但是,可以延时加载)
-枚举单例(线程安全,调用效率高,不能延时加载。并且可以天然的防止反射和反序列化漏洞)
我们对5种单例模式都进行了相应的实现。下面我们主要对饿汉式和懒汉式进行一个详解。
实现的主要思想:由于是属于单例模式,每次创建或者打开得到的是同一个对象,所以我们首先需要将构造器私有化,不能让外部的用户随意的创建对象。而每次创建对象的时候,我们就需要去查看是否已经有这个类的对象存在了,如果存在的话,就不能再创建新对象了,而是将已经创建好的对象直接返回给用户。
(1)首先看一下饿汉式:
package com.peng.singleton;/** * 测试饿汉式单例模式 *一旦此类开始初始化,就已经new出一个对象了,但是此对象后续可能没有使用到。 */public class SingletonDemo01 {
//类初始化时,立即加载这个对象(没有延时加载的优势)。加载类时,天然的是线程安全的! private static SingletonDemo01 singletonDemo01 = new SingletonDemo01(); //私有构造器 private SingletonDemo01() { } //方法没有同步,调用效率高! public static SingletonDemo01 getInstance() { return singletonDemo01; }}
(2)再看一下懒汉式:
package com.peng.singleton;
/** * 测试懒汉式单例模式 */
public class SingletonDemo02 { //类初始化的时候,不初始化这个对象(延迟加载,真正使用的时候再创建) private static SingletonDemo02 instance; //构造器私有化 private SingletonDemo02() { } //方法同步,调用效率低 public static synchronized SingletonDemo02 getInstance() { if (instance == null) { instance = new SingletonDemo02(); } return instance; }}
(3)我们检测一下最后的效果:
package com.peng.singleton;
/** * 检测单例模式 */public class Client { public static void main(String[] args) { SingletonDemo01 s1 = SingletonDemo01.getInstance(); SingletonDemo01 s2 = SingletonDemo01.getInstance(); System.out.println(s1); System.out.println(s2); SingletonDemo02 s3 = SingletonDemo02.getInstance(); SingletonDemo02 s4 = SingletonDemo02.getInstance(); System.out.println(s3 == s4); }}
结果如下所示:
tips:
1.我们分别创建两个饿汉式和懒汉式的单例模式,然后对比每种单例模式最后产生的对象是否相同,由最后的结果可以看出,饿汉式的两个对象s1和s2是相同的,懒汉式的两个对象s3和s4也是相同的。
2.在SingletonDemo01中,我们在类的设计过程时,为了避免外界对构造器的调用,我们首先就将构造器私有化,然后创建一个静态对象singletonDemo01,并且将其初始化一个新对象,最后给外界留一个公开的获取该对象的方法getInstance(),直接将最开始初始化的对象返回给用户,这样就保证了用户无法自己创建该对象,只能获取该对象,而获取到的对象一直都是同一个对象。同时我们也可以看到,在加载此类的时候,局部变量singletonDemo01直接被初始化了,所以这种模式不具有延时加载的效果,被称为饿汉式单例模式。
3.在SingletonDemo02中,思想与SingletonDemo01相似,但是我们注意到两者之间的区别在于,SingletonDemo01中,一旦加载该类,静态对象singletonDemo01就会被立即加载,而在SingletonDemo02中,静态对象instance只有在我们实际调用的时候才会被加载,这就属于一个延时加载,极大的节约了资源。但是在调用的时候需要考虑多线程的问题,避免重复加载,造成单例模式的失效。所以我们在方法getInstance的前面需要加上synchronized进行方法同步。这样也就降低了SingletonDemo02的效率。由于SingletonDemo02只有在需要的时候才去初始化对象,所以我们称SingletonDemo02为懒汉式。
在java中拥有一种动态机制,反射和序列化,这种动态机制(详情见:第15次文章:反射+动态编译+脚本引擎)可以破解上面几种(不包含枚举式)单例实现方式。所以针对这种安全漏洞,我们需要设计相应的处理方法来进行解决。我们新建一个懒汉式类SingletonDemo06来进行说明。
在反射机制中,主要是通过获取类的一个class对象,然后通过这个class对象,调用类对象的构造器,创建相应的类。具体如下所示:
/** * 测试反射和反序列化破解单例模式 */public class Client2 { public static void main(String[] args) throws Exception { SingletonDemo06 s1 = SingletonDemo06.getInstance(); SingletonDemo06 s2 = SingletonDemo06.getInstance(); System.out.println(s1); System.out.println(s2); //通过反射的方式直接调用私有构造器 Class<SingletonDemo06> clazz = (Class<SingletonDemo06>) Class.forName("com.peng.singleton.SingletonDemo06"); Constructor<SingletonDemo06> c = clazz.getDeclaredConstructor(null); c.setAccessible(true); SingletonDemo06 s3 = c.newInstance(null); SingletonDemo06 s4 = c.newInstance(null); System.out.println(s3); System.out.println(s4); }}
查看一下结果:
tips:
如图所示,我们通过反射机制,生成了两个不同的对象s3和s4。这就属于一个单例的漏洞。针对这个问题的关键还是在于懒汉式的构造器。我们的解决方案就可以在构造器中手动抛出异常,针对这个解决方案,我们将SingletonDemo06中的构造器改为:
private SingletonDemo06() { //破解反射构造多个对象 if(instance != null) { throw new RuntimeException(); } }
改造之后,我们再来查看一下结果:
tips:
当我们再利用反射机制时,一旦检测到静态变量instance不是空的时候,就会抛出异常,阻止其继续创建新对象。
序列化的原理在于将对象转换为字节数组进行存储以及获取。我们首先对其进行测试
/** * 测试反射和反序列化破解单例模式 */public class Client2 { public static void main(String[] args) throws Exception { SingletonDemo06 s1 = SingletonDemo06.getInstance(); SingletonDemo06 s2 = SingletonDemo06.getInstance(); System.out.println(s1); System.out.println(s2); //通过反序列化的方式构造多个对象 FileOutputStream fos = new FileOutputStream("G:/java学习/test/a.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s1); oos.close(); fos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("G:/java学习/test/a.txt")); SingletonDemo06 s5 = (SingletonDemo06) ois.readObject(); System.out.println(s5);
}}
查看一下结果:
tips:
如图所示,对于同一个对象s1,经过序列化的处理之后,获取得到的对象成为了一个新对象。所以此处也破解了单例模式的实现。针对序列化的本质核心,我们在SingletonDemo06中过定义readResolve()防止获得不同对象。反序列化时,如果对象所在类定义了readResolve()(实际是一种回调),定义返回哪个对象。在SingletonDemo06中定义的readResolve()方法如下所示:
//反序列化时,如果定义了readResolve()则直接返回此方法指定的对象。而不需要单独再创建新对象! private Object readResolve() throws ObjectStreamException{ return instance; }
修改之后,我们再来查看最后的结果:
在上面的讲解中,我们主要讲解了饿汉式和懒汉式的细节,其余的三种由于和两种主要的单例模式比较类似,枚举式较为特殊,下面直接给出5种模式的对比,便于我们使用时候的选择:
-单例对象占用资源少,不需要延时加载:枚举式好于饿汉式
-单例对象占用资源大,需要延时加载:静态内部类式好于懒汉式