Java Part 9: Object-oriented Programming In Java (OOP Pillars)

Java Part 9: Object-oriented Programming In Java (OOP Pillars)


Please Subscribe Youtube| Like Facebook | Follow Twitter

Object-Oriented Programming (OOP Pillars) In Java

In this article, we will provide a detailed overview of Object-Oriented Programming In Java (OOP Pillars) and provide examples of how they are used in Java programming.

The Pillars of Object-Oriented Programming

OOP is built upon four key pillars: encapsulation, inheritance, polymorphism, and abstraction. In this article we explore them as in previous article we explored classes and objects.

Encapsulation

Encapsulation is one of the fundamental principles of object-oriented programming. It refers to the bundling of data and methods within a class, where the data is kept private and can only be accessed through public methods. In Java, encapsulation is achieved using access modifiers, which specify the visibility and accessibility of class members (variables and methods) from other parts of the program.

There are four levels of encapsulation in Java, determined by different access modifiers:

Public: Members declared as public are accessible from anywhere, both within the class itself and from other classes in the same or different packages. They have the highest level of accessibility.

Protected: Members declared as protected are accessible within the same class, subclasses (even if they are in different packages), and other classes within the same package. Protected members are not accessible outside the package if they are not inherited.

Default (Package-Private): Members declared without an explicit access modifier (i.e., no access modifier specified) are considered to have default accessibility. Default members are accessible within the same class and other classes within the same package, but not accessible outside the package.

Private: Members declared as private are only accessible within the same class. They have the most restricted level of accessibility and are not accessible from any other class, even subclasses.

Example

Below example code snippet that demonstrates encapsulation levels using different access modifiers:

public class EncapsulationExample {
    public static void main(String[] args) {
        Car car = new Car();
        
        // Accessing and modifying encapsulated members
        car.setMake("Toyota");     // Using the public setter method for 'make'
        car.model = "Corolla";     // Direct access to 'model' since it's protected (within the same package)
        car.color = "Blue";        // Direct access to 'color' since it's default (within the same package)
        car.year = 2022;           // Direct access to 'year' since it's public
        
        // Accessing encapsulated members using getter methods
        String carMake = car.getMake();     // Using the public getter method for 'make'
        
        // Displaying car details
        System.out.println("Car Details:");
        car.displayDetails();
    }
}

class Car {
    private String make;        // Private member
    protected String model;    // Protected member
    String color;               // Default (Package-Private) member
    public int year;            // Public member
    
    // Public getter and setter methods for the private member 'make'
    public String getMake() {
        return make;
    }
    
    public void setMake(String make) {
        this.make = make;
    }
    
    // Method to display the details of the car
    public void displayDetails() {
        System.out.println("Make: " + make);
        System.out.println("Model: " + model);
        System.out.println("Color: " + color);
        System.out.println("Year: " + year);
    }
}

Output

Car Details:
Make: Toyota
Model: Corolla
Color: Blue
Year: 2022

In this example, we have a Car class that represents a car object. The class has four member variables: make, model, color, and year. Each member variable is declared with a different access modifier to demonstrate different encapsulation levels.

make is declared as private, so it can only be accessed within the Car class. Getter and setter methods (getMake() and setMake()) are used to interact with the private member variable.

model is declared as protected, allowing it to be accessed within the same package as well as by subclasses (even if they are in different packages).

color is declared without an explicit access modifier, making it default or package-private. It can be accessed within the same package.

year is declared as public, making it accessible from anywhere.

The EncapsulationExample class demonstrates how encapsulated members of the Car class are accessed and modified using appropriate access methods or by directly accessing them based on their access levels.

Overall, this example showcases different encapsulation levels in Java and how access modifiers control the visibility and accessibility of class members to achieve encapsulation.

Inheritance

Inheritance is a fundamental concept in object-oriented programming that allows classes to inherit properties and behaviors from other classes. It promotes code reuse, modularity, and extensibility. In Java, inheritance is achieved using the “extends” keyword. Java supports single inheritance, meaning a class can inherit from only one superclass.

Super keyword

The super keyword in Java is used to refer to the parent class (superclass) from within a subclass. It can be used to access the superclass’s members (variables or methods), invoke the superclass’s constructor, or differentiate between superclass(parent) and subclass(child) members with the same name.

Here’s an example code demonstrating inheritance

// Main class
public class Main {
    public static void main(String[] args) {
        Car car = new Car("Toyota", 4);
        car.start();         // Overridden method with super invocation
        car.displayBrand();  // Accessing superclass and subclass variables
    }
}

// Parent class
class Vehicle {
    protected String brand;

