Interfaces vs Abstract Classes

Understanding the Differences and When to Use Each

← Back to Index

What Are Interfaces and Abstract Classes?

Interfaces and abstract classes are two mechanisms Java provides for achieving abstraction—the ability to define what something does without specifying how it does it. Both allow you to create contracts that other classes must fulfill, but they serve different purposes and have different capabilities. Understanding when to use each is crucial for good object-oriented design.

At their core, both interfaces and abstract classes let you define method signatures without implementations (abstract methods), and both prevent direct instantiation—you can't create an object of an interface or abstract class directly. However, they differ fundamentally in their intent: interfaces define capabilities (what an object can do), while abstract classes define identities (what an object is).

This distinction matters because it affects how classes relate to each other and how flexible your design becomes. A class can implement many interfaces (gaining multiple capabilities), but can only extend one abstract class (having one identity). This single inheritance limitation for classes but multiple inheritance for interfaces is Java's solution to the "diamond problem" that plagued languages like C++.

Historical Context: The Evolution of Interfaces

Java's interface concept was revolutionary when the language was introduced in 1995. While abstract classes existed in earlier languages like C++, Java's interfaces provided a cleaner way to achieve multiple inheritance of type without the complexity and ambiguity of multiple inheritance of implementation.

Interfaces remained purely abstract (no implementation allowed) until Java 8 (2014), which introduced default methods and static methods in interfaces. This was a major evolution driven by the need to add methods to existing interfaces (like adding forEach() to Iterable) without breaking millions of existing implementations. Java 9 added private methods in interfaces, allowing code reuse within default methods. These changes blurred the line between interfaces and abstract classes, making the choice between them more nuanced.

The Big Picture: Where They Fit in Java Design

Interfaces and abstract classes are foundational to nearly every Java framework and library:

/*
 * Interfaces vs Abstract Classes: The Decision Tree
 * ==================================================
 *
 *  Need to define behavior for UNRELATED classes?
 *  │
 *  ├── YES → Use INTERFACE
 *  │         Example: Comparable, Serializable, Runnable
 *  │         (A String, Integer, and your custom class can all be Comparable)
 *  │
 *  └── NO → Classes are RELATED (share "is-a" relationship)?
 *           │
 *           ├── YES → Need to share CODE (fields, constructors, methods)?
 *           │         │
 *           │         ├── YES → Use ABSTRACT CLASS
 *           │         │         Example: Animal → Dog, Cat (share name, age, eat())
 *           │         │
 *           │         └── NO → Use INTERFACE
 *           │                  Example: Shape interface with area(), perimeter()
 *           │
 *           └── NO → Rethink your design!
 */

Real-World Analogies

Interface = Job Description / Contract / Certification

Abstract Class = Partial Template / Family Heritage

Key Differences at a Glance
Feature Interface Abstract Class
Purpose Defines a CONTRACT (what to do) Provides a TEMPLATE (what you are)
Inheritance Can implement MANY interfaces Can extend only ONE abstract class
Methods (pre-Java 8) Only abstract (no body) Both abstract and concrete
Methods (Java 8+) Abstract, default, static, private (9+) Any type of method
Fields Only public static final constants Any type of field
Constructors Not allowed Allowed (called via super)
Keyword implements extends
Access Modifiers Methods implicitly public Any access modifier

Interfaces In Depth

An interface is a completely abstract type that defines a set of methods (and constants) that implementing classes must provide. Interfaces establish a contract: "any class that implements me guarantees to have these capabilities."

Interface Syntax Evolution

// ═══════════════════════════════════════════════════════════════════════════
// CLASSIC INTERFACE (Pre-Java 8) - Pure abstraction
// ═══════════════════════════════════════════════════════════════════════════
public interface Drawable {
    // Constants (implicitly public static final)
    int DEFAULT_COLOR = 0x000000;  // Black

    // Abstract methods (implicitly public abstract)
    void draw();
    void resize(int width, int height);
}

// ═══════════════════════════════════════════════════════════════════════════
// MODERN INTERFACE (Java 8+) - With default and static methods
// ═══════════════════════════════════════════════════════════════════════════
public interface Drawable {
    // Constants
    int DEFAULT_COLOR = 0x000000;

    // Abstract method - implementors MUST provide
    void draw();

    // Default method (Java 8+) - provides default implementation
    // Implementors CAN override, but don't have to
    default void display() {
        System.out.println("Displaying drawable...");
        draw();
    }

