前言
老实说,面试面来面去也就这么五六种设计模式,所以打算抽出点时间详细的了解下这五种设计模式。
单例
考的最多,同时也算是最常用的一种模式了,可以有以下几种实现:
- 懒汉式:只有当需要的时候才创建对象,线程不安全
- 饿汉式:在类加载的时候直接就创建好,非常简单。
- double check:懒汉式的一种,通过两次的if判断来满足线程安全。
- 静态内部类:懒汉式的一种,利用了静态内部类线程安全的特点
- 枚举:JVM自带的一种关键字实现,可以确保线程安全。
饿汉式
1 2 3 4 5 6 7 8 9 10 11
| public class HungrySingleton {
private static final HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton() { }
public static HungrySingleton getInstance() { return INSTANCE; } }
|
优点是简单,而且不会出现线程安全问题。缺点是不管你用不用,都会创建这个实例。
懒汉式(线程不安全)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class LazySingleton { private static LazySingleton lazySingleton;
private LazySingleton() {
}
public static LazySingleton getInstance() { if (lazySingleton == null) { lazySingleton = new LazySingleton(); } return lazySingleton; } }
|
显然假设有两个线程A和B,如果A在已经进入if判断之后时间片到了,那么就会发生两次构造器的调用了。
double check
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class DoublecheckSingleton { private static volatile DoublecheckSingleton doublecheckSingleton;
private DoublecheckSingleton() { }
public static DoublecheckSingleton getInstance() { if (doublecheckSingleton == null) { synchronized (DoublecheckSingleton.class) { if (doublecheckSingleton == null) { doublecheckSingleton = new DoublecheckSingleton(); } } } return doublecheckSingleton; } }
|
这里指的注意的就是用volatile修饰的,也是面试官最喜欢问的。volatile在这里一是为了内存的可见性,二是为了防止指令重排序,导致先分配了空间但是没有初始化。
静态内部类
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class StaticInnerClassSingleton { private StaticInnerClassSingleton() { }
private static class InnerClass { private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton(); }
public static StaticInnerClassSingleton getInstance() { return InnerClass.instance; } }
|
推荐使用,简单好写,且由JDK来进行保证单例的正确性和线程安全。但是会被反射轻松获取。
枚举
1 2 3
| public enum EnumSingleton { instace; }
|
emmm 应该没有比它更简单的单例了吧?而且它线程安全。
代理模式
反射的时候用到过,spring AOP也大量使用。
如果一个对象的功能不能满足你的要求,可以用代理对象来增强。
静态代理
首先需要定义一个接口:
1 2 3
| public interface Raw { void doSomething(); }
|
然后有一个原始的实现类:
1 2 3 4 5 6
| public class RawImpl implements Raw{ @Override public void doSomething() { System.out.println("原始对象"); } }
|
最后用一个类来持有这个原始的实现类,并且对其进行加强:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class RawImplProxy implements Raw {
private RawImpl rawImpl;
public RawImplProxy(RawImpl rawImpl) { this.rawImpl = rawImpl; }
@Override public void doSomething() { System.out.println("proxy start"); rawImpl.doSomething(); System.out.println("proxy end"); } }
|
动态代理
代理的对象不在需要去实现接口了,但是被代理的对象仍然需要实现接口。用的是JDK的API,在内存中动态构件代理对象。
也就是,创建代理不需要你自己做了,JDK已经帮你完成了,你只需要去调用Proxy类下面的静态方法就行了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class RawImplProxy implements Raw {
private Object target;
private Object proxy;
public RawImplProxy(Raw raw) { this.target = raw; this.proxy = Proxy.newProxyInstance(this.getClass().getClassLoader(), this.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("JDK代理开始"); method.invoke(target, args); System.out.println("JDK代理结束"); return null; } }); } @Override public void doSomething() { ((Raw) proxy).doSomething(); } }
|
可以看到上面的类持有两个Object对象,一个是target,也就是需要被代理的类,显然就是RawImpl;然后是proxy,proxy是用jdk的反射方法生成的一个代理对象,然后我们拿着这个代理对象来操作就行了。
cglib
cglib本身是属于动态代理的范畴,但是由于它是使用的继承类来进行增强,而不是使用接口的方法,所以单独拿出来说一说。
核心思路是一样的,都是使用现成的方法来生成对应的代理对象。假设A需要增强B,那么A只需要去实现一个cglib的接口,然后重写里面的方法就可以了..
适配器
也算是比较常用的一种设计模式了。
类适配器
通过继承一个类src,然后实现另外一个类dest的接口,就能把两者融合在一起。
对象适配器
这次不继承src,而是持有src的实例。
观察者模式
这个模式主要应用于当一个对象变化之后,其它依赖这个对象的对象能够得到通知。有点类似现在的xx主播开播了然后推送信息给你一样。或者再往前就跟RSS订阅一样。
其实用起来很简单,java有很好的抽象(从JDK9之后被废弃了),我们只需要继承并且实现相关的代码就好了:
1 2 3 4 5 6 7 8 9 10 11
| public class MyObservable extends Observable {
private int money = 10;
public void changeMoney(int update) { this.money = update; System.out.println("金额变更成功"); setChanged(); notifyObservers(update); } }
|
我们只需要在方法中加入两行,就可以通知观察者了。同时观察者需要处理一下逻辑:
1 2 3 4 5 6
| public class MyObserver implements Observer { @Override public void update(Observable o, Object arg) { System.out.println("当前的金额是 = " + arg); } }
|
然后在使用的时候,只需要将观察者加入进去,然后在调用相关方法的时候就会去调用相关的观察者了:
1 2 3 4 5 6 7 8 9 10 11 12
| public class Main { public static void main(String[] args) { MyObservable observable = new MyObservable(); MyObserver observer1 = new MyObserver(); MyObserver observer2 = new MyObserver();
observable.addObserver(observer1); observable.addObserver(observer2);
observable.changeMoney(100); } }
|
实现简单而又优雅,缺点就是如果一个对象被非常多观察者所观察,那么可能需要不少代码。其次是它不能被序列化,最后是它线程不安全。
策略模式
策略模式简单来说就是一个对象持有一个接口,然后根据给接口传入不同的多态对象来进行不同的处理。最常见的应该就是Comparator了,根据不同的实现类来实现排序。
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 34 35 36 37 38 39 40
| public class Main { public static void main(String[] args) { List<Student> list = new ArrayList<>(5); list.add(new Student("1", 10)); list.add(new Student("2", 5)); list.add(new Student("3", 8)); Collections.sort(list, new AgeComparator()); System.out.println(Arrays.toString(list.toArray())); } }
class Student { private String name; private int age;
public Student(String name, int age) { this.name = name; this.age = age; }
public int getAge() { return age; }
@Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
class AgeComparator implements Comparator<Student> {
@Override public int compare(Student o1, Student o2) { return o1.getAge() - o2.getAge(); } }
|
优点是我们只需要使用不同的”排序器”就可以实现相关的排序,而不需要去大幅度的改动源代码。
享元模式
我是一直没搞懂为什么Flyweight(轻量级)为什么会被翻译成享元,共享元数据的意思么?反正按照英文理解就很容易,就是如果有能够共享的对象,那么就直接使用。最常见的就是线程池、数据库连接池等。