Interfaces in Java — practice with a payment gateway, a shopping cart and a remote control (and the abstract class that couldn’t handle two parents)

When I was asked to model a remote control for a TV, a speaker and a flashlight, I thought: “easy, abstract classes”. I created Device (it switches on and off), then HasSound (it plays music) and HasNetwork (it connects to WiFi). So far, so good. Then the TV showed up, which switches on, makes sound and connects, and without thinking I wrote class Television extends Device, HasSound, HasNetwork. Java answered with a dry '{' expected: a class can only have one parent.

I tried to patch it by chaining them (HasNetwork extends HasSound extends Device), but then the flashlight, which only switches on, ended up inheriting a play() it didn’t want and didn’t know how to do. That’s when I understood what interfaces are for: to describe separate capabilities that each class chooses to combine. That’s the backbone of today’s three programs.

If you’ve already read the interfaces theory, this article is for putting it into practice: a payment gateway that tries payment methods in order, a shop whose cart accepts any product and any payment method, and a remote that controls devices with different capabilities. They’re medium difficulty and work with situations that look like real ones. Each one comes with commented code, the output and the questions that usually come up.

Before you start

Make sure you’re clear on:

  • A class implements an interface with implements and must override all its abstract methods, always with public.
  • A variable of interface type (Payable p) can hold any object whose class implements it, and it only “sees” the interface’s methods.
  • A default method comes with a body that classes can use or override, and if two interfaces bring the same default, the class has to decide with Interface.super.method().
  • instanceof lets you ask whether an object has a capability before you use it.

If any of these sounds shaky, review the theory before going on.

You can put all the files in this article in the same folder and compile them together. Programs 1 and 2 share Payable, Card and Wallet: program 2 reuses those three classes as they are. Program 3 stands on its own.

Program 1: a payment gateway

An online shop accepts several payment methods: a mobile wallet, a card and cash in store. When a purchase comes in, the gateway tries them in order and charges the first one that can pay. The card charges a 1 % fee; the other two don’t.

The idea

Every method has to be able to do two things: try to pay an amount and tell us its name. That’s the contract, the Payable interface. The gateway only knows that contract, so tomorrow you’ll be able to add a new method without touching it.

The fee is a special case: most methods don’t charge one, but the card does. If it were abstract, Wallet and Cash would each have to write return 0. That’s why it’s a default method that returns 0 and that the card overrides.

Before coding, think about what PaymentGateway needs to know about each method: whether it can pay, what it’s called and how much fee it charges. Not one thing more.

The code

Payable.java

public interface Payable {

    boolean pay(double amount);
    String name();

    // By default there is no fee; whoever charges one overrides it
    default double fee(double amount) {
        return 0;
    }
}

Card.java

public class Card implements Payable {

    private double limit;

    public Card(double limit) {
        this.limit = limit;
    }

    @Override
    public double fee(double amount) {
        return amount * 0.01;                      // 1 % fee
    }

    @Override
    public boolean pay(double amount) {
        double total = amount + fee(amount);
        if (total > limit) {
            return false;
        }
        limit -= total;
        return true;
    }

    @Override
    public String name() {
        return "Card";
    }
}

Wallet.java

public class Wallet implements Payable {

    private double balance;

    public Wallet(double balance) {
        this.balance = balance;
    }

    @Override
    public boolean pay(double amount) {
        if (amount > balance) {
            return false;
        }
        balance -= amount;
        return true;
    }

    @Override
    public String name() {
        return "Wallet";
    }
}

Cash.java

public class Cash implements Payable {

    private double purse;

    public Cash(double purse) {
        this.purse = purse;
    }

    @Override
    public boolean pay(double amount) {
        if (amount > purse) {
            return false;
        }
        purse -= amount;
        return true;
    }

    @Override
    public String name() {
        return "Cash";
    }
}

PaymentGateway.java

public class PaymentGateway {

    // Tries the methods IN ORDER and returns the first one that manages to charge (or null)
    static Payable charge(Payable[] methods, double amount) {
        for (Payable method : methods) {
            double total = amount + method.fee(amount);
            if (method.pay(amount)) {
                System.out.printf("  %s: charged $%.2f%n", method.name(), total);
                return method;
            }
            System.out.printf("  %s: declined%n", method.name());
        }
        return null;
    }

    public static void main(String[] args) {
        Payable[] methods = { new Wallet(60), new Card(150), new Cash(25) };
        double[] purchases = { 45, 120, 300 };

        for (int i = 0; i < purchases.length; i++) {
            System.out.printf("Purchase %d: $%.2f%n", i + 1, purchases[i]);
            Payable used = charge(methods, purchases[i]);
            if (used == null) {
                System.out.println("  Could not charge");
            }
        }
    }
}

