Technology  /  Java

โ˜• Java 15 guides ยท updated 2026

From the JVM to streams, generics, and concurrency โ€” the language behind most enterprise backends, taught with modern Java in mind.

Java Interfaces vs Abstract Classes: Contracts, Default Methods, and Sealed Types

โ€œShould this be an interface or an abstract class?โ€ is one of those questions every Java developer answers weekly, often on autopilot. Getting it right shapes how testable, swappable, and future-proof your code is; getting it wrong calcifies designs that canโ€™t evolve.

This guide explains what interfaces really are (contracts, not classes-lite), what default methods changed, why abstract classes still exist, and how sealed types (Java 17) added a third option nobody had for twenty years.


Interfaces: Capability as a Contract

An interface declares what something can do, saying nothing about how:

public interface PaymentGateway {
Receipt charge(String accountId, long amountPaise);
boolean supportsCurrency(String isoCode);
}

Any class can promise the contract with implements, and must then supply every method:

public class RazorpayGateway implements PaymentGateway {
@Override
public Receipt charge(String accountId, long amountPaise) { /* real API calls */ }
@Override
public boolean supportsCurrency(String isoCode) { return "INR".equals(isoCode); }
}
public class FakeGateway implements PaymentGateway { // for tests
@Override
public Receipt charge(String accountId, long amountPaise) {
return Receipt.approved("test-" + accountId);
}
@Override
public boolean supportsCurrency(String isoCode) { return true; }
}

And hereโ€™s the entire reason interfaces exist โ€” code written against the contract works with every implementation, including ones that donโ€™t exist yet:

public class CheckoutService {
private final PaymentGateway gateway; // the interface, never a concrete class
public CheckoutService(PaymentGateway gateway) { // implementation injected
this.gateway = gateway;
}
public void checkout(Cart cart) {
gateway.charge(cart.accountId(), cart.totalPaise());
}
}

CheckoutService compiles with zero knowledge of Razorpay. Swap gateways, wire in the fake for unit tests, add a new provider next year โ€” the service never changes. This is โ€œprogram to an interface, not an implementationโ€, and itโ€™s the single most valuable design habit in this whole series. Itโ€™s also the backbone of dependency injection frameworks like Spring: theyโ€™re industrial machinery for handing implementations to constructors exactly like this.

Mechanics worth knowing:


Default Methods: The Java 8 Earthquake

For Javaโ€™s first two decades, adding a method to a published interface broke every implementor on the planet. That made interfaces effectively frozen โ€” until Java 8 introduced default methods, bodies in the interface itself:

public interface PaymentGateway {
Receipt charge(String accountId, long amountPaise);
boolean supportsCurrency(String isoCode);
default Receipt chargeInr(String accountId, long amountPaise) {
if (!supportsCurrency("INR"))
throw new UnsupportedOperationException("INR not supported");
return charge(accountId, amountPaise);
}
}

Existing implementors keep compiling; they inherit the default and may override it. This is precisely how List gained sort, Collection gained stream, and Iterable gained forEach in Java 8 without breaking thirty years of code.

Guidelines that keep defaults healthy:


Functional Interfaces: One Method, Big Consequences

An interface with exactly one abstract method is a functional interface, and lambdas can implement it inline:

@FunctionalInterface
public interface RetryPolicy {
boolean shouldRetry(int attempt, Exception lastError);
}
RetryPolicy threeTimes = (attempt, err) -> attempt < 3;
RetryPolicy never = (attempt, err) -> false;

The @FunctionalInterface annotation is optional but valuable โ€” it makes the compiler reject a second abstract method, protecting every lambda that depends on there being exactly one.

The JDK ships the vocabulary youโ€™ll use daily โ€” Predicate<T> (T โ†’ boolean), Function<T,R> (T โ†’ R), Supplier<T> (() โ†’ T), Consumer<T> (T โ†’ void), Comparator<T>. Check java.util.function before inventing your own; a custom name only earns its place when it adds domain meaning, like RetryPolicy. Full treatment in Streams and Lambdas.


Abstract Classes: When Shared State Enters the Picture

An abstract class is a partial implementation: it can hold fields, constructors, and a mix of concrete and abstract methods, but canโ€™t be instantiated:

public abstract class BaseRepository<T> {
protected final DataSource ds; // shared state โ€” interfaces can't do this
private final Map<String, T> cache = new HashMap<>();
protected BaseRepository(DataSource ds) { this.ds = ds; }
public T findById(String id) { // shared logic with caching
return cache.computeIfAbsent(id, this::load);
}
protected abstract T load(String id); // each repository fills this in
}

Subclasses (UserRepository, OrderRepository) inherit the caching machinery and implement only load. An interface canโ€™t express this โ€” no fields, no constructor, nowhere to put the cache.

So the honest division of labor:

QuestionInterfaceAbstract class
Shared instance state?โœ—โœ“ fields + constructors
Multiple inheritance?โœ“ implement manyโœ— extend one
Unrelated classes can join?โœ“ (Comparable spans everything)impractical
Lambda-friendly?โœ“ if single-methodโœ—
Evolves without breaking users?โœ“ via defaultsโœ“ via new concrete methods

Working rule: define the type as an interface โ€” thatโ€™s what callers and tests depend on. Add an abstract class alongside it only when several implementations genuinely share state or plumbing (AbstractList next to List is the JDKโ€™s own template). Starting with an abstract class as the public type costs your users their single extends slot and couples them to your state โ€” make that trade knowingly, not by default.


Sealed Interfaces: Closing the Door on Purpose (Java 17)

