---
title: 'Abstract Classes and Interfaces'
source: 'https://academia.sh/en/courses/java-object-model/abstract-classes-and-interfaces'
course: 'Object-Oriented Java'
language: en
updated: '2026-08-17T18:09:40+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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.

```java
// 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

```java
// 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.

```java
// 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(); }
}
```

```java
// 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.

```java
// 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.
