Sitemap
Dev Genius

Coding, Tutorials, News, UX, UI and much more related to development

Learning Java from Scratch with Arbiter — Inheritance & Polymorphism: extends, super, and Dynamic Dispatch

Day 5 of my Arbiter Learning Journey | Building subclass hierarchies with extends and super, overriding behavior with @Override, and proving how the JVM resolves the right method at runtime — not compile time

8 min readAug 11, 2026

--

Press enter or click to view image in full size
Cozy night coding setup with dual screens showing Java inheritance code and neon UML diagrams. Desk has coffee and notebook
Caffeine, a cozy night view, and a deep dive into inheritance. Let’s make this code clean and polymorphically perfect

Before we begin, here’s the source code for today’s lesson:

class Shape {
public double area() {
System.out.println("Generic shape, no area.");
return 0.0;
}
}

class Circle extends Shape {
private double radius;

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

@Override
public double area() {
double circleArea = Math.PI * (radius * radius);
System.out.println("Circle Area: " + circleArea);

return circleArea;
}
}

class Employee {
protected String name;
protected double baseSalary;

public Employee(String name, double baseSalary) {
this.name = name;
this.baseSalary = baseSalary;
}

public double calculatePay() {
return baseSalary;
}
}

class Manager extends Employee {
private int teamSize;

public Manager(String name, double baseSalary, int teamSize) {
super(name, baseSalary);
this.teamSize = teamSize;
}

@Override
public double calculatePay() {
return baseSalary + (teamSize * 200.0);
}
}

class Contractor extends Employee {
private int hoursWorked;
private double hourlyRate;

public Contractor(String name, double baseSalary, int hoursWorked, double hourlyRate) {
super(name, baseSalary);

this.hoursWorked = hoursWorked;
this.hourlyRate = hourlyRate;
}

@Override
public double calculatePay() {
return baseSalary + (hoursWorked * hourlyRate);
}
}

class Intern extends Employee {
public Intern(String name, double baseSalary) {
super(name, baseSalary);
}
}

public class java_5Inheritance {
public static void main(String[] args) {
Shape shape = new Shape();
double area = shape.area();
System.out.println("Shape returned area: " + area);

Shape circle = new Circle(5.0);
System.out.println("\nArea of Circle: " + circle.area());
System.out.println();

Manager manager = new Manager("Manager1", 25000.0, 5);
Contractor contractor1 = new Contractor("Contractor1", 5000.0, 36, 2000.0);
Contractor contractor2 = new Contractor("Contractor2", 8000.0, 48, 4000.0);
Intern intern1 = new Intern("Intern1", 10000.0);
Intern intern2 = new Intern("Intern2", 12000.0);
Intern intern3 = new Intern("Intern3", 8000.0);

Employee[] employees = new Employee[]{intern1, manager, intern2, contractor1, intern3, contractor2};

double totalPayroll = 0.0;
for (int i = 0; i < employees.length; i++) {
System.out.println("Employee Name: " + employees[i].name);
System.out.println("Employee Pay: " + employees[i].calculatePay());
System.out.println();

totalPayroll += employees[i].calculatePay();
}

System.out.println("Total Payroll: " + totalPayroll);
}
}

Every class built so far in this series has stood alone — Book, BankAccount, each a single, self-contained blueprint with no relationship to any other class. That works until two classes are clearly variations on the same idea: a Manager is an Employee, just one that gets paid differently. Writing that relationship out by hand — duplicating name and baseSalary in every payroll class, then hoping every copy stays in sync — is exactly the kind of duplication object-oriented languages exist to eliminate.

That’s what inheritance solves, and what makes it useful is polymorphism: the ability to treat a Manager, a Contractor, and an Intern as nothing more specific than "an Employee," hand them all to the same loop, and have each one still behave like itself.

This is Day 5 of my Arbiter Learning Journey, where I’m learning Java from the ground up before eventually building Arbiter, a complete microservices-based test management platform.

In Day 4, I covered classes, constructors, this, encapsulation, and the static-vs-instance distinction. Today builds directly on top of that: inheritance is what lets one class reuse and extend another's constructor and instance state, rather than redefining it from scratch.

Today’s Topics

  • Inheritance (extends, super)
  • Overriding (@Override) vs. inheriting behavior unchanged
  • Polymorphism and dynamic dispatch

Inheritance — Building “Is-A” Relationships

class Circle extends Shape and class Manager extends Employee (along with Contractor and Intern) establish an is-a relationship: a Circle is a Shape, a Manager is an Employee, with all the fields and methods that implies, plus whatever each subclass adds on top.

The mechanism that actually wires this together is super():

public Manager(String name, double baseSalary, int teamSize) {
super(name, baseSalary);
this.teamSize = teamSize;
}

Manager, Contractor, and Intern all call super(name, baseSalary) as the first line of their constructor, before touching a single field of their own. That's not a style choice — it's how the inherited name and baseSalary state actually gets initialized. A subclass has no direct way to reach into its parent's fields and set them itself; it has to ask the parent's own constructor to do it, via super(...).

Circle follows the same shape with Shape, though Shape's constructor here is the implicit no-arg one — Circle doesn't need to pass anything up because Shape itself holds no fields, only behavior.

Overriding vs. Inheriting Unchanged

Once a class hierarchy exists, a subclass has two options for any method it inherits: redefine it, or leave it alone.

Circle.area(), Manager.calculatePay(), and Contractor.calculatePay() all choose the first option, using @Override to redefine behavior with the exact same method signature as their superclass's version:

@Override
public double calculatePay() {
return baseSalary + (teamSize * 200.0);
}
@Override
public double calculatePay() {
return baseSalary + (hoursWorked * hourlyRate);
}

Intern, by contrast, deliberately declares no calculatePay() at all:

class Intern extends Employee {
public Intern(String name, double baseSalary) {
super(name, baseSalary);
}
}

That’s not a missing piece of the assignment — it’s the second option, chosen on purpose. Intern simply inherits Employee.calculatePay() untouched, which just returns baseSalary with no bonus formula layered on top. Nothing needs to be written for that to work; the parent's implementation is already correct for an intern's case.

Press enter or click to view image in full size
An infographic titled ‘Java Inheritance & Polymorphism’ in three columns. Column 1 shows class hierarchies (Employee -> Manager, Contractor, Intern) with ‘extends’ and ‘super’ for constructors. Column 2 compares ‘@Override calculatePay()’ to ‘no override — inherits parent’s version unchanged’. Column 3 illustrates ‘Dynamic Dispatch’ using a polymorphic Employee array where object types are resolved by the JVM at runtime, not by the array’s declared type
Java Inheritance and Polymorphism, visually explained: Extending classes, super constructors, @Override, and JVM-driven dynamic dispatch

Polymorphism and Dynamic Dispatch — The Actual Point of This Session

Here’s the simplest possible demonstration in today’s code:

Shape circle = new Circle(5.0);
System.out.println("\nArea of Circle: " + circle.area());

The variable circle has declared type Shape, but its runtime type is Circle. Calling circle.area() printed Circle's message — "Circle Area: 78.53..." — and returned Circle's computed value, not Shape's generic "no area" default. The call resolved based on what the object actually is, not what the reference is declared as. That's dynamic dispatch.

Get Someone’s stories in your inbox

Join Medium for free to get updates from this writer.

The harder problem pushed the same idea a lot further. An Employee[] array held a deliberately shuffled mix of Manager, Contractor, and Intern objects:

Employee[] employees = new Employee[]{intern1, manager, intern2, contractor1, intern3, contractor2};

The traversal loop then called .calculatePay() on every element using only the Employee-typed array reference — no instanceof anywhere, no casting, no branching on what kind of employee each one actually was:

for (int i = 0; i < employees.length; i++) {
totalPayroll += employees[i].calculatePay();
}

Every single call still resolved to the correct subclass’s formula — Manager's per-report bonus, Contractor's hourly rate, Intern's inherited flat default — purely because the JVM looks up the object's actual class at the moment calculatePay() is called, every time, regardless of the array's declared element type. The hand-summed total came out to 333,000, and the loop's printed total matched it exactly:

  • Manager: 25,000 + (5 × 200) = 26,000
  • Contractor1: 5,000 + (36 × 2,000) = 77,000
  • Contractor2: 8,000 + (48 × 4,000) = 200,000
  • Intern1 + Intern2 + Intern3: 10,000 + 12,000 + 8,000 = 30,000
  • Total: 333,000

The loop code itself never needed to know or check which subclass each element actually was — and that’s the entire value proposition of polymorphism. It’s not a syntax trick; it’s what makes a single, generic piece of code correct for types that don’t exist yet.

Key Takeaway

Overriding resolves at runtime, based on the object’s actual type — that’s dynamic dispatch. Overloading, covered back in Day 3, resolves at compile time, based on the declared types of the arguments at the call site. Those are two different mechanisms that happen to look similar on the surface, and mixing them up is a common source of confusion.

A base-typed collection calling an overridden method on each element — exactly what the Employee[] loop did — is the concrete mechanism behind "program to a supertype, not a specific implementation." It's not an abstract design slogan; it's the direct, provable behavior of calculatePay() resolving correctly for six objects the loop never individually inspected.

The Arbiter Journey Continues

One concept at a time, this series is building toward Arbiter — a microservices-based test management platform using Java, Spring Boot, databases, testing frameworks, Docker, and AWS. Today’s lesson is the exact idea Spring leans on constantly: when Spring hands back a bean typed as an interface or a base class, the object underneath might actually be a proxy, a specific implementation, or one of several alternatives wired in by configuration — and the calling code never needs to know or care, for precisely the same reason employees[i].calculatePay() didn't need to know which subclass it was touching. Every service layer in Arbiter that depends on an abstraction rather than a concrete class is leaning on today's lesson.

Have a look at the concepts in deep

Follow along with the complete Arbiter playlist

GitHub Repository

You can follow the complete learning journey, source code, explanations, and future lessons here:

Follow the Complete Arbiter Series

Every Guide and Guide Theory article in this series, collected in one place:

Explore My Other Learning Series

If you’re interested in my other programming and technology learning journeys, you may also enjoy:

What’s Next?

In Day 6, we continue Object-Oriented Programming — Advanced: abstraction through abstract classes, interfaces with default and static methods, and the equals(), hashCode(), and toString() contracts that every well-behaved Java object needs to honor.

Happy coding! ☕

--

--

Dev Genius
Dev Genius

Published in Dev Genius

Coding, Tutorials, News, UX, UI and much more related to development