OOP - Implementation

Implementing Classes

Given below is a tutorial you can refer to (from Oracle’s official Java tutorials) to learn how to implement java classes, in the unlikely case you don't know how to do that already.

Implementing Class-Level Members

You can refer to Oracle’s official Java tutorial on class-level members to learn how to implement Java class-level members, in the unlikely case you don't know how to do that already.

Implementing Associations

We use instance level variables to implement associations.

A normal instance-level variable gives us a 0..1 multiplicity (also called optional associations) because a variable can hold a reference to a single object or null.

class Logic {
    Minefield minefield;
    ...
}
class Minefield {
    ...
}

A variable can be used to implement a 1 multiplicity too (also called compulsory associations).

class Logic {
    ConfigGenerator cg = new ConfigGenerator();
    ...
}

Bi-directional associations require matching variables in both classes.

class Foo {
    Bar bar;
    ...
}
class Bar {
    Foo foo;
    ...
}

To implement other multiplicities, choose a suitable data structure such as Arrays, ArrayLists, HashMaps, Sets, etc.

class Minefield {
    Cell[][] cell;
    ...
}

Implementing Dependencies

Dependencies result from interactions between objects that do not result in a long-term link between the said objects.

Example:

class TaxProcessor{
    double rate;

    void addTax(Taxable t){
        t.addTax(rate);
    }
}

The code above results in this dependency.

The code does not indicate an association between the two classes because the TaxProcessor object does not keep the Taxable object (i.e. it’s only a short-term interaction)

Implementing Composition

Composition too is implemented using a normal variable. If correctly implemented, the ‘part’ object will be deleted when the ‘whole’ object is deleted. Ideally, the ‘part’ object may not even be visible to clients of the ‘whole’ object.

Example:

class Car {
    private Engine engine;
  ...
}

Implementing Aggregation

Implementation is similar to that of composition except the containee object can exist even after the container object is deleted.

📦 Example:

class Car {
    Person driver;
    ...
    void drive(Person p) {
        driver = p;
    }
}

Implementing Association Classes

Note that while a special notation is used to indicate an association class, there is no special way to implement an association class.

Example:

At implementation level, an association class is most likely implemented as follows.

Implementing Inheritance

To learn how to implement inheritance in Java, you can follow [Oracle’s Java Tutorials: Inheritance]


A very beginner-friendly video about implementing Java inheritance.


Java requires all class to have a parent class. If you do not specify a parent class, Java automatically assigns the Object class as the parent class.

Implementing Overriding

To override a method inherited from an ancestor class, simply re-implement the method in the target class.

📦 A simple example where the Report#print() method is overridden by EvaluationReport#print() method:


class Report{

    void print(){
        System.out.println("Printing report");
    }

}

class EvaluationReport extends Report{

    @Override  // this annotation is optional
    void print(){
        System.out.println("Printing evaluation report");
    }

}

class ReportMain{

    public static void main(String[] args){
        Report report = new Report();
        report.print(); // prints "Printing report"

        EvaluationReport evaluationReport = new EvaluationReport();
        evaluationReport.print(); // prints "Printing evaluation report"
    }
}

Implementing Overloading

An operation can be overloaded inside the same class or in sub/super classes.

📦 The constructor of the Account class below is overloaded because there are two constructors with different signatures: () and (String, String, double). Furthermore, the save method in the Account class is overloaded in the child class SavingAccount.

class Account {
    Account () {
        ...
    }
    
    Account (String name, String number, double balance) {
        ...
    }
    
    void save(int amount){
        ...
    }
}

class SavingAccount extends Account{
    
    void save(Double amount){
        ...
    }
}

Implementing Interfaces

Java allows multiple inheritance among interfaces. A Java class can implement multiple interfaces (and inherit from one class).

📦 The design below is allowed by Java.

  1. Staff interface inherits (note the solid lines) the interfaces TaxPayer and Citizen.
  2. TA class implements both Student interface and the Staff interface.
  3. Because of point 1 above, TA class has to implement all methods in the interfaces TaxPayer and Citizen.
  4. Because of points 1,2,3, a TA is a Staff, is a TaxPayer and is a Citizen.

Java uses the interface keyword to declare interfaces and implements keyword to indicate that a class implements a given interface. Inheritance among interfaces uses the extends keyword, just like inheritance among classes.

