Object-Oriented Programming

This is a printer-friendly version. It omits exercises, optional topics (i.e., four-star topics), and other extra content such as learning outcomes.

Introduction

What :

Object-Oriented Programming (OOP) is a programming paradigm. A programming paradigm guides programmers to analyze programming problems, and structure programming solutions, in a specific way.

Programming languages have traditionally divided the world into two parts—data and operations on data. Data is static and immutable, except as the operations may change it. The procedures and functions that operate on data have no lasting state of their own; they’re useful only in their ability to affect data.

This division is, of course, grounded in the way computers work, so it’s not one that you can easily ignore or push aside. Like the equally pervasive distinctions between matter and energy and between nouns and verbs, it forms the background against which we work. At some point, all programmers—even object-oriented programmers—must lay out the data structures that their programs will use and define the functions that will act on the data.

With a procedural programming language like C, that’s about all there is to it. The language may offer various kinds of support for organizing data and functions, but it won’t divide the world any differently. Functions and data structures are the basic elements of design.

Object-oriented programming doesn’t so much dispute this view of the world as restructure it at a higher level. It groups operations and data into modular units called objects and lets you combine objects into structured networks to form a complete program. In an object-oriented programming language, objects and object interactions are the basic elements of design.

-- Object-Oriented Programming with Objective-C, Apple

Some other examples of programming paradigms are:

Paradigm Programming Languages
Procedural Programming paradigm C
Functional Programming paradigm F#, Haskel, Scala
Logic Programming paradigm Prolog

Some programming languages support multiple paradigms.

Java is primarily an OOP language but it supports limited forms of functional programming and it can be used to (although not recommended) write procedural code.  e.g. se-edu/addressbook-level1

JavaScript and Python support functional, procedural, and OOP programming.

Objects

What :

Every object has both state (data) and behavior (operations on data). In that, they’re not much different from ordinary physical objects. It’s easy to see how a mechanical device, such as a pocket watch or a piano, embodies both state and behavior. But almost anything that’s designed to do a job does, too. Even simple things with no moving parts such as an ordinary bottle combine state (how full the bottle is, whether or not it’s open, how warm its contents are) with behavior (the ability to dispense its contents at various flow rates, to be opened or closed, to withstand high or low temperatures).

It’s this resemblance to real things that gives objects much of their power and appeal. They can not only model components of real systems, but equally as well fulfill assigned roles as components in software systems.

-- Object-Oriented Programming with Objective-C, Apple

Object Oriented Programming (OOP) views the world as a network of interacting objects.

A real world scenario viewed as a network of interacting objects:

You are asked to find out the average age of a group of people Adam, Beth, Charlie, and Daisy. You take a piece of paper and pen, go to each person, ask for their age, and note it down. After collecting the age of all four, you enter it into a calculator to find the total. And then, use the same calculator to divide the total by four, to get the average age. This can be viewed as the objects You, Pen, Paper, Calculator, Adam, Beth, Charlie, and Daisy interacting to accomplish the end result of calculating the average age of the four persons. These objects can be considered as connected in a certain network of certain structure.

OOP solutions try to create a similar object network inside the computer’s memory – a sort of a virtual simulation of the corresponding real world scenario – so that a similar result can be achieved programmatically.

OOP does not demand that the virtual world object network follow the real world exactly.

Our previous example can be tweaked a bit as follows:

  • Use an object called Main to represent your role in the scenario.
  • As there is no physical writing involved, we can replace the Pen and Paper with an object called AgeList that is able to keep a list of ages.

Every object has both state (data) and behavior (operations on data).

Object Real World? Virtual World? Example of State (i.e. Data) Examples of Behavior (i.e. Operations)
Adam Name, Date of Birth Calculate age based on birthday
Pen - Ink color, Amount of ink remaining Write
AgeList - Recorded ages Give the number of entries, Accept an entry to record
Calculator Numbers already entered Calculate the sum, divide
You/Main Average age, Sum of ages Use other objects to calculate

Every object has an interface and an implementation.

Every real world object has an interface that other objects can interact with and an implementation that supports the interface but may not be accessible to the other object.

The interface and implementation of some real-world objects in our example:

  • Calculator: the buttons and the display are part of the interface; circuits are part of the implementation.
  • Adam: In the context of our 'calculate average age' example, the interface of Adam consists of requests that adam will respond to, e.g. "Give age to the nearest year, as at Jan 1st of this year" "State your name"; the implementation includes the mental calculation Adam uses to calculate the age which is not visible to other objects.

