Lab 08: Interfaces & Abstract Classes

Objective

Define and implement Java interfaces, use default and static interface methods, understand functional interfaces, apply common design patterns (Strategy, Observer), and know when to choose interface vs abstract class.

Background

Interfaces define contracts — what a class can do without specifying how. They enable loose coupling, multiple implementation, and polymorphism across unrelated class hierarchies. Java 8+ interfaces with default methods bridge the gap between interfaces and abstract classes. Functional interfaces power lambdas and the Streams API.

Time

40 minutes

Prerequisites

  • Lab 07 (Inheritance & Polymorphism)

Tools

  • Java 21 (Eclipse Temurin)

  • Docker image: innozverse-java:latest


Lab Instructions

Step 1: Defining and Implementing Interfaces

💡 A class can implement multiple interfaces but extend only one class. Interfaces define capabilities (Printable, Saveable, Comparable) that cross class hierarchies — a Document, Invoice, and Report can all be Printable without sharing an ancestor.

📸 Verified Output:


Step 2: Functional Interfaces & Lambdas

💡 @FunctionalInterface marks an interface as having exactly one abstract method. The compiler enforces this. Lambda expressions and method references can be assigned to any functional interface. The java.util.function package provides Function, Predicate, Consumer, Supplier, and BiFunction for common patterns.

📸 Verified Output:


Step 3: Strategy Pattern

💡 The Strategy pattern encapsulates algorithms behind an interface, letting you swap them at runtime. With functional interfaces, strategies are just lambdas — no need for separate classes. This is how Comparator, Runnable, Callable, and Comparator.comparing().thenComparing() chains work in the JDK.

📸 Verified Output:


Step 4: Observer Pattern with Interfaces

💡 computeIfAbsent creates the list only when the key is first seen — no null check needed. Consumer<T> is a built-in functional interface for void-returning operations. The Observer/Event Bus pattern powers React's event system, Android's LiveData, and Spring's ApplicationEvent.

📸 Verified Output:


Step 5: Comparable & Comparator

💡 Comparable for natural ordering, Comparator for custom ordering. Implement Comparable when there's one obvious "natural" order (semantic versioning, dates, priorities). Use Comparator when you need multiple orderings or can't modify the class. Always implement both consistently with equals.

📸 Verified Output:


Step 6: Interface vs Abstract Class

💡 Rule of thumb: Use an interface when you're defining what something does (a contract). Use an abstract class when you're defining how it partially does it (shared implementation + state). In modern Java, prefer interfaces with default methods — they're more flexible because classes can implement multiple interfaces.

📸 Verified Output:


Step 7: AutoCloseable — try-with-resources

💡 try-with-resources guarantees cleanupclose() is called even if an exception is thrown. Multiple resources are closed in reverse order of declaration (Transaction before Connection). This eliminates the try/finally boilerplate that was required before Java 7.

📸 Verified Output:


Step 8: Complete Example — Plugin Architecture

💡 Plugin architectures use interfaces to define the contract and registries to manage lifecycle. This is how OSGi, JDBC drivers, SPI (Service Provider Interface), and Spring's BeanPostProcessor work. New plugins can be added without modifying the registry — the Open/Closed Principle in action.

📸 Verified Output:


Verification

Summary

Interfaces in Java define contracts, enable multiple inheritance of type, power lambdas, and support patterns like Strategy, Observer, and Plugin Architecture. Default methods let interfaces evolve without breaking existing implementations. Choose interfaces for capabilities and abstract classes for partial implementations with shared state.

Further Reading

Last updated