mattone
dopo mattoneTHE BOOK SERIES
IT/EN
← Java guide

Java 25 · 13/39

13. Interfaces, abstract classes and polymorphism

Java 25 · Complete guide · Draft under review

This guide retains the book’s draft status. Editorial review and comprehension checks with independent readers remain to be completed.

Search the whole guide →

When the same question concerns different objects

In the inheritance chapter we could treat a car as a vehicle. The base class provided a real family relationship and part of the implementation. Now imagine wanting to ask both a thermometer and a loan counter for a numeric value. Neither is a variant of the other. We do not need to invent a superclass gathering “things with a number”: we only want to ask both the question value().

An interface declares a type through operations that classes promise to offer. A class writes implements and provides the required behavior. An interface-type variable can refer to objects of different classes implementing it. When we call the method on that variable, the actual object's implementation executes. This is the subtype polymorphism we will use throughout the rest of the book: the caller depends on a common contract and need not know every concrete class.

Definition – Contract. For an interface, the contract includes method signatures, operation meanings and conditions callers can expect. The compiler checks signatures, but cannot alone prove that value() really represents the same idea in every implementation. Identical names with incompatible meanings do not build a good abstraction.

An int stack can seem too specific: replacing the element type with Object lets it accept any object. This loses type checking: the caller must cast and may discover an error only at runtime. Here we separate the problems. The interface describes what can be done; chapter 15's generics will describe the element type without giving up compiler checking. C08's IntStack remains a concrete example of state and invariants, and does not magically become generic by implementing an interface.

Follow a stack’s operations before choosing how to declare its contract. With capacity two, the sequence push(7), push(9), pop() leaves 7 in the stack and returns 9: the last element in is the first out. For another stack holding titles we would want the same rule, but pop() should return a String, not an int. If we declare Object pop() in the interface, both objects can promise the same signature; but a caller receiving Object does not know whether the result is a number or a title. After Object data = stack.pop(), a cast to String compiles even when the stack contains an Integer; the error appears only when the cast executes. The interface made the operation name uniform, but lost an essential relationship between push's argument and pop's result.

Saying all objects extend Object is not enough. That relationship allows storing heterogeneous references, not promising that every popped object is of the caller's desired type. Nor should pop() return zero when the stack is empty: zero could be a legitimate element and would not distinguish result from error. In C08 we chose an exception for the empty case; in C15 we will use Stack<T> to keep input and output types together. The historical passage from an interface with Object to the generic solution remains important: it shows which problem the type parameter solves instead of presenting angle brackets as a mere graphical innovation.

Key concept – Two different generalizations. A Measurable variable allows a function to work with several classes sharing behavior. The parameter T of Stack<T> will allow it to maintain a relationship among several contract positions. We can use interfaces and generics together, but neither replaces the other.

The first contract

The complete program declares Measurable and Describable inside a demonstration class, keeping all necessary pieces in one file. In a larger application public interfaces would have their own files. The central declaration is small:

interface Measurable {
    int value();

    default boolean exceeds(int threshold) {
        return value() > threshold;
    }

    static int difference(Measurable first, Measurable second) {
        return Math.abs(first.value() - second.value());
    }
}

The method value() has no body: it is implicitly public and abstract. A concrete class implementing Measurable must provide a public int value() method. Coincidentally having a method of the same name is not enough: the class must declare the relationship with implements. An interface has no instance fields or constructors, so does not directly create objects. It can declare constants, implicitly public static final, but a public constant must be chosen because it truly belongs to the contract, not to hide shared state.

exceeds is a default method: it offers common behavior based on the abstract method. If the actual value changes, exceeds always uses the value returned by the current object. difference is instead static: it is invoked as Measurable.difference(a, b), not through an instance. Its job is a useful calculation alongside the contract; it is not overridden by classes implementing the interface. The Java 25 interface specification also describes private methods, useful for sharing a detail among default or static methods without exposing it to callers.

Figure 13.1 distinguishes the relationship between type and implementations. Arrows indicate that the two classes satisfy the same contract, not that one inherits the other's state.

Two implementations of the same contract
Figure 13.1 – An interface lets us ask value() of objects unrelated by class. Temperature and LoanCounter can also implement Describable without giving up their different internal structures.

In the program, Temperature is a record; its value component generates a value() accessor satisfying the interface. We need not write a second identical method. LoanCounter instead inherits value() from an abstract class. The two routes differ, but a caller receiving Measurable uses the same operation. Running the program we read 27 degrees, true, 1 loans and 26: difference uses values 27 and 1. The program also shows an abstraction limit: subtracting degrees and a loan count is mathematically possible but semantically questionable. In a real project the contract name and types should prevent comparing quantities without the same meaning. The example serves precisely to reveal that syntax compatibility alone is insufficient.

Important note – The compiler does not measure meaning. Measurable.difference accepts both objects because both implement the interface. If the domain permits only temperature differences, a more specific contract is needed, or a function accepting only Temperature. Generalizing too early transfers meaning errors from the compiler to readers and users.

Abstract class or interface?

The program's Counter class keeps a private field and implements two operations: increase() and value(). It is declared abstract because we want to use it as a specialization base, not create it directly. LoanCounter inherits both state and behavior, adding the description. A Java class can extend only one class, abstract or concrete, but can implement several interfaces. This is why both the Temperature record and LoanCounter declare implements Measurable, Describable.

An abstract class is suitable when subclasses share state, invariants or an operation sequence the base can govern naturally. An interface is suitable when we want to express a capability usable by otherwise independent classes. This is not a rule applied by counting methods alone: an interface can have default methods, and an abstract class can have no fields. The most useful question is what relationship we promise the type's users and what future evolution we want to allow.

A solid, two responsibilities

A solid makes this distinction concrete: knowing its weight required a specific weight and a volume; knowing its volume instead required knowing which concrete shape we faced. Follow the reasoning in a complete program. MeasurableSolid declares only the questions volume() and surfaceArea(). The abstract class Solid stores density, checks it is positive and finite and offers mass(). Cube knows its side and implements both geometric measures. If another class could calculate volume and surface area without being a Solid subclass, it could still implement MeasurableSolid: that is the freedom granted by the contract.

The program prints 8.0, 24.0 and 24.0. For side length 2 units, cube volume is 2 × 2 × 2 = 8 cubic units; surface area is the sum of six square faces, each of area 2 × 2, so 6 × 4 = 24 square units. In the example density is 3 mass units per volume unit; mass is 3 × 8 = 24 mass units. Geometry is not the chapter's subject, but units make a design issue clear: volume() and surfaceArea() both return double, yet represent different quantities. Numeric type alone does not prevent mistakenly adding them. Names, documentation and, in systems requiring them, distinct domain types complete the contract.

In main, the reference MeasurableSolid measurable = cube exposes only the two geometric measures. measurable.mass() would not compile: mass() belongs to the abstract class and thus the concrete Cube type, not the interface. The reference still points to the same cube; changing the variable's type does not change the object, but changes operations the compiler permits calling. cube.mass() works and calls volume() implemented by Cube. This is a concrete test of the relationship between abstract class and overridden method.

Important note – The formula has assumptions. density × volume calculates mass only if density is homogeneous and units compatible. If density varies inside the solid, the method no longer represents the phenomenon; neither an interface nor a more precise double saves it. A method contract also includes assumptions under which the result makes sense.

For interfaces, we can speak of “multiple inheritance of type”. It is true a class can implement several and an interface can extend several. This does not mean Java freely inherits state from several classes. The distinction avoids imagining field or constructor conflicts belonging to other language models.

When two default methods have the same name

If two interfaces provide the same default method and a class implements both, Java asks the class to resolve the conflict. In the second program, First and Second both declare name(). Choice writes its own name() and explicitly calls First.super.name() and Second.super.name(). The output is first+second.

The compiler cannot choose based on implements order: that order expresses no semantic priority. The class can choose one implementation, combine them or provide completely new behavior, as long as it respects its promised contract. If a base class already provides a compatible instance method, it takes precedence over the interface default. Selection is part of class design, not a detail to leave to chance.

Consider an Author interface needing to grow without breaking existing implementations: after BookAuthor already implemented two methods, the published API wanted to add a third without forcing all implementation authors to write it. The new operation must make sense for every author. If the interface promises name() and birthDate(), it can add default String label() { return name() + " (" + birthDate() + ")"; }. The old class continues to have behavior for label() because the default relies on two operations already required by the contract. A class can still override it, for example if the date must not appear on a public receipt.

The historical example instead added getWeight() with fixed value 100.0. A class would compile then too, but the contract would say something false about many objects. This is the exercise's most interesting point: signature compatibility does not guarantee correctness of meaning. If no sensible general implementation exists, a new interface or separate service is preferable to teaching every class a method returning an invented value. Binary compatibility of an addition must also be assessed in the whole hierarchy's context: methods already present in other interfaces or classes can introduce conflicts. default helps API evolution, but does not exempt us from testing existing implementations.

An interface static method solves a different need. If we want to verify that two measurements are both nonnegative, a static method can work on received values and remain near the contract. The call is written Measurable.methodName(...); implementing classes do not override it with normal overriding. A private method can serve as a detail shared among several defaults or statics: the external caller does not see it. We can thus choose among a common rule each object inherits, a type-associated utility operation and an internal detail, without confusing the three visibility and dispatch levels.

Extended interfaces, anonymous classes and closed boundaries