Similarly, every object in the virtual world has an interface and an implementation.

The interface and implementation of some virtual-world objects in our example:

  • Adam: the interface might have a method getAge(Date asAt); the implementation of that method is not visible to other objects.

Objects interact by sending messages.

Both real world and virtual world object interactions can be viewed as objects sending message to each other. The message can result in the sender object receiving a response and/or the receiving object’s state being changed. Furthermore, the result can vary based on which object received the message, even if the message is identical (see rows 1 and 2 in the example below).

Examples:

World Sender Receiver Message Response State Change
Real You Adam "What is your name?" "Adam" -
Real as above Beth as above "Beth" -
Real You Pen Put nib on paper and apply pressure Makes a mark on your paper Ink level goes down
Virtual Main Calculator (current total is 50) add(int i): int i = 23 73 total = total + 23

Objects as Abstractions :

The concept of Objects in OOP is an abstraction mechanism because it allows us to abstract away the lower level details and work with bigger granularity entities i.e. ignore details of data formats and the method implementation details and work at the level of objects.

We can deal with a Person object that represents the person Adam and query the object for Adam's age instead of dealing with details such as Adam’s date of birth (DoB), in what format the DoB is stored, the algorithm used to calculate the age from the DoB, etc.

Encapsulation Of Objects :

Encapsulation protects an implementation from unintended actions and from inadvertent access.
-- Object-Oriented Programming with Objective-C, Apple

An object is an encapsulation of some data and related behavior in two aspects:

1. The packaging aspect: An object packages data and related behavior together into one self-contained unit.

2. The information hiding aspect: The data in an object is hidden from the outside world and are only accessible using the object's interface.

Classes

What :

Writing an OOP program is essentially writing instructions that the computer uses to,

  1. create the virtual world of object network, and
  2. provide it the inputs to produce the outcome we want.

A class contains instructions for creating a specific kind of objects. It turns out sometimes multiple objects have the same behavior because they are of the same kind. Instructions for creating a one kind (or ‘class’) of objects can be done in one go and use that same instructions to instantiate (i.e. create) objects of that kind. We call such instructions a Class.

Classes and objects in an example scenario

When writing an OOP program to calculate the average age of Adam, Beth, Charlie, and Daisy, instructions for creating objects Adam, Beth, Charlie, and Daisy will be very similar because they are all of the same kind : they all represent ‘persons’ with the same interface, the same kind of data (i.e. name, DoB, etc.), and the same kind of behavior (i.e. getAge(Date), getName(), etc.). Therefore, we can have a class called Person containing instructions on how to create Person objects and use that class to instantiate objects Adam, Beth, Charlie, and Daisy. Similarly, we need classes AgeList, Calculator, and Main classes to instantiate one each of AgeList, Calculator, and Main objects.

Implementing Classes

Given below are some resources on how to implement classes:

Class Level Members :

While all objects of a class has the same attributes, each object has its own copy of the attribute value.

All Person objects have the Name attribute but the value of that attribute varies between Person objects.

However, some attributes are not suitable to be maintained by individual objects. Instead, they should be maintained centrally, shared by all objects of the class. They are like ‘global variables’ but attached to a specific class. Such variables whose value is shared by all instances of a class are called class level attributes.

The attribute totalPersons should be maintained centrally and shared by all Person objects rather than copied at each Person object.

Similarly, when a normal method is being called, a message is being sent to the receiving object and the result may depend on the receiving object.

Sending the getName() message to Adam object results in the response "Adam" while sending the same message to the Beth object gets the response "Beth".

However, there can be methods related to a specific class but not suitable for sending message to a specific object of that class. Such methods that are called using the class instead of a specific instance are called class-level methods.

The method getTotalPersons() is not suitable to send to a specific Person object because a specific object of the Person class should not know about the total number of Person objects.

Class-level attributes and methods are collectively called class-level members (also called static members sometimes because some programming languages use the keyword static to identify class-level members). They are to be accessed using the class name rather than an instance of the class.

Implementing class-level members

