A Complete Guide to Java OOP Concepts

Saima SiddiquiSaima Siddiqui
10 min read

Introduction

Object-oriented programming (OOP) is a paradigm where objects contain data (fields or properties) and code (methods), and this post focuses on Java, a widely used OOP language among others like Python, C++, and C#.

Classes and Objects in Java

In the realm of Java, the concepts of classes and objects constitute the very fabric of its design and functionality. To truly grasp Java programming, one must understand what classes and objects are, how they interact, and their significance in object-oriented programming (OOP).

Understanding Classes

A class in Java can be thought of as a blueprint or a template for creating objects. It defines a datatype by bundling data and methods that operate on the data into a single unit. Classes contain:

  • Fields: Variables that hold the state of an object.

  • Methods: Blocks of code that define the behavior of the object.

Think of a class as a blueprint for a house. It contains the design details but is not the house itself. Similarly, a class defines the structure and capabilities of what its objects will be, but it is not the object itself.

Code Example: Defining a Class

public class Car {
    // Fields
    String make;
    String model;
    int year;

    // Method
    void displayInfo() {
        System.out.println("Car Make: " + make + ", Model: " + model + ", Year: " + year);
    }
}

Creating Objects: Instances of Classes

An object is an instance of a class. When a class is defined, no memory is allocated until an object of that class is created. The object has its own state, behavior, and identity. It’s the implementation of the class blueprint.

Creating an object in Java involves two steps:

  1. Declaration: A variable of the class type is declared.

  2. Instantiation: Using the new keyword, memory is allocated for the object.

Code Example: Creating an Object

public class Main {
    public static void main(String[] args) {
        // Creating an object of Car
        Car myCar = new Car();

        // Assigning values to fields
        myCar.make = "Toyota";
        myCar.model = "Corolla";
        myCar.year = 2021;

        // Calling method
        myCar.displayInfo();
    }
}

Understanding Object Identity and State

Each object in Java has its unique identity (typically, the memory address of where the object is stored), state (the data stored in the object’s fields), and behavior (what the object can do, or what can be done to the object, defined by its methods).

Consider the Car class example. If we create two objects of the Car class, each will have its own make, model, and year. They are separate entities, each with its own unique state.

The Importance of Classes and Objects in Java

The entire ecosystem of Java revolves around classes and objects. They are the fundamental building blocks of any Java application. Understanding them is crucial for any Java programmer, not just for writing code, but for thinking in terms of OOP, which is essential for creating efficient, scalable, and maintainable software.

The four pillars of OOP

Object-Oriented Programming (OOP) in Java revolves around four fundamental concepts: Encapsulation, Inheritance, Polymorphism, and Abstraction.

As follows...

Encapsulation

Encapsulation is a fundamental concept in object-oriented programming (OOP) and is pivotal in Java. It refers to the bundling of data (attributes) and the methods that operate on this data into a single unit, or class. More importantly, encapsulation is about restricting direct access to some of an object’s components, which is a means of preventing accidental interference and misuse of the methods and data.

Why Encapsulation Matters

The main goal of encapsulation is to keep the internal state of an object hidden from the outside. This is often referred to as “data hiding”. By restricting access to the internal state of the object and only allowing modification through methods, we can protect the integrity of the data and ensure the object remains in a valid state.

Implementing Encapsulation in Java

In Java, encapsulation is implemented using access modifiers. There are four access modifiers: public, private, protected, and default (no modifier). By marking the class fields as private and providing public getter and setter methods, we can control how the fields are accessed and modified.

Code Example: Encapsulation in Action

public class Employee {
    private String name;
    private int age;
    private double salary;

    // Constructor
    public Employee(String name, int age, double salary) {
        this.name = name;
        this.age = age;
        this.salary = salary;
    }

    // Getter and Setter methods
    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if(age > 18) {
            this.age = age;
        }
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }
}

public class Main {
    public static void main(String[] args) {
        Employee emp = new Employee("John Doe", 30, 50000);
        emp.setSalary(55000); // Updating salary using setter
        System.out.println("Employee Info: " + emp.getName() + ", " + emp.getAge() + ", " + emp.getSalary());
    }
}

In this example, the Employee class encapsulates the fields name, age, and salary. The data can only be accessed and modified through the getter and setter methods, providing control over the data.

Inheritance: Extending Classes

Inheritance is a cornerstone of object-oriented programming in Java. It allows a new class, known as a subclass, to inherit attributes and methods from an existing class, referred to as a superclass. This mechanism not only facilitates code reuse but also establishes a natural hierarchy in your code.

The Concept of Inheritance

Imagine a family tree. Children inherit traits from their parents, but each child also has unique attributes. Similarly, in Java, a subclass inherits fields and methods from its superclass while having the ability to introduce its own. This relationship not only reduces redundancy but also promotes a logical organization of code.

Syntax of Inheritance in Java

In Java, inheritance is implemented using the extends keyword. When one class extends another, the subclass automatically inherits all public and protected members (fields and methods) of the superclass, except for constructors.

Code Example: Basic Inheritance

// Superclass
public class Vehicle {
    public void move() {
        System.out.println("Vehicle is moving");
    }
}

// Subclass
public class Car extends Vehicle {
    public void display() {
        System.out.println("Car is a type of Vehicle");
    }
}

public class Main {
    public static void main(String[] args) {
        Car myCar = new Car();
        myCar.move();  // Inherited method
        myCar.display(); // Subclass method
    }
}

In this example, Car extends Vehicle. Therefore, it inherits the move() method from Vehicle.

Types of Inheritance in Java

Java supports different types of inheritance:

  1. Single Inheritance: A subclass inherits from one superclass.

  2. Multilevel Inheritance: A subclass inherits from a superclass, which in turn inherits from another superclass.

  3. Hierarchical Inheritance: Multiple subclasses inherit from a single superclass.

