SEN1002 · Week 2 · Inheritance

Inheritance: extends, super and the constructor chain

What a subclass gets from its superclass, in what order constructors run, and the difference between overriding and overloading. A favorite of output questions.

inheritancesuperclasssubclassextendssuperconstructor chainingprotectedmethod overridingmethod overloadingObject classis-a relationshipfinal

Inheritance is building a new class on top of an existing one. The new class (the subclass) takes the fields and methods of the old one (the superclass), and adds its own things or changes some methods. In the exam this topic comes in two forms most often: output questions asking in what order the constructors print, and compile errors involving super.

Golden rules:

  1. Every constructor first calls the superclass constructor. If you do not write it, the compiler adds super();.
  2. Output order is top down: first Object, then the superclass, and the subclass last.
  3. A private field is inherited but cannot be accessed by name. The subclass uses a getter or the field becomes protected.
  4. Override: same signature, runs according to the actual type of the object. Overload: different parameters, chosen at compile time.
  5. Fields are not overridden. Only methods are.

extends and the “is-a” relationship

When you write class Manager extends Employee, you are saying “every Manager is an Employee” (is-a relationship). Manager uses all of Employee’s public and protected members as if they were its own. In Java a class can extend only one class (single inheritance). If you want several types of behavior, you use an interface (week 4).

You tell when to use inheritance with the “is-a” test. “A Manager is an Employee” is true, so inheritance fits. “A Car is an Engine” is false; a car has an engine (has-a). In that case you put an Engine field inside Car (composition, week 7).

class Employee {
    private String name;
    protected double salary;     // subclasses can see it directly

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
        System.out.println("Employee ctor: " + name);
    }
    public String getName() { return name; }
    public double pay() { return salary; }
    @Override
    public String toString() { return "Employee " + name + " " + pay(); }
}

class Manager extends Employee {
    private double bonus;        // specific to Manager

    public Manager(String name, double salary, double bonus) {
        super(name, salary);     // the superclass constructor
        this.bonus = bonus;
        System.out.println("Manager ctor: bonus " + bonus);
    }
    @Override
    public double pay() { return super.pay() + bonus; }
    @Override
    public String toString() { return "Manager " + getName() + " " + pay(); }
}
Manager m = new Manager("Selin", 5000, 1500);
System.out.println(m);
System.out.println(m.pay());
Employee ctor: Selin
Manager ctor: bonus 1500.0
Manager Selin 6500.0
6500.0

In Manager’s toString we wrote getName(), not name. name is private in Employee; if you had written name inside Manager, you would get the compile error name has private access in Employee.

Constructor chaining

Constructors are not inherited. But when a subclass object is created, the superclass part has to be built too. That is why every constructor calls a superclass constructor on its first line:

  • If you write super(arguments), that constructor is called.
  • If you write nothing, the compiler secretly adds super(); (no arguments).
  • If the superclass has no no-argument constructor, this hidden call gives a compile error.
class A { A() { System.out.println("A"); } }
class B extends A { B() { System.out.println("B"); } }      // hidden super()
class C extends B { C() { super(); System.out.println("C"); } }
new C();
A
B
C
new C()
  C() called → first job super() → B()
      B() called → first job super() → A()
          A() called → super() → Object() (silent)
          A body: "A"
      B body: "B"
  C body: "C"

PITFALL · this(...) and super(...) together. A constructor starts with either this(...) or super(...), never both. A constructor that starts with this(...) does not call the superclass itself; the constructor at the end of the chain does. The Truck example below shows this.

class Vehicle {
    int wheels;
    Vehicle(int w) { wheels = w; System.out.println("Vehicle " + w); }
    Vehicle()      { this(4);    System.out.println("Vehicle()"); }
}
class Bike  extends Vehicle { Bike()  { super(2); System.out.println("Bike"); } }
class Truck extends Vehicle { Truck() {           System.out.println("Truck"); } }

Bike b = new Bike();
Truck t = new Truck();
System.out.println(b.wheels + " " + t.wheels);
Vehicle 2
Bike
Vehicle 4
Vehicle()
Truck
2 4

When are field initializers assigned?

