Command Palette

Search for a command to run...

[Java Basics] Interfaces in Java, and How They Differ from Abstract Classes

An interface is a list of things a type can do, with no claim about what that type is. It has no fields to hold state, no constructor to run, and nothing to inherit — which is exactly why a class can implement as many interfaces as it needs while it may extend only one class.

That asymmetry is the whole subject. Inheritance answers "is a" and you get one answer; an interface answers "can do" and you get as many as you like. This article covers the declaration, the implicit modifiers the compiler adds for you, what default, static and private methods brought and why, the diamond problem and its resolution, and closes with the comparison against abstract classes that decides which of the two you should actually write.

One class box against three interface plugs: extends takes one, implements takes many

Every error message and every line of output below was produced by compiling and running the code on OpenJDK 21.0.6. Release attributions for default, static and private interface methods are given as Java 8 and Java 9 because that is when the language gained them, not because a different compiler was used.

What an interface is: a contract with no identity of its own

An interface declares method signatures. A class that says implements promises to supply a body for every one of them, and in exchange the class may be used anywhere the interface type is expected.

interface Drawable {
    void draw();
    double area();
}

class Circle implements Drawable {
    private final double r;

    Circle(double r) { this.r = r; }

    @Override
    public void draw() {
        System.out.println("Circle r=" + r);
    }

    @Override
    public double area() {
        return Math.PI * r * r;
    }
}

class Square implements Drawable {
    private final double side;

    Square(double side) { this.side = side; }

    @Override
    public void draw() {
        System.out.println("Square side=" + side);
    }

    @Override
    public double area() {
        return side * side;
    }
}

public class Basic {
    public static void main(String[] args) {
        Drawable[] shapes = { new Circle(2), new Square(3) };
        for (Drawable d : shapes) {
            d.draw();
            System.out.printf("  area = %.2f%n", d.area());
        }
        System.out.println("Circle is a Drawable? " + (shapes[0] instanceof Drawable));
    }
}
Circle r=2.0
  area = 12.57
Square side=3.0
  area = 9.00
Circle is a Drawable? true

Circle and Square share no superclass, no field and no method body. What they share is a capability, and the array declared as Drawable[] holds both because both satisfy the contract.

The two keywords say different things. extends means "is a specialised kind of" and brings the superclass's fields and method bodies with it. implements means "is able to", and brings nothing but the obligation. Apply the test out loud: a Circle is a Shape, so that is inheritance; a Circle can be drawn, so that is an interface. When the sentence only works with "can", you want an interface.

Declaring an interface and implementing it

The declaration is a .java file like any other, with interface in place of class:

interface Drawable {
    void draw();
    double area();
}

No public, no abstract and no body. A class then lists the interfaces it satisfies after implements, separated by commas, and supplies a body for every method. Marking each one @Override is not required by the compiler, but it turns a typo in the signature into a compile error instead of a silently unused method.

The error when a class does not implement every method

Leave one out and the class is incomplete. javac names the exact method it is missing:

class Triangle implements Drawable {
    @Override
    public void draw() {
        System.out.println("Triangle");
    }
}
Missing.java:6: error: Triangle is not abstract and does not override abstract method area() in Drawable
class Triangle implements Drawable {
^
1 error

The wording is worth reading closely. The compiler is not saying "you forgot something"; it is saying the class is concrete while one of its methods still has no body, and offers you the alternative in the message itself — declare Triangle abstract and the error disappears, because an abstract class is allowed to leave inherited methods unimplemented for its own subclasses to finish.

Interface members are implicitly public; fields are implicitly public static final

You never write public abstract on an interface method or public static final on an interface field, because the compiler writes them for you. Reflection shows the modifiers that actually ended up in the class file:

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

interface Config {
    int MAX_RETRIES = 3;
    void reload();
}

public class Modifiers {
    public static void main(String[] args) throws Exception {
        Field f = Config.class.getDeclaredField("MAX_RETRIES");
        System.out.println("field  MAX_RETRIES -> " + Modifier.toString(f.getModifiers()));
        Method m = Config.class.getDeclaredMethod("reload");
        System.out.println("method reload()    -> " + Modifier.toString(m.getModifiers()));
    }
}
field  MAX_RETRIES -> public static final
method reload()    -> public abstract

Both consequences are enforced at compile time, and both surprise people.

Reducing the visibility of an implemented method

Because the interface method is already public, an implementation cannot be anything narrower. Omit the modifier and the method becomes package-private, which is narrower:

interface Greeter {
    void greet();
}

class Quiet implements Greeter {
    @Override
    void greet() {
        System.out.println("hi");
    }
}
Weaker.java:7: error: greet() in Quiet cannot implement greet() in Greeter
    void greet() {
         ^
  attempting to assign weaker access privileges; was public
1 error

This is the single most common compile error when a beginner forgets public on an implementing method. The rule behind it is general: an override may widen access but never narrow it, or a caller holding the interface reference would be able to reach a method the object refuses to expose.

An interface field is a constant, not state

int MAX_RETRIES = 3; inside an interface is not a field of the implementing objects. It is one static final value shared by everything, and it cannot be reassigned:

interface Config {
    int MAX_RETRIES = 3;
    String NAME = "api";
}

public class Constants {
    public static void main(String[] args) {
        System.out.println(Config.MAX_RETRIES + " " + Config.NAME);
        Config.MAX_RETRIES = 5;
    }
}
Constants.java:9: error: cannot assign a value to static final variable MAX_RETRIES
        Config.MAX_RETRIES = 5;
              ^
1 error

Attempting to use one as per-object state fails the same way, even from inside a default method:

interface Counter {
    int count = 0;

    default void increment() {
        count = count + 1;
    }
}
MutableState.java:5: error: cannot assign a value to static final variable count
        count = count + 1;
        ^
1 error

⚠️ An interface cannot hold mutable state at all. If a design needs a counter, a cache or a connection shared by subclasses, that is an abstract class with a field, not an interface with a constant.

The interface also refuses the two other things a class uses to set state up. A constructor is a syntax error, because there is no object to construct:

NoCtor.java:2: error: <identifier> expected
    Session(String user);
           ^
1 error

And an instance field is a syntax error too, because every field declaration is implicitly final and therefore needs an initialiser:

InstanceField.java:2: error: = expected
    private String user;
                       ^
1 error

One class, many interfaces

A class may extend exactly one class. Write two and the parser gives up at the comma:

TwoParents.java:4: error: '{' expected
class Robot extends Machine, Vehicle { }
                           ^
1 error

implements takes a list, and every name on that list is one more type the object can be passed as:

interface Printable {
    void print();
}

interface Storable {
    String serialize();
}

interface Rankable {
    int rankAgainst(Rankable other);
    int weight();
}

class Report implements Printable, Storable, Rankable {
    private final String title;
    private final int pages;

    Report(String title, int pages) {
        this.title = title;
        this.pages = pages;
    }

    @Override
    public void print() {
        System.out.println("printing " + title + " (" + pages + " pages)");
    }

    @Override
    public String serialize() {
        return "{\"title\":\"" + title + "\",\"pages\":" + pages + "}";
    }

    @Override
    public int weight() { return pages; }

    @Override
    public int rankAgainst(Rankable other) {
        return Integer.compare(weight(), other.weight());
    }
}

Each call site then declares only the capability it needs, and never learns that Report has the other two:

static void render(Printable p) { p.print(); }
static void save(Storable s) { System.out.println("saving " + s.serialize()); }
static void rank(Rankable a, Rankable b) { System.out.println("rank -> " + a.rankAgainst(b)); }

public static void main(String[] args) {
    Report q3 = new Report("Q3", 12);
    Report q4 = new Report("Q4", 30);

    render(q3);
    save(q3);
    rank(q3, q4);

    System.out.println("Printable? " + (q3 instanceof Printable));
    System.out.println("Storable?  " + (q3 instanceof Storable));
    System.out.println("Rankable?  " + (q3 instanceof Rankable));
}
printing Q3 (12 pages)
saving {"title":"Q3","pages":12}
rank -> -1
Printable? true
Storable?  true
Rankable?  true

One extends arrow against three implements arrows, and the same object entering three call sites through three different types

This is what single inheritance cannot give you. Report genuinely has three types beyond its own, and each one is a narrow, honest description of what a particular caller needs.

An interface may extend several interfaces

Interfaces use extends between themselves, and there the list may be longer than one, because there is no state to merge:

interface Loadable { String load(); }
interface Savable  { void save(String data); }
interface Closable { void close(); }

interface Channel extends Loadable, Savable, Closable {
    boolean isOpen();
}

class MemoryChannel implements Channel {
    private final StringBuilder buf = new StringBuilder();
    private boolean open = true;

    @Override public String load() { return buf.toString(); }
    @Override public void save(String data) { buf.append(data); }
    @Override public void close() { open = false; }
    @Override public boolean isOpen() { return open; }
}
load  -> hello interfaces
open  -> true
open  -> false
as Loadable -> hello interfaces
Channel superinterfaces -> [interface Loadable, interface Savable, interface Closable]

MemoryChannel writes implements Channel once and owes four methods. A caller that only reads can take a Loadable, and the same object goes in unchanged.

What interfaces gained over time

For the first eighteen years an interface could contain exactly two things: abstract method signatures and constants. Three additions changed that, and each one exists for a specific reason.

A time axis from Java 1.0 to Java 9 showing abstract methods and constants, then default and static methods, then private methods

interface Validator {
    // Since Java 1.0: abstract methods and constants.
    String NAME = "validator";
    boolean isValid(String input);

    // Java 8: a default method has a body and is inherited by every implementor.
    default String describe(String input) {
        return tag() + (isValid(input) ? " OK   " : " FAIL ") + quote(input);
    }

    // Java 8: a static method belongs to the interface, not to implementors.
    static Validator notBlank() {
        return input -> input != null && !input.isBlank();
    }

    // Java 9: a private method shares code between default methods
    // without exposing it on the public contract.
    private String tag() {
        return "[" + NAME + "]";
    }

    private static String quote(String s) {
        return "\"" + s + "\"";
    }
}

class MaxLength implements Validator {
    private final int max;
    MaxLength(int max) { this.max = max; }
    @Override public boolean isValid(String input) { return input != null && input.length() <= max; }
}
[validator] OK   "java"
[validator] FAIL "interfaces"
[validator] FAIL "   "
[validator] OK   "x"

MaxLength implements one method and gets describe for free. Validator.notBlank() returns an implementation without any named class existing at all.

Why default methods exist

The motivation is not convenience. It is that adding a method to a published interface used to break every class that implemented it, and Java 8 needed to add methods to java.util.Collection and friends without breaking the world.

Start with an interface that already has an implementor compiled against it:

public interface Sink {
    void accept(String line);
}

public class ConsoleSink implements Sink {
    @Override
    public void accept(String line) {
        System.out.println("> " + line);
    }
}

Now add a method as default, and recompile only the interface:

import java.util.List;

public interface Sink {
    void accept(String line);

    // Added later. `default` means every existing implementor already has it.
    default void acceptAll(List<String> lines) {
        for (String line : lines) accept(line);
    }
}
only Sink.java recompiled
ConsoleSink.class byte-identical: 8052f837c422fe5ac7d9ef547dab5b74
> one
> two
> three

ConsoleSink.class was never touched — its checksum is the one produced against the old interface — and yet sink.acceptAll(List.of("two", "three")) runs and dispatches back into the old accept. That is the entire point of the feature.

Declare the same method as abstract instead and the old implementor no longer compiles:

public interface Sink {
    void accept(String line);

    void acceptAll(List<String> lines);   // abstract, not default
}
ConsoleSink.java:1: error: ConsoleSink is not abstract and does not override abstract method acceptAll(List<String>) in Sink
public class ConsoleSink implements Sink {
       ^
1 error

One keyword is the difference between a source-compatible change and a breaking one. That is why you should reach for default when you extend an interface other people implement — and why you should not reach for it as a general place to put code, since a default method still cannot touch any state.

static methods belong to the interface, not to implementors

A static method on an interface is called through the interface name and is not inherited by implementing classes:

System.out.println(Validator.notBlank().isValid("x"));   // fine
System.out.println(MaxLength.notBlank().isValid("x"));   // not inherited
StaticNotInherited.java:13: error: cannot find symbol
        System.out.println(MaxLength.notBlank().isValid("x"));   // not inherited
                                    ^
  symbol:   method notBlank()
  location: class MaxLength
1 error

That is deliberate: it keeps the implementing class's own namespace clean, and it means a static factory can live next to the type it produces instead of in a separate XxxUtils class.

private methods, added in Java 9

Once default methods existed, two of them frequently wanted to share a helper — and before Java 9 the only way was to make that helper public, putting an implementation detail permanently on the contract. A private interface method fixes that. Compile the Validator above against Java 8 and the compiler says exactly which release introduced it:

Evolution.java:18: error: private interface methods are not supported in -source 8
    private String tag() {
                      ^
  (use -source 9 or higher to enable private interface methods)
1 error

There is one member a default method may never provide: anything already declared on java.lang.Object. Every class inherits those from Object, so an interface default could never win.

DefaultToString.java:2: error: default method toString in interface Named overrides a member of java.lang.Object
    default String toString() { return "named"; }
                   ^
1 error

The diamond problem with default methods

Give one class two interfaces that both supply the same default method, and there is no rule that makes one of them correct. C++ resolves this situation with virtual inheritance and a set of rules most people never fully learn. Java refuses to resolve it at all:

interface Logger {
    default String prefix() { return "[log] "; }
}

interface Auditor {
    default String prefix() { return "[audit] "; }
}

class Service implements Logger, Auditor {
}
Diamond.java:9: error: types Logger and Auditor are incompatible;
class Service implements Logger, Auditor {
^
  class Service inherits unrelated defaults for prefix() from types Logger and Auditor
1 error

The word to notice is unrelated. If one interface extends the other, the more specific default wins and there is no error — interface B extends A with an overriding default makes class C implements A, B print B. The error only appears when neither candidate is more specific than the other, which is precisely the case where guessing would be arbitrary.

The unresolved conflict with the real javac error, the Interface.super resolution with its output, and the class-wins rule

Resolving the conflict with Interface.super.method()

The class overrides the method and names which inherited body it wants. Interface.super.method() calls a specific direct superinterface:

class Service implements Logger, Auditor {
    @Override
    public String prefix() {
        return Logger.super.prefix() + Auditor.super.prefix();
    }
}

class LogOnly implements Logger, Auditor {
    @Override
    public String prefix() {
        return Logger.super.prefix();
    }
}
[log] [audit] both
[log] picked one

You may combine both, pick one, or ignore both and write something new. The compiler only insists that you decide. Note that the interface named must be a direct superinterface of the class:

BadSuper.java:5: error: not an enclosing class: A
    @Override public String who() { return A.super.who(); }
                                            ^
1 error

The class-wins rule

When the competition is between a superclass method and an interface default, there is no ambiguity to report: the class wins, always, and the default is never even considered.

interface Greeter {
    default String greet() { return "hello from the interface"; }
}

class Base {
    public String greet() { return "hello from the superclass"; }
}

class Child extends Base implements Greeter {
}
as Child   -> hello from the superclass
as Base    -> hello from the superclass
as Greeter -> hello from the superclass

Casting the object to Greeter changes nothing, because the rule is about which method body is inherited, not about which reference type you hold. The rule holds even when the class method has no body of its own — an abstract method in the superclass still beats the default, and the subclass is then required to implement it:

AbstractWins.java:9: error: Child is not abstract and does not override abstract method greet() in Base
class Child extends Base implements Greeter {
^
1 error

Functional interfaces and lambdas

An interface with exactly one abstract method is a functional interface, and a lambda expression is an implementation of one. @FunctionalInterface is optional documentation that makes the compiler enforce the count:

@FunctionalInterface
interface Transform {
    String apply(String input);
}

public class Functional {
    static String run(Transform t, String input) {
        return t.apply(input);
    }

    public static void main(String[] args) {
        Transform upper = s -> s.toUpperCase();
        Transform reverse = s -> new StringBuilder(s).reverse().toString();

        System.out.println(run(upper, "interface"));
        System.out.println(run(reverse, "interface"));

        Transform anonymous = new Transform() {
            @Override
            public String apply(String input) { return "<" + input + ">"; }
        };
        System.out.println(run(anonymous, "interface"));
        System.out.println("lambda class    -> " + upper.getClass().getInterfaces()[0].getSimpleName());
    }
}
INTERFACE
ecafretni
<interface>
lambda class    -> Transform

Add a second abstract method and the annotation fails the build:

NotFunctional.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
  Transform is not a functional interface
    multiple non-overriding abstract methods found in interface Transform
1 error

default, static and private methods do not count towards the one — only abstract methods do. Lambdas, method references and the ready-made types in java.util.function are a large subject and belong to the advanced course. The only point here is the connection: a lambda is not a new kind of value, it is an object implementing a single-method interface, and the interface is what gives it a type.

Marker interfaces

A marker interface declares nothing at all. Its whole content is its name, and code tests for it with instanceof:

interface Auditable { }   // no members at all

class Order implements Auditable {
    final int id;
    Order(int id) { this.id = id; }
}

class Draft { }
Order audited? true
Draft audited? false
Auditable declares 0 methods

The JDK's own examples are java.io.Serializable and java.lang.Cloneable, and you have already been using both without noticing — every array type implements them:

int[]     -> [interface java.lang.Cloneable, interface java.io.Serializable]

Markers are a pre-annotation technique: today the same information is usually carried by an annotation, which can also hold parameters. Markers survive where the type system has to know, because instanceof and a method parameter can test an interface but not an annotation.

Abstract class versus interface

Both let you declare a method with no body and force a subtype to supply one, which is why the two get confused. Everything else about them differs:

Abstract classInterface
Instance stateYes — any fields, mutable or finalNo — fields are implicitly public static final constants
ConstructorYes, and it runs via super(...)No — a constructor declaration is a syntax error
How a type joins itextends, one onlyimplements, as many as you like
Access levelspublic, protected, package-private, privateMembers are public; private allowed only on methods with a body (Java 9)
Method bodiesOrdinary concrete methodsdefault and static methods (Java 8)
final membersfinal methods and final fields allowedNo final methods; every field is already final
static membersstatic fields and methodsstatic methods only, and they are not inherited
Relationship expressed"is a" — a specialised kind of the parent"can do" — a capability, independent of hierarchy
Adding a member laterAdding an abstract method breaks subclassesAdding a default method breaks nothing
What it is forSharing state and implementation across related typesSharing a capability across unrelated types

Which one to reach for

The rule fits in one sentence: an abstract class when the subtypes share state and implementation, an interface when unrelated types share a capability.

Take a concrete program with both requirements in it. Start with the capability: anything here that can be written as a row of CSV says so, and nothing else.

interface Exportable {
    String toCsvRow();
}

Now the employees. Engineer and Manager are both employees. They both have a name and a base salary, and pay() is base + bonus() for both, with only bonus() differing. That is shared state plus shared implementation plus a genuine "is a" — an abstract class, which also happens to be exportable:

abstract class Employee implements Exportable {
    protected final String name;
    protected final double base;

    protected Employee(String name, double base) {   // an abstract class has a constructor
        this.name = name;
        this.base = base;
    }

    abstract double bonus();                         // each subclass decides

    final double pay() {                             // shared implementation
        return base + bonus();
    }

    @Override
    public String toCsvRow() {
        return name + "," + pay();
    }
}

class Engineer extends Employee {
    Engineer(String name, double base) { super(name, base); }
    @Override double bonus() { return base * 0.10; }
}

class Manager extends Employee {
    private final int reports;
    Manager(String name, double base, int reports) { super(name, base); this.reports = reports; }
    @Override double bonus() { return base * 0.15 + reports * 100; }
}

Now take an Invoice. It is not an employee, it shares no field and no formula, and it will never be in the same hierarchy. The only thing it has in common with Employee is that both can be written to a CSV row — which is exactly what Exportable says:

class Invoice implements Exportable {
    private final String ref;
    private final double amount;
    Invoice(String ref, double amount) { this.ref = ref; this.amount = amount; }
    @Override public String toCsvRow() { return ref + "," + amount; }
}

The two answers are not in competition. Employee is an abstract class that implements Exportable, so one method handles employees and invoices together:

static void dump(Exportable[] items) {
    for (Exportable e : items) System.out.println(e.toCsvRow());
}

public static void main(String[] args) {
    Employee[] staff = { new Engineer("ada", 5000), new Manager("grace", 6000, 4) };
    for (Employee e : staff) System.out.printf("%s pay = %.1f%n", e.name, e.pay());

    System.out.println("--- exported ---");
    dump(new Exportable[]{ staff[0], staff[1], new Invoice("INV-7", 240.5) });
}
ada pay = 5500.0
grace pay = 7300.0
--- exported ---
ada,5500.0
grace,7300.0
INV-7,240.5

Two panels: subclasses sharing state resolved with an abstract class, unrelated types sharing a capability resolved with an interface

In practice the interface comes first in a design and the abstract class comes second, as an optional convenience for the implementors that happen to be related. That ordering is visible all over the JDK, where an interface such as List sits above a partial implementation such as AbstractList.

Programming to an interface

This is the payoff, and it is worth writing out in full because it is the reason an interface earns its extra file. Here is a job done with a type code, which is what the pattern replaces:

public class Coupled {
    // The call site knows every channel by name. Adding one means editing this method.
    static String notify(String channel, String user, String message) {
        if (channel.equals("email")) {
            return "to: " + user + "@example.com | subject: Alert | " + message;
        } else if (channel.equals("sms")) {
            String body = message.length() > 18 ? message.substring(0, 15) + "..." : message;
            return "[" + user + "] " + body;
        }
        throw new IllegalArgumentException("unknown channel: " + channel);
    }

    public static void main(String[] args) {
        String msg = "build 412 failed on main";
        System.out.println(notify("email", "ada", msg));
        System.out.println(notify("sms", "ada", msg));
        System.out.println(notify("webhook", "ada", msg));
    }
}
to: ada@example.com | subject: Alert | build 412 failed on main
[ada] build 412 faile...
Exception in thread "main" java.lang.IllegalArgumentException: unknown channel: webhook
	at Coupled.notify(Coupled.java:10)
	at Coupled.main(Coupled.java:17)

Every new channel means editing notify, retesting it, and hoping nobody forgot a branch. Worse, the failure for an unknown channel is a runtime exception rather than a compile error. The interface version moves each channel into its own class and leaves the call site with nothing to know:

interface Notifier {
    String channel();
    String format(String user, String message);
}

class EmailNotifier implements Notifier {
    @Override public String channel() { return "email"; }
    @Override public String format(String user, String message) {
        return "to: " + user + "@example.com | subject: Alert | " + message;
    }
}

class SmsNotifier implements Notifier {
    @Override public String channel() { return "sms"; }
    @Override public String format(String user, String message) {
        String body = message.length() > 18 ? message.substring(0, 15) + "..." : message;
        return "[" + user + "] " + body;
    }
}

public class Decoupled {
    // The call site never changes. It knows the contract, not the implementations.
    static void send(Notifier notifier, String user, String message) {
        System.out.println("via " + notifier.channel() + ": " + notifier.format(user, message));
    }

    public static void main(String[] args) {
        String msg = "build 412 failed on main";
        for (Notifier n : new Notifier[]{ new EmailNotifier(), new SmsNotifier() }) {
            send(n, "ada", msg);
        }
    }
}
via email: to: ada@example.com | subject: Alert | build 412 failed on main
via sms: [ada] build 412 faile...

Adding a third implementation

The test of the design is what a new channel costs. Here it costs one new file and no edit anywhere else — send is not recompiled, not retested, and does not know WebhookNotifier exists. It even carries a field of its own, which the interface neither knows about nor forbids:

class WebhookNotifier implements Notifier {
    private final String url;
    WebhookNotifier(String url) { this.url = url; }

    @Override public String channel() { return "webhook"; }
    @Override public String format(String user, String message) {
        return "POST " + url + " {\"user\":\"" + user + "\",\"text\":\"" + message + "\"}";
    }
}
via webhook: POST https://hooks.example.com/ci {"user":"ada","text":"build 412 failed on main"}

The habit that follows is short: declare parameters, fields and return types with the most general type that does the job. Write Notifier n, not EmailNotifier n. The call site then works with every implementation that exists today and every one written after it.

Interfaces in the JDK you have already used

Four of them have appeared in this course already, under other names.

Comparable -> [v8, v17, v21]
Runnable   -> ran on worker
List impl  -> ArrayList
Iterable   -> ada alan
ArrayList is a List?     true
ArrayList is an Iterable? true

Comparable is the one Arrays.sort requires on an object array — the sorting article called Arrays.sort on int[], where the ordering is built in; on your own class you supply it by implementing compareTo. Iterable is what the enhanced for loop needs when the thing on the right is not an array, and arrays are notably not Iterable, which is why the compiler generates an index loop for them instead. Runnable is the single-method interface a thread runs, and List is the interface that ArrayList implements — both belong to material past this course, threads and the collections framework respectively.

String is worth one look for the same reason:

String    -> [interface java.io.Serializable, interface java.lang.Comparable, interface java.lang.CharSequence, interface java.lang.constant.Constable, interface java.lang.constant.ConstantDesc]

Five interfaces on a class you have used since the first article. That is the normal shape of a well-designed type.

Common mistakes and the errors they produce

Forgetting public on an implementing method. The interface method is already public, so the default package-private access is a reduction: attempting to assign weaker access privileges; was public. Write public on every implementation, and @Override above it.

Forgetting a method entirely. Triangle is not abstract and does not override abstract method area() in Drawable. Either implement it or declare the class abstract.

Trying to instantiate an interface. There is nothing to construct:

Instantiate.java:7: error: Drawable is abstract; cannot be instantiated
        Drawable d = new Drawable();
                     ^
1 error

The syntax that looks like it does this is an anonymous class, and the trailing braces are the difference:

Drawable d = new Drawable() {
    @Override public void draw() { System.out.println("drawn by an anonymous class"); }
};
drawn by an anonymous class
runtime class -> Anon$1

Expecting an interface field to be per-object state. It is one public static final constant shared by everything, and cannot assign a value to static final variable is what you get for trying. State belongs in a class.

Putting a helper on the interface just to share it between two default methods. Before Java 9 that was the only option and it leaked an implementation detail onto the public contract. Since Java 9 it is a private method.

Calling a static interface method through an implementing class. cannot find symbolstatic interface methods are not inherited. Call them through the interface name.

Reaching for an interface when the types actually share state. Two subclasses that need the same field cannot get it from an interface, and a default method cannot touch state either. That is an abstract class.

FAQ

What is the difference between an interface and an abstract class in Java?

An abstract class can hold instance state, has a constructor, allows any access level and any mix of concrete and abstract methods, and a class may extend only one. An interface has no instance state and no constructor, its members are implicitly public (methods abstract, fields static final), it may carry default, static and private method bodies since Java 8 and 9, and a class may implement as many as it likes. Use the abstract class when subtypes share state and implementation; use the interface when unrelated types share a capability.

Can a Java class implement more than one interface?

Yes, and this is the main reason interfaces exist. class Report implements Printable, Storable, Rankable gives the object three types in addition to its own, and it can be passed to a method expecting any of them. A class may still extend only one class — class Robot extends Machine, Vehicle is a syntax error at the comma.

Why can an interface not have instance fields?

Because an interface has no instances of its own and no constructor to initialise anything. Every field declared in an interface is implicitly public static final, so private String user; fails with = expected — the compiler is asking for the initialiser that a constant must have. If the design needs per-object state, that is a class.

What is a default method and when should I write one?

A default method is an interface method with a body, added in Java 8, and every implementor inherits it without writing anything. It exists so a method can be added to an interface that already has implementors: recompile the interface alone and old class files keep working, where an abstract method would fail every one of them with is not abstract and does not override. Write one when you are extending an interface other people implement, or when a method has one obviously correct implementation in terms of the other methods. Do not use it as a general place for code — a default method still cannot touch any state.

Why does javac say inherits unrelated defaults?

Because the class implements two interfaces that both supply a default method with the same signature, and neither interface is a subtype of the other, so there is no rule making one of them the right answer. Java refuses to guess. Override the method in the class and pick explicitly with Logger.super.prefix(), combine both, or write something new. If one interface does extend the other, the more specific default wins and no error is reported.

Can an interface be instantiated in Java?

No — new Drawable() gives Drawable is abstract; cannot be instantiated. What looks like instantiating an interface is one of two other things: an anonymous class, new Drawable() { ... } with a body supplying the methods, which compiles to a class named like Anon$1; or a lambda, when the interface has exactly one abstract method.

What is a functional interface?

An interface with exactly one abstract method. default, static and private methods do not count. That single method is what a lambda expression implements, so Transform upper = s -> s.toUpperCase(); is legal precisely because Transform has one abstract method. @FunctionalInterface is optional and only asks the compiler to enforce the count — add a second abstract method and the build fails with multiple non-overriding abstract methods found.

Conclusion

An interface is a contract and nothing else: no state, no constructor, no identity. Everything else follows from that. Members are public because a contract that hides its terms is useless, and fields are static final because there is nothing to hold state. A class can sign as many contracts as it wants because signing costs nothing structurally, while it can inherit from only one parent because inheritance brings fields and bodies that would have to be merged. default methods let a published contract grow without breaking its signatories, and when two contracts collide the compiler makes you choose rather than choosing for you.

That also closes the object-oriented part of this course. Encapsulation put the state behind methods so an object controls its own invariants; inheritance let one type reuse and specialise another; polymorphism made the dynamic type decide which body runs; abstract classes let a type declare what its subclasses owe while keeping the state and code they share; and interfaces detached capability from hierarchy altogether. The last two are the same idea approached from opposite ends — one starts from a family and pulls out what is common, the other starts from a capability and lets any type claim it — and a real design uses both, usually an interface at the top and an abstract class immediately under it.

Next in this series: exception handlingtry, catch and finally, the difference between checked and unchecked exceptions, and what throw and throws each actually do.

Related Posts

[Java Basics] Abstraction in Java: Abstract Classes and Abstract Methods

Abstract classes and abstract methods in Java: why Shape cannot be instantiated, what an abstract class still holds, the template method pattern, anonymous subclasses, constructor order, and every compile error reproduced on JDK 21.

[Java Basics] Inheritance in Java: extends and super

How extends works in Java, what a subclass inherits and what it does not, why constructors are never inherited, how super(...) chains constructors up to java.lang.Object and back down, field hiding versus overriding, protected across packages, final classes, the fragile base class problem, and when composition is the better answer.

[Java Basics] Arrays in Java: Declaring, Initializing and Traversing

A complete guide to one-dimensional arrays in Java - every declaration form, default values, the length field, indexing and ArrayIndexOutOfBoundsException, indexed and for-each traversal, the reference trap, real copies with Arrays.copyOf and System.arraycopy, and Arrays.equals, all compiled and run on JDK 21.

[Java Basics] Recursion in Java: How It Works and When to Use It

How recursion works in Java: the base case and the recursive case, factorial traced frame by frame, a real StackOverflowError from a missing base case, recursion depth and -Xss, why naive Fibonacci needs 2692537 calls for fib(30) while memoisation needs 59, recursion versus iteration, and why the JVM does not optimise tail calls.