- What is an Interface?
- Default Methods (Java 8+)
- Static Methods in Interfaces
- Private Methods (Java 9+)
- Functional Interfaces
- Abstract Class vs 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 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:
- Function<T,R> — takes T, returns R
- Predicate<T> — takes T, returns boolean
- Consumer<T> — takes T, returns nothing
- Supplier<T> — takes nothing, returns T
- BiFunction<T,U,R> — takes T and U, returns R
- UnaryOperator<T> — Function<T,T> shorthand
- BinaryOperator<T> — BiFunction<T,T,T> shorthand
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.