Output

Purchase 1: $45.00
  Wallet: charged $45.00
Purchase 2: $120.00
  Wallet: declined
  Card: charged $121.20
Purchase 3: $300.00
  Wallet: declined
  Card: declined
  Cash: declined
  Could not charge

Check it by hand:

  • Purchase 1 ($45): the wallet has $60, so it pays. $15 are left in the wallet. No fee: $45.00.
  • Purchase 2 ($120): the wallet only has $15 and declines. The card charges 120 + 1 % = $121.20, which fits within its limit of 150.
  • Purchase 3 ($300): the wallet has $15, the card only has $28.80 of limit left and the cash $25. None of them can cover it, and charge returns null.

PaymentGateway.charge receives a Payable[]. It doesn’t matter whether there are three different classes inside or twenty: the loop calls method.pay(...) and Java runs the method of the real object. That’s polymorphism with an interface type, which you saw in the theory.

Questions you might ask

**Why is fee a default and not abstract?** Because most methods don’t charge a fee. If it were abstract, Wallet and Cash would be forced to write a fee that returns 0, and every new method would have to repeat it. With default, only the card, which is the exception, overrides it.

**Why does charge receive a Payable[] and not a Card[]? Because then it would only accept cards. With the interface type it accepts anything that meets the contract, today and tomorrow. It’s the central idea of interfaces: program against the contract, not against the class**.

**Why do we calculate total before calling pay?** Because pay only returns a boolean, not how much it charged. To print the amount with the fee, we work it out ourselves using the same fee the card uses inside.

What if the array is empty or nobody can pay? charge returns null, and main checks it to print “Could not charge”. It’s a way of saying “there’s no result” without throwing anything.

Using the result of charge without checking whether it’s null. If in purchase 3 you wrote System.out.println(used.name()), the program would crash with a NullPointerException. Whenever a method can return null, check before using the result.

Try it yourself

  1. Create a Check class that implements Payable, with a fixed fee of $2, and add it to the array **without touching PaymentGateway**. Notice that nothing else needs to change: that’s the whole point.
  2. Change the order of the array (Cash first) and work out which method charges each purchase. Does the result of purchase 2 change?

Program 2: a shop with a cart

Now a shop that sells three types of product: books and T-shirts (which get shipped) and downloads (which don’t). The cart adds up the prices, adds shipping costs only for physical items and can be paid with any method from the previous program.

The idea

There are two different capabilities and it’s worth keeping them apart:

  • Sellable: has a name and a price. Everything in the cart meets it.
  • Shippable: has a shipping cost. Only physical products meet it.

A download is Sellable but not Shippable; a book is both. If we put shippingCost inside Sellable, the download would have to implement a method that makes no sense for it.

Also, finalPrice is a default method that returns the full price; the T-shirt, which has a 10 % discount, overrides it.

For the total, the cart goes through all the items and adds up finalPrice(). For shipping, ask each one whether it’s Shippable using instanceof.

The code

This program reuses Payable, Card and Wallet from the previous one (if you compile in the same folder, you already have them).

Sellable.java

public interface Sellable {

    String name();
    double price();

    // By default the full price is paid; discounted products override it
    default double finalPrice() {
        return price();
    }
}

Shippable.java

public interface Shippable {

    double shippingCost();
}

Book.java

public class Book implements Sellable, Shippable {

    private final String title;
    private final double price;

    public Book(String title, double price) {
        this.title = title;
        this.price = price;
    }

    @Override
    public String name() { return title; }

    @Override
    public double price() { return price; }

    @Override
    public double shippingCost() { return 3.00; }
}

TShirt.java

public class TShirt implements Sellable, Shippable {

    private final String model;
    private final double price;

    public TShirt(String model, double price) {
        this.model = model;
        this.price = price;
    }

    @Override
    public String name() { return model; }

    @Override
    public double price() { return price; }

    @Override
    public double finalPrice() {                  // 10 % discount
        return price * 0.90;
    }

    @Override
    public double shippingCost() { return 4.50; }
}

Download.java

public class Download implements Sellable {     // digital: not Shippable

    private final String title;
    private final double price;

    public Download(String title, double price) {
        this.title = title;
        this.price = price;
    }

    @Override
    public String name() { return title; }

    @Override
    public double price() { return price; }
}

Cart.java

public class Cart {

    private final Sellable[] items = new Sellable[10];
    private int count = 0;