Classic interfaces are open โ€” anyone, anywhere can implement them. Usually thatโ€™s the point. But some domains are closed by nature: a payment result is approved, declined, or errored. Nothing else exists. Sealed types let you say so:

public sealed interface PaymentResult
permits Approved, Declined, Errored { }
public record Approved(String txnId, long amountPaise) implements PaymentResult { }
public record Declined(String reason) implements PaymentResult { }
public record Errored(Exception cause) implements PaymentResult { }

The compiler now knows the complete set of subtypes โ€” which unlocks exhaustive switch with no default:

String message = switch (result) {
case Approved a -> "Paid: " + a.txnId();
case Declined d -> "Declined: " + d.reason();
case Errored e -> "Retry later";
}; // no default needed โ€” and that's a feature

Add a fourth result type next quarter, and every switch like this fails to compile until you handle it. Compare that with a default branch, which would silently swallow the new case. Sealed hierarchies plus records plus pattern matching is modern Javaโ€™s answer to algebraic data types, and itโ€™s changing how domain models are written โ€” states as a fixed family of small immutable types, behavior as exhaustive switches.

(A permitted subtype must itself be final, sealed, or explicitly non-sealed; records are implicitly final, which is why they pair so naturally.)


Choosing in Practice: Three Worked Calls

A notification sender with email/SMS/push variants. Callers need send(msg); implementations share nothing but the contract. โ†’ Interface (NotificationSender), one class per channel, injected where needed. Tests use a lambda or fake.

Report generators that all load data, render differently, and share formatting helpers and a template skeleton. Shared state (formatter config) and shared partial logic. โ†’ Interface for the public type (ReportGenerator) if outsiders depend on it, plus an abstract class (AbstractReportGenerator) providing the skeleton; concrete generators extend it. If itโ€™s all internal, the abstract class alone is fine.

The states of an order: Placed, Shipped, Delivered, Cancelled. Fixed, known set; per-state data differs; logic wants exhaustive handling. โ†’ Sealed interface + records, behavior in pattern-matching switches.

Notice the pattern in all three: the decision driver is never โ€œwhich feels more OOโ€ โ€” itโ€™s who needs to depend on what, and whether the set of subtypes is open or closed.


The Interfaces You Already Depend On

Worth a tour, because implementing the JDKโ€™s own contracts is how your classes plug into the platformโ€™s machinery:

Comparable<T> โ€” โ€œI have a natural order.โ€ Implement compareTo, and your type works with Collections.sort, TreeSet, TreeMap, and Stream.sorted() with no further ceremony. Implement it consistently with equals (the sorted collections use compareTo for equality decisions โ€” inconsistency makes a TreeSet disagree with a HashSet about membership, a genuinely disorienting bug).

Comparator<T> โ€” external, composable ordering for types you donโ€™t own or orders beyond the natural one. Modern construction is entirely via factories: Comparator.comparing(User::lastName).thenComparing(User::firstName).

Iterable<T> โ€” one method (iterator()), and the enhanced for loop works on your type. Any custom collection-ish class earns for-each support this cheaply.

AutoCloseable โ€” one method (close()), and try-with-resources manages your typeโ€™s lifecycle. If your class holds a connection, file handle, or native resource, implementing this is not optional politeness; itโ€™s how callers avoid leaking you.

Runnable / Callable<T> โ€” the task contracts every executor consumes. Their existence as interfaces is why lambdas slot straight into thread pools.

The pattern to absorb: the JDKโ€™s collaboration points are all interfaces, almost all tiny. Your own architecture should look the same โ€” the seams between modules are one- and two-method interfaces, and concrete classes meet only at wiring time.

Frequently Asked Questions

Can an interface have constructors or instance fields? No โ€” interfaces hold no per-instance state, which is precisely what keeps multiple inheritance of interfaces safe. public static final constants are allowed but use them sparingly; an interface as a constants dumping ground is a recognized antipattern (put constants in the class that owns them, or an enum).

What happens if a class implements two interfaces that both declare the same abstract method? Nothing bad โ€” one implementation satisfies both contracts, since neither interface supplied behavior. The conflict rules only kick in when defaults collide, and then the compiler forces you to override and choose.

Should every class get an interface? No โ€” thatโ€™s the โ€œinterface for everythingโ€ cargo cult, and it doubles your file count for zero flexibility. Extract an interface when there are (or will credibly be) multiple implementations, when you need to break a dependency for testing, or when youโ€™re defining a plugin point. A class with exactly one conceivable implementation can just be a class; refactoring to an interface later is mechanical.

Abstract class with all abstract methods vs. interface โ€” any difference? Almost none functionally, but the interface costs users nothing (their extends slot stays free) and gains lambda compatibility if itโ€™s single-method. Choose the interface; itโ€™s why the JDKโ€™s own single-method contracts are all interfaces.

Self-Check

  1. Why can CheckoutService be unit-tested without any payment provider SDK installed?
  2. Your published interface needs a new method and has 40 implementors across teams. What Java 8 feature saves you, and whatโ€™s its constraint?
  3. A colleague makes AbstractPaymentGateway the type every caller references. What flexibility did callers just lose?
  4. When is omitting default from a switch a safety feature rather than sloppiness?

(Answers: it depends only on the PaymentGateway interface, so a fake implementation suffices. Default methods; they canโ€™t use instance state and should delegate to the abstract methods. Their single extends slot, and decoupling from the base classโ€™s state and constructors. When switching over a sealed hierarchy or enum โ€” the compiler then flags unhandled new cases instead of a default silently absorbing them.)

Next: Encapsulation, Records, and Immutability โ€” access modifiers done right, why immutable objects simplify everything, and the record syntax that deleted a thousand lines of boilerplate.