Inheritance in Java — practice with a payroll, some shapes and two kinds of account (and the balance I made public so it would compile)

When I started with inheritance I wrote an Account class with a private balance, just like the theory says, and a CurrentAccount that inherited from it. When I wrote the child’s withdraw:

error: balance has private access in Account

My fix was quick and bad: I changed private to public and moved on. It compiled, it worked… and it came back marked in red: any class in the program could do account.balance = 1000000. What I needed was protected, which opens the balance to the children and keeps it closed to everyone else. That detail shows up in today’s third program.

In the theory article you saw the pieces: extends, super, protected, overriding and overloading, polymorphism, casting and abstract classes. Here we put them together in three complete programs:

  1. A payroll with permanent employees, freelancers and interns, each paid in their own way.
  2. A canvas of abstract shapes with inheritance across several levels.
  3. A bank with savings and current accounts that override the same operation in different ways.

Program 1: a payroll with three kinds of employee

Every employee has a name and gets paid something each month, but each kind works out their salary in its own way. It’s the textbook case for an abstract class.

public abstract class Employee {

    private final String name;

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

    public String getName() { return name; }

    // Each kind of employee gets paid differently: the children decide
    public abstract double monthlySalary();

    public abstract String getType();

    // final: the payroll line is the same for everyone, nobody changes it
    public final String payrollLine() {
        return String.format("%-9s %-7s %9.2f €", getType(), name, monthlySalary());
    }
}
public class PermanentEmployee extends Employee {

    private static final double BONUS_PER_3_YEARS = 30.0;   // €30 for every 3 years

    private final double baseSalary;
    private final int years;

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

    @Override
    public double monthlySalary() {
        int blocks = years / 3;                       // integer division: 7 years = 2 blocks
        return baseSalary + blocks * BONUS_PER_3_YEARS;
    }

    @Override
    public String getType() { return "Permanent"; }
}
public class Freelancer extends Employee {

    private final double hourlyRate;
    private int hours;                                // changes every month

    public Freelancer(String name, double hourlyRate) {
        super(name);
        this.hourlyRate = hourlyRate;
    }

    public void logHours(int hours) {
        if (hours > 0) this.hours += hours;
    }

    @Override
    public double monthlySalary() {
        return hours * hourlyRate;
    }

    @Override
    public String getType() { return "Freelance"; }
}
public class Intern extends PermanentEmployee {

    public Intern(String name) {
        super(name, 600, 0);                          // an intern is a permanent employee with base 600 and 0 years
    }

    @Override
    public String getType() { return "Intern"; }
}
public class Payroll {

    public static void main(String[] args) {
        Freelancer marta = new Freelancer("Marta", 25);
        marta.logHours(40);
        marta.logHours(32);

        Employee[] staff = {
            new PermanentEmployee("Carmen", 1800, 7),
            marta,
            new Intern("Iván"),
            new PermanentEmployee("Jorge", 1500, 2)
        };

        double total = 0;
        for (Employee e : staff) {
            System.out.println(e.payrollLine());      // each one works out its salary
            total += e.monthlySalary();
        }
        System.out.printf("TOTAL%22.2f €%n", total);

        int permanent = 0;
        for (Employee e : staff) {
            if (e instanceof PermanentEmployee) permanent++;   // does the intern count?
        }
        System.out.println("Permanent contract: " + permanent);
    }
}

Output:

Permanent Carmen    1860.00 €
Freelance Marta     1800.00 €
Intern    Iván       600.00 €
Permanent Jorge     1500.00 €
TOTAL               5760.00 €
Permanent contract: 3

How does payrollLine() know how much each one earns?

It doesn’t, and it doesn’t need to. payrollLine() is written only once, in Employee, and it calls monthlySalary() and getType(), which are abstract. When the real object is Carmen, PermanentEmployee‘s versions run; when it’s Marta, Freelancer‘s. The parent writes the “skeleton” and the children fill in the gaps.

And since payrollLine() is final, no child can change the payroll format: every line comes out aligned the same way, whatever kind of employee you add.

Why does Carmen earn 1860 and Jorge 1500?

Because of the length-of-service bonus. Carmen has been there 7 years: 7 / 3 is 2 in integer division, so she gets 2 × 30 = €60 on top of her base. Jorge has 2 years: 2 / 3 is 0, so nothing is added. It’s the integer division you know from the first topics, now inside an overridden method.