    public void add(Sellable v) {
        items[count++] = v;
    }

    public double shipping() {
        double shipping = 0;
        for (int i = 0; i < count; i++) {
            if (items[i] instanceof Shippable e) {     // only physical products
                shipping += e.shippingCost();
            }
        }
        return shipping;
    }

    public double total() {
        double total = shipping();
        for (int i = 0; i < count; i++) {
            total += items[i].finalPrice();
        }
        return total;
    }

    public void receipt() {
        for (int i = 0; i < count; i++) {
            System.out.printf("%s · $%.2f%n", items[i].name(), items[i].finalPrice());
        }
        System.out.printf("Shipping: $%.2f%n", shipping());
        System.out.printf("TOTAL: $%.2f%n", total());
    }

    // The cart does not know whether it is paid by card, wallet or cash: only that it is Payable
    public boolean pay(Payable method) {
        boolean ok = method.pay(total());
        System.out.println("Payment with " + method.name() + ": " + (ok ? "accepted" : "declined"));
        return ok;
    }
}

Shop.java

public class Shop {

    public static void main(String[] args) {
        Cart cart = new Cart();
        cart.add(new Book("Java for FP2", 24.90));
        cart.add(new TShirt("SergioLearns T-shirt", 20.00));
        cart.add(new Download("Python course", 15.00));

        cart.receipt();

        // We try several methods until one works
        Payable[] methods = { new Card(50), new Wallet(100) };
        for (Payable method : methods) {
            if (cart.pay(method)) {
                break;
            }
        }
    }
}

Output

Java for FP2 · $24.90
SergioLearns T-shirt · $18.00
Python course · $15.00
Shipping: $7.50
TOTAL: $65.40
Payment with Card: declined
Payment with Wallet: accepted

Check it by hand:

  • Book: $24.90 · T-shirt: $20 − 10 % = $18.00 · Download: $15.00. Products subtotal: $57.90.
  • Shipping: only the book ($3.00) and the T-shirt ($4.50): $7.50. The download doesn’t count.
  • Total: 57.90 + 7.50 = $65.40 ✔.
  • Payment: the card has a limit of 50, and $65.40 plus its fee don’t fit → declined. The wallet has $100 → accepted, and the break stops it from trying anything else.

Questions you might ask

**Why is Shippable a separate interface and not a method inside Sellable?** Because not everything sellable gets shipped. A download would have to implement shippingCost() returning 0 or throwing an exception, which is exactly what small, separate interfaces avoid. One interface, one capability.

**Why do we use instanceof Shippable e and not instanceof Book || instanceof TShirt?** Because tomorrow a Sticker might arrive that is also shipped. If the cart asks about the capability (Shippable), that new class works without touching the cart. If it asks about concrete classes, you’d have to remember to edit it every time.

**Why is finalPrice a default and not abstract?** Because almost every product is sold at full price. Only the T-shirt overrides it to apply the discount. The rest inherit it without writing anything.

**Why does Cart.pay receive a Payable?** So the cart doesn’t need to know whether it’s paid by card, wallet or cash, and works the same with any method, present or future. The cart depends on the contract, not on any concrete class.

The cart’s array has 10 slots and add checks nothing. If you add the eleventh product, the program crashes with an ArrayIndexOutOfBoundsException. When you use fixed-size arrays, protect the entry point (or check count < items.length before adding).

Try it yourself

  1. Create a Sticker class that is Sellable and Shippable (price $2, shipping $1) and add it to the shop **without modifying Cart**. Check that shipping and the total update by themselves.
  2. Make add return a boolean: true if there was room and false if the cart was full. Try adding 11 products.

Program 3: a remote for devices

The last program is the remote I told you about at the start. We have three devices: a flashlight (it only switches on and off), a speaker (it also plays music) and a television (it also connects to a network, and plays nothing until it has one). The remote controls all of them with the same orders and each device reacts only to what it knows how to do.

The idea

Three capabilities, three interfaces:

  • Switchable: switches on, switches off and says whether it’s on.
  • Playable: plays a track and says which one is playing.
  • Connectable: connects to a network and says which one.

Each device chooses its own combination: the flashlight implements one, the speaker two and the television all three. Something that was impossible with abstract classes.

Switchable and Playable both have a default status() method. In the classes that implement both, we have to resolve the clash by combining what each one says.

The Remote holds a Switchable[], because everything switches on and off. For the orders that not every device understands (connect, play), ask first with instanceof.

The code

Switchable.java

public interface Switchable {

    String name();
    void switchOn();
    void switchOff();
    boolean isOn();

