Abstract Classes and Interfaces in Java

Welcome to this lesson, where we'll explore two fundamental concepts in Java's Object-Oriented Programming (OOP) that are crucial for achieving abstraction and polymorphism: Abstract Classes and Interfaces.

These constructs allow us to define common behaviors and structures without necessarily providing full implementations, leaving that responsibility to concrete (non-abstract) subclasses or implementing classes.

1. Abstract Classes

What is an Abstract Class?

An abstract class is a class that cannot be instantiated (you cannot create an object directly from it). It is declared using the abstract keyword.

Abstract classes serve as blueprints for other classes, providing a common base class for related subclasses. They can contain:

  • Abstract methods: Methods declared with the abstract keyword and no implementation (no method body). Subclasses must provide an implementation for these methods.

  • Concrete (non-abstract) methods: Regular methods with full implementations.

  • Constructors: Yes, abstract classes can have constructors, which are called when an object of a subclass is created.

  • Variables: Both instance variables and static variables.

Why Use Abstract Classes?

  1. Partial Implementation: When you have a group of classes that share some common behavior but also have unique behaviors, an abstract class allows you to implement the common parts once and leave the unique parts as abstract methods for subclasses to define.

  2. Force Implementation: Abstract methods force subclasses to provide their own implementation for those methods. This ensures that certain critical functionalities are present in all concrete subclasses.

  3. "Is-A" Relationship: Abstract classes are best suited when there's a strong "is-a" relationship (inheritance hierarchy) between classes, but the parent concept is too general to be fully implemented.

Syntax and Characteristics

  • Declared with the abstract keyword: public abstract class MyClass { ... }

  • Cannot be instantiated: MyClass obj = new MyClass(); would result in a compile-time error.

  • Can have abstract methods (no body) and non-abstract (concrete) methods (with body).

  • If a class has at least one abstract method, the class itself must be declared abstract.

  • A subclass of an abstract class must implement all of its abstract methods, or else the subclass itself must also be declared abstract.

  • Supports single inheritance (a class can only extend one abstract class, just like a regular class).

Example: Vehicle Abstract Class

Let's imagine a Vehicle hierarchy. All vehicles have a startEngine() method, but the way a car engine starts is different from a bicycle or a boat. However, all vehicles also have a displayInfo() method that works similarly.

// Abstract Class: Vehicle
abstract class Vehicle {
    String brand;
    int year;

    // Constructor (abstract classes can have constructors)
    public Vehicle(String brand, int year) {
        this.brand = brand;
        this.year = year;
        System.out.println("Vehicle constructor called for " + brand);
    }

    // Abstract method: Must be implemented by concrete subclasses
    abstract void startEngine();

    // Concrete method: Has a full implementation
    void displayInfo() {
        System.out.println("Brand: " + brand + ", Year: " + year);
    }

    // Another concrete method
    void stopEngine() {
        System.out.println(brand + " engine stopped.");
    }
}

// Concrete Subclass: Car
class Car extends Vehicle {
    int numberOfDoors;

    public Car(String brand, int year, int numberOfDoors) {
        super(brand, year); // Call the superclass (Vehicle) constructor
        this.numberOfDoors = numberOfDoors;
        System.out.println("Car constructor called.");
    }

    @Override
    void startEngine() {
        System.out.println(brand + " car engine started with a key turn.");
    }

    // Car-specific method
    void accelerate() {
        System.out.println(brand + " car is accelerating.");
    }
}

// Concrete Subclass: Motorcycle
class Motorcycle extends Vehicle {
    boolean hasSidecar;

    public Motorcycle(String brand, int year, boolean hasSidecar) {
        super(brand, year);
        this.hasSidecar = hasSidecar;
        System.out.println("Motorcycle constructor called.");
    }

    @Override
    void startEngine() {
        System.out.println(brand + " motorcycle engine started with a kick-start.");
    }

    // Motorcycle-specific method
    void wheelie() {
        System.out.println(brand + " motorcycle is doing a wheelie.");
    }
}

