Lesson 04 / 13
Abstract Classes and Interfaces
The same capability is set up eight separate ways, and compilation stops on three of them; on the remaining five the compiler decides on its own and never asks which side — a class body beats an interface default, a subinterface beats the one above it, and only two independent interfaces giving the same default get rejected. A state-carrying capability cannot be set up with an interface.
Contents
The previous lesson read eight decisions on a single reference, and the table’s second row
came from an interface: Carrier’s default method could also be overridden, and its decision
also came from the runtime type. The interface itself only passed through as one row.
This lesson makes it the subject. In Java, a capability can be set up with two separate tools: an abstract class with an incomplete body, or an interface. The two look like they do the same job, and the choice is usually told as a design preference. The measurement looks at something else: which setup does the compiler reject, and when it does not, who does it leave the decision to?
The Programming Paradigms course compared the choice between interface and abstract class with design reasoning; that reasoning is not repeated here. What is counted here is where the compiler stops.
Two Tools, Two Separate Constraints
An abstract class is a class: it carries fields, has a constructor, can place bodied and bodyless methods side by side. In exchange, it is bound to Java’s single inheritance — a class can extend only one class.
An interface does the reverse. A class can implement as many interfaces as it likes, so capabilities can be attached to a class side by side. In exchange, an interface cannot hold instance state: field declarations are implicitly constant. Default methods give an interface the ability to carry a body, but not a state to run on.
Put these two constraints together and a question is born. If multiple implementation is free, what happens when two separate interfaces give a bodied method under the same signature? This situation never arises in inheritance, because there is only ever one superclass. The measurement looks right here.
The Measurement Core
The measurement’s first half runs no program at all: every setup is a source text, the compiler is invoked from within the program, and only the exit status is read.
- CI16 — All eight setups establish the same capability: a single method named
carry, returning a string. The only thing that changes is which type form the capability is given through. - CI17 — Every message the compiler produces is swallowed; the only thing read is whether the source is accepted.
// Compile.java — says whether the given source compiles
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.file.*;
import java.util.spi.ToolProvider;
class Compile {
static String outcome(String source) throws Exception {
Path d = Files.createTempDirectory("abstract");
Path f = d.resolve("Trial.java");
Files.writeString(f, source);
PrintWriter sink = new PrintWriter(Writer.nullWriter());
return ToolProvider.findFirst("javac").orElseThrow()
.run(sink, sink, "-d", d.toString(), f.toString()) == 0
? "compiled" : "not compiled";
}
}
Eight Setups, How Many Compile
// Setup.java — the same capability set up eight separate ways, how many compile
public class Setup {
static final String[][] TRIALS = {
{ "extending a single abstract class",
"abstract class A { abstract String carry(); }\nclass C extends A { public String carry() { return \"c\"; } }" },
{ "extending two abstract classes",
"abstract class A { }\nabstract class B { }\nclass C extends A, B { }" },
{ "not implementing the abstract method",
"abstract class A { abstract String carry(); }\nclass C extends A { }" },
{ "implementing two interfaces",
"interface A { default String carry() { return \"a\"; } }\ninterface B { default String name() { return \"b\"; } }\nclass C implements A, B { }" },
{ "two interfaces, same default, unresolved",
"interface A { default String carry() { return \"a\"; } }\ninterface B { default String carry() { return \"b\"; } }\nclass C implements A, B { }" },
{ "two interfaces, same default, class resolves",
"interface A { default String carry() { return \"a\"; } }\ninterface B { default String carry() { return \"b\"; } }\nclass C implements A, B { public String carry() { return \"c\"; } }" },
{ "interface and abstract class, same method",
"interface A { default String carry() { return \"a\"; } }\nabstract class S implements A { public String carry() { return \"s\"; } }\nclass C extends S { }" },
{ "subinterface overrides the one above",
"interface A { default String carry() { return \"a\"; } }\ninterface B extends A { default String carry() { return \"b\"; } }\nclass C implements B { }" },
};
public static void main(String[] args) throws Exception {
System.out.printf("%-46s %s%n", "setup", "result");
int stopped = 0;
for (String[] d : TRIALS) {
String s = Compile.outcome(d[1] + "\n");
if (s.equals("not compiled")) stopped++;
System.out.printf("%-46s %s%n", d[0], s);
}
System.out.println("setups where compilation stopped: " + stopped + " / " + TRIALS.length);
}
}
setup result extending a single abstract class compiled extending two abstract classes not compiled not implementing the abstract method not compiled implementing two interfaces compiled two interfaces, same default, unresolved not compiled two interfaces, same default, class resolves compiled interface and abstract class, same method compiled subinterface overrides the one above compiled setups where compilation stopped: 3 / 8
Compilation stops on three of eight setups, and continues on five. The three stopping ones stop for separate reasons, and all three define this lesson’s two tools.
The second row is the abstract class’s constraint: extending two classes at once is not even writable. Single inheritance is not a design suggestion, it is a spelling the language rejects. The third row is the abstract class’s contract: when a method left bodyless is not filled in by the subclass, that class cannot be concrete. An abstract class declares a gap and forces that gap to be closed at compile time.
The fifth row is the interface’s counterpart, and this lesson’s core measurement. When two independent interfaces give a bodied method under the same signature, the compiler refuses to decide. There is no ambiguity here to resolve — both candidates sit at equal distance, and rather than defining a priority between them, the language sends the source back. This never arises with inheritance; this is exactly the cost of multiple implementation.
The sixth row shows how the cost is paid. Same two interfaces, same clash — but once the class writes its own body, the source compiles. What the compiler wants is not to pick one of the candidates; it wants the choice written in the source.
The two simplest of the compiling rows say why the two tools sit side by side. The first row
is the abstract class’s: once the method left incomplete is filled in by the subclass, the
class becomes concrete and the chain is set up. The fourth is the interface’s: class C
takes on two separate capabilities at once, and since neither signature clashes, there is no
question at all. A clash is not a rule, it is a case of intersection; it is only born when
two independent interfaces give the same signature. The rest of the measurement counts how
that intersection gets closed.
Who Wins When Compilation Does Not Stop
Three of the remaining five setups have more than one candidate present, and compilation still continues. In these cases, who makes the decision?
- CI18 — In all three setups, the call is made through a reference whose declared type is
Carrier; the only thing that changes is the type hierarchy building the runtime type. - CI19 — Every body returns its own origin as text; the winning side is read from the returned value, not by inference.
// Carrier.java — types giving the same capability two separate ways
interface Carrier {
default String carry() { return "interface default"; }
}
interface Labeled extends Carrier {
@Override default String carry() { return "subinterface default"; }
}
interface Second {
default String carry() { return "second interface default"; }
}
abstract class AbstractCarrier implements Carrier {
@Override public String carry() { return "abstract class body"; }
abstract String name();
}
class Specific implements Labeled { }
class ClassWins extends AbstractCarrier implements Carrier {
@Override String name() { return "class"; }
}
class Resolver implements Carrier, Second {
@Override public String carry() { return "class resolved it itself"; }
}
class Picker implements Carrier, Second {
@Override public String carry() { return Second.super.carry(); }
}
// Winner.java — which side wins when compilation does not stop
public class Winner {
public static void main(String[] args) {
Carrier a = new Specific();
Carrier b = new ClassWins();
Carrier c = new Resolver();
Carrier e = new Picker();
System.out.printf("%-44s %s%n", "source", "carry() result");
System.out.printf("%-44s %s%n", "interface, super and sub", a.carry());
System.out.printf("%-44s %s%n", "interface default and class body", b.carry());
System.out.printf("%-44s %s%n", "two independent interfaces, class resolves", c.carry());
System.out.printf("%-44s %s%n", "two independent interfaces, class picks", e.carry());
}
}
source carry() result interface, super and sub subinterface default interface default and class body abstract class body two independent interfaces, class resolves class resolved it itself two independent interfaces, class picks second interface default
Four lines, four separate choices, and only the last two are written in the source.
The first line has two interfaces, but one extends the other. This is not a clash: Labeled
overrides Carrier’s default, and the more specific one wins. The rule is the same one
inheritance uses; when there is a sub–super relationship between two types, the one below is
picked.
The second line is the language’s silent priority. ClassWins gets a body both from an
abstract class and from an interface default. No choice is written in the source, yet
compilation continues and the result comes out abstract class body. A body coming from the
class line always beats an interface default. This rule resolves an ambiguity, but asks no
one about it — whoever wrote the interface can never see, from their own source, that their
body never gets called.
The third line is the fifth setup’s compiling form. Resolver implements two independent
interfaces and writes its own body; the winner is whichever side is written in the source.
Here the clash is not resolved, it is merely stepped over: neither of the two default bodies
ever gets called.
The fourth line is the clash’s real resolution. Picker also writes its own body, but inside
that body it calls one of the candidates by name — the result is second interface
default. What the compiler wanted was not new behavior; it wanted which candidate was
picked to be written down, and the language gives a form for writing that choice. The
difference between a rejected source and an accepted one is not an implementation, it is a
signature.
From here comes the lesson’s main reading. The compiler shows three separate behaviors: if a hierarchy exists, it picks the most specific one; when a class meets an interface, it picks the class; when two equal interfaces meet, it picks nothing. Only the last is visible to the programmer. The first two are decisions too, and both are made at compile time, by looking at the hierarchy of declared types; the runtime type’s share here is limited not to deciding which body gets called, but to deciding what class the body-carrying object belongs to.
A Bounding Measurement: A State-Carrying Capability Cannot Be Set Up with an Interface
The table makes the interface look more flexible because of multiple implementation. The boundary sits in what an interface cannot hold.
- CI20 — All three setups establish the same capability: a single operation incrementing a counter. The only thing that changes is where the counter is held.
// State.java — which tool can a state-carrying capability be set up with
public class State {
static final String[][] TRIALS = {
{ "constant declared in interface",
"interface Counter { int START = 0; }" },
{ "instance field and increment in interface",
"interface Counter { int value = 0; default void increment() { value++; } }" },
{ "field and increment in abstract class",
"abstract class Counter { int value = 0; void increment() { value++; } }" },
};
public static void main(String[] args) throws Exception {
System.out.printf("%-44s %s%n", "how the capability is set up", "result");
for (String[] d : TRIALS)
System.out.printf("%-44s %s%n", d[0], Compile.outcome(d[1] + "\n"));
System.out.println();
Tally x = new Tally(), y = new Tally();
x.increment(); x.increment(); y.increment();
System.out.println("two instances derived from the abstract class: " + x.value + " and " + y.value);
System.out.println("the interface's constant reads without an instance: " + Constant.START);
}
}
abstract class AbstractCounter {
int value = 0;
void increment() { value++; }
}
class Tally extends AbstractCounter { }
interface Constant { int START = 0; }
how the capability is set up result constant declared in interface compiled instance field and increment in interface not compiled field and increment in abstract class compiled two instances derived from the abstract class: 2 and 1 the interface's constant reads without an instance: 0
The first row is misleading: a field can be declared in an interface. The second row says what it actually is — it cannot be incremented, because it is implicitly constant. Every field declaration written in an interface, even unmarked, is class-level and unchangeable. As the last line shows, it reads without any instance ever being constructed, meaning it does not belong to an instance at all.
The third row and the run below it measure the opposite side. The same capability compiles when set up with an abstract class, and two instances count 2 and 1: each object has its own counter. What the capability needed was not a body, it was a place belonging to an instance for that body to work on, and an interface cannot give that place. The two numbers coming out separate is the measurement itself: had a single place been shared, both references would read 3.
The boundary’s meaning reverses the flexibility of this lesson’s first half. An interface looks broader because it can attach in multiples; but what it can actually carry is only behavior. If a capability holds state, multiple implementation cannot even be asked about, because there is only one candidate. The choice between the two tools is not a style question: if the capability is stateless, both paths are open and an interface earns multiple attachment; if it carries state, there is only one path, and taking it also brings single inheritance’s constraint along.
Summary
- Compilation stops on three of eight setups of the same capability: extending two classes at once, not filling in an abstract method, and two independent interfaces giving the same default.
- When two independent interfaces give a body under the same signature, the compiler refuses to decide; the same source compiles once the class writes its own body. What is wanted is not new behavior, it is the choice written in the source — a class can also call one candidate by name.
- When compilation does not stop, the decision is still the compiler’s, and it is silent: if a hierarchy exists, the most specific interface wins; when a class meets an interface, the class body wins.
- The cost of the silent decision is invisibility: whoever wrote the interface default cannot see, from their own source, that their body never gets called.
- A field declared in an interface is implicitly constant and reads without an instance; when incrementing is attempted, the source does not compile.
- The same counter set up with an abstract class counts 2 and 1 across two instances: a state-carrying capability cannot be set up with an interface, and taking that path also brings single inheritance’s constraint.
Next Step
In this lesson, both abstraction tools worked at the instance level: an abstract class’s body and an interface’s default method were both called through an object, and which body would run was always asked together with an instance’s runtime type. The next lesson looks at members belonging to the class itself, and measures how the decision’s side flips there.
To keep your progress and take notes, Log in
My notes
Log in to take notes.