Index

Abstraction

Java Object-Oriented Design

6.1 Abstract Classes

In Java programming, abstraction is a powerful principle that lets you focus on what an object does instead of how it does it. One key tool to achieve abstraction is the abstract class. Abstract classes provide a way to define common behavior and structure for a group of related classes, while leaving some implementation details to be filled in by subclasses.

What Is an Abstract Class?

An abstract class in Java is a class that cannot be instantiated on its own but can be subclassed. It serves as a blueprint for other classes, defining common characteristics and behaviors that related subclasses share. Abstract classes can contain both:

Because abstract classes cannot be instantiated directly, you can only create objects from concrete subclasses that provide implementations for all abstract methods.

When and Why Use Abstract Classes?

Abstract classes are appropriate when:

Abstract classes are more flexible than interfaces (which we'll discuss later) because they allow you to define state (fields) and fully implemented methods.

Syntax and Example

Here is an example of an abstract class that represents a general Vehicle:

abstract class Vehicle {
    private String brand;

    public Vehicle(String brand) {
        this.brand = brand;
    }

    // Concrete method
    public void displayBrand() {
        System.out.println("Brand: " + brand);
    }

    // Abstract method - no implementation here
    public abstract void startEngine();

    // Abstract method
    public abstract void stopEngine();
}

In this example:

Subclassing an Abstract Class

Any class extending Vehicle must implement the abstract methods:

class Car extends Vehicle {
    public Car(String brand) {
        super(brand);
    }

    @Override
    public void startEngine() {
        System.out.println("Car engine started");
    }

    @Override
    public void stopEngine() {
        System.out.println("Car engine stopped");
    }
}

class Motorcycle extends Vehicle {
    public Motorcycle(String brand) {
        super(brand);
    }

    @Override
    public void startEngine() {
        System.out.println("Motorcycle engine started");
    }

    @Override
    public void stopEngine() {
        System.out.println("Motorcycle engine stopped");
    }
}

Using the Abstract Class and Subclasses

public class TestVehicles {
    public static void main(String[] args) {
        Vehicle car = new Car("Toyota");
        car.displayBrand();
        car.startEngine();
        car.stopEngine();

        Vehicle bike = new Motorcycle("Harley-Davidson");
        bike.displayBrand();
        bike.startEngine();
        bike.stopEngine();
    }
}

Output:

Brand: Toyota
Car engine started
Car engine stopped
Brand: Harley-Davidson
Motorcycle engine started
Motorcycle engine stopped
Click to view full runnable Code

abstract class Vehicle {
    private String brand;

    public Vehicle(String brand) {
        this.brand = brand;
    }

    // Concrete method
    public void displayBrand() {
        System.out.println("Brand: " + brand);
    }

    // Abstract methods
    public abstract void startEngine();
    public abstract void stopEngine();
}

class Car extends Vehicle {
    public Car(String brand) {
        super(brand);
    }

    @Override
    public void startEngine() {
        System.out.println("Car engine started");
    }

    @Override
    public void stopEngine() {
        System.out.println("Car engine stopped");
    }
}

class Motorcycle extends Vehicle {
    public Motorcycle(String brand) {
        super(brand);
    }

    @Override
    public void startEngine() {
        System.out.println("Motorcycle engine started");
    }

    @Override
    public void stopEngine() {
        System.out.println("Motorcycle engine stopped");
    }
}

public class TestVehicles {
    public static void main(String[] args) {
        Vehicle car = new Car("Toyota");
        car.displayBrand();
        car.startEngine();
        car.stopEngine();

        Vehicle bike = new Motorcycle("Harley-Davidson");
        bike.displayBrand();
        bike.startEngine();
        bike.stopEngine();
    }
}

Important Rules About Abstract Classes

Summary

Abstract classes strike a balance between a completely abstract interface and a fully concrete class. They let you:

By leveraging abstract classes in your Java designs, you create clear and maintainable architectures that encourage thoughtful subclassing and consistent behavior across related objects. This forms a cornerstone of effective object-oriented design.

Index

6.2 Abstract Methods

What Are Abstract Methods?

An abstract method is a method declared without an implementation (i.e., no method body) inside an abstract class. It defines a method signature—the method’s name, return type, and parameters—without specifying how it works.

The primary purpose of abstract methods is to enforce a contract: any concrete subclass of the abstract class must provide its own implementation of these methods. This ensures that certain behaviors are guaranteed while allowing subclasses the freedom to decide how to implement those behaviors.

Declaring Abstract Methods

Abstract methods are declared using the abstract keyword and end with a semicolon rather than a body:

abstract class Vehicle {
    // Abstract method — no body
    public abstract void startEngine();

    // Another abstract method
    public abstract void stopEngine();
}

In this example, startEngine() and stopEngine() declare what must be done but not how. The Vehicle class itself provides no implementation for these methods.

Enforcing Subclass Contracts

Any concrete subclass of an abstract class must override and implement all abstract methods, or else the subclass itself must be declared abstract.

class Car extends Vehicle {
    @Override
    public void startEngine() {
        System.out.println("Car engine started");
    }

    @Override
    public void stopEngine() {
        System.out.println("Car engine stopped");
    }
}

If Car omitted one of these methods, the Java compiler would report an error, enforcing that Car fulfill the contract established by Vehicle.

Abstract Methods and Design Flexibility

Abstract methods allow you to define common interfaces for related classes without dictating the details. This promotes:

Example: Abstract Method Declaration and Implementation

abstract class Shape {
    // Abstract method to calculate area
    public abstract double area();

    // Concrete method to display area
    public void display() {
        System.out.println("Area is: " + area());
    }
}

class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

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

class Rectangle extends Shape {
    private double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }
}

