Interfaces as contracts
On This Lesson 11 topics
Lesson Overview
In the last module you saw polymorphism: one call, many behaviors. You also saw its weak spot. A parent method with an empty "do nothing" body lets a subclass forget to override it, and the program quietly does the wrong thing. This module gives you tools to say "every type must supply this behavior" and have the compiler enforce it.
The first tool is the interface. Think of a wall socket: the wall does not care whether you plug in a lamp or a phone charger, as long as the plug fits. An interface is the shape of that socket. It lists what a class promises to do, and says nothing about how.
Why this matters: in a backend project, the code that processes an order should not care whether payment goes through one provider or another, or whether a message goes by email or SMS. Interfaces are how Java code expresses that.
This lesson builds on classes, extends, @Override, and polymorphism from the previous modules.
Learning Objectives
By the end of this lesson you will be able to:
- Declare an interface with one or more method signatures.
- Implement an interface in a class with
implements, and explain why every method must be written. - Implement the same interface in several different classes.
- Use an interface as a variable, array, or parameter type.
- Explain that a class can implement many interfaces while extending only one class.
- Describe what is implicitly
public,abstract,static, andfinalinside an interface. - Read the two most common compiler errors about interfaces.
Written Explanation
What an interface is
An interface is a type that lists methods without bodies. It is a contract: any class that signs it must provide every listed method.
interface Shape {
double area();
}There is no { ... } after area(), only a semicolon. That line says: "a Shape is anything that can report its area as a double". It does not say how to compute it.
A class signs the contract with implements and then writes each method:
class Circle implements Shape {
private final double r;
Circle(double r) { this.r = r; }
@Override public double area() { return Math.PI * r * r; }
}Use @Override here for the same reason as in the last module: the compiler checks that the method really implements something from the interface.
The Java tutorial describes interfaces as contracts: implementers may change their internals freely as long as they keep the published methods, and callers can be written against the interface without knowing the implementation.
The compiler enforces the contract
If a class says implements Shape but does not write area(), the code does not compile. Compare this with the empty-body default from the previous module: there, forgetting an override compiled and ran silently. Here, forgetting is impossible.
An interface is a type
You cannot write new Shape(): an interface has no code to run, so there is nothing to build. But you can use it as a type:
Shape[] shapes = { new Circle(1), new Rect(2, 3) };
for (Shape s : shapes) System.out.println(s.area());A Shape variable can refer to any object whose class implements Shape. This is the same upcasting you met in the polymorphism module, now with an interface on the left. The call s.area() runs whichever version the real object has. Through a Shape variable you can call only the methods that Shape declares.
Rules for what is inside an interface
| Member | What it means |
|---|---|
| Method without a body | Implicitly public and abstract. You do not need to write either word. |
| Field | Implicitly public static final: a constant. Interfaces hold no per-object state. |
default and static methods | Allowed, with bodies. Lesson 4 covers them. |
Per the Java Language Specification, a method with no access modifier in an interface is public, and a field is public static final. The consequence you will meet first: when a class implements the method it must also write public, because a class method without a modifier is package-private, and that is weaker access than the interface promised.
Many interfaces, one parent class
A class can extend only one class, but it can implement any number of interfaces, separated by commas:
class SmsNotifier implements Notifier, Auditable { ... }This is how Java lets one class play several roles without the tangles that come from inheriting state from several parents. Interfaces carry no instance state, so there is nothing to clash.
Naming and size
Name an interface after the role: Shape, Notifier, Payable. Keep it small and focused. An interface with one clear job is easy to implement and easy to fake in a test. An interface with ten unrelated methods (a "god" interface) forces every class to write methods it does not want. Tests and callers also become harder to read.
Where the analogy stops
A wall socket fixes the shape of the plug but not the safety of the device. An interface fixes the method signatures, but not what the methods do. A class can implement send by doing nothing at all. Meaning is documented in comments and checked by tests, not by the compiler.
Examples
Example 1: Shapes (simple)
interface Shape { double area(); }
class Circle implements Shape {
private final double r;
Circle(double r) { this.r = r; }
@Override public double area() { return Math.PI * r * r; }
}
class Rect implements Shape {
private final double w, h;
Rect(double w, double h) { this.w = w; this.h = h; }
@Override public double area() { return w * h; }
}
public class Ex1 {
public static void main(String[] args) {
Shape[] shapes = { new Circle(1), new Rect(2, 3) };
for (Shape s : shapes) System.out.printf("%.2f%n", s.area());
}
}Output (compiled and run with JDK 21):
3.14
6.00The loop treats both objects the same way. It does not use instanceof.
Example 2: One variable, different channels (practical)
interface Notifier { void send(String to, String message); }
class ConsoleNotifier implements Notifier {
@Override public void send(String to, String message) {
System.out.println("[console] " + to + ": " + message);
}
}
class EmailNotifier implements Notifier {
@Override public void send(String to, String message) {
System.out.println("[email] To <" + to + "> body: " + message);
}
}
public class Ex2 {
public static void main(String[] args) {
Notifier n = new ConsoleNotifier();
n.send("ana", "Hello");
n = new EmailNotifier();
n.send("ana@example.com", "Hello");
}
}Output:
[console] ana: Hello
[email] To <ana@example.com> body: HelloThe variable n has type Notifier in both lines. Only the object changes. (Printing stands in for real sending; the lab at the end of this module uses this exact interface.)
Example 3: Two interfaces, and what the compiler says (realistic)
interface Notifier { void send(String to, String message); }
interface Auditable { String auditId(); int LIMIT = 3; }
class SmsNotifier implements Notifier, Auditable {
@Override public void send(String to, String message) {
System.out.println("[sms] " + to + ": " + message);
}
@Override public String auditId() { return "sms-1"; }
}
public class Ex3 {
public static void main(String[] args) {
SmsNotifier s = new SmsNotifier();
Notifier n = s; Auditable a = s;
n.send("555", "Hi");
System.out.println(a.auditId() + " limit " + Auditable.LIMIT);
}
}[sms] 555: Hi
sms-1 limit 3One object, two types: seen as a Notifier it can send, seen as Auditable it has an id. LIMIT is a constant, reached through the interface name.
Now the two mistakes you will make sooner or later.
Forgetting the method.
class PushNotifier implements Notifier { }Bad1.java:2: error: PushNotifier is not abstract and does not override abstract method send(String,String) in NotifierForgetting public.
class PushNotifier implements Notifier {
void send(String to, String message) { }
}Bad2.java:3: error: send(String,String) in PushNotifier cannot implement send(String,String) in Notifier
attempting to assign weaker access privileges; was publicAnd trying to build the interface itself:
error: Notifier is abstract; cannot be instantiatedBackend Insight
Almost every boundary in a Spring or Jakarta backend is an interface: a repository that stores data, a service that sends messages, a client that calls another system. The reason is the contract. The code that uses the interface is written once. The implementation behind it (one database today, another tomorrow, a fake in tests) can change without touching the callers.
A practical habit follows: when you design a new piece of a backend, first write the interface from the caller's point of view ("what do I need to ask for?"), and write the class second. Keeping the interface small makes the later steps much easier, for example creating a tiny fake implementation for a test.
Common Mistakes
Mistake 1: Trying to instantiate an interface
- Incorrect:
Notifier n = new Notifier(); - Correct:
Notifier n = new ConsoleNotifier();
The type on the left is the interface. The class after new is a real class.
Mistake 2: Leaving out public on the implementation
Interface methods are public. The implementing method must be too, or you get the "weaker access privileges" error shown above.
Mistake 3: Calling a class-only method through the interface type
If EmailNotifier has an extra method setSubject(...), a Notifier variable cannot call it. Only methods declared in the interface are visible. If the caller needs it, the method probably belongs in the interface, or the caller is depending on a concrete class more than it should.
Mistake 4: The "god" interface
A single Service interface with send, save, print, login, and delete forces every class to write methods that make no sense for it. Split it into small interfaces with one purpose each.
Mistake 5: Putting state in an interface
Fields in an interface are constants, not per-object data. If you need each object to hold its own values, that belongs in a class (the next lesson).
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Write an interface Payable with one method that returns the amount due as a double.
Show solution A1
A1.
interface Payable { double amountDue(); }A2. Complete the sentence with a keyword: a class says it follows an interface using ____, while it inherits from a parent class using ____.
Show solution A2
A2. implements; extends.
A3. True or false: Payable p = new Payable(); compiles if Payable has only one method.
Show solution A3
A3. False. An interface cannot be instantiated: "Payable is abstract; cannot be instantiated".
Level B: Application
B1. Implement Payable in two classes: Invoice (a fixed total) and Subscription (monthly price times a number of months). Write a method static double total(Payable[] items) that adds up all amounts due.
Show solution B1
B1. (Compiled and run.)
interface Payable { double amountDue(); }
class Invoice implements Payable {
private final double total;
Invoice(double total) { this.total = total; }
@Override public double amountDue() { return total; }
}
class Subscription implements Payable {
private final double monthly; private final int months;
Subscription(double monthly, int months) { this.monthly = monthly; this.months = months; }
@Override public double amountDue() { return monthly * months; }
}
static double total(Payable[] items) {
double t = 0;
for (Payable p : items) t += p.amountDue();
return t;
}With new Invoice(120.0) and new Subscription(9.5, 4) the total printed 158.0.
B2. Make Invoice also implement a second interface Named with String name(). Show how one Invoice object can be held by a Payable variable and by a Named variable.
Show solution B2
B2.
interface Named { String name(); }
class Invoice implements Payable, Named {
// fields, constructor and amountDue() as before
@Override public String name() { return "Invoice for " + customer; }
}
Invoice inv = new Invoice("Omar", 10);
Payable p = inv;
Named n = inv;Through n you can call name() only; through p, amountDue() only. It is the same object.
B3. A teammate writes double amountDue() { return 5; } in a class that implements Payable. Predict the compiler message and fix it.
Show solution B3
B3. The compiler reports that amountDue() cannot implement the interface method because of weaker access privileges (it was public). The fix is public double amountDue() { return 5; }.
Level C: Challenge
C1. A colleague proposes one interface Worker with methods work(), sendEmail(), printReport(), payTax(). Explain two problems, and propose a better set of interfaces.
Show solution C1
C1. Problems: (1) a class that only works, such as a Robot, must still write sendEmail and payTax, probably as empty or throwing methods, which brings back the silent-failure problem; (2) callers depend on far more than they use, so tests need to fake all four methods. Better: Workable, EmailSender, Reportable, TaxPayer; each class implements the ones it really supports.
C2. Why can total(Payable[] items) work with a class written next year, after total was already compiled and shipped?
Show solution C2
C2. total is written against the contract (amountDue()), not against the classes. A new class that implements Payable can be put in the array, and the same compiled code calls its version of amountDue().
Mini Challenge
Choose something in a bank that comes in several kinds: a way of paying (card, cash, transfer), a way of exporting a report, or a way of checking a customer's identity. In under 20 lines:
- Write an interface with one focused method (name it for the role).
- Implement it in two classes, with printing in the method bodies.
- In
main, keep one variable of the interface type, point it at each object in turn, and call the method. - Break your code on purpose: delete
publicfrom one implementation and read the compiler message.
Check: your main never mentions a concrete class except after new.
Summary & Next Lesson
Key takeaways
- An interface lists method signatures: a contract that says what, not how.
- A class signs the contract with
implementsand must write every method, withpublic. - The interface works as a type: variables, arrays, and parameters can use it, and calls dispatch to the real object.
- A class can implement many interfaces, but extend only one class.
- Interface fields are constants. Interfaces hold no per-object state.
- Keep interfaces small. A "god" interface forces classes to write methods they cannot support.
Next lesson
The next lesson, "Abstract classes", covers the other half of the toolkit: a parent class that is partly finished, with shared fields and code, and with methods that subclasses are forced to complete.
Self-check
- What keyword does a class use to follow an interface?
- Why does
new Notifier()not compile whenNotifieris an interface? - A class implements an interface method but forgets
public. What happens? - Can one class implement two interfaces? Can it extend two classes?
- Through a
Notifiervariable, which methods can you call?
Show answers
implements.- An interface has no code or state to build an object from; only a real class can be instantiated.
- Compile error: the method tries to assign weaker access than the interface's public method.
- Yes to two (or more) interfaces. No to two classes: Java allows one superclass.
- Only the methods declared in the
Notifierinterface.
References
- Oracle, The Java Tutorials: Defining an Interface. Supports the contract idea, that interfaces cannot be instantiated, and that an implementing class must supply each method.
- Oracle, Java Language Specification SE 21, Chapter 9: Interfaces. Supports implicit
publicandabstractmethods andpublic static finalfields. The page's multiple-interfaces rule (8.1.5) was confirmed from its introduction only; the Java Tutorial's abstract-class page also states that a class may implement any number of interfaces but extend only one class. - Oracle, The Java Tutorials: Abstract Methods and Classes. Supports the one-superclass versus many-interfaces comparison. The compiler messages and outputs above come from compiling and running the examples with JDK 21.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.