    default String status() {
        return isOn() ? "on" : "off";
    }
}

Playable.java

public interface Playable {

    void play(String track);
    String currentTrack();                            // null if nothing is playing

    default String status() {
        return currentTrack() == null ? "silent" : "playing \"" + currentTrack() + "\"";
    }
}

Connectable.java

public interface Connectable {

    void connect(String network);
    String currentNetwork();                              // null if there is no network
}

Flashlight.java

public class Flashlight implements Switchable {        // it can only be switched on and off

    private boolean on;

    @Override
    public String name() { return "Flashlight"; }

    @Override
    public void switchOn() { on = true; }

    @Override
    public void switchOff() { on = false; }

    @Override
    public boolean isOn() { return on; }
}

Speaker.java

public class Speaker implements Switchable, Playable {

    private boolean on;
    private String track;

    @Override
    public String name() { return "Speaker"; }

    @Override
    public void switchOn() { on = true; }

    @Override
    public void switchOff() {
        on = false;
        track = null;
    }

    @Override
    public boolean isOn() { return on; }

    @Override
    public void play(String track) {
        if (on) {
            this.track = track;
        }
    }

    @Override
    public String currentTrack() { return track; }

    // Both interfaces have a default status(): we must decide how to combine them
    @Override
    public String status() {
        return Switchable.super.status() + ", " + Playable.super.status();
    }
}

Television.java

public class Television implements Switchable, Playable, Connectable {

    private boolean on;
    private String track;
    private String network;

    @Override
    public String name() { return "Television"; }

    @Override
    public void switchOn() { on = true; }

    @Override
    public void switchOff() {
        on = false;
        track = null;
    }

    @Override
    public boolean isOn() { return on; }

    @Override
    public void connect(String network) { this.network = network; }

    @Override
    public String currentNetwork() { return network; }

    @Override
    public void play(String track) {
        if (on && network != null) {              // needs to be on AND connected
            this.track = track;
        }
    }

    @Override
    public String currentTrack() { return track; }

    @Override
    public String status() {
        return Switchable.super.status() + ", " + Playable.super.status()
                + ", network: " + (network == null ? "not connected" : network);
    }
}

Remote.java

public class Remote {

    private final Switchable[] devices;

    public Remote(Switchable[] devices) {
        this.devices = devices;
    }

    public void switchAllOn() {
        for (Switchable d : devices) {
            d.switchOn();
        }
    }

    public void switchAllOff() {
        for (Switchable d : devices) {
            d.switchOff();
        }
    }

    // Only those that are also Connectable / Playable get the order
    public void connectAll(String network) {
        for (Switchable d : devices) {
            if (d instanceof Connectable c) {
                c.connect(network);
            }
        }
    }

    public void playAll(String track) {
        for (Switchable d : devices) {
            if (d instanceof Playable r) {
                r.play(track);
            }
        }
    }

    public void show() {
        for (Switchable d : devices) {
            System.out.println("  " + d.name() + ": " + d.status());
        }
    }

    public static void main(String[] args) {
        Remote remote = new Remote(new Switchable[] { new Flashlight(), new Speaker(), new Television() });

        System.out.println("Everything on, no network:");
        remote.switchAllOn();
        remote.playAll("Bohemian Rhapsody");
        remote.show();

        System.out.println("With the network connected:");
        remote.connectAll("Home-WiFi");
        remote.playAll("Bohemian Rhapsody");
        remote.show();

        System.out.println("Switched off:");
        remote.switchAllOff();
        remote.show();
    }
}

Output

Everything on, no network:
  Flashlight: on
  Speaker: on, playing "Bohemian Rhapsody"
  Television: on, silent, network: not connected
With the network connected:
  Flashlight: on
  Speaker: on, playing "Bohemian Rhapsody"
  Television: on, playing "Bohemian Rhapsody", network: Home-WiFi
Switched off:
  Flashlight: off
  Speaker: off, silent
  Television: off, silent, network: Home-WiFi

Check it by hand:

  • Everything on, no network: the speaker plays straight away; the TV is on, but without a network it plays nothing, which is why it says “silent”.
  • With the network connected: the connectAll order only reaches the TV (the only Connectable one). When we ask for the track again, now it plays. The flashlight changes nothing: it can’t play or connect.
  • Switched off: switchOff clears the track, so the speaker and the TV end up “silent”. The TV keeps its network, which is a different piece of data.

Questions you might ask

**Why does Speaker have to override status()?** Because it implements two interfaces with a default status() and Java doesn’t know which to pick. With Switchable.super.status() and Playable.super.status() we call each version and join them with a comma.

