The Magic of OOP, Classes, and Objects in Java

The Magic of OOP, Classes, and Objects in Java

Object-Oriented Programming (OOP) is a programming paradigm centered on the concept of objects, which are instances of classes. Objects in OOP encapsulate data and activities, allowing for a more modular and structured approach to software development. OOP is founded on four fundamental principles: encapsulation, inheritance, polymorphism, and abstraction.

Encapsulation in OOP in Java

In Java, encapsulation refers to the concept of grouping methods (behaviors) and data (attributes) into a single unit called a class. By restricting direct access from outside the class while providing methods to access and alter the data, it gives you the ability to manage who has access to it and ensures data security and integrity, using Getter or Setter Method

Here's an example to illustrate encapsulation in Java:

public class BankAccount {
    // Private data members (attributes)
    private String accountNumber;
    private double balance;
    private String accountHolderName;

    // Constructor to initialize the object
    public BankAccount(String accountNumber, double balance, String accountHolderName) {
        this.accountNumber = accountNumber;
        this.balance = balance;
        this.accountHolderName = accountHolderName;
    }

    // Getter method for account number
    public String getAccountNumber() {
        return accountNumber;
    }

    // Getter method for balance
    public double getBalance() {
        return balance;
    }

    // Getter method for account holder name
    public String getAccountHolderName() {
        return accountHolderName;
    }

    // Method to deposit money into the account
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println(amount + " deposited successfully.");
        } else {
            System.out.println("Invalid amount for deposit.");
        }
    }

    // Method to withdraw money from the account
    public void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println(amount + " withdrawn successfully.");
        } else {
            System.out.println("Insufficient funds or invalid amount for withdrawal.");
        }
    }

    // Method to display information about the account
    public void displayInfo() {
        System.out.println("Account Number: " + accountNumber);
        System.out.println("Account Holder Name: " + accountHolderName);
        System.out.println("Balance: " + balance);
    }
}

In this BankAccount class:

  • We have three private data members accountNumber, balance, and accountHolderName, which are encapsulated within the class.

  • We provide public getter methods (getAccountNumber(), getBalance(), getAccountHolderName()) to access the private data members.

  • We provide public methods (deposit() and withdraw()) to modify the balance of the account. These methods ensure that the balance is updated correctly and that the user cannot deposit or withdraw negative amounts or more than the available balance.

  • The displayInfo() method is a public method that displays information about the account.

Now, let's use the BankAccount class in a Main class:

public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount("123456789", 1000.0, "John Doe");

        // Display initial information
        account.displayInfo();

        // Deposit and withdraw money
        account.deposit(500.0);
        account.withdraw(200.0);

        // Display updated information
        account.displayInfo();
    }
}

In this Main class, we create an object of the BankAccount class named account. We then use the deposit() and withdraw() methods to deposit and withdraw money from the account, respectively. Finally, we call the displayInfo() method to display the updated information about the account.

Encapsulation allows us to hide the internal implementation details of the BankAccount class and control access to its data, thus providing better data security and integrity.

Inheritance in OOP In Java

Inheritance enables subclasses or newly created classes to inherit attributes and methods from other classes (Super Class or Parents Class), encouraging code reuse and establishing a hierarchical structure.

There are different types of inheritance in Java, and here they are explained with simple examples:

Single-Class Inheritance:

In single inheritance, a subclass inherits methods from only one superclass.

Here, as you can see, myDog.eat() where eat() method is inherited from Animal Class which is Super class

Multilevel Inheritance: In multilevel inheritance, a subclass inherits from another subclass, creating a chain of inheritance.

Multiple Inheritances Using Interface in OOP in Java

Multiple inheritance between classes is not supported by Java; that is, a class cannot inherit directly from more than one class. Still, interfaces can be used to accomplish multiple inheritances.

interface Swimming {
    void swim();
}

interface Flying {
    void fly();
}

interface Eating {
    void eat();
}

class Bird implements Swimming, Flying, Eating {
    public void swim() {
        System.out.println("Bird is swimming");
    }

    public void run() {
        System.out.println("Bird is flying");
    }

    public void eat() {
        System.out.println("Eating");
    }
}