Here, the abstract method area() forces every Shape subclass to provide its own area calculation. Meanwhile, the display() method in the abstract class can call area() polymorphically.

Click to view full runnable Code

abstract class Shape {
    // Abstract method to calculate area
    public abstract double area();

    // Concrete method to display area
    public void display() {
        System.out.println("Area is: " + area());
    }
}

class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

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

class Rectangle extends Shape {
    private double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }
}

public class TestShapes {
    public static void main(String[] args) {
        Shape circle = new Circle(5);
        circle.display();  // Area is: 78.53981633974483

        Shape rectangle = new Rectangle(4, 7);
        rectangle.display();  // Area is: 28.0
    }
}

Impact on Code Reuse

While abstract methods require subclasses to implement certain behaviors, the abstract class can still provide concrete methods that use those abstract methods to offer reusable functionality.

For example, Shape’s display() method relies on the area() abstract method but provides a shared implementation to display results. This balance encourages code reuse and design consistency.

Summary

Abstract methods are a fundamental part of Java’s abstraction mechanism. By declaring abstract methods:

Understanding abstract methods helps you build robust, maintainable Java applications that leverage the full power of object-oriented design.

Index

6.3 Interfaces and implements

What Is an Interface?

In Java, an interface is a special kind of reference type that defines a contract for what a class can do, without prescribing how it does it. Interfaces specify method signatures that implementing classes must provide, but they do not hold state (fields) or method implementations—at least traditionally.

Interfaces are a core part of Java’s approach to abstraction, allowing different classes to share common behavior without forcing a strict class hierarchy. They promote loose coupling and support multiple inheritance of behavior, something classes alone cannot do.

How Interfaces Differ from Classes

While both classes and interfaces define types, they differ in several important ways:

Syntax: Implementing Interfaces

To use an interface, a class declares that it implements the interface and provides concrete implementations for all its abstract methods.

Here is the basic syntax:

interface Drivable {
    void drive();
}

class Car implements Drivable {
    @Override
    public void drive() {
        System.out.println("Car is driving");
    }
}

Car promises to fulfill the contract of Drivable by implementing the drive() method.

Implementing Multiple Interfaces

