Skip to content
elephantoo

Interfaces

Lesson 17 of 43 16 min read

Contracts, multiple implementation, default, static and private methods, and interface vs abstract class.


An interface is a contract: a named set of methods that a class promises to provide. It says what an object can do, not how, and it is not tied to any class hierarchy. A Phone, a Drone and a Car have nothing in common as classes, but all of them can be Chargeable.

Interfaces are probably the most important tool for flexible Java design. Almost every major API (List, Comparable, Runnable, JDBC's Connection, Spring's repositories) is built on them.

Declaring and implementing an interface#

Devices.java
public class Devices {
    public static void main(String[] args) {
        Chargeable[] gadgets = { new Phone("Pixel"), new Drone(), new Phone("Galaxy") };
        for (Chargeable c : gadgets) {
            c.charge(30);
        }
        System.out.println("Max watts: " + Chargeable.MAX_WATTS);
    }
}

interface Chargeable {
    int MAX_WATTS = 65;               // implicitly public static final

    void charge(int minutes);         // implicitly public abstract
}

class Phone implements Chargeable {
    private final String model;
    private int battery = 20;

    Phone(String model) { this.model = model; }

    @Override
    public void charge(int minutes) {             // must be public
        battery = Math.min(100, battery + minutes * 2);
        System.out.println(model + " battery: " + battery + "%");
    }
}

class Drone implements Chargeable {
    @Override
    public void charge(int minutes) {
        System.out.println("Drone charging for " + minutes + " min");
    }
}
Output
Pixel battery: 80%
Drone charging for 30 min
Galaxy battery: 80%
Max watts: 65

Points to notice:

  • Interface methods without a body are implicitly public abstract, so the implementing methods must be public. Writing just void charge(int minutes) in Phone gives "attempting to assign weaker access privileges".
  • Interface fields are implicitly public static final: they are constants, never per-object state.
  • You can't write new Chargeable(), but you can use Chargeable as a variable, parameter, array or return type. That is where the power is.

Implementing several interfaces#

A class can extend only one class, but it can implement any number of interfaces. That lets one object play several roles:

Pond.java
public class Pond {
    static void race(Swimmer s) { s.swim(); }
    static void airShow(Flyer f) { f.fly(); }

    public static void main(String[] args) {
        Duck donald = new Duck("Donald");
        race(donald);
        airShow(donald);
        donald.eat();

        Flyer f = donald;            // the same object, seen through different "lenses"
        Swimmer s = donald;
        System.out.println(f instanceof Swimmer);
        System.out.println(s == f);
    }
}

interface Flyer { void fly(); }
interface Swimmer { void swim(); }

class Animal {
    protected final String name;
    Animal(String name) { this.name = name; }
    void eat() { System.out.println(name + " is eating"); }
}

class Duck extends Animal implements Flyer, Swimmer {   // extends first, then implements
    Duck(String name) { super(name); }

    @Override public void fly()  { System.out.println(name + " flies"); }
    @Override public void swim() { System.out.println(name + " swims"); }
}
Output
Donald swims
Donald flies
Donald is eating
true
true

Interfaces can also extend other interfaces, even several at once: interface Amphibious extends Flyer, Swimmer { }.

Program to the interface#

The biggest benefit of interfaces is that calling code depends only on the contract, so implementations become swappable:

Notify.java
public class Notify {
    public static void main(String[] args) {
        OrderService email = new OrderService(new EmailSender());
        OrderService sms = new OrderService(new SmsSender());
        email.placeOrder("A-1001");
        sms.placeOrder("A-1002");
    }
}

interface MessageSender {
    void send(String to, String text);
}

class EmailSender implements MessageSender {
    @Override public void send(String to, String text) {
        System.out.println("EMAIL to " + to + ": " + text);
    }
}

class SmsSender implements MessageSender {
    @Override public void send(String to, String text) {
        System.out.println("SMS to " + to + ": " + text);
    }
}

class OrderService {
    private final MessageSender sender;            // depends on the contract only

    OrderService(MessageSender sender) { this.sender = sender; }

    void placeOrder(String id) {
        // ... save the order ...
        sender.send("customer", "Order " + id + " confirmed");
    }
}
Output
EMAIL to customer: Order A-1001 confirmed
SMS to customer: Order A-1002 confirmed

OrderService doesn't know or care how messages travel. In tests you can pass a fake sender that just records messages. This idea, handing an object the dependencies it needs from outside, is called dependency injection, and it is the heart of Spring.

You already use this habit with collections: List<String> names = new ArrayList<>(); declares the variable as the interface List, so you could switch to another implementation later without changing the rest of the code.

Default, static and private methods#

Since Java 8, interfaces can contain method bodies in three forms:

  • default methods: inherited by implementing classes, which may override them. They let library authors add methods to an interface without breaking every existing implementation.
  • static methods: helpers called on the interface name, like List.of(...) or Comparator.naturalOrder(). They are not inherited by implementing classes.
  • private methods (Java 9+): helpers shared by the default methods, hidden from everyone else.
