Java OOP Concepts with Examples

Object-oriented programming (OOP) refers to a programming methodology based on objects, instead of just functions and procedures as in functional programming. These objects can contain the data (attribute) and the methods (behavior), just like real-life entities that we model into our applications.

This tutorial will teach us four major principles – abstraction, encapsulation, inheritance, and polymorphism. These are also known as the four pillars of the object-oriented programming paradigm.

1. What is OOP or Object-Oriented Programming?

In the early days, people wrote programs with binary code and used mechanical switches to load the programs. Later, as the hardware capabilities evolved, experts tried to simplify the programming using high-level languages where we used compilers to generate machine instructions from the program.

With more evolution, experts created structured programming based on small functions. These functions helped in many ways e.g. code reuse, local variables, code debugging, and code maintainability.

With more computing advancement and demand for more complex applications, the limitations of structured programming started to be visible. The complex applications needed to be more closely modeled with the real-life world and usecases.

Later, the experts developed object-oriented programming. In the center of OOP, we have objects and classes. Just like a real-life entity, an object has two significant characteristics :

  • data – tells about the attributes and the object’s state
  • behavior – gives it the ability to change itself and communicate with other objects

1.1. Class and Object

An object is an instance of a class. Each object has its own state, behavior, and identity. A class is a blueprint or template for its objects.

Objects can communicate with other objects by calling functions. It is sometimes referred to as message passing.

For example, if we are working on an HR application then it consists of entities/actors e.g. employee, manager, department, payslips, vacation, goals, time tracking, etc. To model these entities in computer programs, we can create classes with similar data attributes and behavior as in real-life.

For example, an employee entity can be represented as Employee class:

public class Employee
{
    private long id;
    private String title;
    private String firstName;
    private String middleName;
    private String lastName;
    private Date dateOfBirth;

    private Address mailingAddress;
    private Address permanentAddress;

    // More such attributes, getters and setters according to application requirements
}

The above Employee acts as a template. We can use this class to create as many different employee objects as we need in the application.

Employee e = new Employee(111);
e.setFirstName("Alex");
..
..

int age = e.getAge();

The id field helps in storing and retrieving the detail of any individual employee.

The object identity is generally maintained by the application runtime environment, e.g., its Java Virtual Machine (JVM) for Java applications. Each time we create a Java object, JVM creates a hashcode for this object and assigns it. This way, even if the programmer forgets to add id field, JVM ensures that all objects are uniquely identified.

1.2. Constructor

Constructors are special methods without any return value. Their name is always the same as the name of the class, but they can accept parameters that help set the object’s initial state before the application starts using it.

JVM assigns a default constructor to the class if we do not provide any constructor. This default constructor does not accept any parameter.

Remember, if we assign a constructor to any class, then JVM does not assign the default constructor to it. If needed, we need to specify the default constructor explicitly to the class.

public class Employee
{
    // Default constructor
    public Employee()
    {

    }

    // Custom constructor
    public Employee(int id)
    {
        this.id = id;
    }
}

2. 4 Pillars of OOP

The four major features of object-oriented programming are:

  • Abstraction
  • Encapsulation
  • Inheritance
  • Polymorphism
Object Oriented Programming
OOP Pillars

2.1. Abstraction

Abstraction is very easy to understand when we relate it to a real-time example. For example, when we drive our car, we do not have to be concerned with the exact internal working of the car. We are concerned with interacting with the car via its interfaces like the steering wheel, brake pedal, accelerator pedal, etc. Here the knowledge we have of the car is abstract.

In computer science, abstraction is the process by which data and programs are defined with a representation similar in form to its meaning (semantics) while hiding away the implementation details.

In simpler terms, abstraction hides information that is not relevant to the context or rather shows only relevant information and simplifies it by comparing it to something similar in the real world.

Abstraction captures only those details about an object that is relevant to the current perspective.

Typically abstraction can be seen in two ways:

2.1.1. Data Abstraction

Data abstraction is the way to create complex data types from multiple smaller data types – which is more close to real-life entities. e.g., An Employee class can be a complex object that has various small associations.

public class Employee 
{
    private Department department;
    private Address address;
    private Education education;
    //So on...
}

So, if you want to fetch information about an employee, you ask from Employee object – as you do in real life, ask the person himself.

2.1.2. Control Abstraction

Control abstraction is achieved by hiding the sequence of actions for a complex task – inside a simple method call- so the logic to perform the task can be hidden from the client and could be changed without impacting the client code.

public class EmployeeManager
{
    public Address getPrefferedAddress(Employee e)
    {
        //Get all addresses from database 
        //Apply logic to determine which address is preferred
        //Return address
    }
}

In the above example, tomorrow if you want to change the logic so that everytime domestic address is always the preferred address, you will change the logic inside getPrefferedAddress() method, and the client will be unaffected.

2.2. Encapsulation

Wrapping data and methods within classes in combination with implementation hiding (through access control) is often called encapsulation. The result is a data type with characteristics and behaviors.

Whatever changes, encapsulate it” – A famous design principle