An interface can extend others: interface ReadableArchive extends Describable would add new operations while maintaining the description contract. When a parameter type is the smallest interface sufficient for the method, the caller remains free to pass different implementations. Declaring every parameter with the most general possible interface, however, is not always a virtue: it must still clearly represent what the method requires.

An interface can also be declared inside a class or another interface. The qualified name, for example Archive.Reader, communicates that the contract belongs to Archive's context; it does not automatically create an outer object for every implementation. Visibility depends on declaration location and modifiers. It is useful if the contract truly belongs to that API, less useful when other program parts must use it as an independent concept.

Before lambdas, a small local implementation was often supplied with an anonymous class: a nameless class declared in the expression new Interface() { ... }. It remains useful when several methods must be implemented or small local state kept; for interfaces with one abstract method, in chapter 16 we will often see a more readable lambda. An enumeration too can implement an interface if its values share an operation. These forms do not change the meaning of implements.

A sealed interface limits which classes or interfaces can directly implement it, with a permits clause. It is useful when the domain is intentionally closed and we want to reason about every case, for example in an exhaustive switch. One implementation can be a record, provided it is permitted by the declaration and respects sealed-hierarchy rules. If new cases must be freely introduced by other modules, closing the type would instead be an obstacle. Chapter 9 showed this choice for classes; the principle is the same.

In DemoOutcome.java, a loan produces an Outcome: either Confirmed, carrying a code, or Rejected, carrying a reason. Both records implement a sealed interface. A switch on Outcome treats both cases and returns a description. The compiler knows the permitted case list is closed, so no default branch masking omissions is needed.

static String describe(Outcome outcome) {
    return switch (outcome) {
        case Confirmed confirmed -> "loan " + confirmed.code();
        case Rejected rejected -> "rejected: " + rejected.reason();
    };
}

The output is loan B-17 and rejected: unavailable. If we add a third permitted record in permits, the compiler asks us to update the switch. This is an advantage when the program must handle every outcome, not a reason to close all interfaces. Measurable deliberately remains open: a future class can offer a measurement without asking to modify its declaration. The open/closed choice comes from domain stability, not syntax novelty.

A nameless class and an enum with behavior

Anonymous classes and enumerations can both implement an interface. Before choosing the shortest form, observe what is declared. DemoLocalForms.java defines Operation, whose apply method receives two numbers and returns one. Calculation is an enum: each constant provides its own method implementation. SUM returns the sum, PRODUCT the product. In main an anonymous class instead appears, created with new Operation() { ... }; its implementation subtracts a discount from the total. The three printed results are, in order, 6.0, 8.0 and 15.0.

The first calculation's sequence deserves reading without skipping steps: apply(5, 3) adds 5 and 3, then subtracts discount 2, obtaining 6. The method is in the anonymous class body; the expression new Operation() { ... } creates an instance of that class, implementing the interface. We are not directly creating an instance of an abstract interface. Nor are we using merely a “nameless object” in the generic sense of new String(...) passed without assigning it to a variable: here the class has no name declared in source.

The anonymous class can use discount because that local variable is never reassigned after its initial assignment: it is effectively final. If we added discount = 3; later in the method, the compiler would reject capture, even if reassignment appeared after object creation. The rule concerns the local variable, not the abstract possibility of modifying any object: a stable local reference can still point to a mutable object. Saying final alone guarantees purity or no effects would be wrong. Nor is there a general rule that adding final to every local automatically produces optimizations impossible with an effectively final variable.

For this single operation, a lambda such as (first, second) -> first + second - discount would be shorter. We will use it in C16, where we clarify the target type. The anonymous class remains instructive because it explicitly shows the implemented method and can declare state and several methods when the contract permits. A lambda requires a functional interface, while an anonymous class can also implement an interface with several abstract methods. If the implementation deserves a name, will be reused in several places or needs explanations of its own, a declared class is often more readable than an ever-growing anonymous body.

The enum shows another decision. Calculation.SUM and Calculation.PRODUCT are two values of a closed set, not two public subclasses the caller must construct. The call Calculation.SUM.apply(5, 3) executes the body associated with that constant. We could add SUBTRACTION, but must also give it semantics consistent with Operation. If every constant ended up containing many state details or a policy changing at runtime, an enum would become too rigid a cage; separate classes implementing the contract would better describe that domain.

To observe. Replace Operation subtractDiscount = new Operation() { ... }; with the equivalent lambda and check that the three printed lines remain identical. Then try reassigning discount before calling apply: compilation must fail, and diagnosis concerns local-variable capture. Finally add a SUBTRACTION constant to the enum: merely writing its name is insufficient because the contract still requires apply behavior.

Polymorphisms that must not be confused

In the book we have encountered several uses of polymorphism. Overloading chooses among methods with the same name and different parameters according to signatures available at compile time. Overriding lets a subclass or interface-implementing class provide behavior called through a more general type. Generics let us write an algorithm or container parameterized on a type. The idea of common behavior remains useful, provided we distinguish static type checking from method selection at runtime.