Why does “Permanent contract” give 3 and not 2?

Because Intern extends PermanentEmployee. An intern is a permanent employee (with base 600 and 0 years), so instanceof PermanentEmployee is also true for Iván. instanceof doesn’t ask “are you exactly this class?”, but “are you this class or one of its children?”.

If you wanted to count only the “pure” permanent employees, you could check e.getClass() == PermanentEmployee.class. But think first about whether you really need it: almost always, an intern counting as permanent is exactly what you want.

Notice that Intern doesn’t write monthlySalary(). It inherits PermanentEmployee‘s and only changes getType(). Inheriting from a concrete (non-abstract) class is also fine: you override only what changes.

A formatting detail

%-9s reserves 9 characters, left-aligned (the - is what aligns it left), and %9.2f reserves 9 characters for the number with 2 decimals, right-aligned. “Permanent” and “Freelance” have exactly 9 letters each: with %-8s the whole column went out of line.

Program 2: a canvas of shapes with inheritance across several levels

This time the hierarchy has three floors: Shape → Rectangle → Square. And each floor calls the one above with super.

public abstract class Shape {

    private final String name;

    protected Shape(String name) {                    // protected: only the children use it
        this.name = name;
    }

    public String getName() { return name; }

    public abstract double area();
    public abstract double perimeter();

    @Override
    public String toString() {
        return String.format("%-9s area %7.2f  perimeter %6.2f", name, area(), perimeter());
    }
}
public class Circle extends Shape {

    private final double radius;

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

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

    @Override
    public double perimeter() { return 2 * Math.PI * radius; }
}
public class Rectangle extends Shape {

    private final double width;
    private final double height;

    public Rectangle(double width, double height) {
        this("Rectangle", width, height);             // reuses the constructor below
    }

    protected Rectangle(String name, double width, double height) {
        super(name);
        this.width = width;
        this.height = height;
    }

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

    @Override
    public double perimeter() { return 2 * (width + height); }
}
public class Square extends Rectangle {

    public Square(double side) {
        super("Square", side, side);                  // a square is a rectangle with width = height
    }
    // No need to write area() or perimeter(): it inherits them from Rectangle
}
public class Canvas {

    public static Shape biggest(Shape[] shapes) {
        Shape max = shapes[0];
        for (Shape s : shapes) {
            if (s.area() > max.area()) max = s;       // polymorphism: each one works out its area
        }
        return max;
    }

    public static void main(String[] args) {
        Shape[] shapes = {
            new Circle(2),
            new Rectangle(3, 4.5),
            new Square(3.5),
            new Circle(1)
        };

        double totalArea = 0;
        for (Shape s : shapes) {
            System.out.println(s);
            totalArea += s.area();
        }
        System.out.printf("Total area: %.2f%n", totalArea);
        System.out.println("Biggest: " + biggest(shapes).getName());

        int rectangular = 0;
        for (Shape s : shapes) {
            if (s instanceof Rectangle) rectangular++;
        }
        System.out.println("Rectangular: " + rectangular);
    }
}

Output:

Circle    area   12.57  perimeter  12.57
Rectangle area   13.50  perimeter  15.00
Square    area   12.25  perimeter  14.00
Circle    area    3.14  perimeter   6.28
Total area: 41.46
Biggest: Rectangle
Rectangular: 2

What exactly happens with new Square(3.5)?

It’s a chain of constructors: the calls go up, and the building comes back down:

  1. Square(3.5) calls super("Square", 3.5, 3.5), Rectangle‘s protected constructor.
  2. That constructor calls super("Square"), Shape‘s, which stores the name.
  3. Back in Rectangle, which stores width and height.
  4. Back in Square, which has nothing else to do.

One single object, built in three layers. And since Square inherits area() and perimeter() from Rectangle, a square with side 3.5 has area 3.5 × 3.5 = 12.25 without writing a single extra formula.

Why does Rectangle have two constructors?

The public one, Rectangle(width, height), is the one everybody uses. The protected one, which also takes the name, only exists so that Square can say “I’m a rectangle, but my name is Square”. That way the name stays final in Shape and nobody has to change it later.

