Design Pattern Course
Adapter is a Structural Design Pattern.
Adapter converts the interface of a class into another interface that the client expects, letting two otherwise incompatible interfaces work together.
The most common real-world trigger: integrating a legacy codebase with new code, or wrapping a third-party API whose interface doesn't match what your application expects, without modifying either the client code or the third-party class. The Adapter sits in between and translates calls in one direction.
A classic real-world analogy: a power plug adapter between a European socket (240V, round pins) and a US plug (120V, flat pins). Neither the socket nor the device changes — the adapter bridges the gap.
There are two structural forms of the Adapter pattern:
Object Adapter (preferred): Uses composition — the adapter implements the target interface and holds a reference to the adaptee object, delegating calls to it. This is the more flexible form, because the adapter can work with any subclass of the adaptee and doesn't require inheriting from it.
Class Adapter: Uses multiple inheritance — the adapter extends the adaptee class and implements the target interface. Not available in Java (which has single class inheritance), but possible in C++ and sometimes approximated in Java by extending the adaptee and implementing the target interface.
The example below is an object adapter (correct choice for Java):
public interface ITarget { void request(); }
public class Adaptee { public void specificRequest() { System.out.println("In Adaptee"); } }
public class Adapter implements ITarget { private final Adaptee adaptee; Adapter(Adaptee adaptee) { this.adaptee = adaptee; } @Override public void request() { System.out.println("Using Adapter"); this.adaptee.specificRequest(); } }
public class Main { public static void main(String[] args) { ITarget target = new Adapter(new Adaptee()); target.request(); } }
Output
Using Adapter
In Adaptee
One clarification on the original "no new functionality can be added" limitation listed below: that's not strictly true and is a common oversimplification. An adapter can add logic — reformatting data, translating exceptions, logging, or enriching calls — as long as the primary purpose is interface translation. What it shouldn't do is introduce significant new business logic, because that obscures what the adapter is for.
Benefits
- Lets two incompatible interfaces work together without modifying either side.
- Promotes reuse of existing, already-tested code (the adaptee) rather than rewriting it to fit a new interface.
- Isolates the interface-translation code in one place, making it easy to find and change if either interface evolves.
Limitations
- If you need to adapt many methods across many adaptees, you end up with many adapters, which adds classes and maintenance overhead.
- An adapter can become a maintenance liability if both the target interface and the adaptee change frequently — you're now tracking changes in two places.
- The "no new functionality" constraint above is not an inherent limitation, but it's good discipline: adapters that accumulate business logic become harder to maintain and test.
Adapter Design Pattern Examples in the JDK
java.util.Arrays#asList()— adapts an array to theListinterface.java.util.Collections#list(Enumeration)— adapts a legacyEnumerationto anArrayList.java.util.Collections#enumeration(Collection)— the reverse: adapts a modernCollectionto the legacyEnumerationinterface.java.io.InputStreamReader(InputStream)— adapts a byte-stream (InputStream) to a character-stream (Reader).java.io.OutputStreamWriter(OutputStream)— adapts a byte-stream (OutputStream) to a character-stream (Writer).