Adapter Design Pattern in Java

Learn the Adapter pattern in Java with a runnable Fahrenheit-to-Celsius example, diagrams, and a failure case that preserves the sensor contract.

The Adapter pattern lets application code use an existing class whose API does not match the interface the application expects. An adapter implements the expected interface, translates the call, and delegates the work to the existing object. The caller can keep using its own interface while the existing class remains unchanged.

1. How the Adapter Pattern Works

The Adapter pattern connects a caller to an existing class whose interface does not match what the caller expects. Four participants take part:

Participant Role
Client Calls the operation it needs.
Target Defines the interface the client expects.
Adaptee Provides the useful behavior through a different interface.
Adapter Implements the target and delegates to the adaptee.

The client calls the target method on the adapter. The adapter calls the adaptee, then translates its result or failure to follow the target’s contract. The client can therefore use the existing behavior through its expected interface.

Client calls a target interface implemented by an adapter that holds the adaptee

2. Adapter Pattern in Action

A room-monitoring application displays temperatures in Celsius. The code that requests a reading should be able to ask for Celsius without knowing which device supplies the value. It therefore uses the TemperatureSensor interface, whose readCelsius() method defines the operation it needs.

An available device integration already has a LegacyThermometer class. It supplies a Fahrenheit reading through readFahrenheit(), so it does useful work but does not implement TemperatureSensor. The application uses FahrenheitAdapter to connect the two APIs. The adapter implements TemperatureSensor and holds a LegacyThermometer instance.

These are the pattern participants in this feature:

Pattern participant Java type Responsibility in the example
Client AdapterDemo Calls readCelsius() through TemperatureSensor.
Target TemperatureSensor Declares the Celsius-reading operation.
Adaptee LegacyThermometer Supplies Fahrenheit through readFahrenheit().
Adapter FahrenheitAdapter Converts a valid reading and translates a failed reading.

The adapter has to translate more than the temperature unit. In this example, LegacyThermometer returns -999.0 to signal that a reading failed. That value is a failure marker, not a temperature to convert. FahrenheitAdapter checks for it before doing the arithmetic and throws an exception that the calling code can handle.

The client constructs the adapter and calls the target operation:

TemperatureSensor sensor = new FahrenheitAdapter(new LegacyThermometer(77.0));
double celsius = sensor.readCelsius(); // 25.0

The variable’s type is TemperatureSensor, so the call site depends on the target interface. At runtime, FahrenheitAdapter.readCelsius() calls LegacyThermometer.readFahrenheit(). The legacy method returns 77.0. Because this is a valid reading, the adapter subtracts 32, multiplies by 5, and divides by 9. It returns 25.0 Celsius to the client.

The classes and that call path appear together here:

AdapterDemo uses TemperatureSensor; FahrenheitAdapter holds LegacyThermometer

If the legacy method returns -999.0, the adapter reports an unavailable reading instead of returning a converted number. The implementation and demo sections show both paths in full.

3. Adapter Pattern Implementation

The implementation keeps the target interface, legacy class and adapter inside AdapterDemo. The nested classes are static, which means they do not need an enclosing AdapterDemo instance. This packaging keeps the example in one runnable file; nesting is not required by the Adapter pattern.

The legacy class stores a supplied reading and returns it unchanged. That lets the demo exercise normal and failure cases without connecting to hardware. Its -999.0 failure marker is a sentinel value: a value reserved by this particular API to represent an unavailable reading.

import java.util.Objects;

public class AdapterDemo {
    interface TemperatureSensor {
        double readCelsius();
    }

    static final class LegacyThermometer {
        private final double reading;

        LegacyThermometer(double reading) {
            this.reading = reading;
        }

        double readFahrenheit() {
            return reading; // -999.0 means the legacy sensor failed.
        }
    }

    static final class FahrenheitAdapter implements TemperatureSensor {
        private final LegacyThermometer thermometer;

        FahrenheitAdapter(LegacyThermometer thermometer) {
            this.thermometer = Objects.requireNonNull(thermometer);
        }

        @Override
        public double readCelsius() {
            double fahrenheit = thermometer.readFahrenheit();
            if (fahrenheit == -999.0) {
                throw new IllegalStateException("Sensor reading unavailable");
            }
            return (fahrenheit - 32.0) * 5.0 / 9.0;
        }
    }

    public static void main(String[] args) {
        show(77.0);
        show(32.0);
        show(-999.0);
    }

    private static void show(double fahrenheit) {
        TemperatureSensor sensor = new FahrenheitAdapter(new LegacyThermometer(fahrenheit));
        try {
            System.out.printf("%.1f F -> %.1f C%n", fahrenheit, sensor.readCelsius());
        } catch (IllegalStateException ex) {
            System.out.printf("%.1f F -> %s%n", fahrenheit, ex.getMessage());
        }
    }
}

TemperatureSensor describes what room-monitoring code needs. LegacyThermometer describes what the existing component provides. The adapter implements the former and holds an instance of the latter. Its constructor uses Objects.requireNonNull() so a missing thermometer is rejected when the adapter is created.