Making Square store its own side field on top of inheriting width and height. You end up with duplicated data that can get out of sync. If the parent already stores what you need, pass it up with super and don’t repeat it.

Why does “Rectangular” give 2?

Same as with the intern: Square extends Rectangle, so the square is a rectangle as far as instanceof is concerned. The two circles don’t count.

Did you notice that the circle with radius 2 has the same area and perimeter (12.57)? It’s not a bug: with radius 2, π·r² and 2·π·r give the same result, 4π. It’s the only radius where that happens.

And how does biggest() compare such different shapes?

biggest takes a Shape[] and only uses area(), which every shape has. It doesn’t need to know whether they’re circles or squares. If tomorrow you add a Triangle extends Shape, biggest works without changing a line. That’s the practical advantage of polymorphism: code that works with the parent works for every child, even the ones that don’t exist yet.

Program 3: a bank with savings and current accounts

Here the children override the same method, withdraw, and each one does it differently: one adds rules to the parent’s and the other replaces them.

public class Account {

    private final String holder;
    protected double balance;                         // the children need to touch it

    public Account(String holder, double initialBalance) {
        this.holder = holder;
        this.balance = initialBalance;
    }

    public void deposit(double amount) {
        if (amount > 0) balance += amount;
    }

    // General rule: you can't take out more than there is
    public boolean withdraw(double amount) {
        if (amount <= 0 || amount > balance) return false;
        balance -= amount;
        return true;
    }

    public double getBalance() { return balance; }

    @Override
    public String toString() {
        return String.format("%s: %.2f €", holder, balance);
    }
}
public class SavingsAccount extends Account {

    private static final int MAX_WITHDRAWALS = 2;     // per month

    private final double interest;                    // 0.02 = 2 %
    private int withdrawals = 0;

    public SavingsAccount(String holder, double initialBalance, double interest) {
        super(holder, initialBalance);
        this.interest = interest;
    }

    @Override
    public boolean withdraw(double amount) {
        if (withdrawals == MAX_WITHDRAWALS) return false;  // the child's extra rule
        boolean ok = super.withdraw(amount);          // plus the parent's rule
        if (ok) withdrawals++;
        return ok;
    }

    public void applyInterest() {
        balance += balance * interest;                // protected: we touch it directly
        withdrawals = 0;                              // a new month starts
    }

    @Override
    public String toString() {
        return "[Savings] " + super.toString();
    }
}
public class CurrentAccount extends Account {

    private final double overdraftLimit;              // how far below zero it can go

    public CurrentAccount(String holder, double initialBalance, double overdraftLimit) {
        super(holder, initialBalance);
        this.overdraftLimit = overdraftLimit;
    }

    @Override
    public boolean withdraw(double amount) {
        // We DON'T call super.withdraw() here: its rule (never below 0) is exactly what we change
        if (amount <= 0 || balance - amount < -overdraftLimit) return false;
        balance -= amount;
        return true;
    }

    @Override
    public String toString() {
        return "[Current] " + super.toString();
    }
}
public class Bank {

    public static void main(String[] args) {
        Account[] accounts = {
            new SavingsAccount("Sergio", 500, 0.02),
            new CurrentAccount("Laura", 100, 300),
            new Account("Pablo", 50)
        };

        // The same loop, three different rules
        for (Account a : accounts) {
            System.out.println(a.withdraw(150) + " " + a.withdraw(150) + " " + a.withdraw(150));
        }

        // End of month: interest only for savings accounts
        for (Account a : accounts) {
            if (a instanceof SavingsAccount savings) {
                savings.applyInterest();
            }
            System.out.println(a);
        }
    }
}

Output:

true true false
true true false
false false false
[Savings] Sergio: 204.00 €
[Current] Laura: -200.00 €
Pablo: 50.00 €

withdraw(150) three times, three different results

The first loop calls the three accounts in exactly the same way, but each applies its own rules:

Account1st2nd3rdWhy does the 3rd fail?
Savings (€500)✅ 350✅ 200❌It already has 2 withdrawals this month
Current (€100, overdraft 300)✅ −50✅ −200❌It would drop to −350, below −300
Plain (€50)❌❌❌There was never €150 to begin with

When to call super.withdraw() and when not to?

