Institute of Computer Science
  1. Courses
  2. 2024/25 spring
  3. Object-Oriented Programming (uus) (P2NC.01.083)
ET
Log in

Object-Oriented Programming (uus) 2024/25 spring

Intro

Week 5

Week 6

Week 6

Inheritance, method overriding, class Object

Topics

Inheritance, method overriding, class Object

After this practical lesson, the student will know what is:

  • Inheritance and Class Object
  • Method overriding
  • Dynamic Binding
  • Abstract class

Study new commands:

  • extends
  • super
  • @Override

And solve exercises:

  • Exercise 1: Implementation of the withdraw method
  • Exercise 2: Checking account
  • Exercise 3: Completing banking system
  • Exercise 4: Working with Abstract Classes and Employee Salary Calculations

All exercise solutions must be uploaded to a page in Moodle.

Definition

Inheritance

Official Java documentation definition for Inheritance

Official Java documentation definition for Class Object

Inheritance is a fundamental concept in object-oriented programming where one class can inherit (or "take") properties and methods (behaviors) from another class. The class that shares its attributes and methods is called the parent class (also known as superclass, base class, or ancestor class). The class receiving these attributes and methods is called the child class (also known as subclass, derived class, or descendant class).

Using inheritance allows programmers to reuse code efficiently, avoid duplication, and build organized class structures.

In Java, every class created automatically inherits from a built-in class called java.lang.Object. This means the Object class acts as the ultimate ancestor (root class) for every class in Java, providing fundamental methods (like toString(), equals(), and hashCode()) which all Java objects inherently possess.

When a class is defined without explicitly inheriting from another class, Java automatically makes it a subclass of Object.

Reminder
.equals() method – topic of the fifth week (link)

To create inheritance between classes, the new keyword extends is used :

class ChildClass extends ParentClass {
    // Child class will inherit methods and properties of ParentClass
}
  • Here, ChildClass becomes the subclass (or child class), and ParentClass becomes the superclass (or parent class).
  • The child class can directly access the non-private methods and attributes of the parent class.

In short, inheritance simplifies programming work by enabling developers to create specialized versions of general classes, while Java ensures all classes have a common starting point through java.lang.Object. Also if the class needs to inherit and implement interfaces at the same time, then the parent class must be specified first, followed by the interfaces to be implemented:

 
class ChildClass extends ParentClass implements ExampleInterface {…}

Interesting to Know: Visualizing Inheritance Trees When programs grow larger, inheritance structures (also called class trees) can quickly become complex and challenging to follow. Imagine trying to keep track of multiple generations of parent and child classes—it's easy to lose track!

Thankfully, software tools such as IntelliJ IDEA provide visual ways to navigate these relationships:

  • Type Hierarchy:

Quickly view parent and child relationships of a selected class using Navigate | Type Hierarchy or pressing Ctrl+H.

  • Method Hierarchy:

See exactly where methods are overridden or implemented within the inheritance tree using Navigate | Method Hierarchy or pressing Ctrl+Shift+H.

  • Call Hierarchy:

Analyze which methods invoke specific methods by selecting Navigate | Call Hierarchy or pressing Ctrl+Alt+H.

  • Diagrams:

Choose all classes in project section (in the left menu using Ctrl + left click). Right-click on the classes and select Diagrams > Show Diagram or Ctrl+Alt+Shift+K

Now we can move on to an example (new comand super()) :

In this example, we model a banking system with a base class for bank accounts and one derived class for different types of accounts.

Reminder
Class - topic of the third week

 
class BankAccount {
    protected String accountNumber;
    protected double balance;
    public BankAccount(String accountNumber, double balance) {
        this.accountNumber = accountNumber;
        this.balance = balance;
    }
    //Getters
    public String getAccountNumber() {
        return accountNumber;
    }

    public double getBalance() {
        return balance;
    }