Understanding super Keyword

The super keyword in Java is used within a subclass to refer to the superclass. It can be used to invoke the superclass's methods and constructors. This is especially useful in method overriding, where the subclass method needs to add to the behavior of the superclass method.

Code Example: Using super

public class Animal {
    public void eat() {
        System.out.println("Animal eats");
    }
}

public class Dog extends Animal {
    @Override
    public void eat() {
        super.eat(); // Calls the eat method of Animal
        System.out.println("Dog eats dog food");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        myDog.eat();  // Outputs both "Animal eats" and "Dog eats dog food"
    }
}

Polymorphism

Polymorphism, in the context of object-oriented programming in Java, is the ability of an object to take on many forms. It’s a concept that allows a single interface to be used for a general class of actions. The specific action is determined by the exact nature of the situation. There are two main types of polymorphism in Java: compile-time polymorphism (method overloading) and runtime polymorphism (method overriding).

Understanding Compile-Time Polymorphism: Method Overloading

Method overloading occurs when two or more methods in the same class have the same name but different parameters. It is a way to create several methods with the same name that perform similar tasks but on different inputs.

Code Example: Method Overloading

public class DisplayOverload {
    void display(int a) {
        System.out.println("Got Integer data: " + a);
    }

    void display(String b) {
        System.out.println("Got String data: " + b);
    }
}

public class Main {
    public static void main(String[] args) {
        DisplayOverload obj = new DisplayOverload();
        obj.display(1); // Outputs "Got Integer data: 1"
        obj.display("Hello"); // Outputs "Got String data: Hello"
    }
}

In this example, the display method is overloaded with different parameter types.

Understanding Runtime Polymorphism: Method Overriding

Runtime polymorphism or dynamic method dispatch is a process in which a call to an overridden method is resolved at runtime, not at compile-time. It means that if a subclass has overridden a method of a superclass, then the version of the method in the subclass will be executed.

Code Example: Method Overriding

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

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Bark bark");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Dog();
        myAnimal.makeSound(); // Outputs "Bark bark"
    }
}

Here, Dog overrides the makeSound method of Animal. When an Animal reference type points to a Dog object and makeSound is called, the Dog's version of the method is executed.

Polymorphism in Interfaces

Polymorphism also extends to interfaces in Java. An interface can be used to represent all classes that implement it. This is especially useful in cases where multiple classes implement the same interface but provide different functionalities.

Code Example: Interface Polymorphism

interface Shape {
    void draw();
}

class Circle implements Shape {
    public void draw() {
        System.out.println("Drawing Circle");
    }
}

class Rectangle implements Shape {
    public void draw() {
        System.out.println("Drawing Rectangle");
    }
}

public class Main {
    public static void main(String[] args) {
        Shape shape1 = new Circle();
        Shape shape2 = new Rectangle();

        shape1.draw(); // Drawing Circle
        shape2.draw(); // Drawing Rectangle
    }
}

In this example, both Circle and Rectangle implement the Shape interface but provide different implementations of the draw method.

The Power and Flexibility of Polymorphism

Polymorphism in Java adds flexibility and reusability to your code. It allows you to define one interface and have multiple implementations. It’s a key concept in Java and is used in many real-world applications, such as method callbacks, event handling, and interface-driven designs.

Understanding polymorphism is fundamental to mastering Java programming, as it not only aids in creating flexible and maintainable code but also in harnessing the full power of OOP concepts.

Abstraction

Abstraction in Java is a powerful concept that helps in reducing complexity by hiding the intricate details and showing only the necessary features of an object. It is one of the fundamental principles of object-oriented programming and allows developers to manage large systems more efficiently.

Understanding Abstraction

Abstraction can be understood as a process of handling complexity by breaking down large systems into simpler, more manageable parts. In Java, abstraction is achieved using abstract classes and interfaces.

  1. Abstract Classes: An abstract class in Java is a class that cannot be instantiated and may contain abstract methods, which are methods without a body. The idea is to provide a base class that defines the structure and capabilities of its subclasses. Subclasses provide concrete implementations for these abstract methods.

  2. Interfaces: An interface in Java is a completely abstract class that is used to group related methods with empty bodies. Implementing an interface forces a class to implement all the methods declared in the interface, providing a way to achieve abstraction.

Code Example: Abstract Class

abstract class Animal {
    abstract void makeSound();

    void eat() {
        System.out.println("Animal is eating");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Bark");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myDog = new Dog();
        myDog.makeSound(); // Outputs "Bark"
        myDog.eat(); // Outputs "Animal is eating"
    }
}

In this example, Animal is an abstract class with an abstract method makeSound. The Dog class provides an implementation for the makeSound method.

Code Example: Interface

interface Drawable {
    void draw();
}

class Circle implements Drawable {
    public void draw() {
        System.out.println("Drawing Circle");
    }
}

class Rectangle implements Drawable {
    public void draw() {
        System.out.println("Drawing Rectangle");
    }
}

public class Main {
    public static void main(String[] args) {
        Drawable d1 = new Circle();
        Drawable d2 = new Rectangle();

        d1.draw(); // Drawing Circle
        d2.draw(); // Drawing Rectangle
    }
}

Here, Drawable is an interface that is implemented by both Circle and Rectangle. Each class provides its own implementation of the draw method.

Remember that learning is a continuous journey, and the principles and examples discussed here provide a foundation for building further knowledge and expertise.

I hope you liked the article🤗. Thank you for reading. We can connect through Twitter.

2
Subscribe to my newsletter

Read articles from Saima Siddiqui directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Saima Siddiqui
Saima Siddiqui

Web developer. Code creator.