14.1 创建型设计模式
- 使用设计模式为了代码复用,增加可维护性。
- 设计模式的六大原则:开闭原则、里氏代换原则、依赖倒转原则、接口隔离原则、迪米特法则(最少知道原则)、合成/聚合复用原则
- UML类图:—————▷继承、实现接口-----▷、关联————→、依赖-----→、聚合◇———→、组合◆———→
14.1.1 单例模式
-
Singleton(创建):保证一个类仅有一个实例,并提供一个访问它的全局访问点。如打印机
-
UML类图
-
代码实现:
- 1.饿汉式:属性实例化对象,线程安全,耗费资源。
public class HugerSingletonTest {
//该对象的引用不可修改
private static final HugerSingletonTest ourInstance = new HugerSingletonTest();
public static HugerSingletonTest getInstance() {
return ourInstance;
}
private HugerSingletonTest() {}
}
- 2.饿汉式:在静态代码块实例对象
public class Singleton {
private static Singleton ourInstance;
static {
ourInstance = new Singleton();
}
public static Singleton getInstance() {
return ourInstance;
}
private Singleton() {}
}
- 3.懒汉式:非线程安全
public class Singleton {
private static Singleton ourInstance;
public static Singleton getInstance() {
if (null == ourInstance) {
ourInstance = new Singleton();
}
return ourInstance;
}
private Singleton() {}
}
- 4.线程安全:给方法加锁
public class Singleton {
private static Singleton ourInstance;
public synchronized static Singleton getInstance() {
if (null == ourInstance) {
ourInstance = new Singleton();
}
return ourInstance;
}
private Singleton() {}
}
- 5.线程安全:双重检查锁(同步代码块)
public class Singleton {
private static Singleton ourInstance;
public static Singleton getInstance() {
if (null == ourInstance) {
synchronized (Singleton.class) {
if (null == ourInstance) {
ourInstance = new Singleton();
}
}
}
return ourInstance;
}
private Singleton() {}
}
注:双重检查锁的优化,创建对象的过程包含三个过程:分配内存、初始化、将内存地址指向对象的引用。因为JVM会进行指令重排序,初始化与指向引用的顺序不能保证。可将ourInstance声明成volatile来禁止指令重排序。
- 6.懒汉式:线程安全的静态内部类
public class Singleton {
private static class SingletonHodler {
private static final Singleton ourInstance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHodler.ourInstance;
}
private Singleton() {}
}
- 7.线程安全:枚举
enum SingletonTest {
INSTANCE;
public void whateverMethod() {
}
}
- 8.线程安全:使用ThreadLocal
public class Singleton {
private static final ThreadLocal<Singleton> tlSingleton =
new ThreadLocal<Singleton>() {
@Override
protected Singleton initialValue() {
return new Singleton();
}
};
public static Singleton getInstance() {
return tlSingleton.get();
}
private Singleton() {}
}
- 9.线程安全:CAS锁
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
/**
* 用CAS确保线程安全
*/
public static Singleton getInstance() {
while (true) {
Singleton current = INSTANCE.get();
if (current != null) {
return current;
}
current = new Singleton();
if (INSTANCE.compareAndSet(null, current)) {
return current;
}
}
}
private Singleton() {}
}
- 单例模式在JDK8源码中的使用:Runtime.getRuntime()方法(饿汉式单例模式)
- 优点:减少了内存的开销,避免对资源的多重占用。
- 缺点:没有接口,不能继承,不符合单一职责原则。
14.1.2 工厂方法模式
-
工厂方法模式定义一个创建产品对象的工厂接口,让子类决定实例化哪一个类,使得一个类的实例化延迟到其子类。核心工厂类不再负责产品的创建,它成为了一个抽象工厂角色,仅负责具体工厂子类实现的接口。
-
UML类图:
-
代码实现
/**
* Creator角色
*/
interface Fruits {
String get();
}
class Apples implements Fruits {
@Override
public String get() {
return "苹果";
}
}
class Bananas implements Fruits {
@Override
public String get() {
return "香蕉";
}
}
/**
* Product 角色
*/
interface FruitsFactory {
Fruits getFruits();
}
class ApplesFactory implements FruitsFactory {
@Override
public Fruits getFruits() {
return new Apples();
}
}
class BananasFactory implements FruitsFactory {
@Override
public Fruits getFruits() {
return new Bananas();
}
}
public class FactoryDemo {
public static void main(String[] args) {
FruitsFactory applesFactory = new ApplesFactory();
Fruits apple = applesFactory.getFruits();
System.out.println(apple.get());
FruitsFactory bananasFactory = new BananasFactory();
Fruits banana = bananasFactory.getFruits();
System.out.println(banana.get());
}
}
- 优点:扩展性高,符合开闭原则;屏蔽产品的具体实现,调用者只关心产品的接口。
- 缺点:每次增加一个产品时,都需要增加一个具体类和对象的工厂,增加了系统的复杂度。
14.2 结构型设计模式
14.2.1 代理模式
-
代理模式(Proxy)为其他对象提供一种代理,从而控制对这个对象的访问。
-
UML类图:
-
静态代理:自定义创建或编译生成源代码,即编译时接口、被代理类、代理类已确定。代理类的.class文件在编译期生成。
-
静态代理的代码实现
/**
* subject 角色
*/
interface Book {
String sellBook();
}
/**
* RealSubject 角色
*/
class PublishingHouse implements Book {
public String sellBook() {
return "出版社卖书";
}
}
/**
* proxy 角色
*/
class BookStore implements Book {
private PublishingHouse publish;
public BookStore(PublishingHouse publish) {
this.publish = publish;
}
/**
* 书店代理出版社卖书
* @return
*/
public String sellBook() {
discount();
System.out.println(this.publish.sellBook());
voucher();
return "书店卖书结束";
}
public void discount() {
System.out.println("打折活动");
}
public void voucher() {
System.out.println("使用代金券");
}
}
public class ProxyDemo {
public static void main(String[] args) {
Book book = new BookStore(new PublishingHouse());
System.out.println(book.sellBook());
}
}
-
静态代理的优缺点
- 优点:易于理解,并可以进行功能扩展。
- 缺点:如果有多个接口就需要创建多个代理类,接口若发生改变,代理类也需要修改,较繁琐。
-
JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvocationHandler接口的invoke()方法处理。
-
JDK代理:Proxy.newProxyInstance()方法的三个参数:
- ClassLoader loader:指定当前目标对象使用的类加载器,获取加载器的方法是固定的。JDK内部需要通过类加载作为缓存的key ,并需要类加载器生成class。
- Class<?>[] interfaces:指定目标对象实现的接口的类型,使用泛型方式确认类型。生成的代理类需要实现该接口。
- InvocationHandler:指定动态处理器,执行目标对象的方法时,会触发事件处理器的方法
-
JDK动态代理的代码实现
import java.lang.reflect.*;
/**
* 接口
*/
interface MyBook {
void sellBook();
}
/**
* 真实对象
*/
class MyPublishingHouse implements MyBook {
@Override
public void sellBook() {
System.out.println("出版社卖书");
}
}
/**
* JDK动态代理
*/
public class JDKProxyDemo implements InvocationHandler {
private MyPublishingHouse publish;
private JDKProxyDemo(MyPublishingHouse publish) {
this.publish = publish;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
System.out.println(discount());
Object result = method.invoke(publish, args);
System.out.println(voucher());
return result;
}
private String discount() {
return "打折活动";
}
private String voucher() {
return "使用代金券";
}
public static void main(String[] args) {
MyPublishingHouse publish = new MyPublishingHouse();
// 通过反射调用代理对象
MyBook proxy = (MyBook) Proxy.newProxyInstance(MyPublishingHouse.class.getClassLoader(),
MyPublishingHouse.class.getInterfaces(), new JDKProxyDemo(publish));
proxy.sellBook();
}
}
-
JDK动态代理的优缺点
- 优点:不需要手动创建代理类,真正的代理对象由JDK在运行时动态的创建。
- 缺点:目标对象必须实现接口。
-
Cglib动态代理:在内存中构建一个子类对象从而实现对目标对象功能的扩展。原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,然后织入横切逻辑。
-
Cglib动态代理的代码实现
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/**
* 真实对象,不需要实现接口
*/
class MyPublishingHouse {
public void sellBook() {
System.out.println("出版社卖书");
}
}
/**
* Cglib动态代理
*/
public class CGLBProxyDemo implements MethodInterceptor {
private Object object;
/**
* 创建代理对象
* @param object
* @return
*/
public Object createProxyObject(Object object) {
this.object = object;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(object.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println(discount());
Object result = method.invoke(object, objects);
System.out.println(voucher());
return result;
}
private String discount() {
return "打折活动";
}
private String voucher() {
return "使用代金券";
}
public static void main(String[] args) {
MyPublishingHouse publish = new MyPublishingHouse();
// 创建代理对象
MyPublishingHouse proxy = (MyPublishingHouse) new CGLBProxyDemo().createProxyObject(publish);
proxy.sellBook();
}
}
- Cglib动态代理的优缺点
- 优点:目标对象不需要实现接口, Cglib在程序运行期可以扩展java类与实现java接口。
- 缺点:因为使用继承创建子类,所以对于final修饰的类无法通过Cglib创建动态代理。
- JDK与Cglib动态代理的区别:
- JDK动态代理实现了被代理对象的接口,Cglib是继承了被代理对象。
- JDK和Cglib都是在运行期生成字节码,JDK是直接写Class字节码,Cglib使用ASM框架写Class字节码,Cglib代理实现更复杂,生成代理类比JDK效率低。
- JDK调用代理方法,是通过反射机制调用,Cglib是通过使用字节码处理框架asm修改字节码生成子类。
- JDK动态代理的方式创建代理对象效率较高,执行效率较低,Cglib创建效率较低,执行效率高。所以对于单例的对象,因为无需频繁创建对象,用CGLIB合适,反之适合使用JDK动态代理。
14.2.2 适配器模式
-
适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作。
-
适配器模式的角色包含:目标接口(Target)、客户所期待的接口、需要适配的类(Adaptee)、适配器(Adapter)。
-
UML类图:
-
代码实现:对象适配器(采用对象组合方式实现)
//适配器类实现了目标接口
public class Adapter implements Target{
private Adaptee adaptee ;
public Adapter() {
super();
this.adaptee = new Adaptee();
}
@Override
public void getHeadset2() {
adaptee.getHeadset3();
}
public static void main(String args[]){
Target target = new Adapter();
//表面上调用的是2孔插座方法,但其实调用的三孔插座方法。
target.getHeadset2();
}
}
interface Target{
//两孔插座
void getHeadset2();
}
class Adaptee{
public void getHeadset3(){
System.out.println("我是三孔插座!");
}
}
- 适配器模式在JDK源码的使用
- Arrays.asList(),其中Arrays是目标类,内部类ArrayList是适配器类,而Objects.requireNonNull(array);需要适配的类。
- InputStreamReader .read(),其中Reader 是目标类,InputStreamReader是适配器类,而StreamDecoder 是需要适配的类。
- 优点:扩展性好,符合开闭原则;增加了类的透明性和复用性,同一适配者类可以在多个不同的系统中复用。
- 缺点:因为Java是单继承,所以只能适配一个适配者类。
14.3 行为型模式
14.3.1 策略模式
-
策略模式对算法封装,为所有的算法定义一个算法接口,并通过实现该算法接口对所有的算法加以封装和实现。
-
UML类图:
-
代码实现:
/**
* 策略接口
*/
interface Strategy {
double cost(double num);
}
/**
* 策略A
* 全场8折
*/
class StrategyA implements Strategy {
@Override
public double cost(double num) {
return num * 0.8;
}
}
/**
* 策略B
* 满200减100
*/
class StrategyB implements Strategy {
@Override
public double cost(double num) {
if (num >= 200) {
return num - 100;
}
return num;
}
}
/**
* 封装策略
*/
class OperationStrategy{
private Strategy strategy;
public OperationStrategy(Strategy strategy) {
this.strategy = strategy;
}
/**
* 执行策略
* @param num
*/
public double executeStrategy(double num){
return strategy.cost(num);
}
}
/**
* 策略模式 测试类
*/
public class StrategyDemo {
public static void main(String[] args) {
// 消费金额
double costRMB = 300;
OperationStrategy o1 = new OperationStrategy(new StrategyA());
OperationStrategy o2 = new OperationStrategy(new StrategyB());
System.out.println("活动一结账:" + o1.executeStrategy(costRMB));
System.out.println("活动二结账:" + o2.executeStrategy(costRMB));
}
}
- 优点:可使用继承可以把公共的代码移动到父类里面,从而避免重复的代码;提供了可以替换继承关系的办法,继承可以处理多种算法或行为;可避免使用多重条件转移语句。
- 缺点:只适用于客户端知道所有算法或行为的情况。
- 改进:使用反射+策略模式,就无需知道Strategy的实现类都有哪些。
14.3.2 观察者模式
-
观察者模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生变化时,能够自动通知其他关联对象,自动刷新对象状态。
-
UML类图:
-
代码实现
/**
* 被观察者接口
*/
interface Subject {
void notifyAllObserver();
}
/**
* 具体的被观察者
*/
class Players implements Subject {
private List<Observer> observerList = new ArrayList<>();
private String name;
public Players(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
this.notifyAllObserver();
}
/**
* 添加观察者对象
* @param observer
*/
public void addObserver(Observer observer) {
this.observerList.add(observer);
}
@Override
public void notifyAllObserver() {
for (Observer observer : this.observerList) {
observer.update();
}
}
}
/**
* 观察者接口
*/
interface Observer {
void update();
}
/**
* 1号观察者
*/
class MyObserver1 implements Observer {
@Override
public void update() {
System.out.println("1号观察者发现:玩家昵称改变了");
}
}
/**
* 2号观察者
*/
class MyObserver2 implements Observer {
@Override
public void update() {
System.out.println("2号观察者发现:玩家昵称改变了");
}
}
/**
* 测试类
*/
public class ObserverDemo {
public static void main(String[] args) {
Players players = new Players("大蛇");
players.addObserver(new MyObserver1());
players.addObserver(new MyObserver2());
players.setName("八仙庵");
}
}
- 优点:观察者模式支持广播通讯,被观察者会向所有的登记过的观察者发出通知;实现解耦合,让耦合的双方都依赖于抽象,而非具体。
- 缺点:若被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,导致系统崩溃;观察者只知道观察目标发生了变化,但对具体的变化内容一无所知。