// Main class to demonstrate Abstract Classes
public class AbstractClassDemo {
    public static void main(String[] args) {
        // Vehicle myVehicle = new Vehicle("Generic", 2020); // ERROR: Cannot instantiate abstract class

        Car myCar = new Car("Toyota", 2023, 4);
        myCar.displayInfo();    // Concrete method from Vehicle
        myCar.startEngine();    // Overridden abstract method from Car
        myCar.accelerate();     // Car-specific method
        myCar.stopEngine();     // Concrete method from Vehicle
        System.out.println("---");

        Motorcycle myMotorcycle = new Motorcycle("Harley", 2022, true);
        myMotorcycle.displayInfo(); // Concrete method from Vehicle
        myMotorcycle.startEngine(); // Overridden abstract method from Motorcycle
        myMotorcycle.wheelie();     // Motorcycle-specific method
        myMotorcycle.stopEngine();  // Concrete method from Vehicle
        System.out.println("---");

        // Polymorphism with Abstract Classes:
        // You can use the abstract class as a reference type
        Vehicle genericVehicle1 = new Car("Honda", 2021, 2);
        Vehicle genericVehicle2 = new Motorcycle("Kawasaki", 2020, false);

        System.out.println("Demonstrating polymorphism:");
        genericVehicle1.startEngine(); // Calls Car's startEngine()
        genericVehicle2.startEngine(); // Calls Motorcycle's startEngine()
    }
}

Output:

Vehicle constructor called for Toyota
Car constructor called.
Brand: Toyota, Year: 2023
Toyota car engine started with a key turn.
Toyota car is accelerating.
Toyota engine stopped.
---
Vehicle constructor called for Harley
Motorcycle constructor called.
Brand: Harley, Year: 2022
Harley motorcycle engine started with a kick-start.
Harley motorcycle is doing a wheelie.
Harley engine stopped.
---
Vehicle constructor called for Honda
Car constructor called.
Vehicle constructor called for Kawasaki
Motorcycle constructor called.
Demonstrating polymorphism:
Honda car engine started with a key turn.
Kawasaki motorcycle engine started with a kick-start.

2. Interfaces

What is an Interface?

An interface in Java is a blueprint of a class. It defines a contract that classes can agree to fulfill. It specifies a set of methods that a class implementing the interface must provide.

Before Java 8, interfaces could only contain public static final variables (constants) and public abstract methods. They had no method bodies.

Since Java 8, interfaces can also contain:

  • default methods: Methods with a default implementation. Classes implementing the interface can use this default implementation or override it.

  • static methods: Methods that belong to the interface itself and can be called directly on the interface (e.g., MyInterface.utilityMethod()).

  • Since Java 9, private methods are also allowed for internal utility within default methods.

Why Use Interfaces?

  1. Define Contracts: Interfaces are perfect for defining a contract or a set of behaviors that unrelated classes might share. For example, Flyable could be an interface implemented by Bird, Airplane, and Superman.

  2. Achieve Multiple Inheritance of Type: Unlike abstract classes (and regular classes), a class can implement multiple interfaces. This is Java's way of achieving something similar to "multiple inheritance" of behavior (not state or implementation).

  3. Loose Coupling: Interfaces promote loose coupling between components. Code can interact with objects through their interface types, without needing to know their concrete class.

Syntax and Characteristics

  • Declared with the interface keyword: public interface MyInterface { ... }

  • Cannot be instantiated: MyInterface obj = new MyInterface(); would result in a compile-time error.

  • Methods are implicitly public abstract before Java 8.

  • Variables are implicitly public static final.

  • Cannot have constructors.

  • A class implements an interface using the implements keyword.

  • A class that implements an interface must provide implementations for all its abstract methods (or be an abstract class itself).

  • A class can implement multiple interfaces: class MyClass implements InterfaceA, InterfaceB { ... }

  • An interface can extend another interface: interface InterfaceC extends InterfaceA, InterfaceB { ... } (interfaces can extend multiple other interfaces).

Example: Swimmer Interface

Let's define a Swimmer interface that objects capable of swimming will implement.

// Interface: Swimmer
interface Swimmer {
    // All methods in an interface are implicitly public abstract (before Java 8)
    // From Java 8, you can explicitly use 'public abstract' or just the signature
    void swim(); // Implied public abstract

    // Since Java 8, interfaces can have default methods
    default void dive() {
        System.out.println("Default dive: Jumps into the water.");
    }

    // Since Java 8, interfaces can have static methods
    static void displaySwimmingTip() {
        System.out.println("Static tip: Always swim with a buddy!");
    }
}

// Class: Human implements Swimmer
class Human implements Swimmer {
    String name;

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

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

    // Can override default method
    @Override
    public void dive() {
        System.out.println(name + " performs a perfect competitive dive!");
    }
}

// Class: Fish implements Swimmer
class Fish implements Swimmer {
    String species;

    public Fish(String species) {
        this.species = species;
    }

    @Override
    public void swim() { // Must be public
        System.out.println(species + " is swimming by moving its fins.");
    }
    // Fish will use the default dive() method unless overridden
}

// Main class to demonstrate Interfaces
public class InterfaceDemo {
    public static void main(String[] args) {
        Human alice = new Human("Alice");
        alice.swim();
        alice.dive();
        System.out.println("---");

        Fish nemo = new Fish("Clownfish");
        nemo.swim();
        nemo.dive(); // Uses the default dive from the interface
        System.out.println("---");

        // Calling a static method on the interface
        Swimmer.displaySwimmingTip();
        System.out.println("---");

        // Polymorphism with Interfaces:
        // You can use the interface as a reference type
        Swimmer[] swimmers = new Swimmer[2];
        swimmers[0] = alice; // Human object referenced by Swimmer type
        swimmers[1] = nemo;  // Fish object referenced by Swimmer type

        System.out.println("Demonstrating polymorphism with interfaces:");
        for (Swimmer s : swimmers) {
            s.swim(); // Dynamic method dispatch: calls Human's or Fish's swim()
        }
    }
}

Output:

Alice is swimming gracefully.
Alice performs a perfect competitive dive!
---
Clownfish is swimming by moving its fins.
Default dive: Jumps into the water.
---
Static tip: Always swim with a buddy!
---
Demonstrating polymorphism with interfaces:
Alice is swimming gracefully.
Clownfish is swimming by moving its fins.

3. Key Differences: Abstract Classes vs. Interfaces

Here's a summary of the main distinctions:

4. Similarities

Despite their differences, abstract classes and interfaces share some common ground:

  • Cannot be Instantiated: Neither an abstract class nor an interface can be directly instantiated with the new keyword.

  • Abstraction: Both are used to achieve abstraction, hiding implementation details and showing only necessary features.

  • Polymorphism: Both play a crucial role in enabling polymorphism by allowing objects of different concrete classes to be treated as objects of a common abstract type or interface type.

  • Abstract Methods: Both can contain abstract methods (methods without a body) that must be implemented by concrete subclasses/implementing classes.

5. Choosing Between Abstract Class and Interface

When should you use one over the other?

  • Use an abstract class when:

    • You want to provide a common base implementation for subclasses, but some behavior needs to be defined by each subclass.

    • You need to declare non-public members (e.g., protected instance variables or methods) that are shared by subclasses.

    • You want to provide constructors that initialize the state common to all subclasses.

    • There's a clear "is-a" hierarchy (e.g., Dog is an Animal).

    • You anticipate adding new methods in the future that will have a default implementation for many subclasses.

  • Use an interface when:

    • You want to define a contract for behavior that can be implemented by completely unrelated classes.

    • You need to achieve "multiple inheritance of behavior" (a class needs to have capabilities from multiple distinct sources).

    • You want to define constants that are implicitly public static final.

    • There's a "can-do" or "has-a" capability relationship (e.g., Car can do Driveable, Human can do Swimmable).

    • You want to specify a type without worrying about implementation details.

General Rule of Thumb:

  • If the relationship is "is-a" (strong hierarchy), consider an abstract class.

  • If the relationship is "can-do" (behavioral contract), consider an interface.

Conclusion

Abstract classes and interfaces are powerful tools in Java for designing flexible, maintainable, and extensible object-oriented systems. They help enforce structure, promote abstraction, and are fundamental to leveraging polymorphism effectively. Mastering their usage is key to writing robust and scalable Java applications.

Key Takeaways

  • Both abstract classes and interfaces are used to achieve abstraction and polymorphism in Java, acting as blueprints rather than directly instantiable objects.

    • The main distinctions lie in constructors, instance variables, the ability to provide method implementations (before Java 8 interfaces couldn't), and whether a class can extend/implement multiple of them.

    • Go for an abstract class when you have a strong "is-a" hierarchy, want to provide common implementation details, or need non-public members/constructors.

    • Go for an interface when you want to define a contract for distinct, unrelated classes to adhere to, or when a class needs to exhibit multiple distinct behaviors (multiple inheritance of type).

Quiz