When an object is created, the order is: (1) all fields get default values (0, null, false), (2) super(...) runs, (3) this class’s field initializers (int x = 7;) are assigned, (4) the rest of the constructor body runs. If a superclass constructor calls an overridden method, that method sees the subclass’s fields still at their default values:

class Base {
    Base() { System.out.println("Base ctor"); hello(); }
    void hello() { System.out.println("Base hello"); }
}
class Derived extends Base {
    int x = 7;
    Derived() { System.out.println("Derived ctor x=" + x); }
    @Override void hello() { System.out.println("Derived hello x=" + x); }
}
new Derived();
Base ctor
Derived hello x=0
Derived ctor x=7

PITFALL · the super(...) first-line rule. The textbook and the exam follow the rule that the super(...) call must be the first line of the constructor; that is the case in Java 24 and earlier. The flexible constructor bodies feature in Java 25 allowed lines before it that do not touch this (if you compile with --release 21 you get the error flexible constructors is not supported). If you see an option in the exam like “super is not on the first line”, answer by the old rule: compile error.

protected and access

Member in the superclassAccess from inside the subclass
publicyes
protectedyes (even if the subclass is in another package)
no modifier (package-private)only if in the same package
privateno (it exists in the object, cannot be accessed by name)

protected is convenient but loosens encapsulation a bit. In most cases keeping the field private and providing a getter is better; in the exam both solutions are accepted.

Overriding and overloading

The two concepts are confused because of their names, but they are completely different mechanisms:

OverridingOverloading
Where?In the subclass, you rewrite a method of the superclassIn the same class (or with an inherited method), same name
Parameter listSameDifferent (number, type or order)
Return typeSame or a subtypeDoes not matter, it does not distinguish on its own
AccessCannot narrow (public → public)Free
When is it chosen?At run time, by the actual type of the objectAt compile time, by the argument types
@OverrideWrittenNot written
class Printer {
    void show(int a)    { System.out.println("int " + a); }
    void show(double a) { System.out.println("double " + a); }
    void show(String s) { System.out.println("String " + s); }
}
Printer pr = new Printer();
pr.show(3);
pr.show(3.0);
pr.show('A');     // char widens to int
pr.show(5L);      // long widens to double
pr.show("3");
int 3
double 3.0
int 65
double 5.0
String 3

Inside an overridden method you can call the superclass version with super.method(). This is the standard way to add on top without rewriting the code:

class Shape  { String name() { return "shape"; } }
class Circle extends Shape {
    @Override String name() { return "circle<" + super.name() + ">"; }
}
System.out.println(new Circle().name());
circle<shape>

Field hiding

If a subclass declares a field with the same name as the superclass, there are two separate fields. Which field is read is decided by the type of the variable, not of the object. For methods, the actual type of the object decides.

class P { String tag = "P"; String getTag() { return tag; } }
class Q extends P { String tag = "Q"; @Override String getTag() { return tag; } }
P obj = new Q();
System.out.println(obj.tag + " " + obj.getTag());
P Q

The Object class

Every class for which you do not write extends automatically derives from java.lang.Object. So every object has these methods:

MethodDefault behaviorWhat is usually done?
toString()ClassName@hashOverride for readable text
equals(Object o)this == oOverride for content comparison
hashCode()A number specific to the objectOverridden too if equals is overridden
getClass()The run-time classUsed like getClass().getSimpleName()
Book b1 = new Book("Dune"), b2 = new Book("Dune");   // equals NOT overridden
System.out.println(b1.equals(b2));
System.out.println(b1.getClass().getSimpleName());
System.out.println(b1 instanceof Object);
false
Book
true

final

  • final class: nobody can derive from it (String and Integer are like this).
  • final method: a subclass cannot override it, but overloading is free.
  • final field: assigned once, then it does not change.

Common mistakes

Wrong ideaThe truth
Constructors are inheritedThey are not; every class writes its own constructor and calls the one above with super(...)
If I do not write super, the superclass is not builtThe compiler adds a hidden super(); if the superclass has no no-argument constructor, it is an error
Output starts from the subclassBodies run top down: A, B, C
A subclass can use a private fieldIt cannot access it by name; getter or protected
Same name, different parameters = overrideThat is an overload; for an override the parameters must be the same
I can drop public when overridingAccess cannot be narrowed
Fields are overridden tooThey are hidden; ref.field looks at the type of the variable
The method called by the superclass constructor is the superclass’sIf it is overridden, the subclass’s runs, with its fields still at defaults
super.super.method()There is no such thing, only one level up

