Interfaces in Java — theory with payments, cards and a mobile wallet (and the method where I forgot public)
My first interface had a single useful line: boolean pay(double amount);. I wrote the Card class, copied the signature word for word and hit compile. This is what Java sent back:
A1.java:6: error: pay(double) in Card cannot implement pay(double) in Payable
boolean pay(double amount) { // public is missing!
^
attempting to assign weaker access privileges; was public
1 error
“Weaker? I didn’t put anything in the interface!” And that was exactly the problem: because I hadn’t put anything, Java had already put public for me. What you don’t write in the interface, Java adds for you; what you don’t write in the class, doesn’t appear by itself. That tiny asymmetry sums up most of today’s theory: an interface is a contract, and signing it comes with small print.
This is the theory of interfaces with one example we’ll use from start to finish: a payments system where it doesn’t matter whether you pay with a card, with a mobile wallet (like Bizum in Spain) or in cash. If you’ve already read the inheritance theory, you’ll see that interfaces solve exactly what inheritance falls short on. And if you’re coming from the exceptions theory, the idea will ring a bell: there, a general type (Exception) could hold a more specific one, and here the same happens with interfaces. At the end, I’ll send you to the interfaces practice to apply it with three complete programs.
Table of Contents
What an interface is
An interface is a contract: a list of things an object knows how to do, without saying how it does them. Whoever implements it promises to fulfil every point of the contract; whoever uses it only needs to know the contract, not who fulfils it.
Think of a power socket. The socket doesn’t care whether there’s a lamp, a charger or a toaster behind the plug: it only demands that the device has the right pins. In Java, that socket is the interface, and each pin is a method it declares.
Let’s start with ours:
public interface Payable {
double MAX = 1000.0; // constant: public static final by default
boolean pay(double amount); // public abstract by default
String name();
default String summary(double amount) { // has a body: implementers can use it as is
return name() + " ($" + String.format("%.2f", amount) + ")";
}
static boolean isValidAmount(double amount) { // belongs to the interface, not to the objects
return amount > 0 && amount <= MAX;
}
}
What you can see here, and what you can’t:
payandnamehave no body: just the signature. They are the points of the contract.- There’s no
publicorabstracton the methods because Java adds them itself. Every method in an interface ispublic abstractunless it’sdefaultorstatic. MAXis a constant: in an interface, every field ispublic static finaleven if you don’t write it.summaryis adefaultmethod (it has a body) andisValidAmountisstatic. We’ll look at both calmly further down.
By convention, interface names often end in -able / -ible when they describe a capability (Payable, Comparable, Playable) or are nouns when they describe a role (List, Map). Both are valid.
Interface vs abstract class
This is probably the most common doubt: “if an abstract class also has methods without a body, why do I need interfaces?”. The difference isn’t in the methods, but in what they represent.
| Abstract class | Interface | |
|---|---|---|
| What it expresses | “is a kind of…” (a family) | “knows how to…” (a capability) |
| Keyword | extends | implements |
| How many can I use? | Only one | As many as I want |
| Instance fields (state) | Yes | No (constants only) |
| Constructors | Yes | No |
| Methods with a body | Yes, of any kind | Only default and static |
| Member modifiers | Whatever you like | Everything is public |
Can you new it? | No | No |
Three quick questions to decide:
- Do I need to share state (fields) or constructor code? → abstract class.
- Are the classes unrelated, but they share a capability? (a
Cardand aWalletaren’t “family”, but both can be paid with) → interface. - Does one class have to play several roles at once? → interfaces, because inheritance only allows one parent.
That last one is the key. With inheritance, a class can have only one parent. If you try two, Java doesn’t even try:
class Device { }
class Sound { }
class Speaker extends Device, Sound {
}
A8.java:4: error: '{' expected
class Speaker extends Device, Sound {
^
1 error
A class, on the other hand, can implement as many interfaces as it needs: class Speaker implements Switchable, Playable is perfectly valid. It’s Java’s way of getting “almost” multiple inheritance without the problems it would cause with state.
A practical rule. When in doubt, start with an interface. It’s easier to turn an interface into an abstract class than the other way round, and it doesn’t close the door on inheritance for whoever uses it.
Implementing an interface: implements and public
To fulfil the contract, the class writes implements and overrides every abstract method:
public class Card implements Payable {
protected double limit; // protected: subclasses can see it
public Card(double limit) {
this.limit = limit;
}
@Override
public boolean pay(double amount) { // public: mandatory, the interface declares it public
if (amount > limit) {
return false;
}
limit -= amount;
return true;
}
@Override
public String name() {
return "Card";
}
public double remainingLimit() { // only exists in Card, not in Payable
return limit;
}
}
Three details worth looking at:
@Overrideisn’t mandatory, but it’s highly recommended: if you get the signature wrong, Java warns you instead of quietly creating a different method.publicis mandatory onpayandname. The interface’s methods are public and a class cannot reduce the visibility of what it inherits (the rule that caught me at the start of the article).remainingLimit()is an extra method: the interface doesn’t know about it. A class can have as many methods as it wants besides the ones in the contract.
Two more classes that fulfil the same contract, each in its own way:
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";
}
}
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";
}
@Override
public String summary(double amount) { // we override the default
return "Cash in hand ($" + String.format("%.2f", amount) + ")";
}
}
Cash does something extra: it **overrides summary**, which already came with a body in the interface. We’ll come back to it when we reach default.
Typical mistakes when implementing
If you forget one of the methods in the contract, Java tells you like this:
interface Payable {
boolean pay(double amount);
String name();
}
class Check implements Payable {
public boolean pay(double amount) {
return true;
}
// name() is missing
}
A2.java:6: error: Check is not abstract and does not override abstract method name() in Payable
class Check implements Payable {
^
1 error
“is not abstract and does not override abstract method”. It means the class says implements Payable, but hasn’t written one of the contract’s methods (here, name()). Look at the end of the message: it tells you which method is missing and in which interface it was declared. Write it with public and you’re done, or declare the class abstract if you really don’t want to implement it there.
And the flip side: you can’t instantiate an interface, just like an abstract class.
A3.java:7: error: Payable is abstract; cannot be instantiated
Payable p = new Payable();
^
1 error
new Payable() doesn’t compile: an interface has no objects of its own. What you can do is new Card() and store it in a variable of type Payable, which is exactly the next section.
Polymorphism with the interface type
This is where interfaces show their worth. An interface is a type: you can declare variables, parameters, arrays and return values of type Payable, and store inside any object whose class implements it.
public class Charge {
public static void main(String[] args) {
Payable[] methods = { new Card(100), new Wallet(30), new Cash(20) };
double amount = 25;
if (!Payable.isValidAmount(amount)) {
System.out.println("Invalid amount");
return;
}
for (Payable p : methods) {
boolean ok = p.pay(amount);
System.out.println(p.summary(amount) + " -> " + (ok ? "paid" : "declined"));
if (p instanceof Card t) {
System.out.printf(" Remaining limit: $%.2f%n", t.remainingLimit());
}
}
System.out.println("Maximum allowed: " + Payable.MAX);
}
}
Output:
Card ($25.00) -> paid Remaining limit: $75.00 Wallet ($25.00) -> paid Cash in hand ($25.00) -> declined Maximum allowed: 1000.0
The for (Payable p : methods) loop neither knows nor cares what’s inside each position. It calls p.pay(amount) and Java runs **the real object’s pay** (the one from Card, Wallet or Cash). It’s the same polymorphism you saw in inheritance, but now without the three classes sharing a common parent.
Notice three things in the output:
Cashcame out asCash in hand ($25.00): it used its ownsummary, whileCardandWalletused the interface’s.Cashwas declined because its purse holds $20 and we asked for $25. That’s not a bug: it’s the contract at work.Maximum allowed: 1000.0came from the constant, called with the interface’s name.
What the variable “sees”: the static type
A variable of type Payable **can only use what Payable declares**, even if the object inside is a Card with more methods:
interface Payable {
boolean pay(double amount);
}
class Card implements Payable {
public boolean pay(double amount) { return true; }
public double remainingLimit() { return 75; }
}
public class A6 {
public static void main(String[] args) {
Payable p = new Card();
System.out.println(p.remainingLimit());
}
}
A6.java:13: error: cannot find symbol
System.out.println(p.remainingLimit());
^
symbol: method remainingLimit()
location: variable p of type Payable
1 error
“cannot find symbol … location: variable p of type Payable”. The remainingLimit() method exists in Card, but the variable p is of type Payable and the compiler only looks at the variable’s type, not the object’s. Look at the last line of the message: it tells you what type it was declared with.
To use Card‘s own methods you have to ask first whether the object is of that class and, only then, convert it. That’s what Charge does:
if (p instanceof Card t) { // is it a Card? If so, I call it t
System.out.printf(" Remaining limit: $%.2f%n", t.remainingLimit());
}
That form (instanceof Card t) is called pattern matching and has existed since Java 16: it checks and declares the converted variable in a single step. The classic version, which you’ll still see in lots of notes and in Python Tutor, is:
if (p instanceof Card) {
Card t = (Card) p; // explicit cast
System.out.println(t.remainingLimit());
}
Casting: from interface to class and back
- From class to interface (upwards): it’s automatic, no cast.
Payable p = new Card(100);works as is because everyCardis aPayable. - From interface to class (downwards): you have to write the cast by hand, because the compiler can’t know what’s inside.
A7.java:12: error: incompatible types: Payable cannot be converted to Card
Card t = p;
^
1 error
“incompatible types: Payable cannot be converted to Card”. Without a cast, Java doesn’t trust you. With a cast ((Card) p) it compiles, but if the object inside is not a Card, it fails at runtime with a ClassCastException. That’s why you should always protect it with instanceof.
instanceof also works with interfaces. p instanceof Refundable asks whether the object fulfils that capability, which is very handy when a collection mixes objects with different capabilities.
default, static and constants
Since Java 8, interfaces can carry code. There are three pieces, and each answers a different need.
default methods
A default method is a method with a body inside the interface. It gives a “default” version that classes can use as is or override.
default String summary(double amount) {
return name() + " ($" + String.format("%.2f", amount) + ")";
}
- Notice that
summary**callsname()**, which is abstract. That’s the nice part: the default uses the contract to build something new without knowing who implements it. CardandWalletget it for free.Cashoverrides it and returns a different text. You already saw that in the output.- Thanks to
default, you can add methods to an old interface without breaking the classes that already existed, which is what it was invented for.
static methods
A static method in an interface belongs to the interface, not to the objects. You call it with the interface’s name:
static boolean isValidAmount(double amount) {
return amount > 0 && amount <= MAX;
}
...
if (!Payable.isValidAmount(amount)) { ... }
It’s useful for grouping utilities related to the contract (validations, factories…). Important: it’s not inherited. Card.isValidAmount(5) doesn’t compile; you have to write Payable.isValidAmount(5).
Constants
Every field in an interface is public static final, even if you don’t write it. That’s why MAX = 1000.0 is a global constant you read with Payable.MAX. And that’s why you can’t change it:
interface Payable {
double MAX = 1000.0;
}
public class A4 {
public static void main(String[] args) {
Payable.MAX = 2000;
}
}
A4.java:7: error: cannot assign a value to static final variable MAX
Payable.MAX = 2000;
^
1 error
“cannot assign a value to static final variable”. An interface’s constants are final: they’re written in capitals and never reassigned. If you need a value that changes, it isn’t a constant: make it a field in the class.
Interfaces that inherit from interfaces
An interface can extend another (or several). It inherits all their methods and can add more:
public interface Refundable extends Payable { // an interface can extend another one
boolean refund(double amount);
default String policy() {
return "refund within 30 days";
}
}
Now, whoever implements Refundable has to fulfil both contracts: pay and name (from Payable) plus refund. On top of that, Refundable brings its own default, policy().
Let’s create another, independent capability:
public interface Splittable {
default String policy() {
return "payment in 3 instalments";
}
}
And a class that brings them all together: a premium card that inherits from Card and also implements two interfaces.
public class PremiumCard extends Card implements Refundable, Splittable {
public PremiumCard(double limit) {
super(limit);
}
@Override
public boolean refund(double amount) {
limit += amount;
return true;
}
// Both interfaces bring a default called policy(): Java does not choose for us
@Override
public String policy() {
return Refundable.super.policy() + " and " + Splittable.super.policy();
}
}
Notice the order in the header: first extends (only one parent) and then implements (as many as you want, separated by commas).
The diamond: two defaults with the same name
Refundable and Splittable both have a default called policy(). If a class implements both and doesn’t decide which one wins, Java refuses to choose for you:
interface Refundable {
default String policy() { return "refund within 30 days"; }
}
interface Splittable {
default String policy() { return "payment in 3 instalments"; }
}
class GoldCard implements Refundable, Splittable {
// two defaults with the same name and no decision
}
A5.java:9: error: types Refundable and Splittable are incompatible;
class GoldCard implements Refundable, Splittable {
^
class GoldCard inherits unrelated defaults for policy() from types Refundable and Splittable
1 error
“inherits unrelated defaults for policy() from types…”. There are two candidates for the same method and neither wins. The fix is to override it in the class and decide: pick one, combine them, or write a new one. To call a specific interface’s version you use the syntax InterfaceName.super.method(), as in PremiumCard.
Let’s try it:
public class Premium {
public static void main(String[] args) {
PremiumCard pc = new PremiumCard(200);
pc.pay(50);
pc.refund(20);
System.out.println("Policy: " + pc.policy());
System.out.printf("Limit: $%.2f%n", pc.remainingLimit());
Payable p = pc; // a PremiumCard IS a Payable...
System.out.println(p instanceof Refundable); // ...and also Refundable
System.out.println(p instanceof Splittable); // ...and Splittable
}
}
Policy: refund within 30 days and payment in 3 instalments Limit: $170.00 true true
Look at the last block: one single PremiumCard is a Payable, a Refundable and a Splittable at the same time. Depending on which “glasses” you look at it through (the variable’s type), you’ll use one set of capabilities or another.
If both interfaces declare the method as abstract (no body), there’s no conflict: you implement it once and fulfil both contracts. The problem appears when at least one brings a default. And if the clash is between a class and an interface, the class wins: a method inherited from a class takes priority over a default.
Watch it step by step in Python Tutor
Python Tutor runs the program line by line and draws the variables, objects and references. Choose Java in the dropdown. So it doesn’t get confusing, we keep the example minimal: two classes that fulfil the same contract and a loop that treats them equally. We keep it to the essentials, with no default and no pattern matching, so it’s easy to follow.
public class Tutor {
interface Payable {
boolean pay(double amount);
}
static class Card implements Payable {
double limit = 100;
public boolean pay(double amount) {
if (amount > limit) return false;
limit -= amount;
return true;
}
}
static class Cash implements Payable {
double purse = 20;
public boolean pay(double amount) {
if (amount > purse) return false;
purse -= amount;
return true;
}
}
public static void main(String[] args) {
Payable[] methods = { new Card(), new Cash() };
for (int i = 0; i < methods.length; i++) {
Payable p = methods[i];
boolean ok = p.pay(25);
System.out.println("Method " + i + ": " + ok);
}
}
}
Expected output:
Method 0: true Method 1: false
What to observe step by step:
- When
Payable[] methods = ...runs, notice that the array has two arrows pointing to two different objects: oneCardand oneCash. The interface doesn’t appear as an object: only objects of the concrete classes exist. - On the first lap,
Payable p = methods[i]makesppoint to theCardobject. When you step intop.pay(25), look at the method’s frame: you’re insideCard‘s **pay** andlimitdrops from 100 to 75. - On the second lap, the same line
p.pay(25)now steps into **Cash‘spay**. Since25 > 20, it returnsfalseandpursestays as it was. - Compare the two laps: the code doesn’t change, the object
ppoints to does. That’s polymorphism.
Summary and quick table
| I want to… | I write | Detail |
|---|---|---|
| Declare a contract | interface Payable { … } | Methods are public abstract on their own |
| Fulfil a contract | class Card implements Payable | Override everything, with public |
| Fulfil several contracts | implements A, B, C | Separated by commas |
| Inherit and also implement | class X extends Y implements A, B | extends goes first |
| Extend a contract | interface B extends A | It can extend several |
| Store any object that fulfils it | Payable p = new Card(100); | The variable’s type limits what you see |
| Ask about a capability | p instanceof Refundable | Works with classes too |
| Access what’s specific to the class | if (p instanceof Card t) { t.… } | Or cast (Card) p after checking |
| Give a default method | default String summary() { … } | It can be overridden |
| Resolve a diamond | A.super.method() | Or write a new one |
| Utility on the interface | static … isValidAmount(…) | Called as Payable.isValidAmount(…) |
| Constant | double MAX = 1000.0; | Always public static final |
Three ideas to take away:
- An interface is a contract: it says what can be done, not how. Implementing it forces you to override everything, and with
public. - The variable’s type rules at compile time; the real object rules at runtime. That’s why you need
instanceofto see what’s specific to a class. - One class, one parent; but as many interfaces as you like. If two
defaultmethods clash, you decide withInterface.super.method().
And now, let’s practise
With this you have the complete theory. In the interfaces practice we apply it with a payment gateway, a shopping cart that accepts anything payable and a remote control that operates devices with different capabilities. And if you want to review what lies underneath first (classes, objects and polymorphism), go back to the classes and objects theory.
Next step: practiceInterfaces in Java — practice with a payment gateway, a shopping cart and a remote control (and the abstract class that couldn’t handle two parents)Did this article help you? Here is how you can support SergioLearns →