    // Default method with common behavior
    default void hide() {
        System.out.println("Hiding drawable");
    }

    // Static method (Java 8+) - utility method on the interface itself
    static Drawable createEmpty() {
        return () -> {};  // Lambda for functional interface
    }

    // Static utility method
    static boolean isVisible(Drawable d) {
        return d != null;
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// JAVA 9+ INTERFACE - With private methods for code reuse
// ═══════════════════════════════════════════════════════════════════════════
public interface Logger {
    void log(String message);

    default void logInfo(String message) {
        log(formatMessage("INFO", message));  // Uses private method
    }

    default void logError(String message) {
        log(formatMessage("ERROR", message));  // Uses private method
    }

    default void logWarning(String message) {
        log(formatMessage("WARNING", message));
    }

    // Private method (Java 9+) - for code reuse within default methods
    // NOT visible to implementing classes
    private String formatMessage(String level, String message) {
        return "[" + level + "] " + java.time.LocalDateTime.now() + ": " + message;
    }

    // Private static method (Java 9+)
    private static String getTimestamp() {
        return java.time.Instant.now().toString();
    }
}

Multiple Interface Implementation

// A class can implement MULTIPLE interfaces - this is Java's answer to
// multiple inheritance (of type, not implementation)

interface Flyable {
    void fly();
    default int getMaxAltitude() { return 10000; }
}

interface Swimmable {
    void swim();
    default int getMaxDepth() { return 100; }
}

interface Walkable {
    void walk();
}

// Duck can fly, swim, AND walk!
public class Duck implements Flyable, Swimmable, Walkable {
    private String name;

    public Duck(String name) {
        this.name = name;
    }

    @Override
    public void fly() {
        System.out.println(name + " is flying");
    }

    @Override
    public void swim() {
        System.out.println(name + " is swimming");
    }

    @Override
    public void walk() {
        System.out.println(name + " is walking");
    }
}

// Penguin can swim and walk, but NOT fly
public class Penguin implements Swimmable, Walkable {
    @Override
    public void swim() {
        System.out.println("Penguin swimming gracefully");
    }

    @Override
    public void walk() {
        System.out.println("Penguin waddling");
    }

    @Override
    public int getMaxDepth() {
        return 500;  // Penguins can dive deep!
    }
}

// Airplane can fly but is NOT an animal
public class Airplane implements Flyable {
    @Override
    public void fly() {
        System.out.println("Airplane taking off");
    }

    @Override
    public int getMaxAltitude() {
        return 40000;  // Commercial jets fly high
    }
}

// The beauty: process ANY flyable thing uniformly
public void takeOff(Flyable f) {
    f.fly();  // Works with Duck, Airplane, or any future Flyable
}

Functional Interfaces and Lambdas

// A functional interface has exactly ONE abstract method
// Can be used with lambda expressions

@FunctionalInterface  // Optional but recommended - compiler enforces single abstract method
public interface Processor<T> {
    T process(T input);

    // Default and static methods don't count against "single abstract method" rule
    default Processor<T> andThen(Processor<T> after) {
        return input -> after.process(this.process(input));
    }
}

// Usage with lambda
Processor<String> trimmer = s -> s.trim();
Processor<String> upper = s -> s.toUpperCase();
Processor<String> combined = trimmer.andThen(upper);

String result = combined.process("  hello world  ");  // "HELLO WORLD"

// Built-in functional interfaces in java.util.function:
// - Function<T,R>    : T -> R
// - Consumer<T>      : T -> void
// - Supplier<T>      : () -> T
// - Predicate<T>     : T -> boolean
// - BiFunction<T,U,R>: (T, U) -> R
When to Use Interfaces
  • Defining capabilities: Comparable, Serializable, Runnable
  • Unrelated classes need same behavior: Both Duck and Airplane can Fly
  • Multiple inheritance of type: Duck implements Flyable, Swimmable, Walkable
  • API contracts: List, Set, Map define what collections can do
  • Dependency injection: Program to interface, inject implementation
  • Callback/strategy patterns: Pass behavior as parameter

Abstract Classes In Depth

An abstract class is a class that cannot be instantiated and may contain both abstract methods (without implementation) and concrete methods (with implementation). Abstract classes serve as templates for related classes, providing shared code while requiring subclasses to implement specific behaviors.

Abstract Class Syntax

public abstract class Animal {
    // ═══════════════════════════════════════════════════════════════════════
    // INSTANCE FIELDS - Can have state (interfaces cannot)
    // ═══════════════════════════════════════════════════════════════════════
    protected String name;
    protected int age;
    private String id;  // Can have private fields

    // ═══════════════════════════════════════════════════════════════════════
    // CONSTRUCTORS - Can have constructors (interfaces cannot)
    // ═══════════════════════════════════════════════════════════════════════
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
        this.id = generateId();
    }

    // Overloaded constructor
    public Animal(String name) {
        this(name, 0);  // Constructor chaining
    }

    // ═══════════════════════════════════════════════════════════════════════
    // ABSTRACT METHODS - Must be implemented by concrete subclasses
    // ═══════════════════════════════════════════════════════════════════════
    public abstract void makeSound();  // Each animal sounds different
    public abstract void move();       // Each animal moves differently
    protected abstract String getSpecies();  // Can have non-public abstract methods

    // ═══════════════════════════════════════════════════════════════════════
    // CONCRETE METHODS - Shared implementation inherited by all subclasses
    // ═══════════════════════════════════════════════════════════════════════
    public void eat() {
        System.out.println(name + " is eating");
    }

    public void sleep() {
        System.out.println(name + " is sleeping for 8 hours");
    }

    public void displayInfo() {
        System.out.println("Animal: " + name + ", Age: " + age + ", Species: " + getSpecies());
    }

    // ═══════════════════════════════════════════════════════════════════════
    // FINAL METHODS - Cannot be overridden by subclasses
    // ═══════════════════════════════════════════════════════════════════════
    public final String getId() {
        return id;  // ID generation is fixed, subclasses can't change it
    }

    // ═══════════════════════════════════════════════════════════════════════
    // PRIVATE METHODS - Internal helpers not visible to subclasses
    // ═══════════════════════════════════════════════════════════════════════
    private String generateId() {
        return "ANIMAL-" + System.currentTimeMillis();
    }

    // ═══════════════════════════════════════════════════════════════════════
    // STATIC METHODS - Belong to class, not instances
    // ═══════════════════════════════════════════════════════════════════════
    public static boolean isAdult(Animal animal) {
        return animal.age >= 2;
    }

    // Getters
    public String getName() { return name; }
    public int getAge() { return age; }
}

// ═══════════════════════════════════════════════════════════════════════════
// CONCRETE SUBCLASS - Implements all abstract methods
// ═══════════════════════════════════════════════════════════════════════════
public class Dog extends Animal {
    private String breed;