**Why does Remote hold a Switchable[] and not an array of Object?** Because with Switchable the remote can call switchOn() and switchOff() directly. With Object you’d have to ask about the type everywhere. The interface type says exactly what you need to see.

**Why does connectAll ask instanceof Connectable c?** Because not every device connects. The question avoids calling a method the flashlight doesn’t even know about, and it also leaves the remote ready for any new device that one day is Connectable.

Why does the television need a network to make sound? It’s a rule of its own play, not of the contract: each class decides how it keeps its promise. The interface only says that play exists; what it does inside is up to each class.

If you remove the status() method from Speaker, the program doesn’t compile: “types Switchable and Playable are incompatible; class Speaker inherits unrelated defaults for status()…”. Two default methods with the same name in two interfaces force you to decide in the class.

Try it yourself

  1. Add an interface HasVolume with up() and down(), make the speaker and the television implement it, and add a volumeUp() method to the remote that only affects the devices that have it.
  2. Create a Lamp that implements only Switchable and add it to the remote. Do you have to change anything in Remote?

Common mistakes in this practice

  • **Forgetting public** when overriding an interface method: “attempting to assign weaker access privileges; was public”.
  • Leaving a method unimplemented: “is not abstract and does not override abstract method…”. The message tells you which one is missing.
  • Using a method of the class through a variable of interface type: “cannot find symbol”. First instanceof and a cast.
  • **Not resolving the clash of two default methods** with the same name in a class that implements both interfaces.
  • Casting without checking with instanceof, which can end in a ClassCastException.
  • **Using the result of a method that can return null** without checking it.

Watch it step by step in Python Tutor

Python Tutor runs code line by line and draws variables, objects and references. Choose Java. Here’s a cut-down version of the program 2 cart, with a variable of interface type, an instanceof and the check for a second capability. It’s written with the classic instanceof and cast (no pattern matching), which is easier to follow.

public class Tutor {
    interface Sellable {
        double price();
    }
    interface Shippable {
        double shipping();
    }
    static class Book implements Sellable, Shippable {
        public double price() { return 24.90; }
        public double shipping() { return 3.00; }
    }
    static class Download implements Sellable {
        public double price() { return 15.00; }
    }
    public static void main(String[] args) {
        Sellable[] cart = { new Book(), new Download() };
        double total = 0;
        for (Sellable v : cart) {
            total += v.price();
            if (v instanceof Shippable) {
                Shippable e = (Shippable) v;
                total += e.shipping();
            }
        }
        System.out.println("Total: " + total);
    }
}

Expected output:

Total: 42.9

What to watch, step by step:

  1. In Sellable[] cart = ..., notice that the array holds two references to different objects, a Book and a Download. Even though the array is of type Sellable, the objects keep their real class.
  2. On the first pass, total += v.price() goes into the price() of **Book**. Then the condition v instanceof Shippable is true: watch a new variable e appear, pointing to the same object as v (two names, one object).
  3. When total += e.shipping() runs, total goes from 24.9 to 27.9: the book gets shipped.
  4. On the second pass, v points to the Download. The condition v instanceof Shippable is false, so the block is skipped and e is never declared. The final total is 42.9.

Summary

  • An interface defines a contract of capabilities; a class can meet several at once, which is impossible with inheritance.
  • Variables, parameters and arrays of interface type let you write code that works with any class, present or future, that meets the contract.
  • default avoids repeating what almost everyone does the same way; whoever is different overrides it.
  • instanceof asks about a capability before using it; that way the code doesn’t depend on concrete classes.
  • If two interfaces bring the same default, the class decides with Interface.super.method().
  • A small, specific interface (Shippable) is better than a big one full of methods that not everyone uses.

Checklist: have you practised it?

  • [ ] I’ve compiled and run all three programs.
  • [ ] I’ve checked by hand at least one output of each program.
  • [ ] I’ve added a new class (Check, Sticker or Lamp) without touching the code that uses it.
  • [ ] I’ve triggered the duplicate default error and I know how to fix it with Interface.super.
  • [ ] I’ve tried instanceof with interfaces and I understand why it’s better than asking about concrete classes.
  • [ ] I’ve looked at the Python Tutor program and seen two variables pointing to the same object.

And now, on to the exercises: interfaces exercises, with three levels, three snippets to debug and a cheat sheet at the end. See you there!

Next step: exercisesInterfaces in Java — exercises with shapes, a library and sensors (and the cast that worked until a T-shirt showed up)

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *