Demo3 装饰者模式在 Java IO中的使用
Java IO 类库如下图所示:
InputStream 是一个抽象类,FileInputStream 是专门用来读取文件流的子类。BufferedInputStream 是一个支持带缓存功能的数据读取类,可以提高数据读取效率。
现在有一个需求,读取文件 test.txt。
InputStream in = new FileInputStream("/user/1202/test.txt"); InputStream bin = new BufferedInputStream(in); byte[] data = new byte[128]; while (bin.read(data) != -1) { //... }
如果是基于继承的设计,设计成 BufferedFileInputStream,需要附加更多的增强功能,那就会导致组合爆炸,类继承结构变得无比复杂,代码既不好扩展,也不好维护。
InputStream bin = new BufferedFileInputStream("/user/1202/test.txt"); byte[] data = new byte[128]; while (bin.read(data) != -1) { //... }
基于装饰器模式的设计模式的 Java IO 源码,核心思想:组合优于继承,InputStream 类下有 FileInputStream (读取文件)、BufferedInputStream (增加缓存,使读取文件速度大大提升)等子类都在不修改InputStream 代码的情况下扩展了它的功能。
public abstract class InputStream { //... public int read(byte b[]) throws IOException { return read(b, 0, b.length); } public int read(byte b[], int off, int len) throws IOException { //... } public long skip(long n) throws IOException { //... } public int available() throws IOException { return 0; } public void close() throws IOException {} public synchronized void mark(int readlimit) {} public synchronized void reset() throws IOException { throw new IOException("mark/reset not supported"); } public boolean markSupported() { return false; } } public class BufferedInputStream extends InputStream { protected volatile InputStream in; protected BufferedInputStream(InputStream in) { this.in = in; } //...实现基于缓存的读数据接口... } public class DataInputStream extends InputStream { protected volatile InputStream in; protected DataInputStream(InputStream in) { this.in = in; } //...实现读取基本类型数据的接口 }
9.4、适配器设计模式 Adapter
概念:适配器模式用来将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作,用于消除接口不匹配所造成的类的兼容性问题。
生活中的使用案例:插座,为了适应各种插头,然后上面有两个孔的,三个空的,基本都能适应。还有万能充电器、USB接口等。
代理模式、装饰器模式提供的都是跟原始类相同的接口,而适配器提供跟原始类不同的接口。
主要分为两类:
1、类的适配器模式 使用继承关系来实现
2、对象的适配器模式 使用组合关系来实现
Demo1:ITarget 表示要转化成的接口定义。Adaptee 是一组不兼容 ITarget 接口定义的接口,Adaptor 将 Adaptee 转化成一组符合 ITarget 接口定义的接口
- 类适配器: 基于继承
// 适配接口 public interface ITarget { void f1(); void f2(); void fc(); } //被适配者 public class Adaptee { public void fa() { //... } public void fb() { //... } public void fc() { //... } } //适配者 继承被适配者,实现适配接口 public class Adaptor extends Adaptee implements ITarget { public void f1() { super.fa(); } public void f2() { //...重新实现f2()... } // 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点 } 缺点:需要继承被适配者
- 对象适配器:基于组合
// 最终需要的输出 public interface ITarget { void f1(); void f2(); void fc(); } //被适配者 public class Adaptee { public void fa() { //... } public void fb() { //... } public void fc() { //... } } //适配者 组合被适配者,实现适配接口 public class Adaptor implements ITarget { private Adaptee adaptee; public Adaptor(Adaptee adaptee) { this.adaptee = adaptee; } public void f1() { //委托给Adaptee adaptee.fa(); } public void f2() { //...重新实现f2()... } public void fc() { adaptee.fc(); } } 使用组合替代继承,解决了类适配器必须继承 被是适配者的问题
在开发中,到底该如何选择使用哪一种呢?
①一个是 Adaptee 接口的个数
②另一个是 Adaptee 和 ITarget 的契合程度
怎么简便怎么来。相对而言:更加推荐使用对象适配器,因为组合结构相对于继承更加灵活
- 符合“合成复用原则”
③ 接口适配
- 当不需要全部实现某接口提供的方法时,可以先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求
使用场景:
- ①封装有缺陷的接口设计, 对外部系统提供的接口进行二次封装
- ②替换外部系统
- ③兼容老版本接口重点
Demo2 修改配置中心SDK
public class InstanceServiceClient { //... public Response<List<InstanceRO>> fetchInstanceByIndustry(InstanceSearchDTO param){ //... } public Response<InstanceRO> fetchInstanceByCondition(SearchCondition param){ //... } public void tooManyParamsFunction3(int paramA, int paramB, ...) { //... } //... } // 使用适配器模式进行重构 public class ITarget { void function1(InstanceSearchDTO param); void function2(SearchCondition param); void fucntion3(ParamsWrapperDefinition paramsWrapper); //... } public class InstanceServiceClientAdaptor extends InstanceServiceClient implements ITarget { //... public void function1() { super.fetchInstanceByIndustry(param); } public void function2() { super.fetchInstanceByCondition(); } public void function3(ParamsWrapperDefinition paramsWrapper) { super.tooManyParamsFunction3(paramsWrapper.getParamA(), ...); } }
Demo3 替换外部系统
// 外部系统A public interface IA { //... void fa(); } public class A implements IA { //... public void fa() { //... } } // 在我们的项目中,外部系统A的使用示例 public class Demo { private IA a; public Demo(IA a) { this.a = a; } //... } Demo d = new Demo(new A()); // 将外部系统A 替换成外部系统B public class BAdaptor implemnts IA { private B b; public BAdaptor(B b) { this.b= b; } public void fa() { //... b.fb(); } } // 借助BAdaptor,Demo的代码中,调用IA接口的地方都无需改动, // 只需要将BAdaptor如下注入到Demo即可。 Demo d = new Demo(new BAdaptor(new B()));
Demo4 兼容升级 ☆
在做版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且
标注为 deprecated,并将内部实现逻辑委托为新的接口实现。
好处:让项目有个过渡期,而不是强制进行代码修改
场景: 兼容api升级,API治理可以使用这种方案
// Emueration jdk1.0提供 jdk2.0 改为了 iterator public class Collections { public static <T> Emueration<T> emumeration(final Collection<T> c) { return new Enumeration<T>() { private Iterator<T> i = c.iterator(); public boolean hasMoreElments() { return i.hashNext(); } public T nextElement() { return i.next(): } } } }
- Action:实际使用中,非常实用,避免出现线上故障
- 例如:原来提供的dubbo接口有4个入参,现在需要补充一个入参,我们能直接修改原来接口的签名吗?风险极大。可以选择的方案是:提供一个新的接口,入参使用dto,方便拓展,然后原来的dubbo接口底层实现时,调用新提供的接口
Demo5 适配器模式在 Java 日志中的应用
- Slf4j 这个日志框架,它相当于 JDBC 规范,提供了一套打印日志的统一接口规范。不过,它只定义了接口,并没有提供具体的实现,需要配合其他日志框架(log4j、logback……)来使用。
- 它不仅仅提供了统一的接口定义,还提供了针对不同日志框架的适配器。对不同日志框
架的接口进行二次封装,适配成统一的 Slf4j 接口定义。
package org.slf4j; public interface Logger { public boolean isTraceEnabled(); public void trace(String msg); public void trace(String format, Object arg); public void trace(String format, Object arg1, Object arg2); public void trace(String format, Object[] argArray); public void trace(String msg, Throwable t); public boolean isDebugEnabled(); public void debug(String msg); public void debug(String format, Object arg); public void debug(String format, Object arg1, Object arg2) public void debug(String format, Object[] argArray) public void debug(String msg, Throwable t); //...省略info、warn、error等一堆接口 } // log4j日志框架的适配器 // Log4jLoggerAdapter实现了LocationAwareLogger接口, // 其中LocationAwareLogger继承自Logger接口, // 也就相当于Log4jLoggerAdapter实现了Logger接口。 package org.slf4j.impl; public final class Log4jLoggerAdapter extends MarkerIgnoringBase implements LocationAwareLogger, Serializable { final transient org.apache.log4j.Logger logger; // log4j public boolean isDebugEnabled() { return logger.isDebugEnabled(); } public void debug(String msg) { logger.log(FQCN, Level.DEBUG, msg, null); } public void debug(String format, Object arg) { if (logger.isDebugEnabled()) { FormattingTuple ft = MessageFormatter.format(format, arg); logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable()); } } public void debug(String format, Object arg1, Object arg2) { if (logger.isDebugEnabled()) { FormattingTuple ft = MessageFormatter.format(format, arg1, arg2); logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable()); } } public void debug(String format, Object[] argArray) { if (logger.isDebugEnabled()) { FormattingTuple ft = MessageFormatter.arrayFormat(format, argArray); logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable()); } } public void debug(String msg, Throwable t) { logger.log(FQCN, Level.DEBUG, msg, t); } //...省略一堆接口的实现... }
Demo6 适配器模式在 SpringMVC 框架应用的源码剖析
SpringMVC 处理用户请求的流程
SpringMVC 中的 HandlerAdapter, 就使用了适配器模式
//多种Controller实现 public interface Controller { } class HttpController implements Controller { public void doHttpHandler() { System.out.println("http..."); } } class SimpleController implements Controller { public void doSimpleHandler() { System.out.println("simple..."); } } class AnnotationController implements Controller { public void doAnnotationHandler() { System.out.println("annotation..."); } } public class DispatchServlet extends FrameworkServlet { public static List<HandlerAdapter> handlerAdapters = new ArrayList<HandlerAdapter>(); public DispatchServlet() { handlerAdapters.add(new AnnotationHandlerAdapter()); handlerAdapters.add(new HttpHandlerAdapter()); handlerAdapters.add(new SimpleHandlerAdapter()); } public void doDispatch() { // 此处模拟 SpringMVC 从 request 取 handler 的对象,适配器可以获取到想要的Controller HttpController controller = new HttpController(); //AnnotationController controller = new AnnotationController(); //SimpleController controller = new SimpleController(); // 得到对应适配器 HandlerAdapter adapter = getHandler(controller); // 通过适配器执行对应的controller对应方法 adapter.handle(controller); } public HandlerAdapter getHandler(Controller controller) { //遍历:根据得到的controller(handler), 返回对应适配器 for (HandlerAdapter adapter : this.handlerAdapters) { if (adapter.supports(controller)) { return adapter; } } return null; } public static void main(String[] args) { new DispatchServlet().doDispatch(); // http... } } ///定义一个Adapter接口 public interface HandlerAdapter { public boolean supports(Object handler); public void handle(Object handler); } // 多种适配器类 class SimpleHandlerAdapter implements HandlerAdapter { public boolean supports(Object handler) { return (handler instanceof SimpleController); } public void handle(Object handler) { ((SimpleController) handler).doSimplerHandler(); } } class HttpHandlerAdapter implements HandlerAdapter { public boolean supports(Object handler) { return (handler instanceof HttpController); } public void handle(Object handler) { ((HttpController) handler).doHttpHandler(); } } class AnnotationHandlerAdapter implements HandlerAdapter { public boolean supports(Object handler) { return (handler instanceof AnnotationController); } public void handle(Object handler) { ((AnnotationController) handler).doAnnotationHandler(); } }
使用HandlerAdapter 的原因:
可以看到处理器的类型不同,有多重实现方式,那么调用方式就不是确定的,如果需要直接调用Controller方法,需要调用的时候就得不断是使用if else来进行判断是哪一种子类然后执行。那么如果后面要扩展Controller,就得修改原来的代码,这样违背了OCP原则。
SpringMVC使用适配器设计模式的好处:
• Spring 定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类;
• 适配器代替 Controller 执行相应的方法;
• 扩展 Controller 时,只需要增加一个适配器类就完成了SpringMVC的扩展了
代理、桥接、装饰器、适配器 4 种设计模式的区别
代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,控制访问,而非加强功能
桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变
装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用
适配器模式:一种补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
20211203 done
9.5、你在编码时最常用的设计模式有哪些?在什么场景下用?在业务代码中,经常发现大量XXFacade,门面模式是解决什么问题?适用于什么场景?
9.5.1、门面模式定义
- 门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用,合理的使用门面模式,可以帮我们更好的划分访问层次。
使用场景:
- ① 解决易用性问题 封装底层的复杂实现;
- ② 解决性能问题 将多个接口组装为一个大而全接口,减少网络开销;
- ③维护遗留的大型系统时,可能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个Facade类,来提供遗留系统的比较清晰简单的接口,让新系统与Facade类交互, 提高复用性。
门面模式在商品中心使用广泛,但是有些 facade 类并不是出于封装底层子系统复杂实现的目的编写的,这些业务逻辑不复杂的接口去除 facade 后缀为好。
Demo1 facade模式的使用姿势
例如:业务方需要在一个接口中返回商品信息(配置项、类目、属性、价格、库存)
public class ItemReadFacade { //定义各个子系统对象 private ItemConfig itemConfig; private Category category; private Attributes attributes; private Price price; private Stock stock; //构造器 public ItemReadFacade() { super(); this.itemConfig = ItemConfig.getInstance(); this.category = Category.getInstance(); this.attributes = Attributes.getInstance(); this.price = Price.getInstance(); this.stock = Stock.getInstance(); } //操作分成 4 步 public ItemWrapperDTO read() { itemConfig.read(); category.read(); attributes.read(); price.read(); stock.read(); } } public class ItemConfig { //使用单例模式, 使用饿汉式 private static ItemConfig instance = new ItemConfig(); public static ItemConfig getInstanc() { return instance; } public ItemConfig read() { System.out.println(" ItemConfig "); } } ... public static void main(String[] args) { ItemReadFacade itemReadFacade = new ItemReadFacade(); itemReadFacade.read(); }
Demo2 门面模式在 MyBatis 框架应用的分析
MyBatis 中的Configuration 去创建MetaObject 对象使用到门面模式
public class Configuration { protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory(); protected ObjectFactory objectFactory = new DefaultObjectFactory(); protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory(); public MetaObject newMetaObject(Object object) { // 封装了子系统的复杂性 return MetaObject.forObject(object, objectFactory, objectWrapperFactory, reflectorFactory); } } public class MetaObject{ private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) { this.originalObject = object; this.objectFactory = objectFactory; this.objectWrapperFactory = objectWrapperFactory; this.reflectorFactory = reflectorFactory; if (object instanceof ObjectWrapper) { this.objectWrapper = (ObjectWrapper)object; } else if (objectWrapperFactory.hasWrapperFor(object)) { this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object); } else if (object instanceof Map) { this.objectWrapper = new MapWrapper(this, (Map)object); } else if (object instanceof Collection) { this.objectWrapper = new CollectionWrapper(this, (Collection)object); } else { this.objectWrapper = new BeanWrapper(this, object); } } public static MetaObject forObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) { return object == null ? SystemMetaObject.NULL_META_OBJECT : new MetaObject(object, objectFactory, objectWrapperFactory, reflectorFactory); } }
9.6、组合模式(不常用)
概念:将对象组合成树状结构以表示 “整体-部分”的层次关系。
使用场景:用来处理树状结构数据。
- 需要遍历组织机构,或者处理的对象具有树形结构时, 非常适合使用组合模式;业务需求可以通过在树上的递归遍历算法来实现。
- 如果节点和叶子有很多差异性的话,比如很多方法和属性不一样, 不适合使用组合模式
组合模式的角色及职责
- Component:这是组合中对象声明接口,在适当情况下,实现所有类共有的接口默认行为,用于访问和管理Component 子部件, Component 可以是抽象类或者接口
- Leaf:在组合中表示叶子节点,叶子结点没有子节点;
3)Composite:非叶子节点,用于存储子部件,在 Component 接口实现子部件的相关操作,比如增加、删除。
Demo1 需求:设计一个“类目”类**,能方便地实现下面这些功能:
- 动态地添加、删除某个类目下的父类目或子类目;
- 统计指定类目下的类目个数;
- 统计指定类目下的标签个数;
代码如下所示,把父类目和子类目统一用 CategoryNode 类来表示,并通过hasChildren 属性来区分父子节点。
public class CategoryNode { //类目名称 private String name; //是否有叶子节点 private boolean hasChildren; //子节点 private List<CategoryNode> subNodes = new ArrayList<>(); public CategoryNode(String name, boolean hasChildren) { this.name = name; this.hasChildren = hasChildren; } public int countNumOfCategories() { if (!hasChildren) { return 1; } int numOfCategories = 0; for (CategoryNode categoryNode : subNodes) { numOfCategories += categoryNode.countNumOfCategories(); } } public String getName() { return name; } public void addSubNode(CategoryNode categoryNode) { subNodes.add(categoryNode); } public void removeSubNode(CategoryNode categoryNode) { int size = subNodes.size(); int i = 0; for (; i < size; ++i) { if (subNodes.get(i).getName().equals(categoryNode.getName())){ break; } } if (i < size) { subNodes.remove(i); } } } public class Demo { public static void main(String[] args) { CategoryNode pCategoryNode = new CategoryNode("名贵花木"); CategoryNode node_1 = new CategoryNode("笔记本电脑"); CategoryNode node_2 = new CategoryNode("台式整机"); pCategoryNode.addSubNode(node_1); pCategoryNode.addSubNode(node_2); System.out.println("category num:" + pCategoryNode.countNumOfCategories()); } }
Demo2 构建整个公司的人员架构图(部门、子部门、员工的隶属关系)
- 提供接口计算出部门的薪资成本(隶属于这个部门的所有员工的薪资和),部门包含子部门和员工,这是一种嵌套结构,可以表示成树这种数据结构。计算每个部门的薪资开支这样一个需求,也可以通过在树上的遍历算法来实现。从这个角度来看,该场景可以使用组合模式来设计和实现。
- HumanResource 是部门类(Department)和员工类(Employee)抽象出来的父类,提供统一薪资的处理逻辑。
public abstract class HumanResource { protected long id; protected double salary; public HumanResource(long id) { this.id = id; } public long getId() { return id; } public abstract double calculateSalary(); } // 员工 public class Employee extends HumanResource { public Employee(long id, double salary) { super(id); this.salary = salary; } @Override public double calculateSalary() { return salary; } } // 部门 public class Department extends HumanResource { private List<HumanResource> subNodes = new ArrayList<>(); public Department(long id) { super(id); } @Override public double calculateSalary() { double totalSalary = 0; for (HumanResource hr : subNodes) { totalSalary += hr.calculateSalary(); } this.salary = totalSalary; return totalSalary; } } public void addSubNode(HumanResource hr) { subNodes.add(hr); } } // 构建组织架构的代码 public class Demo { private static final long ORGANIZATION_ROOT_ID = 1001; // 依赖注入 private DepartmentRepo departmentRepo; // 依赖注入 private EmployeeRepo employeeRepo; public void buildOrganization() { Department rootDepartment = new Department(ORGANIZATION_ROOT_ID); buildOrganization(rootDepartment); } private void buildOrganization(Department department) { List<Long> subDepartmentIds = departmentRepo.getSubDepartmentIds(department); for (Long subDepartmentId : subDepartmentIds) { Department subDepartment = new Department(subDepartmentId); department.addSubNode(subDepartment); buildOrganization(subDepartment); } List<Long> employeeIds = employeeRepo.getDepartmentEmployeeIds(department.getId()); for (Long employeeId : employeeIds) { double salary = employeeRepo.getEmployeeSalary(employeeId); department.addSubNode(new Employee(employeeId, salary)); } } }
Demo3 组合模式在 HashMap 的源码分析
职责:
①Map起 Component 的作用,提供通用的能力;
②Node 起 leaf 的作用,在组合中表示叶子节点;
②HashMap起 Composite 的作用,继承 Map接口,借助 Node 实现 Map 的功能
public interface Map<K,V> { V put(K key, V value); V remove(Object key); ... } public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } // 调用Node节点的put实现功能 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; } } static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; }
9.7、享元模式(被共享的单元)
9.7.1、享元模式定义
- 运用共享技术有效地支持大量细粒度的对象。
- 当一个系统中存在大量重复对象的时候,我们就可以利用享元模式,将对象设计成享元,在内存中只保留一份实例,供多处代码引用,这样可以减少内存中对象的数量,以起到节省内存的目的。
享元模式可以分为以下两种角色
- 享元角色:存储在享元对象内部且不会随环境而改变
- 不可共享角色:随环境改变而改变、不可共享的状态。
使用场景:各类池技术:String常量池、数据库连接池、缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式。
享元模式的缺点:享元模式对 JVM 的垃圾回收并不友好。因为享元工厂类一直保存了对享元对象的引用,这就导致享元对象在没有任何代码使用的情况下,也并不会被 JVM 垃圾回收机制自动回收掉。因此不要过度使用这个模式。
Demo1 棋牌游戏
- 一个游戏厅中有成千上万个“房间”,每个房间对应一个棋局。棋局要保存每个棋子的数据,比如:棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。利用这些数据,我们就能显示一个完整的棋盘给玩家。
- 我们可以将棋子的 id、text、color 属性拆分出来,设计成独立的类,并且作为享元供多个棋盘复用。
// 享元角色 public class ChessPieceUnit { private int id; private String text; private Color color; public ChessPieceUnit(int id, String text, Color color) { this.id = id; this.text = text; this.color = color; } public static enum Color { RED, BLACK } // ...省略其他属性和getter方法... } // 享元工厂,通过一个 Map 来缓存已经创建过的享元对象,来达到复用的目的 public class ChessPieceUnitFactory { private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>(); static { pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK)); pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK)); //...省略摆放其他棋子的代码... } public static ChessPieceUnit getChessPiece(int chessPieceId) { return pieces.get(chessPieceId); } } // 不可共享的角色 public class ChessPiece { private ChessPieceUnit chessPieceUnit; private int positionX; private int positionY; public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) { this.chessPieceUnit = unit; this.positionX = positionX; this.positionY = positionY; } // 省略getter、setter方法 } // Client public class ChessBoard { private Map<Integer, ChessPiece> chessPieces = new HashMap<>(); public ChessBoard() { init(); } private void init() { chessPieces.put(1, new ChessPiece( ChessPieceUnitFactory.getChessPiece(1), 0,0)); chessPieces.put(1, new ChessPiece( ChessPieceUnitFactory.getChessPiece(2), 1,0)); //...省略摆放其他棋子的代码... } public void move(int chessPieceId, int toPositionX, int toPositionY) { //...省略... } }
Demo2 剖析一下享元模式在 Java Integer、String 中的应用。
通过自动装箱,也就是调用 valueOf() 来创建 Integer 对象时,如果要创建的 Integer 对象的值在 -128 到 127 之间,会从 IntegerCache 类中直接返回,否则才调用 new 方法创建。
- JDK库中的Integer cache(-128~127)
Integer i1 = 56; //自动装箱 Integer i = Integer.valueOf(59); Integer i2 = 56; Integer i3 = 129; Integer i4 = 129; System.out.println(i1 == i2); // true System.out.println(i3 == i4); //false public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high") if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} }
可以用如下方式,将缓存的最大值从 127 调整到 255
//方法一: -Djava.lang.Integer.IntegerCache.high=255 //方法二: -XX:AutoBoxCacheMax=255
使用建议:在日常开发中,对于下面这样三种创建整型对象的方式,我们优先使用后两种
Integer a = new Integer(123); // 并不会使用到 IntegerCache Integer a = 123; Integer a = Integer.valueOf(123);
享元模式在 Java String 中的应用
JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫作“字符串常量池”。对于字符串来说,没法事先知道要共享哪些字符串常量,只能在某个字符串常量第一次被用到时,存储到常量池中,当之后再用到时,直接引用常量池中已经存在的即可,就不需要再重新创建了。
String s1 = "设计模式"; String s2 = "设计模式"; String s3 = new String("设计模式"); System.out.println(s1 == s2); //true System.out.println(s1 == s3); //false
享元模式和单例模式的区别?
单例模式是创建型模式,重在只能有一个对象。而享元模式是结构型模式,重在节约内存使用,提升程序性能。
享元模式:把一个或者多可对象缓存起来,用的时候,直接从缓存里获取。也就是说享元模式不一定只有一个对象