Java, from nothing · chapter 6 of 33
Abstract classes, and when an interface is better
Chapter 1.6 · Part 1, Java from nothing · about 25 minutes
What you need before this chapter: chapter 1.5, Interfaces: naming a contract. You should be able to write an interface, implement it in more than one class, and explain why a class can implement many interfaces but extend only one class.
When you finish this chapter you will be able to:
- Write an abstract class that holds shared fields, a shared constructor, and a shared method, while leaving one method for each subclass to finish
- Say exactly what an abstract class can hold that an interface cannot, and why
- Choose correctly between an interface and an abstract class for a given class design, and defend the choice
- Read the compiler errors for instantiating an abstract class directly and for leaving one unfinished
1. The one idea in this chapter
Chapter 1.5 left HourlyTicket and FlatRateTicket as two separate classes, each implementing Priceable, each storing its own plate field:
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 add a requirement: every ticket should be able to print its own receipt, in a fixed format, without Checkout needing to know how. The obvious first move is a printReceipt() method on each class:
public void printReceipt() {
System.out.println(plate + " owes Rs" + feeInRupees());
}
This line is correct. The problem is where it has to live. Priceable cannot hold it, because an interface cannot hold a field, and this method needs plate. So the method, word for word identical, has to be typed into HourlyTicket and again into FlatRateTicket, and again into every future ticket class. Two copies of the same six words are a minor cost today. The real cost shows up the day someone changes the receipt format, adding a currency code, say. Whoever makes that change has to remember every class that copied the method, edit each one identically, and hope none were missed. A method that exists in two places is a promise that both places will always agree, kept only by whoever remembers to keep it.
An interface cannot fix this, because its whole point is holding no state and no shared code, only a contract. What actually fixes it sits between "plain class" and "interface": a class that can hold real fields, a real constructor, and real method bodies, exactly like any other class. It can also declare a method with no body at all, the way an interface does, and refuse to let anyone create an instance of itself directly. Java calls this an abstract class:
public abstract class Ticket implements Priceable {
protected final String plate;
protected Ticket(String plate) {
this.plate = plate;
}
@Override
public abstract int feeInRupees();
public void printReceipt() {
System.out.println(plate + " owes Rs" + feeInRupees());
}
}
plate is stored once, here. The constructor that sets it is written once, here. printReceipt() is written once, here, and every subclass gets it for free through extends, the same inheritance chapter 1.4 already covered. Only feeInRupees() is left as a method with no body, marked abstract, for each subclass to supply. It is the one piece of behaviour that genuinely differs between an hourly ticket and a flat-rate ticket.
This gives you the rule the rest of the chapter fills in. An interface is the right tool when all you need is a shared promise, with nothing to store and nothing to hand out for free. It is particularly valuable because a class can implement as many interfaces as it needs. An abstract class is the right tool the moment there is real state or real behaviour worth sharing. Reaching for one costs something specific. Java allows a class to extends only one thing, ever, so the superclass slot spent on Ticket cannot also be spent on some other shared class HourlyTicket might need later. Prefer an interface. Reach for an abstract class only when you have shared state or shared implementation that genuinely needs a home.
2. Type this
Four files. First, Priceable.java, unchanged from chapter 1.5:
public interface Priceable {
int feeInRupees();
}
Now Ticket.java:
public abstract class Ticket implements Priceable {
protected final String plate;
protected Ticket(String plate) {
this.plate = plate;
}
@Override
public abstract int feeInRupees();
public void printReceipt() {
System.out.println(plate + " owes Rs" + feeInRupees());
}
}
Now HourlyTicket.java and FlatRateTicket.java, both rewritten to extend Ticket instead of implementing Priceable directly:
public class HourlyTicket extends Ticket {
private final int minutesParked;
public HourlyTicket(String plate, int minutesParked) {
super(plate);
this.minutesParked = minutesParked;
}
@Override
public int feeInRupees() {
int hours = (minutesParked + 59) / 60;
return hours * 20;
}
}
public class FlatRateTicket extends Ticket {
public FlatRateTicket(String plate) {
super(plate);
}
@Override
public int feeInRupees() {
return 100;
}
}
And Checkout.java:
public class Checkout {
public static void main(String[] args) {
HourlyTicket hourly = new HourlyTicket("KA-01-4432", 130);
FlatRateTicket flat = new FlatRateTicket("MH-12-9001");
hourly.printReceipt();
flat.printReceipt();
}
}
3. Run it
javac Priceable.java Ticket.java HourlyTicket.java FlatRateTicket.java Checkout.java
java Checkout
You should see exactly this:
KA-01-4432 owes Rs60
MH-12-9001 owes Rs100
Neither HourlyTicket nor FlatRateTicket defines printReceipt() anywhere in its own source. Both calls run the one copy that lives in Ticket.
4. What just happened, line by line
public abstract class Ticket implements Priceable { declares an abstract class that also implements an interface, which is legal and common. An abstract class can sit in the middle of a hierarchy, partly fulfilling a contract and leaving the rest for subclasses. abstract on the class means Java will refuse new Ticket(...) anywhere in the program, which section 5 shows directly.
protected final String plate; and protected Ticket(String plate) { use protected instead of private. private would hide plate even from HourlyTicket and FlatRateTicket, which need to reach it indirectly through printReceipt(), but a protected member is visible to the class that declares it and to every subclass, anywhere. protected is the access level built specifically for fields and methods meant to be used by subclasses and nobody else.
public abstract int feeInRupees(); redeclares a method Priceable already requires, this time explicitly marked abstract and ending in a semicolon, exactly like an interface method. This line is not strictly required. Ticket would already count as abstract just by failing to implement a method Priceable demands. Writing it out states directly, for a reader of this class, exactly which method every subclass is responsible for.
public void printReceipt() { is a complete, ordinary method, with a body, sitting in an abstract class. This is the capability an interface never had: Ticket can provide real, finished behaviour that every subclass inherits without writing a line of it.
public class HourlyTicket extends Ticket { uses extends, not implements, because Ticket is a class, not an interface. super(plate) in HourlyTicket's constructor calls Ticket's constructor, exactly the way chapter 1.4 covered, and it is what actually sets plate for this object. HourlyTicket never touches plate directly. It cannot see a private field, and it has no need to reach past Ticket's own constructor to set a protected one.
hourly.printReceipt(); compiles and runs against a method HourlyTicket never wrote, because HourlyTicket inherited it from Ticket. Inside that inherited method, feeInRupees() still resolves to HourlyTicket's own version, through the same dynamic dispatch chapter 1.4 introduced. The method body lives in the shared superclass, but which feeInRupees() runs depends on the real object it is called against.
5. Errors you are likely to hit
You try to build an abstract class directly.
public class Instantiate {
public static void main(String[] args) {
Ticket t = new Ticket("KA-01-4432");
}
}
Instantiate.java:3: error: Ticket is abstract; cannot be instantiated
Ticket t = new Ticket("KA-01-4432");
^
An abstract class is, by design, an incomplete description. Ticket does not know how to price itself; only a subclass does. Java refuses to let one exist without that gap filled. That is exactly the guarantee an interviewer wants to hear you name: an abstract class cannot accidentally be used as if it were a finished type.
You extend it without finishing it.
public class Incomplete extends Ticket {
public Incomplete(String plate) {
super(plate);
}
}
Incomplete.java:1: error: Incomplete is not abstract and does not override abstract method feeInRupees() in Ticket
public class Incomplete extends Ticket {
^
The same rule from chapter 1.5's interface errors applies one level further down the hierarchy. A concrete, non-abstract class must supply a body for every abstract method it inherits, whether that method was declared directly on an interface or passed down through an abstract class in between.
6. Your turn
Add a StaffTicket class, extending Ticket, that always charges 0. Then print its receipt from Checkout. Do it before reading on.
The answer.
public class StaffTicket extends Ticket {
public StaffTicket(String plate) {
super(plate);
}
@Override
public int feeInRupees() {
return 0;
}
}
Called as new StaffTicket("KA-01-4432").printReceipt();, this prints:
KA-01-4432 owes Rs0
StaffTicket wrote nothing beyond its constructor and its one differing method. plate and printReceipt() came free from Ticket, which is the entire payoff of putting them there.
7. Going deeper: what an interface is structurally forbidden from holding
Section 1 asserted that an interface cannot hold state. This is not a stylistic convention. The compiler enforces it, and the error it produces is worth seeing directly, because it looks at first like it is describing a completely different mistake.
public interface StatefulInterface {
int count = 0;
default void increment() {
count = count + 1;
}
}
StatefulInterface.java:5: error: cannot assign a value to static final variable count
count = count + 1;
^
Nothing here declared count as static final. Look again at the field: int count = 0;, with no modifiers written at all. The compiler's message names modifiers that are not in the source, and that is the fact worth sitting with. Every field declared inside an interface is implicitly `public static final, exactly the way every method is implicitly public`.
static means the field belongs to the interface itself, one single copy shared by every implementer. It is not one copy per object, the way Ticket's plate works. final means it can be assigned once and never again, so count = count + 1 is rejected the same way reassigning a final field is rejected anywhere else. An interface cannot structurally hold per-object, mutable state, because every field it declares is a shared constant whether you asked for that or not.
This is the actual reason the rule in section 1 exists, stated as a language fact rather than a guideline. Java gives every class multiple inheritance of type, through interfaces, but deliberately withholds multiple inheritance of state. A class can promise to be many things at once, but its actual per-object data always comes from exactly one line of ancestry, the single chain of extends. Two interfaces can never disagree about what a field holds, because interfaces are not allowed to hold one. The one narrow exception, default methods disagreeing about behaviour rather than data, is exactly the diamond chapter 1.5's "Going deeper" section already showed you failing to compile.
8. Why this matters in an interview
"Interface or abstract class?" is asked directly, often as exactly those three words. The honest answer is short: default to an interface. Only reach for an abstract class when there is real state or real behaviour to share, because that choice spends the one extends slot the eventual class gets. Ticket earned an abstract class the moment printReceipt() needed somewhere to live that also had plate to read. Before that requirement existed, Priceable alone was already the right and complete answer. Naming that moment correctly, rather than reaching for an abstract class out of habit, is what separates a candidate who has memorized two keywords from one who understands what each is for.
The deeper fact worth having ready is from section 7. Interfaces give Java multiple inheritance of type without multiple inheritance of state, and that is a deliberate design decision, not an accident of syntax. It is the same decision C++ made the opposite way, with virtual inheritance. Naming that trade-off, and not only the syntax, is what a design round is actually testing for when it asks this question at all.
Next: chapter 1.7, The three collections you need: List, Map, Set. Every class so far has managed exactly one or two tickets by hand, one variable at a time; a real parking lot manages thousands, and that is what a collection is for.
← 1.5 Interfaces: naming a contract · All chapters · 1.7 The three collections you need: List, Map, Set →