Skip to content

OOP

Introduction

The concept of Object Oriented Programming (OOP) is related to programming languages that use objects. OOP introduces and combines concepts such as:

  • Inheritance
  • polymorphism
  • encapsulation
  • abstraction
  • composition

Inheritance

One of the most important pillars of OOP is class inheritance. This mechanism allows for sharing behavior between classes. A class can inherit from another class, which means that in addition to the existing functionality, it can define new behaviors and properties. Inheritance in Java is based on the extends keyword, which we place after the declared class name.

public class Car {

    public void turnOnEngine(){
        System.out.println("Turn on engine!");
    }
}

public class SportCar extends Car { // SportCar class inherits after Car class

    public void drive() {
        turnOnEngine();
        System.out.println("I'm driving!");
    }
}


Inheritance hierarchy on the Java platform

In Java, all classes inherit from the Object class in thejava.lang package. It contains common logic and implementation for each derived class that exists within the framework or is yet to be created.

Java Platform class hierachy

Derived class properties

Inheriting class:

  • inherits all base class methods and fields
  • can only extend one class at most
  • has access to all fields and methods that have been defined using the public andprotected access modifiers
  • has access to items with the default access modifier (package-private) if the base class is in the same package

Such a class can directly use the capabilities of the base class, ie without any references it can e.g. call methods to which it has access. Also, it can still "behave" like a regular [object] (../ javaBasics/classes_objects.md), i.e. it can define new methods, fields or constants.

In addition, it has the ability to override or extend the method from the base class, using appropriate annotations. In order to extend the capabilities of a constructor or base method, we need to use the super keyword, which allows us to call logic from the base class. Such a call, in the case of a constructor, looks like a method call, i.e. inside super () we can pass arguments. Their number and type are defined by the declaration in the base class, moreover, they must be used in the first line of the constructor's body. In turn, when calling a method from a base class, we treat the super keyword as a reference to the base class. The name of the method is given after the dot, e.g. super.someMethodFromSuperclass ().

The following example shows class inheritance:

public class Computer {
    private String cpu;
    private String ram;
    private String gpu;

    public Computer(String cpu, String ram, String gpu) {
        this.cpu = cpu;
        this.ram = ram;
        this.gpu = gpu;
    }

    public void configure() {
        System.out.println("Booting ... ");
        System.out.println("Configure cpu: " + cpu);
        System.out.println("Configure ram: " + ram);
        System.out.println("Configure gpu: " + gpu);
    }
}

public class Laptop extends Computer { // Computer is the base class of the Laptop class

    private int battery;

    public Laptop(String cpu, String ram, String gpu, int battery) {
        // in the case of a derived class, calling the base class's default constructor is required
        super(cpu, ram, gpu);
        this.battery = battery;
    }

    @Override // (1)
    public void configure() {
        // we call functionality from the base class
        super.configure();
        System.out.println("Configure battery: " + battery);
    }
}

NOTE: For more information on the annotation in the line marked (1), see this section.

NOTE: You can inherit from a class that also inherits from another class.

Composition

An alternative way to implement reusable components that, unlike inheritance, it does not extend the behavior of the base classes, but focuses on assembling objects. A class aggregates other classes within itself.

The following example shows a simple application of a theme:

public class Computer {
    private Processor processor;
    private Ram ram;

    public Computer(Processor processor, Ram ram) {
        this.processor = processor;
        this.ram = ram;
    }

    public void run() {
        // usage of processor and ram object
    }
}

class Processor {
    String name;
    int numberOfCores;
}

class Ram {
    String name;
    int size;
}

Composition and inheritance