    public Dog(String name, int age, String breed) {
        super(name, age);  // MUST call parent constructor
        this.breed = breed;
    }

    @Override
    public void makeSound() {
        System.out.println(name + " barks: Woof! Woof!");
    }

    @Override
    public void move() {
        System.out.println(name + " runs on four legs");
    }

    @Override
    protected String getSpecies() {
        return "Canis familiaris";
    }

    // Dog-specific method
    public void fetch() {
        System.out.println(name + " fetches the ball!");
    }
}

public class Bird extends Animal {
    private double wingspan;

    public Bird(String name, int age, double wingspan) {
        super(name, age);
        this.wingspan = wingspan;
    }

    @Override
    public void makeSound() {
        System.out.println(name + " chirps: Tweet tweet!");
    }

    @Override
    public void move() {
        System.out.println(name + " flies with " + wingspan + "m wingspan");
    }

    @Override
    protected String getSpecies() {
        return "Aves";
    }

    @Override
    public void sleep() {
        // Override to customize
        System.out.println(name + " sleeps in a nest");
    }
}

Template Method Pattern

// Abstract classes are perfect for the Template Method pattern
// Define the skeleton of an algorithm, let subclasses fill in the steps

public abstract class DataProcessor {
    // TEMPLATE METHOD - final so subclasses can't change the algorithm
    public final void process() {
        openConnection();
        readData();
        processData();       // Abstract - subclasses customize
        writeResults();      // Abstract - subclasses customize
        closeConnection();
    }

    // Concrete methods - same for all subclasses
    private void openConnection() {
        System.out.println("Opening connection...");
    }

    private void closeConnection() {
        System.out.println("Closing connection...");
    }

    protected void readData() {
        System.out.println("Reading data...");
    }

