设计模式面试复习

前言

老实说,面试面来面去也就这么五六种设计模式,所以打算抽出点时间详细的了解下这五种设计模式。

单例

考的最多,同时也算是最常用的一种模式了,可以有以下几种实现:

  • 懒汉式:只有当需要的时候才创建对象,线程不安全
  • 饿汉式:在类加载的时候直接就创建好,非常简单。
  • 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(轻量级)为什么会被翻译成享元,共享元数据的意思么?反正按照英文理解就很容易,就是如果有能够共享的对象,那么就直接使用。最常见的就是线程池、数据库连接池等。