适配器模式与Java应用
在设计模式中或者说在很多编程场景下经常会使用到适配器,有心或无意。顾名思义,适配器就是将一个类的接口适配(包装/转换)成客户(调用者)希望的另一个接口。适配模式又可以细分为两类:
- 对象适配器
- 类适配器
对象适配器和类适配器的区别在于适配器(Adapter)与被适配者(Adaptee)的关系,对象适配器与被适配者关联(成员变量),或者称为委托(方法调用),而类适配器通过继承被适配者来实现。实现细节不同,但最终的目的和效果是一样的,下面以对象适配器为例来进一步认识这个设计模式。
1、代码模型
// 需要被适配的类(已存在的、存在特殊功能但不符合既有的标准接口的类)
class Adaptee {
public void specialMethod() {
System.out.println("Invoking specialMethod.");
}
}
// 目标接口
interface Target {
public void method1();
public void method2();
public void method3();
}
// 适配器类
class Adapter implements Target {
// 直接关联被适配类
private Adaptee adaptee;
// 通过通过构造方法传入需要被适配的对象
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
// 通过委托的方式调用被适配对象的特殊方法
public void method1() {
this.adaptee.specialMethod();
}
public void method2() {
System.out.println("Invoking method2.");
}
public void method3() {
System.out.println("Invoking method3.");
}
}
2、JDK中的应用
- java.io.InputStreamReader(InputStream)
- java.io.OutputStreamWriter(OutputStream)
- java.util.Collections#synchronizedCollection()
- java.util.Arrays#asList()
- …
InputStreamReader类部分源码:
public class InputStreamReader extends Reader {
private final StreamDecoder sd;
public InputStreamReader(InputStream in) {
super(in);
try {
sd = StreamDecoder.forInputStreamReader(in, this, (String)null);
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}
}
public int read() throws IOException {
return sd.read();
}
public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length);
}
}
InputStreamReader是字节流转换为字符流的桥梁(适配器)。其中,Reader对应Target(接口或抽象类),InputStreamReader对应Adapter,InputStream对应Adaptee。
再看看java.util.Collections#synchronizedCollection()的相关实现(部分源码):
public class Collections {
public static <T> Collection<T> synchronizedCollection(Collection<T> c) {
return new SynchronizedCollection<>(c);
}
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
private static final long serialVersionUID = 3053995032091335093L;
final Collection<E> c; // Backing Collection
final Object mutex; // Object on which to synchronize
SynchronizedCollection(Collection<E> c) {
this.c = Objects.requireNonNull(c);
mutex = this;
}
SynchronizedCollection(Collection<E> c, Object mutex) {
this.c = Objects.requireNonNull(c);
this.mutex = Objects.requireNonNull(mutex);
}
public int size() {
synchronized (mutex) {return c.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return c.isEmpty();}
}
public boolean contains(Object o) {
synchronized (mutex) {return c.contains(o);}
}
public Object[] toArray() {
synchronized (mutex) {return c.toArray();}
}
public <T> T[] toArray(T[] a) {
synchronized (mutex) {return c.toArray(a);}
}
public Iterator<E> iterator() {
return c.iterator(); // Must be manually synched by user!
}
public boolean add(E e) {
synchronized (mutex) {return c.add(e);}
}
public boolean remove(Object o) {
synchronized (mutex) {return c.remove(o);}
}
public boolean containsAll(Collection<?> coll) {
synchronized (mutex) {return c.containsAll(coll);}
}
}
}
Java中常用的集合框架中的实现类HashSet、TreeSet、ArrayList、ArrayDeque、LinkedList、HashMap、TreeMap都是线程不安全的,如果有多个线程同时访问它们,且同时有多个线程修改他们的时候,将会出现如读脏数据等错误。Collections给出了解决方案,提供了synchronizedCollection方法来实现线程安全,该方法返回一个线程安全容器,可以理解为适配器。从源码中可以看到,SynchronizedCollection关联现有的collection对象,通过委托的方式调用现有对象的方法,只做了synchronized同步处理。
3、思考和总结
之前在维护一些旧程序时,发现线程安全问题导致程序异常、数据串行,原因是依赖的类库中某个类是非线程安全的,而应用中使用了多线程并发调用了这个类的方法。由于一些原因,无法改动依赖的类库。遇到这种问题,可以有两种方法解决:
- 同步应用代码块,在调用类库方法的地方加锁。
- 参考java.util.Collections#synchronizedCollection(),通过委托的方式同步调用类库的方法。
很明显,第二种方法(应用适配器模式)会更好。对于原有代码的改动最小或者几乎没有,只需要实现一个适配器(新增一个类),即可快速优雅的解决问题。
设计模式,归根究底就是一些经验总结,经过了反复使用和验证。还是学生的时候,就把二十多种设计模式看了一遍,当时觉得每个模式都讲的好有道理,好像很厉害,但现在想来,还真的就只是看了而已。后来,参加工作,写过和读过的代码越来越多,才发现能够应用才是比较重要的。例如上述的源码和解决方案,通过适配器,代码简洁优雅,清晰明了。学习,理解和应用,应当是一个持续和连贯的过程。