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.
Table of Contents
Before you start
Make sure you’re clear on:
- A class implements an interface with
implementsand must override all its abstract methods, always withpublic. - A variable of interface type (
Payable p) can hold any object whose class implements it, and it only “sees” the interface’s methods. - A
defaultmethod comes with a body that classes can use or override, and if two interfaces bring the samedefault, the class has to decide withInterface.super.method(). instanceoflets 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
chargereturnsnull.
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
- Create a
Checkclass that implementsPayable, with a fixed fee of $2, and add it to the array **without touchingPaymentGateway**. Notice that nothing else needs to change: that’s the whole point. - Change the order of the array (
Cashfirst) 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
breakstops 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
- Create a
Stickerclass that isSellableandShippable(price $2, shipping $1) and add it to the shop **without modifyingCart**. Check that shipping and the total update by themselves. - Make
addreturn aboolean:trueif there was room andfalseif 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
connectAllorder only reaches the TV (the onlyConnectableone). When we ask for the track again, now it plays. The flashlight changes nothing: it can’t play or connect. - Switched off:
switchOffclears 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
- Add an interface
HasVolumewithup()anddown(), make the speaker and the television implement it, and add avolumeUp()method to the remote that only affects the devices that have it. - Create a
Lampthat implements onlySwitchableand add it to the remote. Do you have to change anything inRemote?
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
instanceofand a cast. - **Not resolving the clash of two
defaultmethods** with the same name in a class that implements both interfaces. - Casting without checking with
instanceof, which can end in aClassCastException. - **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:
- In
Sellable[] cart = ..., notice that the array holds two references to different objects, aBookand aDownload. Even though the array is of typeSellable, the objects keep their real class. - On the first pass,
total += v.price()goes into theprice()of **Book**. Then the conditionv instanceof Shippableis true: watch a new variableeappear, pointing to the same object asv(two names, one object). - When
total += e.shipping()runs,totalgoes from 24.9 to 27.9: the book gets shipped. - On the second pass,
vpoints to theDownload. The conditionv instanceof Shippableis false, so the block is skipped andeis 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.
defaultavoids repeating what almost everyone does the same way; whoever is different overrides it.instanceofasks 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 withInterface.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,StickerorLamp) without touching the code that uses it. - [ ] I’ve triggered the duplicate
defaulterror and I know how to fix it withInterface.super. - [ ] I’ve tried
instanceofwith 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 →

One Comment