Test yourself

Exam-style questions. Solve them on paper first, then reveal the answer.

Question 1easy

What is printed by new C();?

class A { A() { System.out.println("A"); } }
class B extends A { B() { System.out.println("B"); } }
class C extends B { C() { super(); System.out.println("C"); } }
Show answer
A, B, C (one per line)

The first job of every constructor is to call the superclass constructor. Even if super() is not written in B, the compiler adds it. The calls go down C → B → A, but the bodies run starting from the top: A prints first, then B, and C last.

Question 2medium

What is printed?

class Employee {
    private String name; protected double salary;
    Employee(String name, double salary) {
        this.name = name; this.salary = salary;
        System.out.println("Employee ctor: " + name);
    }
    String getName() { return name; }
    double pay() { return salary; }
}
class Manager extends Employee {
    private double bonus;
    Manager(String n, double s, double b) {
        super(n, s); bonus = b;
        System.out.println("Manager ctor: bonus " + bonus);
    }
    @Override double pay() { return super.pay() + bonus; }
}
// in main:
Manager m = new Manager("Selin", 5000, 1500);
System.out.println(m.pay());
Show answer
Employee ctor: Selin / Manager ctor: bonus 1500.0 / 6500.0

super(n, s) first runs the Employee constructor: Employee ctor: Selin. Then the Manager body: since bonus is a double, 1500.0. pay() is overridden: super.pay() returns 5000.0, and 1500 is added on top: 6500.0.

Question 3medium

Find the compile error.

class Vehicle {
    int wheels;
    Vehicle(int w) { wheels = w; }
}
class Car extends Vehicle {
    Car() { System.out.println("Car"); }
}
Show answer
Car() has a hidden super() call, and Vehicle has no no-argument constructor

If you do not write super(...), the compiler adds super();. Because Vehicle only defines Vehicle(int), there is no no-argument constructor. javac: constructor Vehicle in class Vehicle cannot be applied to given types; required: int. Fix: write super(4); as the first line of Car.

Question 4easy

Find the compile error.

class Employee {
    private double salary = 100;
}
class Manager extends Employee {
    double pay() { return salary * 2; }
}
Show answer
salary is private, even a subclass cannot access it directly

A subclass inherits a private field (it exists inside the object) but cannot access it by name. javac: salary has private access in Employee. Fix: make the field protected, or add getSalary() to Employee and call that.

Question 5hard

What is printed?

class Printer {
    void show(int a)    { System.out.println("int " + a); }
    void show(double a) { System.out.println("double " + a); }
    void show(String s) { System.out.println("String " + s); }
}
// in main:
Printer p = new Printer();
p.show('A');
p.show(5L);
Show answer
int 65 and double 5.0

In overloading, the compiler chooses which method to call by looking at the argument type. 'A' is a char and there is no exact match; the closest type a char can widen to is int: int 65 (the code of A). 5L is a long; a long cannot narrow to int, it widens to double: double 5.0.

Question 6medium

What is printed?

class Animal {
    String sound() { return "..."; }
    String describe() { return "I say " + sound(); }
}
class Dog extends Animal {
    @Override String sound() { return "Woof"; }
}
// in main:
Animal d = new Dog();
System.out.println(d.describe());
Show answer
I say Woof

describe() is written in Animal, but the sound() call inside it runs the version of the current object, that is, Dog's. An overridden method is always chosen from the actual class of the object (dynamic binding in week 3).

Question 7medium

Find the compile error.

class Animal {
    public String sound() { return "..."; }
}
class Cat extends Animal {
    String sound() { return "Meow"; }
}
Show answer
An overriding method cannot narrow access (public → package-private)

When overriding, the access modifier can stay the same or widen, but it cannot narrow. The method that is public in Animal is written without a modifier (package-private) in Cat. javac: attempting to assign weaker access privileges; was public. Fix: write public in Cat too.

Question 8hard

What is printed?

