Skip to main content

Object oriented programming

Core principles of Oops

Object oriented programming core principles

Encapsulation

Encapsulation refers to binding together the data and functions that manipulate the data, and keeping both safe from outside interference and misuse.

In Java, we achieve encapsulation by:

  1. Declaring class variables/attributes as private
  2. Providing public setter and getter methods to access and update the value of private variables

Encapsulation Benefits:

  • Control over class attributes and methods
  • Class can have hidden data (private attributes)
  • Flexibility to change internal implementation without affecting outer classes

Let's model a Bank Account class:

public class BankAccount {

    private long accountNumber; 
    private String ownerName;
    private String branch;
    private double balance;

    public long getAccountNumber() {
        return accountNumber;
    }

    public void setAccountNumber(long accountNumber) {
        this.accountNumber = accountNumber;
    }

    public String getOwnerName() {
        return ownerName; 
    }

    public void setOwnerName(String ownerName) {
        this.ownerName = ownerName;
    }
    
    public String getBranch() {
        return branch;
    }

    public void setBranch(String branch) {
        this.branch = branch;
    }

    public double getBalance() {
        return balance;
    }

    public void deposit(double amount) {
        // perform validation before applying
        balance += amount; 
    }

    public void withdraw(double amount) {
        if(balance - amount >= 0) {
            balance -= amount;
        } else {
            System.out.println("Insufficient funds");
        }
    }

}

In the above BankAccount class:

  • The attributes are declared private
  • The public getter/setter methods are used to access and update these attributes in a controlled fashion
  • Methods like deposit() and withdraw() encapsulate the logic to manipulate the balance attribute

This encapsulation provides abstraction barrier between object state and behavior and how other parts of the code use it. For example, we can change the data type of balance to BigDecimal without any code change required in other classes.

Abstraction

Abstraction refers to exposing only essential details and hiding complexity from users. It focuses on the outside view of an object.

In Java, abstraction can be achieved using:

  1. Abstract classes
  2. Interfaces

Abstract Class:

  • An abstract class declares common abstract methods that concrete sub-classes must implement
  • An abstract class can have abstract and concrete methods
  • It cannot be instantiated directly. A sub-class needs to extend it and provide implementations to its abstract methods

Example:

public abstract class Shape {
    private String color;

    //abstract method
    public abstract double getArea();

    //concrete method
    public void setColor(String color) {
       this.color = color;
    }
}

public class Circle extends Shape {
   private double radius;

   public double getArea() {
      return Math.PI * radius * radius;
   }
}

public class Rectangle extends Shape {

    private double length;
    private double width;

   public double getArea() {
      return length * width;
   }
}

In this example, Shape is an abstract class that provides a template/blueprint for classes like Circle and Rectangle. The getArea() is defined as an abstract method that the sub-classes override to provide their specific implementation.

Another example of abstraction using interface

public interface Vehicle {

    //all methods are by default public and abstract

    void changeGear(int gear);

    void speedUp(int increment);

    void applyBrakes(int decrement);

}

public class Bicycle implements Vehicle{

    private int currentGear;
    private int currentSpeed;

    public void changeGear(int newGear){
        currentGear = newGear;
    }

    public void speedUp(int increment){
        currentSpeed += increment;
    }

    public void applyBrakes(int decrement){
        currentSpeed -= decrement;
    }
}

public class Car implements Vehicle {

    private int currentGear;
    private int currentSpeed;

    public void changeGear(int newGear){
        currentGear = newGear;
    }

    public void speedUp(int increment){
        currentSpeed += increment;
    }

    public void applyBrakes(int decrement){
        currentSpeed -= decrement;
    }
}

In this example:

  • Vehicle is an interface that provides an abstraction for vehicles
  • It declares methods like changeGear, speedUp, applyBrakes that vehicle objects should implement
  • Bicycle and Car classes implement these methods to provide custom vehicle behavior

Key benefits:

  • vehicle interface separates abstraction from implementation -new vehicles can provide specific logic while complying to interface -client code can use interface to work across all vehicles uniformly

