Java includes two important building blocks for developing custom types: Abstract Classes and Interfaces. Both are essential for establishing abstraction and polymorphism, but they serve different purposes and have various advantages. In this piece, we will look at these principles, their applications, and best practices for their use.
Java is a statically typed language, which means that each variable, parameter, return value, and object must be assigned a type at compile time. This enables rigorous type checking and minimizes runtime errors. Abstract classes and interfaces are two mechanism that help developers define new types in a systematic and flexible manner. They allow for the creation of reusable, extensible, and maintainable code while adhering to the principles of object-oriented programming.
Abstract classes allow you to define a type that encapsulate shared behavior and state across related classes. Interfaces, on the other hand, describe a type as a contract that specifies methods that every class that follows it must implement. These two components lay the groundwork for developing flexible APIs and implementing polymorphism in Java.
An abstract class cannot be instantiated directly. It may include both abstract methods (no implementation) and concrete methods (with implementation). Abstract classes serve as the blueprint for subclasses.
public abstract class Vehicle { protected String brand; protected int year; // Concrete method with implementation public void start() { System.out.println("Vehicle starting..."); } // Abstract method - must be implemented by subclasses public abstract double calculateFuelEfficiency(); // Another abstract method public abstract void maintenance(); } // Concrete implementation public class Car extends Vehicle { @Override public double calculateFuelEfficiency() { return 25.5; // Miles per gallon } @Override public void maintenance() { System.out.println("Performing car maintenance"); } }
abstract class Animal { private String name; public Animal(String name) { this.name = name; } public String getName() { return name; } // Abstract method public abstract void makeSound(); } class Dog extends Animal { public Dog(String name) { super(name); } @Override public void makeSound() { System.out.println(getName() + " says Woof!"); } } public class Main { public static void main(String[] args) { Animal dog = new Dog("Buddy"); dog.makeSound(); } }
Abstract classes are key in obtaining runtime polymorphism. As shown above, the variable dog is declared as Animal but references a Dog instance. This gives for more freedom in method overriding and object substitution.
Vehicle car = new Car(); Vehicle truck = new Truck(); // Polymorphic calls car.maintenance(); // Calls Car's implementation truck.maintenance(); // Calls Truck's implementation
Before Java 8, an interface was a completely abstract type. It could only have method declarations and constants. Methods declared in an interface that do not have an implementation are by default public and abstract. Interfaces defines a contract that implementing classes must fulfill.
public interface Payable { double calculatePay(); void processPay(); } public interface Taxable { double calculateTax(); }
interface Vehicle { void startEngine(); void stopEngine(); } class Car implements Vehicle { @Override public void startEngine() { System.out.println("Car engine started."); } @Override public void stopEngine() { System.out.println("Car engine stopped."); } } public class Main { public static void main(String[] args) { Vehicle car = new Car(); car.startEngine(); car.stopEngine(); } }
Interfaces are important in creating the Factory Pattern, which abstracts object creation logic into factory methods.
interface Shape { void draw(); } class Circle implements Shape { @Override public void draw() { System.out.println("Drawing a Circle."); } } class Rectangle implements Shape { @Override public void draw() { System.out.println("Drawing a Rectangle."); } } class Triangle implements Shape { @Override public void draw() { System.out.println("Drawing a Triangle."); } } class ShapeFactory { public static Shape getShape(String type) { if (type.equalsIgnoreCase("circle")) { return new Circle(); } else if (type.equalsIgnoreCase("rectangle")) { return new Rectangle(); } else if (type.equalsIgnoreCase("triangle")) { return new Triangle(); } throw new IllegalArgumentException("Unknown shape type"); } } public class Main { public static void main(String[] args) { Shape circle = ShapeFactory.getShape("circle"); circle.draw(); Shape rectangle = ShapeFactory.getShape("rectangle"); rectangle.draw(); Shape triangle = ShapeFactory.getShape("triangle"); triangle.draw(); } }
Interfaces are extremely versatile. They can act as:
Vehicle vehicle = new Car();
public void operateVehicle(Vehicle vehicle) { vehicle.startEngine(); }
public Vehicle getVehicle() { return new Car(); }
Use interfaces when:
Use abstract classes when:
Prior to Java 8, introducing new methods to an interface required all implementing classes to update their code. This resulted in the "Evolving API Problem." The introduction of default methods allows for the addition of new functionality to interfaces without breaking existing implementations.
interface Printer { void print(); default void showStatus() { System.out.println("Printer is online."); } } class LaserPrinter implements Printer { @Override public void print() { System.out.println("Printing document..."); } } public class Main { public static void main(String[] args) { Printer printer = new LaserPrinter(); printer.print(); printer.showStatus(); } }
At DevelopersMonk, we share tutorials, tips, and insights on modern programming frameworks like React, Next.js, Spring Boot, and more. Join us on our journey to simplify coding and empower developers worldwide!