class Vehicle {
    int wheels;
    Vehicle(int w) { wheels = w; System.out.println("Vehicle " + w); }
    Vehicle() { this(4); System.out.println("Vehicle()"); }
}
class Bike extends Vehicle { Bike() { super(2); System.out.println("Bike"); } }
class Truck extends Vehicle { Truck() { System.out.println("Truck"); } }
// in main:
Bike b = new Bike();
Truck t = new Truck();
System.out.println(b.wheels + " " + t.wheels);
Show answer
Vehicle 2 / Bike / Vehicle 4 / Vehicle() / Truck / 2 4

Bike explicitly calls super(2): Vehicle 2, then Bike. Truck has no super written, so the hidden super() calls the no-argument Vehicle, which delegates to Vehicle(int) with this(4): first Vehicle 4, then Vehicle(), and last Truck. The wheels are 2 and 4.

Question 9hard

What is printed?

class Base {
    Base() { System.out.println("Base ctor"); hello(); }
    void hello() { System.out.println("Base hello"); }
}
class Derived extends Base {
    int x = 7;
    Derived() { System.out.println("Derived ctor x=" + x); }
    @Override void hello() { System.out.println("Derived hello x=" + x); }
}
// in main:
new Derived();
Show answer
Base ctor / Derived hello x=0 / Derived ctor x=7

Because the object is a Derived, even the hello() call inside Base's constructor runs Derived's version. But at that moment Derived's field initializers (x = 7) have not been executed yet; that assignment happens after super() finishes. x is printed with its default value 0. Then x becomes 7 and Derived's body runs.

Question 10hard

What is printed?

class P { String tag = "P"; String getTag() { return tag; } }
class Q extends P { String tag = "Q"; @Override String getTag() { return tag; } }
// in main:
P obj = new Q();
System.out.println(obj.tag + " " + obj.getTag());
Show answer
P Q

Fields are not overridden, they are hidden (field hiding). obj.tag looks at the compile-time type (P): "P". Methods, however, are overridden: getTag() runs Q's version and returns Q's tag: "Q". That is why you should not declare a field in a subclass with the same name as one in the superclass.

Question 11easy

Which statement about final is true? (a) A final method cannot be overloaded. (b) A final class cannot be extended. (c) A final field can be changed in a subclass.

  1. (a)
  2. (b)
  3. (c)
Show answer
(b)

A final class cannot be inherited: for class Key extends Locked, javac says cannot inherit from final Locked (String is also a final class). A final method cannot be overridden but it can be overloaded; (a) is wrong. A final field is assigned once and nobody can change it; (c) is wrong.

Question 12medium

Write a class Student that extends Person (which has a private name, a constructor Person(String name), getName() and toString() returning Person: <name>). Student adds a private double gpa, a constructor Student(String name, double gpa), and overrides toString() to return Person: Deniz, GPA 3.4 style text.

Show answer
a subclass that uses super(name) + super.toString()

name is private in Person, so Student can only fill it with super(name). In toString, instead of rewriting the superclass text, call super.toString():

public class Student extends Person {
    private double gpa;

    public Student(String name, double gpa) {
        super(name);          // first line
        this.gpa = gpa;
    }
    public double getGpa() { return gpa; }

    @Override
    public String toString() {
        return super.toString() + ", GPA " + gpa;
    }
}
Printing new Student("Deniz", 3.4) gives: Person: Deniz, GPA 3.4.

Verification: All code examples and answers were run with JDK 26 (java verify/SEN1002/W02.java); the compile errors were tried with javac (verify/SEN1002/err/E02a-e.java, E02d also with --release 21).

Review card

class Sub extends Super → "Sub is-a Super". Java has single inheritance (one extends). Subclass: uses public/protected members, CANNOT SEE private ones by name. Constructors are not inherited. Every ctor first calls the superclass ctor. If you do not write super(...), the compiler adds super() → ERROR if the superclass has no no-argument ctor. Calls go bottom to top, execution goes TOP TO BOTTOM: A, B, C. Field initializers (int x = 7) are assigned AFTER super() finishes. super.method() → the superclass version. There is no super.super. Override: same name + same parameters; same return type (or a subtype); access cannot narrow. Overload: same name, DIFFERENT parameter list; chosen at compile time. Fields are not overridden, they are hidden: ref.field → type of the variable; ref.method() → type of the object. final class: no extends. final method: no override. static method: no override (hidden). Object sits at the top of every class: toString, equals, hashCode, getClass.