    // Abstract methods - subclasses MUST implement
    protected abstract void processData();
    protected abstract void writeResults();
}

public class CSVProcessor extends DataProcessor {
    @Override
    protected void processData() {
        System.out.println("Parsing CSV rows...");
    }

    @Override
    protected void writeResults() {
        System.out.println("Writing CSV output...");
    }
}

public class JSONProcessor extends DataProcessor {
    @Override
    protected void processData() {
        System.out.println("Parsing JSON objects...");
    }

    @Override
    protected void writeResults() {
        System.out.println("Writing JSON output...");
    }
}
When to Use Abstract Classes
  • Sharing code: Common implementation among related classes
  • Non-static/non-final fields: Need instance variables
  • Constructors: Complex initialization logic
  • Template Method pattern: Define algorithm skeleton
  • Protected members: Need package/subclass access
  • "Is-a" relationship: Dog IS-A Animal, not just "can do animal things"

Combining Interfaces and Abstract Classes

The most powerful designs often combine interfaces and abstract classes. This is exactly what the Java Collections Framework does.

/*
 * Real-World Pattern: Collections Framework Design
 * =================================================
 *
 *  INTERFACE (contract)           ABSTRACT CLASS (partial impl)      CONCRETE (full impl)
 *  ==================            =========================           ===================
 *
 *       List<E>                     AbstractList<E>                   ArrayList<E>
 *         │                              │                              LinkedList<E>
 *         │                              │
 *         └──────────────────────────────┴──────────────────────────────────┘
 *
 *  Why this design?
 *  - Interface: defines what ALL lists must do (add, get, remove, etc.)
 *  - Abstract class: provides common implementation (indexOf, equals, hashCode)
 *  - Concrete classes: provide specific storage mechanisms
 */

// Let's recreate this pattern for a messaging system

// STEP 1: Define the capability (interface)
public interface MessageSender {
    void send(String recipient, String message);
    boolean isAvailable();

    default void sendIfAvailable(String recipient, String message) {
        if (isAvailable()) {
            send(recipient, message);
        } else {
            System.out.println("Service unavailable");
        }
    }
}

// STEP 2: Provide shared implementation (abstract class)
public abstract class AbstractMessageSender implements MessageSender {
    protected String senderName;
    protected List<String> sentMessages = new ArrayList<>();

    public AbstractMessageSender(String senderName) {
        this.senderName = senderName;
    }

    // Template method
    @Override
    public final void send(String recipient, String message) {
        validateRecipient(recipient);
        String formattedMessage = formatMessage(message);
        doSend(recipient, formattedMessage);  // Abstract - subclass provides
        logMessage(recipient, formattedMessage);
    }

    // Shared implementation
    protected void validateRecipient(String recipient) {
        if (recipient == null || recipient.isEmpty()) {
            throw new IllegalArgumentException("Invalid recipient");
        }
    }

    protected String formatMessage(String message) {
        return "[" + senderName + "] " + message;
    }

    protected void logMessage(String recipient, String message) {
        sentMessages.add(recipient + ": " + message);
    }

    // Abstract method - subclasses implement actual sending
    protected abstract void doSend(String recipient, String message);

    public List<String> getSentMessages() {
        return new ArrayList<>(sentMessages);  // Defensive copy
    }
}

// STEP 3: Concrete implementations
public class EmailSender extends AbstractMessageSender {
    private String smtpServer;

    public EmailSender(String senderName, String smtpServer) {
        super(senderName);
        this.smtpServer = smtpServer;
    }

    @Override
    protected void doSend(String recipient, String message) {
        System.out.println("Sending email via " + smtpServer + " to " + recipient);
        System.out.println("Content: " + message);
    }

    @Override
    public boolean isAvailable() {
        // Check SMTP server connectivity
        return true;
    }

    @Override
    protected void validateRecipient(String recipient) {
        super.validateRecipient(recipient);
        if (!recipient.contains("@")) {
            throw new IllegalArgumentException("Invalid email address");
        }
    }
}

public class SMSSender extends AbstractMessageSender {
    private String apiKey;

    public SMSSender(String senderName, String apiKey) {
        super(senderName);
        this.apiKey = apiKey;
    }

    @Override
    protected void doSend(String recipient, String message) {
        System.out.println("Sending SMS to " + recipient);
        System.out.println("Content: " + message.substring(0, Math.min(message.length(), 160)));
    }

    @Override
    public boolean isAvailable() {
        // Check SMS API availability
        return apiKey != null;
    }
}

// Usage - polymorphism in action
MessageSender email = new EmailSender("System", "smtp.example.com");
MessageSender sms = new SMSSender("System", "api-key-123");

// Same code works with both!
for (MessageSender sender : Arrays.asList(email, sms)) {
    sender.sendIfAvailable("user@example.com", "Hello!");
}

The Diamond Problem and Default Methods

Java 8's default methods introduced potential ambiguity when a class implements multiple interfaces with the same default method. Java has specific rules to resolve this.

interface A {
    default void hello() {
        System.out.println("Hello from A");
    }
}

interface B {
    default void hello() {
        System.out.println("Hello from B");
    }
}

// Class implements both - which hello() is used?
public class C implements A, B {
    // MUST override to resolve ambiguity
    @Override
    public void hello() {
        // Option 1: Provide your own implementation
        System.out.println("Hello from C");

        // Option 2: Explicitly choose one interface's version
        A.super.hello();  // Calls A's default

        // Option 3: Call both
        A.super.hello();
        B.super.hello();
    }
}

/*
 * Diamond Problem Resolution Rules:
 * =================================
 *
 * 1. Class wins over interface
 *    - If a superclass provides a method, it beats interface defaults
 *
 * 2. Sub-interface wins over super-interface
 *    - More specific interface wins
 *
 * 3. No winner = Must override
 *    - If rules 1 and 2 don't resolve, class must explicitly override
 */

// Example: Class wins over interface
class Parent {
    public void hello() {
        System.out.println("Hello from Parent class");
    }
}

class Child extends Parent implements A {
    // No override needed - Parent.hello() wins over A.hello()
}

new Child().hello();  // "Hello from Parent class"

Best Practices

Interface Design Guidelines

Abstract Class Design Guidelines

// ❌ BAD: Interface used for constants (anti-pattern)
public interface Constants {
    int MAX_SIZE = 100;
    String PREFIX = "APP_";
}
class MyClass implements Constants { ... }  // Pollutes class hierarchy

// ✅ GOOD: Use final class for constants
public final class Constants {
    public static final int MAX_SIZE = 100;
    public static final String PREFIX = "APP_";
    private Constants() {}  // Prevent instantiation
}

// ❌ BAD: Calling abstract method from constructor
public abstract class Dangerous {
    public Dangerous() {
        initialize();  // DANGER: Calls subclass method before subclass is initialized!
    }
    protected abstract void initialize();
}

// ✅ GOOD: Use factory method or separate initialization
public abstract class Safe {
    public static Safe create() {
        Safe instance = new ConcreteImplementation();
        instance.initialize();
        return instance;
    }
    protected abstract void initialize();
}

Common Pitfalls

Pitfall 1: Cannot Instantiate Abstract Types
// ❌ ERROR: Cannot instantiate
Animal animal = new Animal("Rex", 5);  // Abstract class
Drawable d = new Drawable();           // Interface

// ✅ CORRECT: Instantiate concrete implementations
Animal dog = new Dog("Rex", 5, "Labrador");
Drawable circle = new Circle();

// ✅ CORRECT: Anonymous class (creates unnamed subclass)
Drawable anonymous = new Drawable() {
    @Override
    public void draw() {
        System.out.println("Drawing");
    }
};
Pitfall 2: Forgetting to Call super Constructor
public abstract class Vehicle {
    protected String brand;

    public Vehicle(String brand) {  // No default constructor!
        this.brand = brand;
    }
}

// ❌ ERROR: No default constructor in Vehicle
public class Car extends Vehicle {
    public Car() {
        // Implicit super() call fails - Vehicle has no no-arg constructor
    }
}

// ✅ CORRECT: Explicitly call super with required argument
public class Car extends Vehicle {
    public Car(String brand) {
        super(brand);  // Must be first statement
    }
}
Pitfall 3: Missing Abstract Method Implementation
public abstract class Shape {
    public abstract double area();
    public abstract double perimeter();
}

// ❌ ERROR: Circle is not abstract and does not override perimeter()
public class Circle extends Shape {
    private double radius;

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
    // Forgot perimeter()!
}

// ✅ CORRECT: Implement ALL abstract methods
public class Circle extends Shape {
    private double radius;

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }

    @Override
    public double perimeter() {
        return 2 * Math.PI * radius;
    }
}
Pitfall 4: Incorrect Access Modifier in Override
interface Service {
    void execute();  // Implicitly public
}

