适配器模式(Adapter Pattern)是一种结构模式(Structural Design Pattern)。在日常生活中,人们经常会使用适配器。例如,当去欧洲旅游时,人们需要携带电源插头适配器,因为中国的电源接口与欧洲的电源接口不同。当人们连接电脑显示器时,也可能会使用DVI(Digital Visual Interface)转HDMI(High-Definition Multimedia Interface)适配器。这些适配器的作用和功能与适配器模式的作用和功能十分相似。
适配器模式的主要功能是适配不兼容的接口。当开发人员认为需要的功能已基本实现,但是因为接口不同无法直接使用时,可考虑使用适配器模式进行接口适配。适配器模式包含以下四个参与方。
图一 适配器模式结构图
我们使用一个日期和时间的例子来说明适配器模式的使用方法。如下面的代码所示,我们定义了一个接口DateFormatter,用于将LocalDate对象格式化为一个可读的字符串。DateFormatterImpl实现了这个接口。DateFormatter是适配器模式中的Adaptee,已实现的功能。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public interface DateFormatter {
String formatDate(LocalDate day);
}
public class DateFormatterImpl implements DateFormatter {
@Override
public formatDate(LocalDate date) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy MM dd");
return date.format(formatter);
}
}
可是,在项目推进过程中,开发人员还需要通过使用年、月、日三个参数的方式格式化一个日期字符串。显然,上述的DateFormatter并不支持这种传入三个参数的接口。因此,我们计划定义一个新的YearMonthDayFormatter接口,但是复用DateFormatter的功能。YearMonthDayFormatter是适配器模式中的Target。
public interface YearMonthDayFormatter {
String formatYearMonthDay(Integer year, Integer month, Integer day);
}
最后,我们开发一个适配器类DateFormatterAdapter,它实现了YearMonthDayFormatter的接口。因此,开发人员可以用年、月、日三个参数生成格式化字符串。在DateFormatterAdapter中保留了对DateFormatter的引用,因为,DateFormatterAdapter需要使用DateFormatter的功能来实现字符串格式化。DateFormatterAdapter是适配器模式中的adapter。
import java.time.LocalDate;
public class DateFormatterAdapter implements YearMonthDayFormatter {
private DateFormatter dateFormatter = null;
public DateFormatterAdapter(DateFormatter dateFormatter) {
this.dateFormatter = dateFormatter;
}
@Override
public String formatYearMonthDay(Integer year, Integer month, Integer day) {
return this.dateFormatter.formatDate(LocalDate.of(year, month, day));
}
}
在使用时,我们不能直接使用DateFormatter接口,而是需要通过YearMonthDayFormatter适配器,将不兼容的DateFormatter接口转换为可以直接使用的YearMonthDayFormatter接口。
public class AdapterPatternExample {
public static void main(String args) {
DateFormatter formatter = new DateFormatterImpl();
YearMonthDayFormatter adapter = new DateFormatterAdapter(formatter);
String dateString = adapter.format(2020, 10, 15);
}
}
在Java标准库中,我们常常使用适配器模式。我们将在本小节中展示两个例子InputStreamReader和Arrays.asList()。
Java标准库中的输入流(java.io.InputStream)和输出流(java.io.OutputStream)中运用了适配器模式。我们以输入流InputStream为例,阅读一下InputStream和InputStreamReader的源代码。类似的应用还存在于其他InputStream中和相应的OutputStream中。如果对它们的实现感兴趣,读者可自行阅读。
java.io.InputStream是Java标准库中的输入流。当调用read()方法时,程序会从输入流中读取数据,并存放在传入的数组参数中。可是,InputStream中的read()方法只能接受字节数组(Byte Array),而开发人员往往希望使用字符数组(Character Array),因为,程序可能处理的是字符串数据。因此,此时遇到了接口不兼容问题。
// OpenJDK 15
package java.io;
public abstract class InputStream implements Closeable {
...
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
...
}
...
}
为了解决这个接口不兼容问题,Java标准库还提供了java.io.Reader类,用于读取数据,并存放在字符数组中。Reader类中的read()方法的接口与InputStream中read()方法的功能非常相似,只是参数类型不同而以。
// OpenJDK 15
package java.io;
public abstract class Reader implements Readable, Closeable {
...
public int read(char cbuf[]) throws IOException {
return read(cbuf, 0, cbuf.length);
}
public abstract int read(char cbuf[], int off, int len) throws IOException;
...
}
java.io.InputStreamReader使用了适配器模式。它继承自Reader类,实现了read()方法。在InputStreamReader的构造函数中,它接受一个InputStream参数,用于保存读出数据的输入流对象。
// OpenJDK 15
package java.io;
public class InputStreamReader extends Reader {
private final StreamDecoder sd;
...
public InputStreamReader(InputStream in) {
...
}
public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length);
}
...
}
因此,开发人员可以使用InputStreamReader从一个InputStream对象中读取数据,并将其存放在字符数组中。InputStreamReader完成了接口的适配。在这个例子中,InputStreamReader是Adapter;Reader是Target;InputStream是Adaptee。
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
public class InputStreamReaderExample {
public void read(InputStream in) throws IOException {
char[] buf = new char[2048];
InputStreamReader reader = new InputStreamReader(in);
reader.read(buf);
}
}
java.util.List是Java标准库中用于表示链表的接口,也是Java程序中最为常见的集合接口(Collection)。通常情况下,开发人员可以使用new操作符来创建一个新链表对象。然而,List接口并未提供根据一组元素来创建链表的接口。
例如,当开发人员希望创建一个包含整数1和2的链表时,List接口无法满足这样的需求。开发人员不得不先创建一个空的链表对象,再向这个链表对象中插入元素1和2。这样实现的源代码较为冗长。
为了解决这个问题,Java标准库设计了Arrays.asList()方法。这个方法实际上运用的是适配器模式的思想。asList()方法接受可变个数的参数,返回一个包含传入元素的链表对象。换句话说,用户可以使用asList(1, 2)创建一个包含2个元素的链表。也可以使用asList(1, 2, 3, 4, 5)创建一个包含5个元素的链表。
import java.util.List;
import java.util.LinkedList;
import java.util.Arrays;
public class ListExample {
public static void main(String[] args) {
List l1 = new LinkedList();
l1.add(1);
l1.add(2);
List l2 = Arrays.asList(1, 2);
List l3 = Arrays.asList(1, 2, 3, 4, 5);
}
}
在java.util.Arrays类中,实际上asList()方法接受可变个数的参数a。在Arrays类中,还实现了一个私有的(private)内部类ArrayList。这个类不是开发人员常用的java.util.ArrayList类,只不过它们恰好名字相同。
Arrays类中的内部类ArrayList继承自AbstractList,实现了List接口。它的构造函数接受一个数组。所以,asList()方法能够直接使用可变参数a创建并返回一个List对象。
// OpenJDK 15
package java.util;
public class Arrays {
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
...
}
}
本章介绍了适配器模式。当项目中已实现类似的功能,但是又不能直接使用这个功能时,开发人员可以考虑设计和使用适配器模式,进行接口转换。
注册用户登陆后可留言