Pillars of OOP

Today we will discuss 4 pillars of OOP (Object Oriented Programming).

  1. Abstraction
  2. Encapsulation
  3. Inheritance
  4. Polymorphism
Abstraction

Abstraction is a process of hiding implementation details and exposes only the functionality to the user. This means the user will only know what it does rather than how it does. For example, we know how to capture a photo using a camera, but we don’t need to know how it works internally in order to enjoy it.

There are two ways to achieve abstraction in Java –

  1. Abstract class : We can achieve 0 to 100% abstraction.
  2. Interface : We can achieve 100% abstraction.

We will discuss abstract class and interface in detail in a dedicated chapter.

Encapsulation

Encapsulation (also known as data hiding) is all about hiding the instance variables from direct access. We can achieve that by making the instance variables private and providing public getters (accessors) and setters (mutators) methods to access or update the instance variable.

Now the question is, why would you bother to do that? Consider a class Human as shown below. Instance variables are public here.

public class Human {
    public String name = "Aditi";
    public int height = 6;
}

Now create an object of the class and as the instance variables are public, you are allowed to change the values directly. So, try to change the height to 0 (oops!).

Human h = new Human();
h.height = 0;

But can we have a human with height 0? No, right. So, what is wrong here? The problem here is, we are directly allowing others to access and update the instance variables and they can really do some nasty stuff. But if we encapsulate the Human class, we should be good.

public class Human {
    private String name = "Aditi";
    private int height = 6;
	
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        if (height > 0) {
            this.height = height;
        }
    }

}

Do you see the difference? We have made the instance variables private to prevent direct access. To update the height, you have to use the setHeight() method. Here we have the opportunity to check whether the height is valid or not. If that is valid, allow the caller to update the value. So, as a best practice, always encapsulate your class.

Inheritance

Inheritance is the process by which one class inherits functionalities from another class. The purpose behind inheritance is that you can create new classes on top of existing classes so that we can reuse methods and fields of the parent class. Moreover, you can add new methods and fields in your current class also. We call the inheriting class a subclass or a child class and the original class is called the parent class. Inheritance in Java is implemented using extends keyword. Let’s understand it with an example. 

Let’s consider a Camera class. This class is capable of taking photos.

public class Camera {
    public void takePhoto() {
        System.out.println("Taking photo ...");
    }
}

You can use this Camera class to create an object and call takePhoto() method to capture a photo. Now let’s consider, you need the zoom feature. Will you really create everything from scratch? No right? Because how to capture the photo is already developed. You just need to add a zoom feature. So, we can extend this Camera class and plug-in only the zoom feature.

public class ZoomCamera extends Camera {
    public void takePhotoWithZoom() {
        applyZoom();
        super.takePhoto();
    }
	
    public void applyZoom() {
        System.out.print("Applying zoom - ");
    }
}

Here to capture photos without zoom, we can use the super class method. Also we have added a new method applyZoom() to apply zoom before taking a photo with zoom. The super keyword is indicating we are using a method from superclass.

ZoomCamera camera = new ZoomCamera();
camera.takePhoto();
camera.takePhotoWithZoom();
 
Output:
Taking photo ...
Applying zoom - Taking photo ...

So, a child class is capable of doing everything that the parent class can do and along with that it can do something extra. We will discuss inheritance in detail in a dedicated chapter.

Polymorphism

If you think about the Greek roots of the term, it will be clearer –

  1. Poly = many
  2. Morph = form

So polymorphism is the ability to present the same superclass or interface for differing underlying forms. 

The beauty of polymorphism is that a code working with the different classes does not need to know which class it is using as long as all of those classes are capable of doing some agreed task.

Confused? Okay, to understand it better, let’s consider an Animal interface (will discuss interface later in detail). Interface just provides a contract that any class that will implement this interface will be capable of doing some particular task, but doesn’t care how they will do it.

public interface Animal {
     void makeNoise();
}

Here in our Animal interface, we have defined a makeNoise() method, but there is no implementation. So, any class that will implement the Animal interface will be capable of making some noise, but how they will make noise is purely the responsibility of the implementor.

Now let’s create two classes Cat and Dog who will implement this Animal interface. When a class implements an interface, they have to provide implementation of the interface methods. Otherwise code will not compile.

public class Cat implements Animal {
    public void makeNoise() {
        System.out.println("Meow, meow ...");
    }
}
public class Dog implements Animal {
    public void makeNoise() {
        System.out.println("Woof, woof ...");
    }
}

This is called overriding. If a subclass (child class) has a method with the same signature as the parent class, it is known as method overriding.

Please remember, a superclass reference can be used to refer to any subclass object. But the actual object at runtime will determine which method will be called. In our example, a cat or dog object can be referred to by Animal class.

Animal animal1 = new Cat();
Animal animal2 = new Dog();

And as I said –

  • When we call the makeNoise() method on animal1 object, it will call the makeNoise() method of the cat object.
  • When we will call the makeNoise() method on animal2 object, it will call the method defined inside the Dog class.
animal1.makeNoise(); ⇒ Meow, meow ...
animal2.makeNoise(); ⇒ Woof, woof ...

Remember what I said about polymorphism – it is the ability to present the same superclass or interface for differing underlying forms. Here we are using Animal (interface) reference for both animal1 and animal2. So as per the contract of Animal interface, it is capable of making noise and we will be able to call the method makeNoise() which is defined inside the interface to make noise. We did override this method in both the child classes. Based on the object type, at runtime, makeNoise() method is working differently. As this is happening at runtime, overriding is also called run-time polymorphism or dynamic polymorphism.

There is also another form of polymorphism, that is called method overloading or static polymorphism or compile time polymorphism.

When we have several methods with the same name and different signature inside a class, we call that overloading. As the compiler has to decide which method to call based on the arguments, this is called Compile time polymorphism.

When I say signature, I mean –

  1. Number of parameters.
  2. Type of parameters.
  3. Order of parameters.

For overloading, one or more of the above criteria must be different. We can not have two methods with the same name and same signature. Please remember, return type doesn’t matter for overloading. We cannot have two methods inside a class having the same name and same signature with different return types.

Let’s consider a Shape class and we have two getArea() methods. One for square shape, where we will pass one length of one side and another for rectangle, where we will pass length and width.

public class Shape {
    public void getArea(int length) {
        System.out.println("Area : " + length*length);
    }
	
    public void getArea(int length, int width) {
        System.out.println("Area : " + length*width);
    }
}

Here this getArea() method will behave differently based on the number of arguments passed. Here we have two overloaded getArea() methods.

Shape shape = new Shape();
shape.getArea(2);     ⇒ Output : 4
shape.getArea(4, 6);   ⇒ Output : 24

That’s all for now. Hope you have enjoyed this course. We’ll meet again with new topic. Till then, bye bye.