Suppose we have Measurable measurement = new Temperature(27);. The reference's declared type is Measurable, so we can call only methods that contract makes available. The actual object is Temperature, and value() executes the behavior provided by the record. We cannot directly call description() through measurement, because Describable is not promised by the declared type, even if that specific object implements it. A cast or instanceof pattern could recover the other capability, but if the method always requires both capabilities, designing a type expressing both is more honest.

The abstract class presents another useful boundary. Counter owns a field and the increase() method, but we cannot write new Counter(): the class is incomplete as an entity to expose directly. This does not mean its constructor, if defined, does not execute when a subclass is created. The base-class part is still initialized before the subclass constructor completes work, according to rules seen in C09. Calling an overridable method from the abstract constructor is risky: it might observe uninitialized subclass fields.

Further reading – A private method in an interface. An interface with several default methods may repeat a small internal rule. A private interface method allows naming and reusing it without adding it to the public contract. It is not an abstract method to implement in concrete classes. This illustrates how a modern interface can contain shared behavior without becoming a class with instance fields.

Three tests on the same reference

To truly see polymorphism, declaring two classes with implements is not enough. Return to Measurable measurement = new Temperature(27) and perform three mental tests. First call measurement.value(): compilation succeeds because value() is in Measurable's contract, and the call returns 27 from the Temperature object. Then assign measurement = new LoanCounter() and call measurement.value() again: the call's source is identical, but the executed implementation now belongs to the counter. Finally try measurement.description(): the call does not compile, although both concrete classes have that capability, because the variable's declared type does not promise it.

We could solve the last case with a cast, but first ask what the caller knows. If every value allowed there must also be describable, a more specific interface extending both capabilities makes the requirement explicit. If only some are, an instanceof Describable describable check lets us handle the case without assuming every object qualifies. The distinction is not about avoiding a compiler message: it avoids turning an undeclared assumption into a runtime error.

The idea that different objects can present common operations requires a recognisable contract. In Java that form is a declared type, so the compiler checks the relationship. A class coincidentally having a method called value is not enough: without implements Measurable, it cannot be used as Measurable. This makes the contract verifiable and lets documentation describe a common meaning. Its price is an explicit declaration, which in a well-designed library also becomes a reader's guide.

Designing a contract that survives implementations

A useful interface does not come from copying every method of an existing class. It comes from a question several callers must ask without knowing the response's details. Imagine a method printing a loan receipt. If it only needs a description, receiving Describable is clearer than receiving a LoanCounter object merely because that is the first available implementation. But if it must also change the loan count, Describable promises too little: a contract explicitly exposing the permitted operation is needed.

Contract segregation means avoiding a caller's dependence on operations it does not use. It does not mean creating an interface for each individual method regardless of context. Measurable and Describable are separate because they ask different things: a measurement and a textual representation. A class can implement both, but a method requiring only the measurement does not thereby receive the right to describe or modify the object. This narrows expectations and makes replacing an implementation easier.

Correct substitution also requires coherent behavior. If Measurable.value() promises a stable measurement during a single operation, an implementation randomly changing the value at every read breaks that promise despite compiling. If Describable.description() is intended for a user receipt, an implementation returning an incomprehensible internal code respects the signature but not the use. Contract documentation must therefore explain meaning, any allowed values, errors and result stability, not just show int value();.

Evolution also deserves attention. Adding a new abstract method to a public interface may force every implementing class to change. A default method can provide compatible initial behavior when a sensible general rule exists, such as exceeds based on value(). It must not mask a question some implementations cannot answer correctly. If new behavior is optional or belongs to another capability, another interface may model it more faithfully.

Finally, an interface does not make an object immutable or thread-safe. LoanCounter can change its number; two threads updating it would require further concurrency rules. The contract can declare these guarantees if needed, but the word interface does not create them. This caution prepares collection chapters: List says which operations exist, while implementation choice and API contract establish practical properties such as mutability, order and concurrent use.

To verify

Compile the three programs with javac --release 25 -Xlint:all -d build DemoInterfaces.java DemoDefaultConflict.java DemoOutcome.java. Then try removing name() from Choice: the compiler must reject the default conflict. In the first program, change exceeds's parameter from 25 to 30 and explain why only the boolean line changes. In the third, add a new permitted outcome and observe the switch diagnosis before updating it. The application activity in C13–C18 will revisit these contracts to introduce a generic type without casts.

Try the examples

Running the programs requires JDK 25. Download the individual Java files linked in the chapter or the complete example project, which includes instructions and a launcher script. The explanations also compare expected output: predict it before running the program.

Massimiliano Tarquini · CC BY-NC 4.0

Back to the top ↑