Inheritance & Polymorphism
Explore how Java classes extend one another, override methods, and leverage abstract classes for polymorphic behavior.
Extending Classes
Inheritance in Java uses the extends keyword to create a child class that inherits fields and methods from a parent class. Java supports single inheritance, meaning each class can extend only one direct superclass. The child class inherits all non-private members and can add new fields and methods or override existing ones. Inheritance establishes an is-a relationship, so a subclass should truly be a specialized version of its parent.
public class Animal {
protected String name;
protected int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + " is eating.");
}
public void sleep() {
System.out.println(name + " is sleeping.");
}
public String getInfo() {
return name + " (age: " + age + ")";
}
}
class Dog extends Animal {
private String breed;
public Dog(String name, int age, String breed) {
super(name, age); // Call parent constructor
this.breed = breed;
}
public void fetch() {
System.out.println(name + " is fetching the ball!");
}
@Override
public String getInfo() {
return super.getInfo() + " [" + breed + "]";
}
}
Method Overriding and super
Method overriding allows a subclass to provide a specific implementation for a method defined in its superclass. The overriding method must have the same name, return type, and parameter list as the parent method. The @Override annotation tells the compiler to verify the method actually overrides a parent method, catching typos at compile time. The super keyword provides access to the parent class's methods and constructors, enabling you to build upon rather than replace parent behavior.
public class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
public double area() {
return 0;
}
@Override
public String toString() {
return String.format("%s (area: %.2f)", getClass().getSimpleName(), area());
}
}
class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle extends Shape {
private double width, height;
public Rectangle(String color, double width, double height) {
super(color);
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
// Usage in main:
// Shape s1 = new Circle("red", 5);
// Shape s2 = new Rectangle("blue", 4, 6);
// System.out.println(s1); // Circle (area: 78.54)
// System.out.println(s2); // Rectangle (area: 24.00)
Abstract Classes
Abstract classes cannot be instantiated directly and are designed to serve as base classes for concrete subclasses. They can contain both abstract methods, which have no body and must be implemented by subclasses, and concrete methods with full implementations. Abstract classes allow you to define a common interface and shared behavior while forcing subclasses to fill in the specific details. They represent a middle ground between regular classes and interfaces in Java's type system.
public abstract class Vehicle {
protected String make;
protected String model;
protected int year;
public Vehicle(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
}
// Abstract methods - must be implemented by subclasses
public abstract double fuelEfficiency();
public abstract String fuelType();
// Concrete method - shared by all subclasses
public String getDescription() {
return String.format("%d %s %s (%s, %.1f mpg)",
year, make, model, fuelType(), fuelEfficiency());
}
}
class GasCar extends Vehicle {
private double mpg;
public GasCar(String make, String model, int year, double mpg) {
super(make, model, year);
this.mpg = mpg;
}
@Override
public double fuelEfficiency() { return mpg; }
@Override
public String fuelType() { return "Gasoline"; }
}
class ElectricCar extends Vehicle {
private double milesPerKwh;
public ElectricCar(String make, String model, int year, double mpkwh) {
super(make, model, year);
this.milesPerKwh = mpkwh;
}
@Override
public double fuelEfficiency() { return milesPerKwh * 33.7; }
@Override
public String fuelType() { return "Electric"; }
}
Polymorphism in Action
Polymorphism allows objects of different classes to be treated through a common superclass reference, with the actual method called determined at runtime. This enables you to write flexible code that works with any subclass without knowing the specific type. Method dispatch in Java uses dynamic binding, meaning the JVM looks at the actual object type to determine which overridden method to execute. Polymorphism is one of the most powerful features of OOP, enabling extensible architectures where new types can be added without modifying existing code.
import java.util.List;
import java.util.ArrayList;
public class PolymorphismDemo {
// Method accepting the base type
public static void printArea(Shape shape) {
System.out.printf("%s area: %.2f%n",
shape.getClass().getSimpleName(), shape.area());
}
// Process any collection of shapes
public static double totalArea(List<Shape> shapes) {
double total = 0;
for (Shape shape : shapes) {
total += shape.area();
}
return total;
}
public static void main(String[] args) {
// Polymorphic references
List<Shape> shapes = new ArrayList<>();
shapes.add(new Circle("red", 5));
shapes.add(new Rectangle("blue", 4, 6));
shapes.add(new Circle("green", 3));
shapes.add(new Rectangle("yellow", 7, 2));
// Each shape uses its own area() implementation
for (Shape shape : shapes) {
printArea(shape);
}
System.out.printf("Total area: %.2f%n", totalArea(shapes));
// instanceof check
Shape s = new Circle("red", 10);
if (s instanceof Circle c) { // Pattern matching (Java 16+)
System.out.println("It's a circle with area: " + c.area());
}
}
}