    public Vehicle(String brand) {
        this.brand = brand;
    }

    public void start() {
        System.out.println("Starting the vehicle.");
    }

    public String getBrand() {
        return brand;
    }
}

// Child class inheriting from Vehicle
class Car extends Vehicle {
    private int numDoors;

    public Car(String brand, int numDoors) {
        super(brand); // Invoking the superclass constructor
        this.numDoors = numDoors;
    }

    public void start() {
        super.start(); // Invoking the superclass method
        System.out.println("Warming up the engine.");
    }

    public void displayBrand() {
        System.out.println("Brand: " + super.getBrand()); // Accessing superclass variable using getter method
        System.out.println("Brand: " + this.brand); // Accessing subclass variable
    }
}

Output

Starting the vehicle.
Warming up the engine.
Brand: Toyota
Brand: Toyota

The example code showcases inheritance in Java. It includes a parent class Vehicle and a child class Car that inherits from Vehicle.

The Vehicle class has a protected brand variable, a constructor that accepts a brand parameter, a start() method that prints a message, and a getBrand() method that returns the brand value.

The Car class extends Vehicle and introduces a private numDoors variable. It has a constructor that takes both brand and numDoors parameters, invokes the superclass constructor using super(brand), and assigns the numDoors value. It also overrides the start() method to add a specific behavior and includes a displayBrand() method to demonstrate accessing the brand variable using super.getBrand() (inherited from the superclass) and this.brand (in the subclass).

The Main class contains the main() method, where an instance of Car is created, and its methods are invoked to observe the behavior of inheritance, method overriding, and accessing superclass and subclass variables.

This example illustrates the usage of the extends keyword to establish an inheritance relationship between classes, allowing child classes to inherit and extend the properties and behaviors of the parent class.

Polymorphism

Polymorphism is a fundamental concept in object-oriented programming that allows objects of different classes to be treated as objects of a common superclass. It provides flexibility and extensibility by enabling objects to be used interchangeably and to exhibit different behaviors based on their actual class at runtime.

There are two main types of polymorphism in Java

Method Overloading (Compile-time Polymorphism)

Method overloading is a feature in Java that allows a class to have multiple methods with the same name but different parameters. It enables developers to define several methods with different input parameters but the same method name, allowing for flexibility and code reusability.

Method Overriding (Runtime Polymorphism)

Method overriding occurs when a subclass provides its own implementation of a method that is already defined in its superclass. The subclass method must have the same name, return type, and parameters as the method in the superclass. This allows the subclass to provide specialized behavior while maintaining the same method signature.

The method signature in Java is a combination of the method name and the parameter list. It specifies the unique identifier of a method and helps distinguish it from other methods in the same class. The method signature does not include the return type, access modifiers, or exceptions. It is determined by the method name and the types, order, and number of parameters accepted by the method.

Example:

public class PolymorphismExample {
    public static void main(String[] args) {
        Shape shape1 = new Circle();
        Shape shape2 = new Square();
        
        shape1.draw(); // Output: Drawing a circle. (Method Overriding)
        shape1.draw("Red"); // Output: Drawing a shape with color: Red (Method Overloading)
        
        shape2.draw(); // Output: Drawing a square. (Method Overriding)
        shape2.draw("Blue"); // Output: Drawing a shape with color: Blue (Method Overloading)
        
        Circle circle = new Circle();
        circle.draw(5); // Output: Drawing a circle with radius: 5 (Method Overloading)
        
        Square square = new Square();
        square.draw(4.5); // Output: Drawing a square with side: 4.5 (Method Overloading)
    }
}

class Shape {
    // Method Overriding
    public void draw() {
        System.out.println("Drawing a shape.");
    }
    
    // Method Overloading
    public void draw(String color) {
        System.out.println("Drawing a shape with color: " + color);
    }
}

class Circle extends Shape {
    // Method Overriding
    @Override
    public void draw() {
        System.out.println("Drawing a circle.");
    }
    
    // Method Overloading
    public void draw(int radius) {
        System.out.println("Drawing a circle with radius: " + radius);
    }
}

class Square extends Shape {
    // Method Overriding
    @Override
    public void draw() {
        System.out.println("Drawing a square.");
    }
    
    // Method Overloading
    public void draw(double side) {
        System.out.println("Drawing a square with side: " + side);
    }
}

Output

Drawing a circle.
Drawing a shape with color: Red
Drawing a square.
Drawing a shape with color: Blue
Drawing a circle with radius: 5
Drawing a square with side: 4.5