public class MultipleInheritance {
    public static void main(String[] args) {
        Bird myBird = new Bird();
        myBird.swim(); // Inherited From Swimming Interface
        myBird.fly();  // Inherited From Flying Interface
        myBird.eat(); // Inherited From Eating Interface
    }

}

Polymorphism in OOP In Java

Polymorphism allows objects to adopt numerous forms or behaviors depending on their circumstances. This enables dynamic method dispatch and method overloading, which improves code flexibility and readability.

1) Compile Time polymorphism (static binding)
When two or more methods in the same class have the same name but different parameters (different types or numbers of parameters), this is known as compile-time polymorphism, sometimes referred to as static binding or method overloading.

In this example, the Calculator class has two methods named add with different parameter types. The appropriate method is selected at compile time based on the method's invocation and the parameter's type or number of parameters passed to the method

  1. Run-Time Polymorphism (Dynamic Binding):

Run-time polymorphism, also known as dynamic binding or method overriding, occurs when a subclass provides a specific implementation for a method that is already defined in its superclass. It allows the same method name to be used in both the superclass and the subclass.

In this example, the Animal class has a method named makeSound, and the Dog class overrides this method. At runtime, the actual type of the object (Dog) is determined, and the overridden method in the subclass is called.

3) Polymorphism Through Interfaces:

Interfaces provide another form of polymorphism in Java. Multiple classes can implement the same interface, and objects of these classes can be referred to using a reference to the interface type.

Example of Polymorphism Through Interfaces:

// Interface
interface SoundMaker {
    void makeSound();
}

// Classes implementing the interface
class Dog implements SoundMaker {
    public void makeSound() {
        System.out.println("Bark! Bark!");
    }
}

class Cat implements SoundMaker {
    public void makeSound() {
        System.out.println("Meow!");
    }
}

public class Main {
    public static void main(String[] args) {
        SoundMaker myDog = new Dog();
        SoundMaker myCat = new Cat();

        myDog.makeSound();  // Calls the makeSound method in Dog: Bark! Bark!
        myCat.makeSound();  // Calls the makeSound method in Cat: Meow!
    }
}

In this example, both the Dog and Cat classes implement the SoundMaker interface, providing their own implementations of the makeSound method. Objects of these classes can be referred to using a reference of the interface type (SoundMaker), promoting polymorphism

Abstraction in Java

In Java, abstraction refers to the idea of hiding a class's or method's complex implementation details and only showing its key components to the outside world. By concentrating on an object's function rather than its mechanism, it helps in the management of complexity.

Here's a simple example to illustrate abstraction in Java:

Let's say we have a class called Vehicle:

public abstract class Vehicle {
    // Abstract method (does not have a body)
    public abstract void drive();
}

In this example, Vehicle is an abstract class, marked by the keyword abstract. It contains an abstract method drive(). An abstract method is a method without a body, meaning it doesn't have any implementation details. It's like a blueprint for methods that will be implemented by subclasses.

Now, let's create a subclass called Car that extends the Vehicle class and implements the drive() method:

public class Car extends Vehicle {
    // Implementing the abstract method drive()
    public void drive() {
        System.out.println("Car is being driven.");
    }
}

In this Car class, we provide the implementation for the drive() method by simply printing a message indicating that the car is being driven.

Similarly, we can create another subclass called Motorcycle:

public class Motorcycle extends Vehicle {
    // Implementing the abstract method drive()
    public void drive() {
        System.out.println("Motorcycle is being ridden.");
    }
}

Here, the Motorcycle class also provides its own implementation of the drive() method.

Now, let's see how abstraction works in practice:

public class Main {
    public static void main(String[] args) {
        Vehicle car = new Car(); // Creating a Car object
        Vehicle motorcycle = new Motorcycle(); // Creating a Motorcycle object

        car.drive(); // Calling the drive method on Car object
        motorcycle.drive(); // Calling the drive method on Motorcycle object
    }
}

In this Main class, we create objects of both Car and Motorcycle classes and call the drive() method on each object. We don't need to know the internal details of how each vehicle is driven; we simply use the drive() method provided by the Vehicle class, which represents the abstraction of driving behavior. This way, abstraction allows us to focus on the essential functionality (driving) without worrying about the specific implementation details of each vehicle type.