    //Setters
    public void setAccountNumber(String accountNumber) {
        this.accountNumber = accountNumber;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    public double calculateMonthlyInterest() {
        // Default implementation (for non-interest accounts)
        return 0.0;
    }
}

class SavingsAccount extends BankAccount {
    private double annualInterestRate; 
    public SavingsAccount(String accountNumber, double balance, double annualInterestRate) {
        super(accountNumber, balance);
        this.annualInterestRate = annualInterestRate;
    }
}

We created a class BankAccount that held basic information about an account, such as an account number and balance. It also provided methods like showBalance() to display the current balance and calculateMonthlyInterest() which, in the base class, returned 0.0 because not every account was designed to earn interest.

Next, we created a SavingsAccount class that extended BankAccount. This meant that SavingsAccount automatically inherited all public and protected fields and methods from BankAccount. For example, it could use the showBalance() method without needing to rewrite it. This is one of the key benefits of inheritance: code reuse, which helped avoid duplication and kept the code cleaner.

The constructor of the SavingsAccount class accepted three parameters: the account number, balance, and an additional field called annualInterestRate. Inside its constructor, it explicitly called the parent’s constructor using super(…) and the parameters that we want to inherite (accountNumber, balance). This call was essential because it initialized the inherited fields in a consistent way, using the logic already defined in the BankAccount class. After calling super(), the constructor then set the annualInterestRate specific to the SavingsAccount.

If a constructor was not specified in a child class, Java would have automatically generated a default constructor that called the parent's no-argument constructor. However, in this case, since BankAccount did not have a no-argument constructor (the constructor we have requires an account number and balance), it was necessary to explicitly define a constructor in SavingsAccount to call the appropriate BankAccount constructor. For example, if BankAccount had had a no-argument constructor, one could have omitted the constructor in SavingsAccount and relied on the default one, but that was not possible here.

In situations when the superclass have multiple constructors, there is the flexibility to choose which one to call in the child class by passing the appropriate arguments in the super() call. This allowed the parent part of the object to be initialized using different initialization strategies based on the needs.

The IDE could be used to generate suitable constructors automatically. For IntelliJ, one could use the Generate option from the Code menu or Alt + Insrert button combination.

It's worth noting that the generated subclass inherits all the properties of the superclass, but only those fields and methods that do not have a private delimiter can be used directly from the subclass. In order to access the private fields of the superclass, either the necessary get methods must be added to the superclass, or the program must be arranged so that no private fields need to be accessed from the subclass.

Exercise 1: Implementation of the withdraw method

In this exercise, the goal is to extend the BankAccount class by implementing a method
( void withdraw(…) ) to handle withdrawals. The method should accept a numerical value representing the amount to withdraw. It needs to check that the requested amount is positive and does not exceed the current balance. If both conditions are met, the method should deduct the amount from the balance variable and then output the withdrawn amount and the updated balance. However, if the amount is negative or greater than the available balance, the method should output an appropriate error message to indicate that the withdrawal cannot be processed (print).

Definition

Method overriding (new command @Override )

Reminder
Polymorphism – topic of the fifth week (link)

Method overriding is an essential concept in object-oriented programming, where a subclass provides a specific implementation of a method that is already defined in its superclass (like public double calculateMonthlyInterest()). In other words, the subclass modifies or customizes the behavior of the inherited method to meet its own requirements. Method overriding helps programmers achieve flexibility, as different subclasses can implement methods differently while sharing the same method name and parameters. It is considered one form of polymorphism—an important principle in object-oriented programming—allowing methods to perform different functions depending on the object that invokes them. Through method overriding, classes can maintain a consistent interface while offering distinct behaviors.

For example:

 
class SavingsAccount extends BankAccount {
    private double annualInterestRate; 
    public SavingsAccount(String accountNumber, double balance, double annualInterestRate) {
        super(accountNumber, balance);
        this.annualInterestRate = annualInterestRate;
    }
    @Override
    public double calculateMonthlyInterest() {
        // Calculate interest based on the current balance and annual rate divided by 12
        return getBalance() * (annualInterestRate / 12);
    }
}

In this updated example, the SavingsAccount class provided its own version of the calculateMonthlyInterest() method, thereby overriding the one defined in the BankAccount superclass. Originally, the BankAccount class’s implementation of calculateMonthlyInterest() simply returned 0.0 because it served as a generic placeholder for accounts that did not earn interest. However, the SavingsAccount class needed a more specific calculation for interest accumulation.

To cover all superclass methods they must be neither static nor private. It is a good idea to add an @Override annotation in front of the overridden methods (intellij adds it automatically). The annotation has no effect on the behavior of the program - it is merely a hint to the compiler that the programmer intended the method to be overridden. If the compiler discovers that there is no method with the same signature(name and parameter) in the superclass, it will refuse to compile the code.

Exercise 2: Checking account

A checking account is typically used for everyday transactions and often involves fees for specific operations, such as withdrawals. In this exercise, the task is to extend the functionality of the BankAccount class by creating a subclass that models a checking account, which includes the concept of a transaction fee for each withdrawal. (for example 2.00-10.00)

The CheckingAccount class introduces a private field to store the transaction fee associated with withdrawals
(double transactionFee). Its constructor takes three parameters: an account number, an initial balance, and a transaction fee. It then calls the parent class’s constructor to initialize the common attributes (account number and balance) and sets the transaction fee for the checking account.

Note that although CheckingAccount uses the parent class’s constructor to initialize its inherited fields, balance and accountNumber are still declared as private in the BankAccount class. This means they are not directly accessible in the CheckingAccount subclass. Even though the subclass uses these fields, it does not own them directly. That is why, in order to access or modify these fields in CheckingAccount, it is necessary to use the getters and setters provided by the BankAccount class.

Furthermore, it is necessary to override the withdraw method inherited from BankAccount. Within this overridden method, the total amount deducted is calculated by adding the withdrawal amount and the transaction fee (withdraw + fee).

That's where we need getters and setters, the method checks that the withdrawal amount is positive and more than 0 and that the total amount (withdrawal + fee) does not exceed the available balance. If these conditions are met, the balance is reduced by the total amount, and a message is displayed showing the withdrawn amount, the fee charged and updated balance (print). If the conditions are not met, an error message is printed indicating an invalid withdrawal or insufficient funds. Do not forget @Override annotation this annotation ensures that the method is correctly overriding the method from the superclass, and it helps the compiler catch any mistakes in the method signature.

Definition

Dynamic Binding

What happens if we call a method on an object, but that method is defined only in a parent class — or also in both the parent and the child class?

This is where dynamic binding comes in.

Dynamic binding means that Java decides at runtime which version of a method to run — based on the actual class of the object, not just the type of the variable.

If the method is overridden in the child class, that version is used. If it's not overridden, Java searches up the inheritance chain until it finds the method.

Compile-time = When a code is being translated into bytecode by the Java compiler
Runtime = When the compiled program is actually being executed by the Java

 
class Animal {
    public String speak() {
        return "Some generic animal sound";
    }
}
class Cat extends Animal {
    // No speak() method here
}
class Kitten extends Cat {
    // Also no speak() method here
}
public class Main {
    public static void main(String[] args) {
        Kitten myPet = new Kitten();
        System.out.println(myPet.speak()); // Output: Some generic animal sound
    }
}

*Note that a subclass commonly overrides a method, but sometimes the parent version of the method is still needed. In such cases, the parent method can be called using super.methodName().

Exercise 3: Completing banking system

The objective of this exercise is to extend the functionality of the BankAccount class and its subclasses by introducing additional methods and overriding existing ones to provide more meaningful output. The modifications were centered around two primary enhancements: adding a deposit method and overriding the toString() method in order to return human-readable representations of the objects.