That’s the key question in this program:

  • SavingsAccount adds a rule (maximum withdrawals) to the parent’s (don’t take out more than there is). So it checks its own rule and then delegates to super.withdraw(). That way it doesn’t repeat the balance check.
  • CurrentAccount changes the parent’s rule: it specifically wants to allow a negative balance. If it called super.withdraw(), the parent would reject any withdrawal that leaves the balance below 0. So it doesn’t call it and writes its own complete version.

Ask yourself “is what the parent does still true for my child?”. If so, call super and add your bit. If not, override without super.

Why is balance protected?

Because applyInterest() and CurrentAccount‘s withdraw need to modify it, and there’s no public setBalance (we don’t want anyone outside setting whatever balance they like). protected is exactly the middle ground: the children can see it, the rest of the program can’t. It’s my story from the start, but solved properly.

The holder, on the other hand, stays private: no child needs it, and the parent’s toString already prints it.

Making balance public “so it compiles”. It works just the same in your main, but it breaks encapsulation: any class could skip withdraw and its rules. If a child needs a field, protected; if nobody else needs it, private.

Why is instanceof needed for the interest?

Because applyInterest() only exists in SavingsAccount. The variable a is of type Account, and a.applyInterest() wouldn’t compile. With a instanceof SavingsAccount savings we check the type and get the variable savings already converted. Sergio had €200 and, at 2 %, it becomes €204.

super.toString() so you don’t repeat yourself

Both children override toString() to add the [Savings] or [Current] label, but the rest of the text comes from super.toString(). If tomorrow you change how the balance is formatted in Account, it changes in all three at once.

Try it yourself

  • In the payroll, add a SalesRep extends PermanentEmployee type that earns their permanent salary plus 5 % of their monthly sales. Hint: super.monthlySalary().
  • On the canvas, add a Triangle (with its three sides) and check that biggest() still works without changing it. What happens if the sides can’t form a triangle?
  • In the bank, make the CurrentAccount charge a €2 fee every time a withdrawal leaves it negative. Which method does it go in? Do you need protected for anything else?

Watch it step by step in Python Tutor

Python Tutor runs code line by line and draws the objects and the calls. Choose Java, paste this program and step through it with Next:

public class Tutor {
    static class Account {
        double balance;
        Account(double balance) { this.balance = balance; }
        boolean withdraw(double amount) {
            if (amount > balance) return false;
            balance -= amount;
            return true;
        }
    }
    static class Savings extends Account {
        int withdrawals = 0;
        Savings(double balance) { super(balance); }
        boolean withdraw(double amount) {
            if (withdrawals == 1) return false;        // only one withdrawal
            boolean ok = super.withdraw(amount);       // jumps to the parent's method
            if (ok) withdrawals++;
            return ok;
        }
    }
    public static void main(String[] args) {
        Account[] accounts = { new Account(100), new Savings(100) };
        for (Account a : accounts) {
            System.out.println(a.withdraw(30) + " " + a.withdraw(30) + " " + a.balance);
        }
    }
}

Output:

true true 40.0
true false 70.0

What to watch:

  1. The array holds two objects of different classes. The Savings one has two fields: balance (inherited) and withdrawals (its own).
  2. On the first pass, a.withdraw(30) goes straight into Account‘s withdraw: there’s only one frame on the stack.
  3. On the second pass, the same line goes into Savings‘s withdraw. When it reaches super.withdraw(amount), you’ll see two frames stacked: Savings‘s waiting and Account‘s doing the work. Both have this pointing to the same object.
  4. On the second withdrawal from the savings account, the method returns on its first line (withdrawals == 1) without ever calling the parent: that’s why the balance stays at 70.

Summary

ProgramKey ideaWhat you take away
PayrollAbstract class + final method that uses abstract methodsThe parent defines the skeleton and the children fill in the gaps
Shape canvasInheritance across several levels + protected constructorEach floor calls the one above with super; instanceof includes grandchildren
BankOverriding the same method with and without super + protectedIf the parent’s rule still holds, super; if not, replace it

In the exercises you’ll do it on your own: an animal hierarchy, some shapes that compare themselves to each other and the full staff of a restaurant, plus three tricky snippets.

Next step: exercisesInheritance in Java — exercises with animals, shapes and a restaurant’s staff (and the cast that turned my cat into a dog)

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *