Java, from nothing · chapter 5 of 33
Interfaces: naming a contract
Chapter 1.5 · Part 1, Java from nothing · about 30 minutes
What you need before this chapter: chapter 1.4, Inheritance and polymorphism: extends, super, overriding, and dynamic dispatch. You should be able to write a subclass with extends, override a method with @Override, and explain why a variable's declared type and an object's real type can differ.
When you finish this chapter you will be able to:
- Explain what an interface is, in terms of what it promises rather than what it contains
- Write an interface and a class that implements it, and call the method through the interface type
- Say precisely why a class can implement many interfaces but extend only one class
- Read the compiler error for an unfinished implementation and the one for a genuine method conflict between two interfaces
1. The one idea in this chapter
Start with a Ticket that can be priced two different ways: by the hour, or a flat daily rate.
public class Ticket {
private final String plate;
private final int minutesParked;
private final boolean flatRate;
public Ticket(String plate, int minutesParked, boolean flatRate) {
this.plate = plate;
this.minutesParked = minutesParked;
this.flatRate = flatRate;
}
public int feeInRupees() {
if (flatRate) {
return 100;
}
int hours = (minutesParked + 59) / 60;
return hours * 20;
}
}
This works, and for two pricing rules it is a reasonable way to write it. Now a parking lot adds a monthly pass, a flat 1500 regardless of how the car is used, and a free staff ticket, fee zero. Both are a third and fourth branch in the same if. feeInRupees() grows a branch for every new pricing rule, and nothing checks that the flags on any one ticket actually agree with each other. Nothing stops someone from constructing a ticket with flatRate = true and also expecting hourly billing, because both fields exist on every object whether they apply or not.
A more natural shape is one class per kind of ticket: HourlyTicket, FlatRateTicket, MonthlyTicket. Each one only holds the fields its own pricing actually needs. You already know a tool for sharing behaviour across classes like this, from chapter 1.4: a common superclass with extends, and a method each subclass overrides. That works, and chapter 1.6 comes back to exactly that option. But it commits you to something before you have even written the classes. Every one of these ticket types now has to extend that one particular superclass, and a Java class can extends only one thing. If HourlyTicket later needs to extend something else, a class shared with a different part of the system, that path is closed, because the slot is already spent.
What you actually need here is smaller than a shared superclass. Every kind of ticket just needs to promise it can answer one question, "what do you cost?" It should not need to care how each one arrives at the number, or be forced into one family tree to make that promise. That promise, with nothing else attached to it, is what Java calls an interface:
public interface Priceable {
int feeInRupees();
}
Priceable is not a class. It declares one method with no body at all, just its name, its parameters, and what it returns. Any class can say implements Priceable, and by doing so, it promises to provide a real body for feeInRupees(). The interface holds no fields and no state of its own. It names a capability, and leaves every detail of how that capability works to whichever class implements it. A variable declared with the interface as its type, Priceable p, can hold a reference to an object of any class that implements Priceable. Calling p.feeInRupees() then runs whichever version that particular object actually provides, the same dynamic dispatch chapter 1.4 introduced for extends and @Override, working here without any shared superclass at all.
One paragraph of vocabulary before moving on, because interviewers open with this question directly. Object-oriented Java rests on four ideas, and this course teaches all four without ever bundling them into one chapter. Encapsulation, hiding an object's data behind methods that guard it, is chapter 2.1. Inheritance builds one class on another with extends. Polymorphism lets one reference type cover many real runtime types. Both are chapter 1.4, the chapter just behind you. Abstraction is the remaining one, and it is this chapter's actual subject: separating what something does from how it does it. Code written against the "what" then keeps working no matter how many "how"s get added later. An interface is Java's most direct tool for abstraction. Priceable says what a priceable thing can do and says nothing at all about how.
2. Type this
Four files, in the same folder. First, the interface itself, Priceable.java:
public interface Priceable {
int feeInRupees();
}
Now HourlyTicket.java:
public class HourlyTicket implements Priceable {
private final String plate;
private final int minutesParked;
public HourlyTicket(String plate, int minutesParked) {
this.plate = plate;
this.minutesParked = minutesParked;
}
@Override
public int feeInRupees() {
int hours = (minutesParked + 59) / 60;
return hours * 20;
}
}
Now FlatRateTicket.java:
public class FlatRateTicket implements Priceable {
private final String plate;
public FlatRateTicket(String plate) {
this.plate = plate;
}
@Override
public int feeInRupees() {
return 100;
}
}
And finally Checkout.java, which uses both without knowing, or needing to know, which is which:
public class Checkout {
static void printReceipt(Priceable ticket) {
System.out.println("fee: " + ticket.feeInRupees());
}
public static void main(String[] args) {
HourlyTicket hourly = new HourlyTicket("KA-01-4432", 130);
FlatRateTicket flat = new FlatRateTicket("MH-12-9001");
printReceipt(hourly);
printReceipt(flat);
Priceable seasonPass = () -> 0;
printReceipt(seasonPass);
}
}
That last block before printReceipt(seasonPass) deserves a note before you run it. () -> 0 is a lambda expression: a compact way to provide a body for an interface that declares exactly one method, without writing a whole class for it. Priceable has exactly one method, so Java can tell that () -> 0 means "here is a feeInRupees() that takes no arguments and returns 0." This only works because there is exactly one method to mean. An interface with two methods could never be written this way, because Java would have no way to know which method the lambda is a body for. This course does not go further into lambdas here. The point worth keeping is narrow: a single-method interface and a lambda are two views of the same thing in modern Java.
3. Run it
javac Priceable.java HourlyTicket.java FlatRateTicket.java Checkout.java
java Checkout
You should see exactly this:
fee: 60
fee: 100
fee: 0
hourly was parked 130 minutes; (130 + 59) / 60 is 3 using integer division, and 3 * 20 is 60. flat always costs 100. seasonPass, built from a lambda instead of a class, costs 0. Three completely different kinds of object, one line of code that handles all of them.
4. What just happened, line by line
public interface Priceable { declares the interface. Every method inside an interface is implicitly public, whether you write the word or not. An interface with a private contract would be useless, since nothing outside could ever see what it promises.
int feeInRupees(); ends in a semicolon, not a body. This is legal only inside an interface, and it means exactly what it looks like: a name and a signature with no implementation. Any concrete class implementing Priceable must supply the body.
public class HourlyTicket implements Priceable { is the class making the promise. implements, not extends, because HourlyTicket is not building on Priceable the way Car built on Vehicle in chapter 1.4. It is agreeing to a contract, not inheriting a starting point. HourlyTicket still extends Object implicitly, the way every class does, and it can extend one more class beyond that if it needs to; implements Priceable costs it nothing.
@Override public int feeInRupees() { provides the body the interface demanded. @Override here means the same thing it meant in chapter 1.4: this method fills a slot that already exists, declared by Priceable. The compiler checks that a slot with this exact signature actually exists to fill.
static void printReceipt(Priceable ticket) { takes a parameter of the interface type. This is where the whole design pays off: printReceipt was written once, against Priceable, and it works for every class that will ever implement Priceable, including ones that do not exist yet. Add a SeasonPassTicket class next year, and printReceipt needs no change at all, because it never knew about HourlyTicket or FlatRateTicket specifically. It only ever knew about the promise.
Priceable seasonPass = () -> 0; declares a variable of the interface type and assigns it a lambda. seasonPass never has a class of its own that you wrote; Java generates one behind the scenes to hold that single method body. From printReceipt's point of view, it is indistinguishable from any other Priceable.
5. Errors you are likely to hit
You implement an interface without finishing it. Declare a class that says implements Priceable and never provide feeInRupees():
public class Broken implements Priceable {
private final String plate;
public Broken(String plate) {
this.plate = plate;
}
}
Broken.java:1: error: Broken is not abstract and does not override abstract method feeInRupees() in Priceable
public class Broken implements Priceable {
^
Every method an interface declares without a body is implicitly abstract. A class implementing that interface must supply a body for every one of them. The alternative is declaring the class itself abstract and leaving a further subclass to finish it, which chapter 1.6 covers. Ordinary classes do not get to implement an interface halfway.
You call a method the interface type does not know about. Declare ticket as Priceable, then try to call a method that only HourlyTicket actually has:
public class WrongCall {
public static void main(String[] args) {
Priceable ticket = new HourlyTicket("KA-01-4432", 130);
System.out.println(ticket.getPlate());
}
}
WrongCall.java:4: error: cannot find symbol
System.out.println(ticket.getPlate());
^
symbol: method getPlate()
location: variable ticket of type Priceable
The object really does have a plate, but the compiler checks the declared type of the variable, Priceable, not the real class of the object it happens to hold right now. Priceable never promised getPlate(), so code holding a Priceable reference cannot call it, even though the actual object underneath could answer it. This is the same rule chapter 1.4 introduced for Vehicle and Car: what you can call depends on the reference type, and what runs depends on the real type.
6. Your turn
Add a fourth kind of ticket, MonthlyTicket, implementing Priceable, charging a flat 1500 no matter how long the car stays. Then call printReceipt with one. Do it before reading on.
The answer.
public class MonthlyTicket implements Priceable {
private final String plate;
public MonthlyTicket(String plate) {
this.plate = plate;
}
@Override
public int feeInRupees() {
return 1500;
}
}
Called as printReceipt(new MonthlyTicket("KA-01-4432")), this prints:
fee: 1500
Notice that Checkout needed no change at all beyond the one new call. That is the entire benefit this chapter has been building toward: a fourth pricing rule cost one new file.
7. Going deeper: the one diamond Java actually has
You may have heard that Java has no diamond problem. That is the classic issue from languages that let a class inherit implementation from two parents at once, leaving it ambiguous which parent's version of a shared method wins. Java genuinely avoids that for extends, since a class can extend only one other class. But interfaces can carry actual code too, through the default keyword, and that reopens a narrower version of the same problem.
interface Flyer {
default String move() {
return "flying";
}
}
interface Swimmer {
default String move() {
return "swimming";
}
}
class Duck implements Flyer, Swimmer {
}
public class Diamond {
public static void main(String[] args) {
System.out.println(new Duck().move());
}
}
Save this as Diamond.java and compile it, and:
Diamond.java:13: error: types Flyer and Swimmer are incompatible;
class Duck implements Flyer, Swimmer {
^
class Duck inherits unrelated defaults for move() from types Flyer and Swimmer
1 error
Java refuses to guess which move() a Duck should get, and there is no built-in resolution rule the way some other languages have. The fix is to override move() in Duck yourself, and inside that override, you can name exactly which interface's version to run using InterfaceName.super.method():
class Duck implements Flyer, Swimmer {
@Override
public String move() {
return Flyer.super.move() + " and " + Swimmer.super.move();
}
}
Replace Duck in the file with this version and it compiles. Running it prints this single line:
flying and swimming
The honest reason default methods exist is narrower than it looks. They let a library add a new method to an interface that already has implementers scattered across the world, without breaking every one of them the day the interface changes. Reaching for default in your own code is different. Sharing behaviour between implementers of an interface you wrote yourself is usually a sign you want the abstract class from chapter 1.6 instead. It can hold state a default method never can.
8. Why this matters in an interview
The Priceable interface is a small version of a named pattern, Strategy, covered directly in chapter 3.1: a family of interchangeable behaviours, chosen by which object you hand to a method. That method knows only the interface. Recognising that shape is a consistently rewarded move in a design interview: "this varies by type, and the caller should not need a conditional to handle it." The if (flatRate) version from the start of this chapter is exactly the warning sign interviewers watch for, a method that keeps growing branches every time the business adds a new case.
The second thing worth carrying forward is the distinction the errors in section 5 were built to teach. A variable's declared type sets the ceiling on what you can call through it, no matter what object it actually holds at runtime. Expecting a Priceable reference to expose behaviour only one implementation has is a common source of confusion for people who have only just met interfaces. Being able to say precisely why the compiler is right to refuse it is worth more in an interview than getting the design right by accident.
Next: chapter 1.6, Abstract classes, and when an interface is better. HourlyTicket and FlatRateTicket both store a plate and both need to print a receipt the same way, and copying that into every class that implements Priceable is the cost the next chapter exists to remove.
← 1.4 Inheritance and polymorphism: extends, super, overriding, and dynamic dispatch · All chapters · 1.6 Abstract classes, and when an interface is better →