Some resources to learn how to implement class-level members:

  • Python Programming Basics: OOP: Class-Level Members

  • Enumerations :

    An Enumeration is a fixed set of values that can be considered as a data type.

    An enumeration is often useful when using a regular data type such as int or String would allow invalid values to be assigned to a variable. e.g. if a variable can only take values 0 and 1, declaring it as an int would allow invalid values such as 2 to be assigned to it. But if you define an enumeration called Binary that has values 0 and 1 only, a variable of type Binary will never be assigned an invalid value such as 2 because the compiler is able to catch the error.

    Priority can be considered an enumeration because it has only three values.

    Priority: HIGH, MEDIUM, LOW

    Associations

    What

    Objects in an OO solution need to be connected to each other to form a network so that they can interact with each other. Such connections between objects are called associations.

    Suppose an OOP program for managing a learning management system creates an object structure to represent the related objects. In that object structure we can expect to have associations between aCourse object that represents a specific course and Student objects that represents students taking that course.

    Associations in an object structure can change over time.

    To continue the previous example, the associations between a Course object and Student objects can change as students enroll in the module or drop the module over time.

    Associations among objects can be generalized as associations between the corresponding classles too.

    In our example, as some Course objects can have associations with some Student objects, we can view it as an association between the Course class and the Student class.

    Implementing associations

    We use instance level variables to implement associations.

    When two classes are linked by an association, it does not necessarily mean both classes know about each other. The concept of which class in the association knows about the other class is called navigability.

    Multiplicity

    Multiplicity is the aspect of an OOP solution that dictates how many objects take part in each association.

    The navigability of the association between Course objects and Student objects tells you how many Course objects can be associated with one Student object and vice versa.

    Implementing multiplicity

    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.

    In the code below, the Logic class has a variable that can hold 0..1 i.e., zero or one Minefield objects.

    Java

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

      

    Python

    class Logic:
      
      def __init__(self, minefield):
        self.minefield = minefield
        
      # ...
    
    
    class Minefield:
      # ...
    

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

    In the code below, the Logic class will always have a ConfigGenerator object, provided the variable is not set to null at some point.

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

    class Logic:
      
      def __init__(self):
        self.config_gen = ConfigGenerator()
    

    Bi-directional associations require matching variables in both classes.

    In the code below, the Foo class has a variable to hold a Bar object and vice versa i.e., each object can have an association with an object of the other type.

    Java

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

      

    Python

    class Foo:
      
      def __init__(self, bar):
        self.bar = bar;
    
    
    class Bar:
      
      def __init__(self, foo):
        self.foo = foo;
        
    

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

    Java

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

      

    Python

    class Minefield:
      
      def __init__(self):
        self.cells = {1:[], 2:[], 3:[]}
    

    Dependencies

    Dependencies are objects that are not directly linked in the object network can still interact with each other. These are a weaker form of associations we call dependencies.

    A Course object can have a dependency on a Registrar object to obtain the maximum number of students it can support.

    Implementing dependencies

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

    In the code below, Foo has a dependency on Bar but it is not an association because it is only a transient interaction and there is no long term relationship between a Foo object and a Bar object. i.e. the Foo object does not keep the Bar object it receives as a parameter.

    Java

    class Foo{
        
        int calculate(Bar bar){
            return bar.getValue();
        }
    }
    
    class Bar{
        int value;
        
        int getValue(){
            return value;
        }
    }
    

      

    Python

    class Foo:
        
        def calculate(self, bar):
            return bar.value;
    
    class Bar:
        
        def __init__(self, value):
          self.value = value
    

    The code above results in a dependency from Foo class to the Bar class.

    Composition

    A composition is an association that represents a strong whole-part relationship. When the whole is destroyed, parts are destroyed too.

    A Board (used for playing board games) consists of Square objects.

    Composition also implies that there cannot be cyclical links.

    The ‘sub-folder’ association between Folder objects is a composition type association. That means if the Folder object foo is a sub folder of Folder object bar, bar cannot be a sub-folder of foo.

    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.

    In the code below, the Email has a composition type relationship with the Subject class, in the sense that the subject is part of the email.

    Java

    class Email {
        private Subject subject;
      ...
    }
    

      

    Python

    class Email:
      
      def __init__(self):
        self.__subject = Subject()
    

    Aggregation

    Aggregation represents a container-contained relationship. It is a weaker relationship than composition.

    SportsClub acts as a container for Person objects. Person objects can survive without a SportsClub object.

    Implementing aggregation

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

    In the code below, there is an aggregation association between the Team class and the Person in that a Team contains Person a object who is the leader of the team.

    Java

    class Team {
        Person leader;
        ...
        void drive(Person p) {
            leader = p;
        }
    }
    

      

    Python

    class Team:
      
      def __init__(self):
        self.__leader = None
        
      def drive(self, person):
        self.__leader = person
    

    Inheritance

    What :

    The OOP concept Inheritance allows you to define a new class based on an existing class.

    For example, you can use inheritance to define an EvaluationReport class based on an existing Report class so that the EvaluationReport class does not have to duplicate code that is already implemented in the Report class. The EvaluationReport can inherit the wordCount attribute and the print() method from the base class Report.

    • Other names for Base class: Parent class, Super class
    • Other names for Derived class: Child class, Sub class, Extended class

    A super class is said to be more general than the sub class. Conversely, a sub class is said to be more specialized than the super class.

    Applying inheritance on a group of similar classes can result in the common parts among classes being extracted into more general classes.

    Man and Woman behaves the same way for certain things. However, the two classes cannot be simply replaced with a more general class Person because of the need to distinguish between Man and Woman for certain other things. A solution is to add the Person class as a super class (to contain the code common to men and woment) and let Man and Woman inherit from Person class.

    Inheritance implies the derived class can be considered as a sub-type of the base class (and the base class is a super-type of the derived class), resulting in an is a relationship.

    Inheritance does not necessarily mean a sub-type relationship exists. However, the two often go hand-in-hand. For simplicity, at this point let us assume inheritance implies a sub-type relationship.

    To continue the previous example,

    • Woman is a Person
    • Man is a Person

    Inheritance relationships through a chain of classes can result in inheritance hierarchies (aka inheritance trees).

    Two inheritance hierarchies/trees are given below. Note that Parrot is a Bird as well as it is an Animal. Note that the triangle points to the parent class.

    Multiple Inheritance is when a class inherits directly from multiple classes. Multiple inheritance among classes is allowed in some languages (e.g., Python, C++) but not in other languages (e.g., Java, C#).

    The Honey class inherits from the Food class and the Medicine class because honey can be consumed as a food as well as a medicine (in some oriental medicine practices).

    Implementing inheritance

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

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


    💡 Python automatically assigns the object class as the parent class.


    Overriding :

    Method overriding is when a sub-class changes the behavior inherited from the parent class by re-implementing the method. Overridden methods have the same name, same type signature, and same return type.

    In a case where EvaluationReport class inherits the Report class,

    • Report#print() method is overridden by EvaluationReport#print() method.
    • Report#write(String) method is overridden by EvaluationReport#write(String) method.
    • Report#read():String method is NOT overridden by EvaluationReport#read(int):String method.  Reason: the two methods have different signatures; EvaluationReport#read(int):String overloads (rather than overrides) the Report#read():String method.

    Design → Object Oriented Programming → Inheritance →

    Overloading

    Method overloading is when there are multiple methods with the same name but different type signatures. Overloading is used to indicate that multiple operations do similar things but take different parameters.

    Type Signature: The type signature of an operation is the type sequence of the parameters. The return type and parameter names are not part of the type signature. However, the parameter order is significant.

    Method Type Signature
    int add(int X, int Y) (int, int)
    void add(int A, int B) (int, int)
    void m(int X, double Y) (int, double)
    void m(double X, int Y) (double, int)

    In the case below, the calculate method is overloaded because the two methods have the same name but different type signatures (String) and (int)

    • calculate(String): void
    • calculate(int): void
    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){
            ...
        }
    }
    

    Resources:

    ❗️ Python does not have direct support for method overloading.

    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"
        }
    }
    

    Resources:

    In the example below, Student class overrides the print_info method of its parent class Person:

    class Person:
      def __init__(self, name):
        self.name = name
        
      def print_info(self):
        print('My name is', self.name)
        
    

      

    class Student(Person):
      def __init__(self, name, matric):
        self.name = name
        self.matric = matric
        
      def print_info(self):
        print(self.name, 'is a student')
    

    amy = Person('Amy')
    amy.print_info()
    
    ben = Student('Ben', 'A12345')
    ben.print_info()
    

     → 

    My name is Amy
    Ben is a student
    

    Overloading :

    Method overloading is when there are multiple methods with the same name but different type signatures. Overloading is used to indicate that multiple operations do similar things but take different parameters.

    Type Signature: The type signature of an operation is the type sequence of the parameters. The return type and parameter names are not part of the type signature. However, the parameter order is significant.

    Method Type Signature
    int add(int X, int Y) (int, int)
    void add(int A, int B) (int, int)
    void m(int X, double Y) (int, double)
    void m(double X, int Y) (double, int)

    In the case below, the calculate method is overloaded because the two methods have the same name but different type signatures (String) and (int)

    • calculate(String): void
    • calculate(int): void
    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){
            ...
        }
    }
    

    Resources:

    ❗️ Python does not have direct support for method overloading.

    Interfaces :

    An interface is a behavior specification i.e. a collection of method specifications. If a class implements the interface, it means the class is able to support the behaviors specified by the said interface.

    There are a number of situations in software engineering when it is important for disparate groups of programmers to agree to a "contract" that spells out how their software interacts. Each group should be able to write their code without any knowledge of how the other group's code is written. Generally speaking, interfaces are such contracts. --Oracle Docs on Java

    Suppose SalariedStaff is an interface that contains two methods setSalary(int) and getSalary(). AcademicStaff can declare itself as implementing the SalariedStaff interface, which means the AcademicStaff class must implement all the methods specified by the SalariedStaff interface i.e., setSalary(int) and getSalary().

    A class implementing an interface results in an is-a relationship, just like in class inheritance.

    In the example above, AcademicStaff is a SalariedStaff. An AcademicStaff object can be used anywhere a SalariedStaff object is expected e.g. SalariedStaff ss = new AcademicStaff().

    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. In case you are not familiar with UML notation used: solid lines indicate normal inheritance; dashed lines indicate interface inheritance; the triangle points to pare parent.

    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() {
            //...
        }
    }
    

    Abstract Classes :

    Abstract Class:

    An abstract class is a class that is declared abstract—it may or may not include abstract methods. Abstract classes cannot be instantiated, but they can be subclassed. -- Oracle's Java Documentation

    An abstract method is simply the method interface without the implementation.

    The Account class has an abstract method (addInterest()).

    A class that has an abstract method becomes an abstract class. Evan a class that does not have any abstract method can be declared as an abstract class. Abstract classes cannot be instantiated.

    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. The addInterest() method in the Account class is abstract and therefore the Account class itself is abstract. The CurrentAccount class need not be abstract because it overrides the abstract method inherited from the parent class.

    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'.

    Substitutability :

    Every instance of a subclass is an instance of the superclass, but not vice-versa. As a result, inheritance allows substitutability : the ability to substitute a child class object where a parent class object is expected.

    an Academic is an instance of a Staff, but a Staff is not necessarily an instance of an Academic. i.e. wherever an object of the superclass is expected, it can be substituted by an object of any of its subclasses.

    The following code is valid because an AcademicStaff object is substitutable as a Staff object.

    Staff staff = new AcademicStaff (); // OK
    

    But the following code is not valid  because staff is declared as a Staff type and therefore its value may or may not be of type AcademicStaff, which is the type expected by variable academicStaff.

    Staff staff;
    ...
    AcademicStaff academicStaff = staff; // Not OK
    

    Dynamic and Static Binding :

    Dynamic Binding ( aka late binding) : a mechanism where method calls in code are resolved at runtime, rather than at compile time.

    Overridden methods are resolved using dynamic binding, and therefore resolves to the implementation in the actual type of the object.

    Design → Object Oriented Programming → Inheritance →

    Overriding

    Method overriding is when a sub-class changes the behavior inherited from the parent class by re-implementing the method. Overridden methods have the same name, same type signature, and same return type.

    In a case where EvaluationReport class inherits the Report class,

    • Report#print() method is overridden by EvaluationReport#print() method.
    • Report#write(String) method is overridden by EvaluationReport#write(String) method.
    • Report#read():String method is NOT overridden by EvaluationReport#read(int):String method.  Reason: the two methods have different signatures; EvaluationReport#read(int):String overloads (rather than overrides) the Report#read():String method.

    Design → Object Oriented Programming → Inheritance →

    Overloading

    Method overloading is when there are multiple methods with the same name but different type signatures. Overloading is used to indicate that multiple operations do similar things but take different parameters.

    Type Signature: The type signature of an operation is the type sequence of the parameters. The return type and parameter names are not part of the type signature. However, the parameter order is significant.

    Method Type Signature
    int add(int X, int Y) (int, int)
    void add(int A, int B) (int, int)
    void m(int X, double Y) (int, double)
    void m(double X, int Y) (double, int)

    In the case below, the calculate method is overloaded because the two methods have the same name but different type signatures (String) and (int)

    • calculate(String): void
    • calculate(int): void
    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){
            ...
        }
    }
    

    Resources:

    ❗️ Python does not have direct support for method overloading.

    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"
        }
    }
    

    Resources:

    In the example below, Student class overrides the print_info method of its parent class Person:

    class Person:
      def __init__(self, name):
        self.name = name
        
      def print_info(self):
        print('My name is', self.name)
        
    

      

    class Student(Person):
      def __init__(self, name, matric):
        self.name = name
        self.matric = matric
        
      def print_info(self):
        print(self.name, 'is a student')
    

    amy = Person('Amy')
    amy.print_info()
    
    ben = Student('Ben', 'A12345')
    ben.print_info()
    

     → 

    My name is Amy
    Ben is a student
    

    Which of these methods override another method? A is the parent class. B inherits A.

    • a
    • b
    • c
    • d
    • e

    d

    Explanation: Method overriding requires a method in a child class to use the same method name and same parameter sequence used by one of its ancestors

    Consider the code below. The declared type of s is Staff and it appears as if the adjustSalary(int) operation of the Staff class is invoked.

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

    However, at runtime s can receive an object of any sub class of Staff. That means the adjustSalary(int) operation of the actual subclass object will be called. If the subclass does not override that operation, the operation defined in the superclass (in this case, Staff class) will be called.

    Static binding (aka early binding): When a method call is resolved at compile time.

    In contrast, overloaded methods are resolved using static binding.

    Design → Object Oriented Programming → Inheritance →

    Overloading

    Method overloading is when there are multiple methods with the same name but different type signatures. Overloading is used to indicate that multiple operations do similar things but take different parameters.

    Type Signature: The type signature of an operation is the type sequence of the parameters. The return type and parameter names are not part of the type signature. However, the parameter order is significant.

    Method Type Signature
    int add(int X, int Y) (int, int)
    void add(int A, int B) (int, int)
    void m(int X, double Y) (int, double)
    void m(double X, int Y) (double, int)

    In the case below, the calculate method is overloaded because the two methods have the same name but different type signatures (String) and (int)

    • calculate(String): void
    • calculate(int): void
    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){
            ...
        }
    }
    

    Resources:

    ❗️ Python does not have direct support for method overloading.

    Note how the constructor is overloaded in the class below. The method call new Account() is bound to the first constructor at compile time.

    class Account {
        Account () {
            // Signature: ()
            ...
        }
        Account (String name, String number, double balance) {
            // Signature: (String, String, double)
            ...
        }
    }
    

    Similarly, the calcuateGrade method is overloaded in the code below and a method call calculateGrade("A1213232") is bound to the second implementation, at compile time.

    void calculateGrade (int[] averages) { ... }
    void calculateGrade (String matric) { ... }
    

    Polymorphism

    What :

    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.

    Here is the implementation of the adjustSalary(int) operation.

    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.

    How :

    Three concepts combine to achieve polymorphism: substitutability, operation overriding, and dynamic binding.

    • Substitutability: Because of substitutability, you can write code that expects object of a parent class and yet use that code with objects of child classes. That is how polymorphism is able to treat objects of different types as one type.
    • Overriding: To get polymorphic behavior from an operation, the operation in the superclass needs to be overridden in each of the subclasses. That is how overriding allows objects of different sub classes to display different behaviors in response to the same method call.
    • Dynamic binding: Calls to overridden methods are bound to the implementation of the actual object's class dynamically during the runtime. That is how the polymorphic code can call the method of the parent class and yet execute the implementation of the child class.
    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 →

    What

    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.

    Here is the implementation of the adjustSalary(int) operation.

    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);
            }
        }
    }
    

    More

    Miscellaneous

    What’s the difference between a Class, an Abstract Class, and an Interface?

    • An interface is a behavior specification with no implementation.
    • A class is a behavior specification + implementation.
    • An abstract class is a behavior specification + a possibly incomplete implementation.

    How does overriding differ from overloading?

    Overloading is used to indicate that multiple operations do similar things but take different parameters. Overloaded methods have the same method name but different method signatures and possibly different return types.

    Overriding is when a sub-class redefines an operation using the same method name and the same type signature. Overridden methods have the same name, same method signature, and same return type.