Encapsulation essentially has both i.e. information hiding and implementation hiding.

  • Information hiding is done by using access control modifiers (public, private, protected), and implementation hiding is achieved by creating an interface for a class.
  • Implementation hiding allows the designer to modify how an object fulfills the responsibility. This is especially valuable when the designs (or even the requirements) are likely to change.

Let’s take an example to make it more clear.

2.2.1. Information Hiding

class InformationHiding
{
    //Restrict direct access to inward data
    private ArrayList items = new ArrayList();

    //Provide a way to access data - internal logic can safely be changed in future
    public ArrayList getItems(){
        return items;
    }
}

2.2.2. Implementation Hiding

interface ImplemenatationHiding {
    Integer sumAllItems(ArrayList items);
}

class InformationHiding implements ImplemenatationHiding
{
    //Restrict direct access to inward data
    private ArrayList items = new ArrayList();

    //Provide a way to access data - internal logic can safely be changed in future
    public ArrayList getItems(){
        return items;
    }

    public Integer sumAllItems(ArrayList items) {
        //Here you may do N number of things in any sequence
        //Which you do not want your clients to know
        //You can change the sequence or even whole logic
        //without affecting the client
    }
}

2.3. Inheritance

Inheritance is another important concept in object-oriented programming. Inheritance is a mechanism by which one class acquires the properties and behaviors of the parent class. It’s essentially creating a parent-child relationship between classes. In Java, we will use inheritance mainly for code reusability and maintainability.

The keyword “extends” is used to inherit a class in Java. The “extends” keyword indicates that we are making a new class that derives from an existing class.

In the terminology of Java, a class that is inherited is called a super class. The new class is called a subclass.

A subclass inherits all the non-private members (fields, methods, and nested classes) from its superclass. Constructors are not members, so they are not inherited by subclasses, but the constructor of the superclass can be invoked from the subclass.

2.3.1. Inheritance Example

public class Employee
{
    private Department department;
    private Address address;
    private Education education;
    //So on...
}

public class Manager extends Employee {
    private List<Employee> reportees;
}

In the above code, Manager is a specialized version of Employee and reuses department, address and education from Employee class as well as defines its own reportees list.

2.3.2. Types of Inheritance

Single inheritance – A child class is derived from one parent class.

class Parent {
    //code
}
 
class Child extends Parent {
    //code
}

Multiple inheritances – A child can derive from multiple parents. Till JDK 1.7, multiple inheritance was not possible in java through the use of classes. But from JDK 1.8 onwards, multiple inheritance is possible via the use of interfaces with default methods.

interface MyInterface1 {
        
}
 
interface MyInterface2 {
       
}
 
class MyClass implements MyInterface1, MyInterface2 {
 
}

Multi-level inheritance – refers to inheritance between more than three classes in such a way that a child class will act as parent class for another child class.

class A {
 
}
 
class B extends A {
 
}
 
class C extends B {
 
}

Hierarchical inheritance refers to inheritance when there is one superclass and more than one sub class extending the super class.

class A {
 
}
 
class B extends A {
 
}
 
class C extends A {
 
}
 
class D extends A {
 
}

Hybrid inheritance – is a combination of two or more types of inheritance. So when the relationship between classes contains inheritance of two or more types, then we say classes implement hybrid inheritance.

interface A {
 
}
 
interface B extends A {
 
}
 
class C implements A {
 
}
 
class D extends C impements B {
 
}

2.4. Polymorphism

Polymorphism is the ability by which, we can create functions or reference variables that behave differently in a different programmatic context. It is often referred to as one name with many forms.

For example, in most programming languages, '+' operator is used for adding two numbers and concatenating two strings. Based on the type of variables, the operator changes its behavior. It is known as operator overloading.

In Java, polymorphism is essentially considered into two types:

2.4.1. Compile-time Polymorphism

In compile-time polymorphism, the compiler can bind the appropriate methods to the respective objects at compile time because it has all the necessary information and knows which method to call during program compilation.

It is often referred to as static binding or early binding.

In Java, it is achieved with the use of method overloading. In method overloading, the method parameters can vary with a number, order, or type of parameter.

class PlusOperator {

       int sum(int x, int y) {
             return x + y;
       }

       double sum(double x, double y) {
             return x + y;
       }

       String sum(String s1, String s2) {
             return s1.concat(s2);
       }
}

2.4.2. Runtime Polymorphism

In runtime polymorphism, the call to an overridden method is resolved dynamically at runtime. The object, on which the method will be executed, is determined at runtime – generally depending on user-driven context.

It is often referred to as dynamic binding or method overriding. We may have heard it with the name dynamic method dispatch.

In runtime polymorphism, we generally have a parent class and a minimum of one child class. In a class, we write a statement to execute a method present in the parent and child classes.

The method call is given using the variable of the type of parent class. The actual instance of the class is determined at runtime because a parent class type variable can store the reference to the instance of the parent class as well as child class also.

class Animal {
   public void sound() {
         System.out.println("Some sound");
   }
}

class Lion extends Animal {
   public void sound() {
         System.out.println("Roar");
   }
}