One major advantage of interfaces is that a single class can implement multiple interfaces, gaining the behaviors of several types simultaneously. This is Java’s way to achieve multiple inheritance of type.

Example:

interface Drivable {
    void drive();
}

interface Electric {
    void chargeBattery();
}

class Tesla implements Drivable, Electric {
    @Override
    public void drive() {
        System.out.println("Tesla is driving silently");
    }

    @Override
    public void chargeBattery() {
        System.out.println("Tesla battery is charging");
    }
}

Here, Tesla acts as both a Drivable and an Electric vehicle, implementing methods from both interfaces.

Default and Static Methods in Interfaces (Since Java 8)

Prior to Java 8, interfaces could only declare abstract methods. This limitation made evolving interfaces challenging, as adding new methods would break existing implementations.

Java 8 introduced two important features to address this:

interface Printable {
    default void print() {
        System.out.println("Printing from Printable interface");
    }
}

class Document implements Printable {
    // Inherits default print() or can override
}
interface MathOperations {
    static int add(int a, int b) {
        return a + b;
    }
}

These methods can be called via MathOperations.add(5, 3);.

Click to view full runnable Code

interface Drivable {
    void drive();
}

interface Electric {
    void chargeBattery();
}

interface Printable {
    default void print() {
        System.out.println("Printing from Printable interface");
    }
}

interface MathOperations {
    static int add(int a, int b) {
        return a + b;
    }
}

class Tesla implements Drivable, Electric, Printable {
    @Override
    public void drive() {
        System.out.println("Tesla is driving silently");
    }

    @Override
    public void chargeBattery() {
        System.out.println("Tesla battery is charging");
    }

    // Inherits default print() method from Printable
}

public class TestInterfaces {
    public static void main(String[] args) {
        Tesla myTesla = new Tesla();
        myTesla.drive();
        myTesla.chargeBattery();
        myTesla.print();  // default method from Printable

        int sum = MathOperations.add(10, 20);  // static method from interface
        System.out.println("Sum: " + sum);
    }
}

Why Use Interfaces?

Example: Interface in Action

interface Animal {
    void makeSound();
}

interface Runnable {
    void run();
}

class Dog implements Animal, Runnable {
    @Override
    public void makeSound() {
        System.out.println("Woof!");
    }

    @Override
    public void run() {
        System.out.println("Dog is running");
    }
}

Client code can interact with Dog objects through either Animal or Runnable references:

Animal animal = new Dog();
animal.makeSound();

Runnable runner = new Dog();
runner.run();

This flexibility is one of the core strengths of interfaces in Java’s OOP design.

Summary

Interfaces are a powerful abstraction tool in Java, enabling classes to promise certain behaviors without restricting class inheritance. By using the implements keyword, classes commit to providing concrete implementations for interface methods, fostering polymorphism and decoupled designs.

With the introduction of default and static methods, interfaces have become even more flexible, allowing method evolution without breaking existing code. Mastering interfaces is essential for writing scalable, maintainable Java applications that follow modern object-oriented design principles.

Index

6.4 Differences Between Abstract Classes and Interfaces

In Java, abstract classes and interfaces are both tools for abstraction that allow you to define contracts and share behavior among related types. However, they serve different purposes, have distinct capabilities, and fit different design scenarios. Understanding their differences is crucial to making informed design decisions.

Key Differences

Feature Abstract Class Interface
Inheritance Type Single inheritance (a class can extend only one abstract class) Multiple inheritance (a class can implement multiple interfaces)
Method Types Can have abstract and concrete methods Before Java 8: only abstract methods; since Java 8: can have default and static methods
Fields Can have instance variables (state) Only public static final constants
Constructors Can have constructors Cannot have constructors
Access Modifiers Methods and fields can have any access level Methods are implicitly public; fields are public static final
When to Use When classes share common base behavior and state To define roles or capabilities that unrelated classes can implement
Flexibility Less flexible due to single inheritance More flexible, allows multiple behaviors
Backward Compatibility Adding methods can break subclasses unless they are default methods Default methods since Java 8 allow evolving interfaces without breaking implementations