  • In the BankAccount class, a void deposit() method needs to be added. This method accepts a monetary amount, verifies that the amount is positive, and then adds it to the balance variable. If the deposit is valid, it prints a message indicating the amount deposited; otherwise, it displays an error message. This method reinforces proper handling of monetary transactions by incorporating a simple validation check before modifying the state of the account.
  • Additionally, the BankAccount class should include an override of the Object class’s toString() method. The purpose of this override is to return a concise summary of the account’s details. Specifically, it returns a String containing the account number and the current balance in a format similar to "Account Number: [accountNumber], Balance: [balance]". This approach ensures that when an instance of BankAccount is printed, it displays meaningful information rather than the default output provided by the Object class.
  • The CheckingAccount subclass extends BankAccount and introduces a new field to store a transaction fee. In this subclass, the toString() method should also be overridden. The customized toString() method in CheckingAccount returns a string that begins with "Checking", followed by the base account information (as provided by the superclass’s toString() method, so use Dynamic Binding and method from the parent class) and concludes with the transaction fee details, for example: "Checking Account Number: [accountNumber], Balance: [balance], Transaction Fee: [transactionFee]". This allows users to clearly distinguish checking accounts and view additional fee-related information.
  • Similarly, the SavingsAccount subclass extends BankAccount by adding a field for the annual interest rate
    (double annualInterestRate). Its constructor also calls the superclass constructor to initialize common attributes and then sets the interest rate. An additional method, applyMonthlyInterest(), should be implemented in SavingsAccount. This method calculates the monthly interest (using a method calculateMonthlyInterest()), which performs the appropriate arithmetic based on the annual interest rate and the current balance), deposits the interest into the account by reusing the deposit method, and prints a message indicating the interest applied. The toString() method in SavingsAccount is also should be overridden to return a string that starts with "Savings", followed by the standard account details (Dynamic Binding) and the annual interest rate, for example: "Savings Account Number: [accountNumber], Balance: [balance], Annual Interest Rate: [annualInterestRate]". This enables a clear and informative output that distinguishes savings accounts from other account types.