class Main {
   public static void main(String[] args)    {

        //Parent class reference is pointing to a parent object
        Animal animal = new Animal();
        animal.sound(); //Some sound

        //Parent class reference is pointing to a child object
        Animal animal = new Lion();
        animal.sound(); //Roar
   }
}

3. More Object Oriented Programming Concepts

Apart from the above 4 building blocks of OOP, we have a few more concepts that play an important role in building the whole understanding.

Before going deeper, we shall understand the term module. In general programming, a module is a class or sub-application that performs unique functionality. In an HR application, a class can perform various functions such as sending emails, generating salary slips, calculating the age of the employee, etc.

3.1. Coupling

Coupling is the measure of the degree of interdependence between the modules. Coupling refers to how strongly a software element is connected to other elements. A good software will have low coupling.

It means a class should perform a unique task or only tasks that are independent of other tasks. E.g. an EmailValidator class will only validate the email. Similarly, EmailSender class will only send emails.

If we include both functionalities within a single class EmailUtils then it is an example of tight coupling.

3.2. Cohesion

Cohesion is the internal glue that keeps the module together. Good software design will have high cohesion.

It means a class/module should include all the information needed to perform its function without any dependency. For example, an EmailSender class should be able to configure SMTP server, and accept the sender’s email, subject and content. Basically, it should focus on sending emails only.

The application should not use EmailSender for any other function other than sending an email. Low cohesion results in monolithic classes that are difficult to maintain, understand and reduce reusability.

3.3. Association

Association refers to the relationship between objects who have independent lifecycles without ownership of each other.

Let’s take an example of a teacher and student. Multiple students can associate with a single teacher, and a single student can associate with multiple teachers, but both have their own lifecycles.

Both can be created and deleted independently so when a teacher leaves the school, we don’t need to delete any students, and when a student leaves the school, we don’t need to delete any teachers.

3.4. Aggregation

Association refers to the relationship between objects who have independent lifecycles, but ‘WITH ownership’. It is between child and parent classes where child objects cannot belong to another parent object.

Let’s take an example of a cell phone and a cell phone battery. A single battery can belong to only one phone at a time. If the phone stops working, and we delete it from our database, the phone battery will not be deleted because it may still be functional. So in aggregation, while there is ownership, objects have their own lifecycle.

3.5. Composition

Composition refers to relationships when objects don’t have an independent lifecycle. All child objects will be deleted if the parent object is deleted.

For example, the relationship between questions and answers. Single questions can have multiple answers, but the answers cannot belong to multiple questions. If we delete a question, all its answers will automatically be deleted.

4. Best Practices

4.1. Favor Composition over Inheritance

Inheritance and composition, both promote code reusability. But the use of composition is preferred over inheritance.

An implementation of composition over inheritance typically begins with the creation of various interfaces representing the behaviors that the system must exhibit. Interfaces enable polymorphic behavior. Classes implementing the identified interfaces are built and added to business domain classes as needed. Thus, system behaviors are realized without inheritance.

interface Printable {
    print();
}

interface Convertible {
    print();
}

class HtmlReport implements Printable, Convertible
{

}

class PdfReport implements Printable
{

}

class XmlReport implements Convertible
{

}

4.2. Program to Interface, Not to the Implementation

This leads to flexible code that can work with any new implementation of the interface. We should aim to use interfaces as variables, as return types of a method or as the argument type of methods.

Interfaces act as superclass types. In this way, we can create more specializations of the interface in the future without modifying the existing code.

4.3. DRY (Don’t Repeat Yourself)

Don’t write duplicate code, instead use abstraction to abstract common things in one place.

As a thumb rule, if you write the same piece of code at two places – consider extracting in a separate function and calling the function at both places.

4.4. Encapsulate What Changes

All software gets changes over time. So, encapsulate the code you expect or suspect to be changed in the future.

In Java, use private methods to hide such implementations from clients so that when you make a change, the client is not forced to change its code.

The use of design patterns is also recommended to achieve encapsulation. For example, the factory design pattern encapsulates object creation code and provides flexibility to introduce a new type later without impacting existing clients.

4.5. Single Responsibility Principle

It is one of the Solid principles of OOP class design. It emphasizes that one class should have one and only one responsibility.

In other words, we should write, change, and maintain a class for only one purpose. This will give us the flexibility to make future changes without worrying about changes’ impacts on another entity.

4.6. Open-Closed Principle

It emphasizes that software components should be open for extension, but closed for modification.

This means that our classes should be designed so that whenever fellow developers want to change the flow of control in specific conditions in the application, all they need to do is extend our class and override some functions, and that’s it.

If other developers cannot design desired behavior due to constraints put by our class, then we should reconsider changing our class.

There are a lot of other concepts and definitions in the whole OOP paradigm which we will learn in other tutorials.

5. Summary

This Java OOP tutorial discusses the 4 major pillars of OOP in Java, with easy-to-understand programs and snippets. Drop your questions in the comments section.

Happy Learning !!

Comments

Subscribe
Notify of
guest
14 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode