Interfaces: Default, Static and Private Methods
Java interfaces started as pure contracts: collections of abstract method signatures that implementing classes were required to fulfil. Java 8 changed that story significantly by allowing default and static methods directly inside an interface. Java 9 went further with private methods. Understanding these additions is essential for reading modern Java APIs and designing flexible libraries.
What is an Interface?
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 publicpublic 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 implementsabstract
extendsimplementsDefault Methods (Java 8+)
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
The classic motivating example is Collection.stream()Collection
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();
}
}
Note:
Static Methods in Interfaces
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()
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]
}
}
Private Methods (Java 9+)
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
Functional Interfaces
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
@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
: takes T, returns RFunction<T,R> : takes T, returns booleanPredicate<T> : takes T, returns nothingConsumer<T> : takes nothing, returns TSupplier<T> : takes T and U, returns RBiFunction<T,U,R> : Function<T,T> shorthandUnaryOperator<T> : BiFunction<T,T,T> shorthandBinaryOperator<T>
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
}
}
Abstract Class vs Interface
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 |
Yes, always |
| State (mutable) | No | Yes |
| Access modifiers on methods | Only publicprivate |
Any (publicprotected |
| 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"; }
}