To sum up the task is to:

  • Add deposit(…) method to the BankAccount class
  • Add toString() method to:
    • BankAccount class
    • SavingsAccount class
    • CheckingAccount class
  • Add applyMonthlyInterest() method to the SavingsAccount class

Definition

Abstract class

Official Java documentation definition

An abstract class in Java serves as a blueprintfor other classes. It provides a foundation by defining common properties and behaviors that related classes share, yet it cannot be instantiated on its own. Essentially, an abstract class can include implemented methods and state (fields), as well as declare abstract methods—methods without an implementation—that its subclasses must complete. This makes it ideal when there is a strong “is-a” relationship among classes. For example, consider a generic BankAccount class that contains shared attributes like account number and balance along with methods for depositing funds, then specific account types such as SavingsAccount or CheckingAccount can extend this abstract class to add their unique behavior.

In contrast, an interface defines a contract by listing method signatures that implementing classes must provide. Interfaces typically do not contain any state or complex behavior, though they may include default methods with implementations in modern Java. They are used to specify common capabilities that can be adopted by any class, even if those classes do not share a common ancestry. For instance, an interface called Controllable might declare methods such as turnOn() and turnOff() that various electronic devices (like televisions, radios, or computers) would implement.

In terms of design and usage, abstract classes are best suited for situations where a group of classes share significant commonalities in both behavior and state—like different types of bank accounts that all inherit from a BankAccount. On the other hand, interfaces are appropriate when ensuring that diverse classes implement a specific set of methods, regardless of their position in the class hierarchy.

“is-a” - means relation ship between classes for example Dog “is-a” animal

Exercise 4: Working with Abstract Classes and Employee Salary Calculations

This exercise focuses on abstract classes, so the task is to creat an abstract class called Employee that will serve as a blueprint for different types of employees in a company. The Employee class should include common fields such as the employee’s name, the hire date (using Java's built-in LocalDate), and the base salary which is a year salary (name, hireDate, baseSalary) and a constructor with these fields. Implement a non-abstract method int getTenure() which returns int value that represents period in years between hire and todays date has to be created and getter for the baseSalary.

  • Hint: use Period.between() and getYears();

Employee should also declare an abstract method named double calculateMonthlySalary() which will be implemented by its subclasses.

Two subclasses: FullTimeEmployee and PartTimeEmployee will then be created. In the FullTimeEmployee class, the monthly salary should be computed by taking the annual base salary and adding a bonus. To accomplish this, first a new field must be added to the constructor double bonusPercentage. The method calculateMonthlySalary() should work like this:

  • It finds tenure using mehod in the parent class
  • It calculates the month bonus: the product of annual salary, bonus percentage and tenure divided by 12
  • Returns sum of annual salary devided by 12 and bonus

In the PartTimeEmployee class, assume that the monthly salary is determined solely by multiplying the number of hours worked in a month by an hourly rate. That means it is necessary to add 2 fields to the constructor: double hourlyRate and int hoursWorked;

Main abstract class should override the toString() method so that it returns a well-formatted string displaying the employee’s name, hire date, base salary, and the calculated monthly salary. This method will help in easily printing and reviewing the details of each employee.

 
//End resault example:

import java.time.LocalDate;
public class EmployeeTest {
    public static void main(String[] args) {
        // Create a full-time employee with a hire date 5 years ago, a base salary, and a bonus percentage.
        FullTimeEmployee fullTime = new FullTimeEmployee("Alice", LocalDate.now().minusYears(5), 60000, 0.02);

        // Create a part-time employee with a hire date 2 years ago, an hourly rate, and a fixed number of hours worked in a month.
        PartTimeEmployee partTime = new PartTimeEmployee("Bob", LocalDate.now().minusYears(2), 0, 10, 80);

        // Print employee details using the overridden toString() method.
        System.out.println(fullTime);
        System.out.println(partTime);
    }
}


Output:
Name: Alice, Hire Date: 2020-03-23, Base Salary: 60000.0, Monthly Salary: 5500.0
Name: Bob, Hire Date: 2023-03-23, Base Salary: 0.0, Monthly Salary: 800.0

Process finished with exit code 0

Please leave your feedback on the materials

  • Institute of Computer Science
  • Faculty of Science and Technology
  • University of Tartu
In case of technical problems or questions write to:

Contact the course organizers with the organizational and course content questions.
The proprietary copyrights of educational materials belong to the University of Tartu. The use of educational materials is permitted for the purposes and under the conditions provided for in the copyright law for the free use of a work. When using educational materials, the user is obligated to give credit to the author of the educational materials.
The use of educational materials for other purposes is allowed only with the prior written consent of the University of Tartu.
Terms of use for the Courses environment