Usage Scenarios

Single Inheritance vs Multiple Interfaces

Java allows a class to extend only one class (abstract or concrete) due to the complexity and ambiguity that can arise from multiple inheritance. However, a class can implement multiple interfaces, enabling it to assume multiple roles or behaviors.

This is a crucial difference: interfaces provide flexibility, while abstract classes provide structure and shared code.

When to Prefer Abstract Classes

When to Prefer Interfaces

Decision Checklist

Question Choose Abstract Class Choose Interface
Do you need to share common code (method bodies)? ✘ (prior to Java 8, limited support)
Do you need to define instance fields?
Should the type support multiple inheritance?
Are you defining a capability or role?
Do you need constructors in the base type?
Is backward compatibility critical? Use interfaces with default methods Use interfaces with default methods
Are the classes closely related in hierarchy?

Summary

While both abstract classes and interfaces provide abstraction, they are designed for different situations. Abstract classes are best when there is a clear “is-a” relationship with shared implementation and state. Interfaces are ideal when defining roles or capabilities that can crosscut various unrelated classes.

Knowing when and how to use each effectively enables you to design robust, flexible, and maintainable Java applications. In practice, a combination of both often yields the best results—using abstract classes to share code and interfaces to define contracts.

Understanding these differences is a foundational skill for mastering Java’s object-oriented design principles.

Index

6.5 Functional Interfaces (Intro)

With the introduction of Java 8, the concept of functional interfaces became a cornerstone for supporting functional programming features in Java. Functional interfaces enable a new, concise way of writing code using lambda expressions, which are anonymous functions that can be passed around like objects.

What Are Functional Interfaces?

A functional interface is an interface that contains exactly one abstract method. This single-method requirement allows instances of the interface to be created with lambda expressions or method references, dramatically simplifying code for cases where you would otherwise create an anonymous class.

The @FunctionalInterface Annotation

While any interface with one abstract method can be considered functional, Java provides a special annotation, @FunctionalInterface, to explicitly declare your intention. This annotation helps:

Example:

@FunctionalInterface
public interface MyFunction {
    void apply();
}

If you add another abstract method to MyFunction, the compiler will flag an error.

Common Functional Interfaces in Java

Java’s standard library includes many widely used functional interfaces, making it easy to adopt functional programming idioms:

These interfaces became even more useful with lambda expressions, eliminating boilerplate anonymous classes.

How Functional Interfaces Enable Lambda Expressions

Lambda expressions provide a concise syntax to implement functional interfaces. Instead of writing an anonymous class, you can write a short, readable expression representing the method implementation.

For example, with Runnable:

Without Lambda:

Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("Task running");
    }
};

With Lambda:

Runnable task = () -> System.out.println("Task running");

Both create a Runnable instance, but the lambda is much cleaner.

Simple Example Using Lambda and Functional Interface

Suppose we define a custom functional interface:

@FunctionalInterface
interface Greeting {
    void sayHello(String name);
}

Using a lambda, you can implement it like this:

Greeting greet = (name) -> System.out.println("Hello, " + name + "!");
greet.sayHello("Alice");

This will output:

Hello, Alice!
Click to view full runnable Code

@FunctionalInterface
interface Greeting {
    void sayHello(String name);
}

public class LambdaDemo {
    public static void main(String[] args) {
        Greeting greet = (name) -> System.out.println("Hello, " + name + "!");
        greet.sayHello("Alice");
    }
}

Summary

Functional interfaces are a key Java 8+ feature that allow developers to write more expressive and compact code using lambda expressions. By defining interfaces with a single abstract method and optionally marking them with @FunctionalInterface, Java enables powerful functional programming constructs while maintaining strong type safety and readability.

Mastering functional interfaces unlocks new design possibilities and modern coding styles in Java, which will be explored further in later chapters.

Index