Real systems have families of related things: circles, rectangles and squares are all shapes; NetSuite, Salesforce and HubSpot connectors are all connectors. Java gives you three tools to model that: inheritance, interfaces and polymorphism.
Inheritance: extends
class Rectangle extends Shape {
protected final double width, height;
Rectangle(double width, double height) { this.width = width; this.height = height; }
@Override
double area() { return width * height; }
}
class Square extends Rectangle {
Square(double side) { super(side, side); } // call the parent's constructor
}
A Square is a Rectangle: it inherits its fields and methods. super(...) calls the parent’s constructor; super.method() calls the parent’s version of a method you’ve overridden.
@Override tells the compiler “I mean to replace a parent method” — if you misspell the name, you get an error instead of a silent new method.
Abstract classes
abstract class Shape implements Describable {
abstract double area(); // every shape must implement this
public String describe() { // shared behaviour
return getClass().getSimpleName() + " with area " + area();
}
}
You can’t create a plain Shape — only its concrete subclasses. Abstract classes are good for sharing code and forcing subclasses to fill in the gaps.
Interfaces: a contract
interface Describable {
String describe();
default String label() { // default method (Java 8+)
return "[" + describe() + "]";
}
}
An interface says what a type can do, not how. A class can extend only one class but implement many interfaces — so interfaces are the main way Java models “can do” relationships (Comparable, Runnable, AutoCloseable…). Default methods let interfaces carry shared behaviour too.
| Abstract class | Interface | |
|---|---|---|
| Fields | any | constants only |
| Constructors | yes | no |
| A class can use | one | many |
| Use it for | a family sharing code and state | a capability or contract |
Interfaces with a single abstract method are functional interfaces — the foundation of lambda expressions.
Polymorphism: one call, many behaviours
List<Shape> shapes = List.of(new Circle(1), new Rectangle(2, 3), new Square(2));
for (Shape s : shapes) {
System.out.println(s.label()); // each shape runs its own area()/describe()
}
The loop only knows it has Shapes, yet each object runs its own version of area(). Add a Triangle tomorrow and this loop works unchanged — that’s the real payoff.
Checking the actual type: pattern matching
if (s instanceof Square sq) { // Java 16+: test and cast in one step
System.out.println(sq.width);
}
Needing many instanceof checks is often a hint that the behaviour belongs in a method on the classes themselves.
The full example
// Shapes.java — inheritance, abstract classes, interfaces and polymorphism.
import java.util.List;
public class Shapes {
public static void main(String[] args) {
List<Shape> shapes = List.of(new Circle(1), new Rectangle(2, 3), new Square(2));
double total = 0;
for (Shape s : shapes) { // one loop, many shapes: polymorphism
System.out.println(s.label());
total += s.area();
}
System.out.printf("Total area: %.2f%n", total);
Shape s = shapes.get(2);
if (s instanceof Square sq) { // pattern matching for instanceof (Java 16+)
System.out.println("Found a square: " + sq.width);
}
}
}
interface Describable {
String describe();
// Default methods give interfaces shared behaviour (Java 8+)
default String label() {
return "[" + describe() + "]";
}
}
abstract class Shape implements Describable {
abstract double area();
@Override
public String describe() {
return getClass().getSimpleName() + " with area " + String.format("%.2f", area());
}
}
class Circle extends Shape {
private final double radius;
Circle(double radius) { this.radius = radius; }
@Override
double area() { return Math.PI * radius * radius; }
}
class Rectangle extends Shape {
protected final double width, height;
Rectangle(double width, double height) { this.width = width; this.height = height; }
@Override
double area() { return width * height; }
}
class Square extends Rectangle {
Square(double side) { super(side, side); } // call the parent constructor
@Override
public String describe() {
return "Square " + width + "x" + width + " -> " + super.describe();
}
}
Try it yourself
Add a Triangle class with a base and height (area = base × height ÷ 2) and add one to the list. Nothing else should need to change.
Show a solution
class Triangle extends Shape {
private final double base, height;
Triangle(double base, double height) { this.base = base; this.height = height; }
@Override
double area() { return base * height / 2; }
}
// List.of(new Circle(1), new Rectangle(2, 3), new Square(2), new Triangle(3, 4))
Key takeaways
extendsreuses a parent class;superreaches the parent;@Overrideguards against typos.- Abstract classes share code; interfaces define contracts — and a class can implement many.
- Polymorphism lets one piece of code work with every subtype.
- Prefer methods on the types over long chains of
instanceof.