Appearance
Adapter
Convert the interface of one class into another interface that the client expects, so that those classes that cannot work together due to incompatible interfaces can work together.
The adapter pattern is known as Adapter or Wrapper. It refers to a situation where an interface requires interface B, but the object to be passed in is interface A. What should we do?
Let's take an example. If we go to the United States, the appliances we carry cannot be used directly because the standard of American sockets is different from that of China. Therefore, we need an adapter:
In program design, the adapter works similarly. We already have a Task
class that implements the Callable
interface:
java
public class Task implements Callable<Long> {
private long num;
public Task(long num) {
this.num = num;
}
public Long call() throws Exception {
long r = 0;
for (long n = 1; n <= this.num; n++) {
r = r + n;
}
System.out.println("Result: " + r);
return r;
}
}
Now, we want to execute it using a thread:
java
Callable<Long> callable = new Task(123450000L);
Thread thread = new Thread(callable); // compile error!
thread.start();
We find that it does not compile! This is because Thread
accepts the Runnable
interface but does not accept the Callable
interface. What should we do?
One way is to rewrite the Task
class, changing the implementation from Callable
to Runnable
, but this is not a good idea because Task
is likely referenced as a Callable
elsewhere. Changing the interface of Task
would cause other functioning code to fail to compile.
Another approach is to use an Adapter that transforms this Callable
interface into a Runnable
interface, which allows for successful compilation:
java
Callable<Long> callable = new Task(123450000L);
Thread thread = new Thread(new RunnableAdapter(callable));
thread.start();
The RunnableAdapter
class is the Adapter; it takes a Callable
and outputs a Runnable
. How do we implement this RunnableAdapter
? Let's first look at the complete code:
java
public class RunnableAdapter implements Runnable {
// Reference to the interface to be converted:
private Callable<?> callable;
public RunnableAdapter(Callable<?> callable) {
this.callable = callable;
}
// Implement the specified interface:
public void run() {
// Delegate the call to the converted interface:
try {
callable.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
The steps to create an Adapter are as follows:
- Implement the target interface, in this case,
Runnable
. - Internally hold a reference to the interface to be converted, here holding the
Callable
interface through a field. - In the implementation method of the target interface, call the method of the interface to be converted.
In this way, Thread
can accept this RunnableAdapter
because it implements the Runnable
interface. Thread
, as the caller, will call the run()
method of RunnableAdapter
, which in turn calls the call()
method of Callable
, effectively allowing Thread
to indirectly invoke Callable
's call()
method through a layer of transformation.
The Adapter pattern is widely used in the Java standard library. For example, if we hold a data type of String[]
but need the List
interface, we can use an Adapter:
java
String[] exist = new String[] {"Good", "morning", "Bob", "and", "Alice"};
Set<String> set = new HashSet<>(Arrays.asList(exist));
Notice that List<T> Arrays.asList(T[])
acts as a converter, allowing an array to be converted into a List.
Let’s look at another example: suppose we hold an InputStream
and want to call the readText(Reader)
method, but its parameter type is Reader
, not InputStream
. What should we do?
Of course, we use an adapter to transform InputStream
into Reader
:
java
InputStream input = Files.newInputStream(Paths.get("/path/to/file"));
Reader reader = new InputStreamReader(input, "UTF-8");
readText(reader);
InputStreamReader
is an Adapter provided by the Java standard library, responsible for adapting an InputStream
to a Reader
. There are similar classes like OutputStreamWriter
.
What would happen if we changed the parameter of readText(Reader)
from Reader
to FileReader
? In this case, since we need a FileReader
type, we must adapt InputStream
to FileReader
:
java
FileReader reader = new InputStreamReader(input, "UTF-8"); // compile error!
Using InputStreamReader
directly as an Adapter won't work because it only converts to the Reader
interface. In fact, converting InputStream
to FileReader
is possible but requires much more effort. This is where the principle of programming to abstractions comes into play: holding high-level interfaces not only makes the code more flexible but also makes it easier to combine various interfaces. Once a specific subclass type is held, making changes becomes very difficult.
Exercise
Use the Adapter pattern to adapt the Callable
interface to Runnable
.
Summary
The Adapter pattern can convert an A interface to a B interface, making the new object conform to the B interface specification.
Writing an Adapter essentially involves creating a class that implements the B interface while internally holding an A interface:
java
public class BAdapter implements B {
private A a;
public BAdapter(A a) {
this.a = a;
}
public void b() {
a.a();
}
}
In the Adapter, calls to the B interface are "converted" to calls to the A interface.
The Adapter pattern can be simply implemented when both A and B interfaces are abstract interfaces.