Class inheritance is defined statically, which means that you cannot change the implementation at runtime. The implementation of subclasses depends on the base class implementation, so changes to the base class often force changes to the subclasses as well. Extensive inheritance hierarchies also negatively affect code testing and analysis of the implementation structure. Thanks to the use of compositions, we can dynamically change the implementation (e.g. thanks to [poliformism] (oop.md # polymorphism), about which in a moment), without the need to modify the aggregated components, and vice versa. This solution is transparent and does not require cascading changes to the code.

Encapsulation

Encapsulation is a mechanism by which data and methods can be hidden from the "outside world". Encapsulation allows you to provide only those mechanisms and data that you want to be visible from outside the class. Hiding data is done through access modifiers.

In the example below, access to employee data has been hidden for the id and dateOfBirth fields. It has been restricted to read-only by methods like getter. For the name and lastName fields both read access to variables (getter methods) and to their modification (setter methods) has been granted.

public class Employee {
    private String id;
    private LocalDate dateOfBirth;
    private String firstName;
    private String lastName;

    public Employee(String id, String firstName, String lastName, LocalDate dateOfBirth) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.dateOfBirth = dateOfBirth;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getId() {
        return id;
    }

    public LocalDate getDateOfBirth() {
        return dateOfBirth;
    }
}

NOTE: We should always hide the implementation details of a given class from the end user of the class, using encapsulation.

Abstraction

This mechanism focuses on hiding implementation details from the user. Its purpose is to provide only functionality, without going into the technical details of the delivered solution. More specifically, the user should only see what what the object is doing, and not how it does it.

Let us consider the functionality of sending e-mails. The user should know that he enters the data and can send it without going into details such as the communication protocol, data security or the fact how it was done. In Java, abstraction is realized using [abstract classes] (classes_and_abstract_methods.md) and interfaces.

In the example below, we are dealing with the abstraction of data transformation. The abstract class does not give us any details about what kind of conversion we are going to be dealing with. We know that we will convert the data in text form (String) into an object of theData type and we know how the data will be delivered and how validation works. However, the parse details themselves are defined at the class level: CSVParser and JsonParser.

public abstract class DataParser { // abstract class

    protected String data; // the protected modifier allows the use of a field in derived classes

    public abstract Data parse();

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }

    public void validateData() {
        if(data == null || data.isEmpty()) {
            throw new IllegalArgumentException("data are not valid!");
        }
    }
}

public class Data {
    private String info;
    private int value;

    public Data(String info, int value) {
        this.info = info;
        this.value = value;
    }
}

public class CSVParser extends DataParser {

    @Override
    public Data parse() {
        validateData();
        String[] splitData = data.split(",");
        return new Data(splitData[0], Integer.valueOf(splitData[1]));
    }
}

public class JsonParser extends DataParser {

    @Override
    public Data parse() {
        validateData();
        Gson gson = new Gson();
        return gson.fromJson(data, Data.class);
    }
}

Polymorphism

Mechanism otherwise known as multiforme. It mainly consists in the fact that a programmer using an object does not need to know whether its functionality comes from a class that is a type of the object, or from some other class that implements or [inherits] (oop.md # inheritance) from an interface or class base. In other words, it is the ability of objects to return different responses to the same request.

Polymorphism is very often used when working with collections, e.g.:

List<Integer> ints = new ArrayList<>(); // the programmer works on the INTERFACE, using some implementation
ints.add(1);
ints.add(7);
for (int idx = 0; idx < ints.size(); idx++) {
  System.out.println(ints.get(idx));
}

List<Integer> ints = new LinkedList<>(); // the programmer works on the INTERFACE, using a different implementation
ints.add(1);
ints.add(7);
for (int idx = 0; idx < ints.size(); idx++) {
  System.out.println(ints.get(idx));
}

In the next example, we define some abstraction - VodPlayer and its three implementations,NetflixPlayer, HBOGoPlayer, andDefaultPlayer. In the Android TV class, we use polymorphism by working on abstraction, the implementation of which is selected based on the application's input arguments.

public abstract class VodPlayer {
  public abstract void play(String title);
}

public class NetflixPlayer extends VodPlayer {
  @Override
  public void play(final String title) {
    System.out.println("Playing " + title + " on Netflix");
  }
}

public class HBOGoPlayer extends VodPlayer {
  @Override
  public void play(final String title) {
    System.out.println("Playing " + title + " on HBO");
  }
}

public class DefaultPlayer extends VodPlayer {
  @Override
  public void play(final String title) {
    System.out.println("Playing " + title + " on default player");
  }
}


public class AndroidTV {
  public static void main(String[] args) {
    final String player = args[0];
    VodPlayer vodPlayer = null;
    if (player.equals("Netflix")) {
      vodPlayer = new NetflixPlayer();
    } else if (player.equals("HBO")) {
      vodPlayer = new HBOGoPlayer();
    } else {
      vodPlayer = new DefaultPlayer();
    }
    playEpisode(vodPlayer, "GOT_S1E1");
  }

  static void playEpisode(VodPlayer vodPlayer, String title) {
    // we don't know what implementation we are dealing with
    vodPlayer.play(title);
  }
}