// ❌ ERROR: Cannot reduce visibility
class MyService implements Service {
    void execute() {  // Package-private - more restrictive than public!
        System.out.println("Executing");
    }
}

// ✅ CORRECT: Must be public (same or more accessible)
class MyService implements Service {
    @Override
    public void execute() {
        System.out.println("Executing");
    }
}

Troubleshooting Guide

Error: "Class is not abstract and does not override abstract method"

// Problem: Concrete class doesn't implement all abstract methods
// Solution: Either implement the method OR make the class abstract

// Option 1: Implement the missing method
public class MyClass implements MyInterface {
    @Override
    public void missingMethod() {
        // Implementation here
    }
}

// Option 2: Make the class abstract (defer implementation to subclasses)
public abstract class MyClass implements MyInterface {
    // missingMethod() stays abstract
}

Error: "Constructor call must be first statement in constructor"

// Problem: super() or this() not first
public Child(String name) {
    validate(name);  // ERROR: Can't do this before super()
    super(name);
}

// Solution: Put super() first, validate after
public Child(String name) {
    super(name);  // First!
    validate(name);
}

// Or use static validation in super() call
public Child(String name) {
    super(validateAndReturn(name));  // Static method call is OK
}

private static String validateAndReturn(String name) {
    if (name == null) throw new IllegalArgumentException();
    return name;
}

Error: "Duplicate default methods"

// Problem: Multiple interfaces have same default method
interface A { default void foo() { } }
interface B { default void foo() { } }
class C implements A, B { }  // ERROR: inherits unrelated defaults

// Solution: Override and choose which to use
class C implements A, B {
    @Override
    public void foo() {
        A.super.foo();  // Choose A's version
        // OR: B.super.foo();
        // OR: your own implementation
    }
}

Interview Questions

Q1: What is the difference between an interface and an abstract class?

Answer: Key differences: (1) A class can implement multiple interfaces but extend only one abstract class. (2) Interfaces can only have constants; abstract classes can have any fields. (3) Interface methods are implicitly public; abstract classes can have any access modifier. (4) Interfaces define capabilities (CAN-DO); abstract classes define identity (IS-A). (5) Pre-Java 8, interfaces had no implementation; now they can have default/static/private methods. Use interfaces for contracts across unrelated classes; use abstract classes for sharing code among related classes.

Q2: Can an interface extend another interface? Can it extend multiple?

Answer: Yes, an interface can extend one or MORE interfaces (using extends, not implements). This creates an interface that inherits all methods from parent interfaces. Example: interface C extends A, B { }. This is different from classes, which can only extend one class.

Q3: What are default methods and why were they added?

Answer: Default methods (Java 8+) allow interfaces to have method implementations. They were added primarily to enable API evolution - adding methods to interfaces (like forEach() to Iterable) without breaking existing implementations. Without default methods, adding any method to an interface would break all implementing classes.

Q4: Can we have static methods in an interface?

Answer: Yes, since Java 8. Static methods in interfaces are useful for utility methods related to the interface. They're called using the interface name: InterfaceName.staticMethod(). Unlike default methods, static methods are not inherited by implementing classes or sub-interfaces.

Q5: What happens when a class implements two interfaces with the same default method?

Answer: The compiler reports an error - the class must override the method to resolve ambiguity. In the override, you can: (1) provide your own implementation, (2) call one interface's version using InterfaceName.super.method(), or (3) call both versions.

Q6: Can an abstract class have a constructor?

Answer: Yes. Although you cannot instantiate an abstract class directly, its constructors are called when concrete subclasses are instantiated (via super()). Constructors initialize fields and enforce invariants. This is a key difference from interfaces, which cannot have constructors.

Q7: When would you use an abstract class with only abstract methods?

Answer: Rarely now that interfaces have default methods. However, you might use an abstract class over an interface when you need: (1) constructors for initialization, (2) non-public methods, (3) non-static/non-final fields, or (4) to prevent multiple inheritance (Java classes can extend only one class). Before Java 8, this pattern was more common.

Q8: What is a functional interface?

Answer: A functional interface has exactly one abstract method (SAM - Single Abstract Method). It can have any number of default and static methods. Functional interfaces can be implemented using lambda expressions or method references. Examples: Runnable, Comparator, Function. The @FunctionalInterface annotation is optional but recommended as it enables compiler checking.

See Also