An interface in Java is a reference type that defines a contract — a set of method signatures (and optionally constants) that any implementing class must honour. All methods declared in an interface are implicitly public, and all fields are implicitly public static final.

Interfaces achieve multiple type inheritance in Java. A class can implement any number of interfaces, whereas it can extend only one class. This makes interfaces the primary tool for designing polymorphic APIs.

// Basic interface — a contract for shapes public interface Shape { // Implicitly public abstract double area(); double perimeter(); // Constant — implicitly public static final double PI = 3.14159265358979; } // Implementing the contract public class Circle implements Shape { private final double radius; public Circle(double radius) { this.radius = radius; } @Override public double area() { return PI * radius * radius; } @Override public double perimeter() { return 2 * PI * radius; } } public class Rectangle implements Shape { private final double width; private final double height; public Rectangle(double width, double height) { this.width = width; this.height = height; } @Override public double area() { return width * height; } @Override public double perimeter() { return 2 * (width + height); } } // Polymorphic usage public class Main { public static void printInfo(Shape s) { System.out.printf("Area: %.2f Perimeter: %.2f%n", s.area(), s.perimeter()); } public static void main(String[] args) { printInfo(new Circle(5)); // Area: 78.54 Perimeter: 31.42 printInfo(new Rectangle(4, 6)); // Area: 24.00 Perimeter: 20.00 } }

A class declares that it implements an interface with the implements keyword. If it does not provide a concrete body for every abstract method, the class must itself be declared abstract.

An interface can extend one or more other interfaces using the extends keyword (not implements). This lets you compose narrow contracts into broader ones without touching implementing classes.

Before Java 8, adding a new method to a published interface broke every existing implementation — they all had to be updated. Java 8 introduced default methods: interface methods with a body, marked with the default keyword. Implementors can use the provided body or override it.

The classic motivating example is Collection.stream(), which was added to the Collection interface in Java 8 as a default method, avoiding a breaking change across thousands of existing collection implementations.

public interface Validator<T> { // Abstract — must be implemented boolean isValid(T value); // Default — optional to override default void validate(T value) { if (!isValid(value)) { throw new IllegalArgumentException("Invalid value: " + value); } } // Default method that composes two validators default Validator<T> and(Validator<T> other) { return value -> this.isValid(value) && other.isValid(value); } default Validator<T> or(Validator<T> other) { return value -> this.isValid(value) || other.isValid(value); } } // Minimal implementation — only isValid is required public class PositiveValidator implements Validator<Integer> { @Override public boolean isValid(Integer value) { return value != null && value > 0; } } public class RangeValidator implements Validator<Integer> { private final int min; private final int max; public RangeValidator(int min, int max) { this.min = min; this.max = max; } @Override public boolean isValid(Integer value) { return value != null && value >= min && value <= max; } } public class Main { public static void main(String[] args) { Validator<Integer> positive = new PositiveValidator(); Validator<Integer> range = new RangeValidator(1, 100); // Compose with default 'and' method Validator<Integer> strict = positive.and(range); System.out.println(strict.isValid(50)); // true System.out.println(strict.isValid(-5)); // false System.out.println(strict.isValid(200)); // false // Uses the default validate() — throws on invalid input strict.validate(42); // OK strict.validate(-1); // throws IllegalArgumentException } }

When a class implements two interfaces that both declare a default method with the same signature, the compiler forces you to override the method and resolve the conflict explicitly.

interface A { default String greet() { return "Hello from A"; } } interface B { default String greet() { return "Hello from B"; } } // Compiler error without explicit override class C implements A, B { @Override public String greet() { // Explicitly choose one, or combine both return A.super.greet() + " | " + B.super.greet(); } }

Also introduced in Java 8, static methods in interfaces belong to the interface type itself — they cannot be inherited by implementing classes or called through a reference of the implementing type. They serve as factory or utility methods closely related to the interface's contract.

public interface Comparators { // Static factory methods — live on the interface itself static <T extends Comparable<T>> java.util.Comparator<T> natural() { return Comparable::compareTo; } static <T extends Comparable<T>> java.util.Comparator<T> reversed() { return (a, b) -> b.compareTo(a); } } // Usage List<String> names = Arrays.asList("Charlie", "Alice", "Bob"); names.sort(Comparators.natural()); // [Alice, Bob, Charlie] names.sort(Comparators.reversed()); // [Charlie, Bob, Alice]

A practical pattern is pairing an interface with its own static factory, following the same convention as Comparator.comparing() in the JDK:

public interface Parser<T> { T parse(String input); // Static factories — convenient entry points static Parser<Integer> intParser() { return Integer::parseInt; } static Parser<Double> doubleParser() { return Double::parseDouble; } static <T> Parser<List<T>> listParser(Parser<T> elementParser, String delimiter) { return input -> Arrays.stream(input.split(delimiter)) .map(String::trim) .map(elementParser::parse) .collect(Collectors.toList()); } } public class Main { public static void main(String[] args) { Parser<Integer> ints = Parser.intParser(); System.out.println(ints.parse("42")); // 42 Parser<List<Integer>> listParser = Parser.listParser(Parser.intParser(), ","); System.out.println(listParser.parse("1, 2, 3, 4")); // [1, 2, 3, 4] } }

Java 9 added private methods (both instance and static) to interfaces. Their sole purpose is code reuse between default methods within the same interface, avoiding duplication without exposing internal helpers to implementing classes.

public interface HttpClient { String get(String url); String post(String url, String body); default String getJson(String url) { // Delegate to private helper return withJsonHeaders(() -> get(url)); } default String postJson(String url, String body) { return withJsonHeaders(() -> post(url, body)); } // Private helper — not visible to implementors or callers private String withJsonHeaders(java.util.function.Supplier<String> action) { System.out.println("Setting Accept: application/json"); String result = action.get(); System.out.println("Received response, length=" + result.length()); return result; } // Private static helper for shared utility logic private static String encode(String value) { return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8); } default String getWithParam(String url, String key, String value) { return get(url + "?" + encode(key) + "=" + encode(value)); } }

Without private methods, the only alternative was to duplicate the withJsonHeaders logic inside each default method or move it to a package-private helper class — both solutions worse than a private interface method.

Private interface methods follow the same accessibility rules as private class members: they are invisible outside the interface, cannot be inherited, and cannot be called on an instance of the implementing class.

A functional interface has exactly one abstract method. This makes it eligible as the target type for a lambda expression or method reference. The @FunctionalInterface annotation asks the compiler to enforce this constraint.

@FunctionalInterface public interface Transformer<A, B> { B transform(A input); // Default and static methods don't count toward the "one abstract method" rule default <C> Transformer<A, C> andThen(Transformer<B, C> after) { return input -> after.transform(this.transform(input)); } static <T> Transformer<T, T> identity() { return t -> t; } } public class Main { public static void main(String[] args) { // Lambda — concise implementation of transform() Transformer<String, Integer> length = String::length; Transformer<Integer, String> stars = n -> "*".repeat(n); // Compose with andThen default method Transformer<String, String> starBar = length.andThen(stars); System.out.println(starBar.transform("Hello")); // ***** System.out.println(starBar.transform("Java 21")); // ******* } }

The JDK's java.util.function package ships with many ready-made functional interfaces. The most common ones to know:

import java.util.function.*; public class FunctionalDemo { public static void main(String[] args) { Predicate<String> notEmpty = s -> !s.isEmpty(); Predicate<String> longWord = s -> s.length() > 5; // Compose predicates with and/or/negate — all default methods on Predicate Predicate<String> longNonEmpty = notEmpty.and(longWord); List.of("hi", "hello", "Java", "interfaces") .stream() .filter(longNonEmpty) .forEach(System.out::println); // Prints: interfaces } }

With default and private methods, interfaces can carry significant behaviour. The line between abstract classes and interfaces has blurred, but important differences remain.

Feature Interface Abstract Class
Multiple inheritance Yes — a class can implement many No — a class can extend only one
Instance fields No (only public static final) Yes — any visibility, any mutability
Constructors No Yes
Concrete methods Via default (Java 8+) Yes, always
State (mutable) No Yes
Access modifiers on methods Only public or private Any (public, protected, package-private)
Use for lambda target type Yes (if functional interface) No

The rule of thumb: prefer interfaces when you are defining a type that many unrelated classes can implement. Use an abstract class when you need to share mutable state, protected helpers, or a common constructor among a tightly related hierarchy.

// Abstract class — captures shared mutable state and a template algorithm public abstract class BaseRepository<T, ID> { // Shared state — not possible in an interface protected final java.util.Map<ID, T> store = new java.util.HashMap<>(); // Template method — skeleton defined here, steps deferred public final T findByIdOrThrow(ID id) { T entity = store.get(id); if (entity == null) { throw new java.util.NoSuchElementException("Not found: " + id); } return entity; } // Subclass provides the ID extraction strategy protected abstract ID extractId(T entity); public void save(T entity) { store.put(extractId(entity), entity); } } // Interface — pure contract, no state public interface Auditable { java.time.Instant createdAt(); java.time.Instant updatedAt(); String createdBy(); } // Concrete class inherits behaviour from abstract class AND satisfies interface public class UserRepository extends BaseRepository<User, Long> implements Auditable { private final java.time.Instant created = java.time.Instant.now(); @Override protected Long extractId(User user) { return user.getId(); } @Override public java.time.Instant createdAt() { return created; } @Override public java.time.Instant updatedAt() { return java.time.Instant.now(); } @Override public String createdBy() { return "system"; } } Avoid using default methods to share complex mutable state logic. If you find yourself wanting state in an interface, that is usually a signal to reach for an abstract class instead.