So with interfaces we define a contract for capabilities that implementing classes should provide. This helps hide unnecessary details from outside code.

Key points:

  • Helps focus on essential qualities rather than implementation details
  • Defines a contract that all implementations must follow
  • Reduces code duplication
  • Allows changes in linked classes without affecting other code

Inheritance

Inheritance allows a new class to be based on an existing class, inheriting its attributes and behaviors. The existing base class is called superclass or parent class while the new class is called subclass or child class.

The subclass inherits the public and protected members of the superclass. It can also override inherited methods and add new attributes and behaviors. This enables code reusability.

Here is an example of inheritance from the open source Spring Framework

public abstract class DAOSupport {

    private JdbcTemplate jdbcTemplate;

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    protected final JdbcTemplate getJdbcTemplate() {
        return this.jdbcTemplate;
    }

}

public class UserDao extends DAOSupport {

    public int getTotalUsers() {
        // reuse inherited jdbcTemplate
        return getJdbcTemplate().queryForObject("", int.class);
    }

}

public class ProductDao extends DAOSupport {

    public List<Product> listProducts() {
         // reuse inherited jdbcTemplate
         return getJdbcTemplate().query("", productRowMapper);
    }

}

The key benefits provided by inheritance here:

  1. Eliminates duplicate code:
    • DAOSupport declares common JdbcTemplate dependency
    • Subclasses reuse this instead of declaring separately
  2. Extensibility:
    • UserDao and ProductDao extend and customize as per need
  3. Maintainability:
    • Later if JdbcTemplate usage changes, only change in one place

So inheritance encourages code reuse while allowing specialization. Reduces a lot of duplicate code across subclasses. Also easier to maintain as subclasses depend upon super class abstractions.

polymorphism

Polymorphism means "many forms". It allows objects with different underlying forms to be treated uniformly. In Java, polymorphism is achieved via:

  1. Method overloading
  2. Method overriding

Method overloading refers to methods with same name but different parameters. Based on parameters passed, the appropriate method is invoked.

Method overriding refers to sub-classes providing specific implementation for methods declared in parent class. The call is bound to implementation in the runtime based on type of object.

public interface HttpClient {
    Response execute(Request request);
}

public class DefaultHttpClient implements HttpClient {

    public Response execute(Request request) {
        //default implementation
    }
}

public class CachingHttpClient implements HttpClient {

    private HttpClient httpClient;

    public CachingHttpClient(HttpClient httpClient) {
        this.httpClient = httpClient;
    }

    public Response execute(Request request) {
       //custom caching logic
       return response;
    }
}

//client code
HttpClient client = new DefaultHttpClient();
//OR
HttpClient cachingClient = new CachingHttpClient(new DefaultHttpClient());

Here polymorphism allows the substitute of DefaultHttpClient with CachingHttpClient without needing to change client code. The client can uniformly call execute method without worrying about actual implementation.

Another example

Here is an example demonstrating both method overloading and overriding polymorphism

public abstract class JsonParser {

   public abstract JsonToken nextToken() throws IOException;

   public abstract String getText() throws IOException;

   public abstract boolean isExpectedStartArrayToken() throws IOException;

}

public final class JacksonJsonParser extends JsonParser {

   //overloaded getText() methods
   public String getText(JsonToken t) {...}

   @Override
   public String getText() {
      //custom logic before delegating to overloaded method
      return getText(this.currToken);
   }

   //overriding base class nextToken()
   @Override
   public JsonToken nextToken() throws IOException {
      //custom jackson implementation
   }

}

Method Overloading:

  • getText() method is overloaded with same name but different parameters
  • Enables reusing method name for similar functionality

Method Overriding:

  • nextToken() defined in the base class is overridden by the subclass
  • Custom Jackson implementation is provided

So JacksonJsonParser leverages overloading for code reuse and overriding for customization of inherited methods.

This enables:

  • Reuse of common interface
  • Custom implementation binding
  • Extensibility with new classes
  • Reduced coupling between client and implementations

So polymorphism helps write extensible and maintainable object oriented code.