Discounts.java
public class Discounts {
    public static void main(String[] args) {
        PriceRule festive = new PercentOff(20);
        PriceRule flat = new FlatOff(150);

        System.out.println(festive.apply(1000));
        System.out.println(flat.apply(1000));
        System.out.println(flat.applyAll(new double[] {100, 500, 1000}));
        System.out.println(PriceRule.none().apply(1000));
    }
}

interface PriceRule {
    double apply(double price);                       // abstract: each rule decides

    default String applyAll(double[] prices) {        // default: shared behaviour
        StringBuilder sb = new StringBuilder();
        for (double p : prices) {
            sb.append(format(apply(p))).append(' ');
        }
        return sb.toString().trim();
    }

    static PriceRule none() {                         // static factory
        return new FlatOff(0);
    }

    private String format(double amount) {            // private helper
        return String.format("Rs%.0f", amount);
    }
}

class PercentOff implements PriceRule {
    private final int percent;
    PercentOff(int percent) { this.percent = percent; }

    @Override public double apply(double price) { return price * (100 - percent) / 100; }
}

class FlatOff implements PriceRule {
    private final double amount;
    FlatOff(double amount) { this.amount = amount; }

    @Override public double apply(double price) { return Math.max(0, price - amount); }
}
Output
800.0
850.0
Rs0 Rs350 Rs850
1000.0

The diamond: two defaults with the same name

If a class inherits the same default method from two interfaces, Java refuses to guess. The class must override it, and can pick one parent's version with Interface.super.method():

Diamond.java
public class Diamond {
    public static void main(String[] args) {
        System.out.println(new SmartWatch().status());
    }
}

interface Watch {
    default String status() { return "Time: 10:30"; }
}

interface FitnessTracker {
    default String status() { return "Steps: 8421"; }
}

class SmartWatch implements Watch, FitnessTracker {
    @Override
    public String status() {                     // required: the defaults conflict
        return Watch.super.status() + " | " + FitnessTracker.super.status();
    }
}
Output
Time: 10:30 | Steps: 8421

A related rule: classes win. If a superclass already has a method with the same signature as an interface default, the class's version is used.

Functional interfaces (a preview)#

An interface with exactly one abstract method is a functional interface. Instead of writing a whole class, you can implement it with a lambda:

LambdaPreview.java
public class LambdaPreview {
    public static void main(String[] args) {
        Calculator add = (a, b) -> a + b;
        Calculator power = (a, b) -> (int) Math.pow(a, b);
        System.out.println(add.compute(6, 7));
        System.out.println(power.compute(2, 10));
    }
}

@FunctionalInterface          // optional: compiler checks there is exactly one abstract method
interface Calculator {
    int compute(int a, int b);
}
Output
13
1024

Runnable, Comparator and everything in java.util.function are functional interfaces. The Lambdas lesson goes deep on this.

Marker interfaces and well-known JDK interfaces#

Some interfaces have no methods at all and just "tag" a class, like java.io.Serializable. More often you will implement interfaces the JDK already understands:

InterfaceMethod(s) you implementWhat you get
Comparable<T>compareTo(T other)Collections.sort and TreeSet can order your objects
Iterable<T>iterator()your class works in an enhanced for loop
AutoCloseableclose()your class works with try-with-resources
Runnablerun()can run on a thread

Interface vs abstract class#

InterfaceAbstract class
A class can have...manyonly one
Instance fields (state)❌ only constants✅
Constructors❌✅
Method bodiesdefault, static, privateany method
Member accesspublic (except private helpers)any modifier
Modelsa capability ("can be charged", "can be compared")a base type with shared code ("is an Employee")

Rule of thumb: start with an interface. Reach for an abstract class when implementations genuinely share state or a lot of code. Many designs use both: an interface for the contract and an abstract class that partially implements it (List and AbstractList).

Common mistakes#

  • Forgetting public on the implementing method (weaker access is not allowed).
  • Trying to store per-object state in an interface field; it's a shared constant.
  • Writing implements before extends: the order is class A extends B implements C, D.
  • Using interface X implements Y; interfaces extends other interfaces.
  • Calling a static interface method through an implementing class or instance (PercentOff.none() doesn't compile); call it on the interface: PriceRule.none().

What's next#

So far, every data-holding class needed a constructor, getters, toString, equals and hashCode written by hand. Records generate all of that from a single line.

Check your understanding

Quick quiz

0/3 answered
  1. 1.Which declaration is legal in Java?

  2. 2.A field declared in an interface as int MAX = 10; is implicitly...

  3. 3.A class implements two interfaces that both provide a default String hello() method. What must it do?

Finished reading?

Mark this lesson complete to track your progress.