The order inside readCelsius() matters. First, the adapter obtains the Fahrenheit reading. Next, it checks the sentinel. If the reading equals the failure sentinel, it throws IllegalStateException; otherwise it calculates and returns Celsius. Converting first would turn a failure marker into an apparently valid numeric result.

This is an object adapter because it delegates through a stored instance. LegacyThermometer is final, so it cannot be extended, but it can still be wrapped. The target contract returns a Celsius reading or throws IllegalStateException when a reading is unavailable. Application code can use this adapter or another implementation that honors that contract.

The demo’s show() helper catches the translated failure and prints a message. An application could display an unavailable-reading state or follow another recovery policy. The example recognizes only its defined sentinel; a real device integration must also define how to handle other invalid values and communication failures.

4. Demo

From the cloned repository root, compile and run the example:

javac -d out src/main/java/com/howtodoinjava/patterns/structural/adapter/AdapterDemo.java
java -cp out com.howtodoinjava.patterns.structural.adapter.AdapterDemo

The program submits 77.0, 32.0 and the failure sentinel. With a dot-decimal output locale, it prints:

77.0 F -> 25.0 C
32.0 F -> 0.0 C
-999.0 F -> Sensor reading unavailable

The 77.0 reading becomes 25.0 Celsius. The next call changes only the supplied reading to 32.0, which becomes 0.0 Celsius. Both calls return normally through the same TemperatureSensor operation.

The final call supplies the legacy failure marker. readCelsius() throws before reaching the conversion formula, and show() catches that exception. The third line consequently contains an error message instead of a Celsius value. Without that check, the formula would produce a numeric result from -999.0 even though no temperature had been measured.

5. Similar Patterns

A class adapter uses inheritance: it extends the adaptee and implements the target interface. Its adapting methods can then call inherited behavior. This requires the adaptee to permit subclassing and uses the adapter’s one available class superclass in Java.

Our object adapter uses composition: its field refers to a separately constructed adaptee. It can wrap the final LegacyThermometer even though Java prohibits extending that class. The constructor also makes the particular thermometer being adapted explicit.

Other wrapper-based patterns can look similar in code. Their purpose helps distinguish them:

Pattern Choose it when
Adapter An existing API does useful work but does not match the interface your caller needs.
Decorator You want to add behavior while keeping the same interface.
Facade You want a simpler entry point to a set of operations.
Bridge Two parts of a design need to vary independently.

Adapter solves an interface mismatch; Decorator keeps the interface and changes behavior. A wrapper can have more than one role, so judge it by what the caller needs it to accomplish.

6. Frequently Asked Questions

The adapter needs a clear target contract, including how it returns valid results and reports failures. These questions help define that boundary.

6.1. When should I use an Adapter?

Use one when you have a useful existing class but changing its API is impractical or would force callers to understand its details. Define the interface your application needs, then keep the translation at its boundary. If the old component cannot support an operation promised by that interface, an adapter cannot create that capability; the contract needs a clear limitation or another implementation.

6.2. Is forwarding a call enough?

Sometimes the only mismatch is a method name, so forwarding is enough. When the APIs disagree about units or errors, the adapter must translate those differences too. In this example, successful readings change units while unavailable readings remain failures.

Resource-handling APIs need another decision: which component closes the adapted resource? That responsibility should be part of the target contract so callers know whether closing the adapter also closes the component it wraps.

6.3. Is there an Adapter in the Java API?

InputStreamReader accepts an InputStream, which supplies bytes, and exposes the Reader API, which supplies characters. The translation includes decoding bytes with a character set; it is more than renaming a reading method.

For example, constructing the reader with StandardCharsets.UTF_8 selects UTF-8 decoding. The correct character set depends on how the input was encoded. The application must also account for closing the reader and its underlying stream, rather than assuming that every wrapper has an independent resource lifetime.

7. Conclusion

Adapter makes an existing API usable through the interface the application expects. The wrapper is successful when the new method preserves the old operation’s meaning, including valid results and failures.

Happy Learning!

The complete example is available in the GitHub source folder.

Leave a Comment

  1. Adapter class is a good example of object composition. Adapter class “has a” instance of the adaptee class.

  2. Can we say Hibernate is a real time example of Adaptor design pattern? It converts RDBMS specific query.

  3. Dear Lokesh,
    Adapter pattern is simple and powerful. Could you please post for Facade pattern as well would be appreciated,
    Thanks,
    Bala

  4. Hi Lokesh ,

    I liked your simple and straight explanation about concept and you wont eat time in telling big story, that’s really made me crazy to read your articles as and when I get time from my Job.

    Please do you have real time examples to focus on real time entities on Design pattern planned in future..awaiting for those:)

    Singleton : Per JVM one object Example: Helper, Utility classes and Service classes using spring
    Strategy Pattern : WE wanted to display price and based on rule defined by the Author , through UI he switch the price pattern then Spring I used to inject respective.
    Factory : Used this one scenario for payment gateway Integration, when command pattern asked me to care an object , then Factory will give back the Objects
    Builder pattern : DB and Front end data compatibility issue, DB stored in Interger and Fromt end need Integer or Front end want with formatting price bla bla …

    Thanks for your Adptor design pattern.

    Best Regards
    Vinod

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.