📦 The code for the example design given in the previous example:

interface TaxPayer {
    void payTax();
}

interface Citizen {
    void vote();
}

interface Staff extends Citizen, TaxPayer{
    void work();
}

interface Student {
    void learn();
}

class TA implements Student, Staff{

    @Override
    public void payTax() {
        //...
    }

    @Override
    public void vote() {
        //...
    }

    @Override
    public void work() {
        //...
    }

    @Override
    public void learn() {
        //...
    }
}

Implementing Abstract Classes

Use the abstract keyword to identify abstract classes/methods.

📦 Partial code below gives an example of how to declare abstract classes/methods.

abstract class Account {
    
    int number;
    
    abstract void addInterest();
    
    void close(){
        //...
    }
}

class CurrentAccount extends Account{

    @Override
    void addInterest() {
        //...
    }
}

In Java, if a class contains an abstract method, the class itself should be an abstract class  i.e. if any methods of the class is 'incomplete', the class itself is 'incomplete'.

Implementing Polymorphism

We can use inheritance to achieve polymorphism.

📦 Continuing with the example given in [ 🎓 OOP → Polymorphism → Introduction ], given below is the minimum code for Staff, Admin, and Academic classes that achieves the desired polymorphism.

Design → Object Oriented Programming → Polymorphism →

Introduction

Polymorphism:

The ability of different objects to respond, each in its own way, to identical messages is called polymorphism. -- Object-Oriented Programming with Objective-C, Apple

Take the example of writing a payroll application for a university to facilitate payroll processing of university staff. Suppose an adjustSalary(int) operation adjusts the salaries of all staff members. This operation will be executed whenever the university initiates a salary adjustment for its staff. However, the adjustment formula is different for different staff categories, say admin and academic. Here is one possible way of designing the classes in the Payroll system.

📦 Calling adjustSalary() method for each Staff type:

Here is the implementation of the adjustSalary(int) operation from the above design.

class Payroll1 {
    ArrayList< Admin > admins;
    ArrayList< Academic > academics;
    // ...

    void adjustSalary(int byPercent) {
        for (Admin ad: admins) {
            ad.adjustSalary(byPercent);
        }
        for (Academic ac: academics) {
            ac.adjustSalary(byPercent);
        }
    }
}

Note how processing is similar for the two staff types. It is as if the type of staff members is irrelevant to how they are processed inside this operation! If that is the case, can the staff type be "abstracted away" from this method? Here is such an implementation of adjustSalary(int):

class Payroll2 {
    ArrayList< Staff > staff;
    // ...

    void adjustSalary(int byPercent) {
        for (Staff s: staff) {
            s.adjustSalary(byPercent);
        }
    }
}

Notice the following:

  • Only one data structure ArrayList< Staff >. It contains both Admin and Academic objects but treats them as Staff objects
  • Only one loop
  • Outcome of the s.adjustSalary(byPercent) method call depends on whether s is an Academic or Admin object

The above code is better in several ways:

  • It is shorter.
  • It is simpler.
  • It is more flexible (this code will remain the same even if more staff types are added).

This does not mean we are getting rid of the Academic and Admin classes completely and replacing them with a more general class called Staff. Rather, this part of the code “treats” both Admin and Academic objects as one type called Staff.

For example, ArrayList staff contains both Admin and Academic objects although it treats all of them as Staff objects. However, when the adjustSalary(int) operation of these objects is called, the resulting salary adjustment will be different for Admin objects and Academic objects. Therefore, different types of objects are treated as a single general type, but yet each type of object exhibits a different kind of behavior. This is called polymorphism (literally, it means “ability to take many forms”). In this example, an object that is perceived as type Staff can be an Admin object or an Academic object.

class Staff {
    String name;
    double salary;

    void adjustSalary(int percent) {
        // do nothing
    }
}

//------------------------------------------

class Admin extends Staff {

    @Override
    void adjustSalary(int percent) {
        salary = salary * percent;
    }
}

//------------------------------------------

class Academic extends Staff {

    @Override
    void adjustSalary(int percent) {
        salary = salary * percent * 2;
    }
}

//------------------------------------------

class Payroll {
    ArrayList< Staff > staff;
    // ...

    void adjustSalary(int byPercent) {
        for (Staff s: staff) {
            s.adjustSalary(byPercent);
        }
    }
}