In this example, we have a Shape class as the superclass, and Circle and Square classes as subclasses. The Shape class has two overloaded draw() methods—one without any parameters and another with a String parameter for specifying the color. The Circle class overrides the draw() method from the superclass and introduces an additional overloaded draw() method that takes an int parameter for specifying the radius. Similarly, the Square class overrides the draw() method from the superclass and introduces an additional overloaded draw() method that takes a double parameter for specifying the side length.

The main() method demonstrates polymorphism by creating instances of Circle and Square and storing them in variables of type Shape. The methods are called on these variables, invoking the appropriate overridden or overloaded methods based on the actual object type.

Abstraction

Abstraction is a fundamental concept in object-oriented programming that focuses on hiding unnecessary implementation details and exposing only the essential features and behaviors of an object. It allows us to create abstract classes and interfaces that define a common structure and behavior for a group of related classes, without providing the actual implementation.

Abstract Class

An abstract class is a class that cannot be instantiated and can only be used as a superclass for other classes. It may contain both abstract and non-abstract methods. Abstract methods are declared without an implementation and must be overridden by the concrete subclasses.

Interface

An interface is a collection of abstract methods that define a contract for implementing classes. It is similar to an abstract class but can only contain abstract methods and constants. Classes can implement one or more interfaces to inherit their abstract methods.

By using abstraction through abstract classes and interfaces, we can define common behaviors and structures, enforce method implementation in subclasses, and achieve loose coupling and code modularity.

Example

Now, let’s proceed with an example code that demonstrates abstraction using an abstract class and an interface.

public class AbstractionExample {
    public static void main(String[] args) {
        Vehicle car = new Car("Toyota");
        car.displayBrand(); // Output: Brand: Toyota
        car.start();        // Output: Car is starting.
        
        Engine engine = new Car("Honda");
        engine.run();       // Output: Car engine is running.
    }
}

// Abstract class representing a vehicle
abstract class Vehicle {
    private String brand;
    
    public Vehicle(String brand) {
        this.brand = brand;
    }
    
    public abstract void start();   // Abstract method
    
    public void displayBrand() {
        System.out.println("Brand: " + brand);
    }
}

// Interface representing a vehicle engine
interface Engine {
    void run();
}

// Concrete class representing a car
class Car extends Vehicle implements Engine {
    public Car(String brand) {
        super(brand);
    }
    
    @Override
    public void start() {
        System.out.println("Car is starting.");
    }
    
    @Override
    public void run() {
        System.out.println("Car engine is running.");
    }
}

Output

Brand: Toyota
Car is starting.
Car engine is running.

In this example, we have an abstract class called Vehicle representing a general concept of a vehicle. It has an abstract method start() that must be implemented by its subclasses. The Vehicle class also has a non-abstract method displayBrand() that displays the brand of the vehicle.

Next, we have an interface called Engine that defines the contract for implementing classes to have an run() method.

The Car class is a concrete class that extends the Vehicle abstract class and implements the Engine interface. It provides its own implementation for the start() method from the Vehicle class and the run() method from the Engine interface.

In the main() method of the AbstractionExample class, we create an instance of Car and store it in a variable of type Vehicle. We invoke the displayBrand() method to display the brand of the vehicle, and the start() method to start the car. Additionally, we create another instance of Car and store it in a variable of type Engine. We invoke the run() method to simulate the engine running.

This example demonstrates abstraction by utilizing an abstract class and an interface to define common behaviors and structures, enforce method implementations, and achieve loose coupling and code modularity.

Differences between an abstract class and an interface in Java

TermAbstract ClassInterface
DefinitionA class that cannot be instantiated and may contain both concrete and abstract methods, as well as member variablesA reference type that defines a contract for classes to implement, containing only abstract methods and constants
InheritanceCan be extended by a subclass using the ‘extends’ keywordCan be implemented by a class using the ‘implements’ keyword
MembersCan have both abstract and non-abstract methodsCan only have abstract methods and constants
Method BodiesCan have method bodies (concrete implementation)Cannot have method bodies (only method signatures)
VariablesCan have instance variables and constantsCan only have constants (public static final variables)
ConstructorCan have a constructorCannot have a constructor
MultipleCannot extend multiple classesCan implement multiple interfaces

Conclusion

Encapsulation, inheritance, polymorphism, and abstraction are four fundamental concepts in Java object-oriented programming. Together, they provide a powerful and flexible foundation for creating modular, maintainable, and extensible code.

Java Beginner Tutorial Series

Please Subscribe Youtube| Like Facebook | Follow Twitter


Leave a Reply

Your email address will not be published. Required fields are marked *