Java Mastery: Top 3 Powerful Strategies for Object-Oriented Programming Success

Table of Contents

Java, known for its versatility and portability, has been a stalwart in the world of programming for decades. One of its key strengths lies in its support for Object-Oriented Programming (OOP), a paradigm that facilitates modular and organized code. To truly master Java, one must delve deep into the intricacies of OOP. In this blog, we will explore powerful strategies that will elevate your Java OOP skills and set you on the path to programming success.

Understanding Object-Oriented Programming (OOP)

Before diving into Java-specific strategies, it’s crucial to have a solid understanding of OOP fundamentals. Grasp concepts like data hiding, data abstraction, encapsulation, inheritance, and polymorphism. These pillars form the foundation of Java’s OOP paradigm.

Object-Oriented Programming (OOP)

Data Hiding:

Data hiding is an object-oriented programming (OOP) feature where external entities are prevented from directly accessing our data. This means that our internal data should not be exposed directly to the outside. Through the use of encapsulation and access control mechanisms, such as validation, we can restrict access to our own functions, ensuring that only the intended parts of the program can interact with and manipulate the data. This helps enhance the security and integrity of the codebase.

Java
public class Account {
    private int balance;

    public Account() {
        this.balance = 0; // Initial balance is set to zero
    }

    public int getBalance() {
        return balance;
    }

    public void deposit(int amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("Deposited: " + amount);
        } else {
            System.out.println("Invalid deposit amount");
        }
    }

    public void withdraw(int amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println("Withdrawn: " + amount);
        } else {
            System.out.println("Invalid withdrawal amount or insufficient balance");
        }
    }
}

 

In the above example, the concept of data hiding is implemented through the use of private access modifiers for the balance field. Let’s break down how this example adheres to the principle of data hiding:

Private Access Modifier:

Java
private int balance;

 

The balance field is declared as private. This means that it can only be accessed within the Account class itself. Other classes cannot directly access or modify the balance field.

Encapsulation:

The concept of data hiding is closely tied to encapsulation. Encapsulation involves bundling data and methods that operate on that data into a single unit or class. We will explore this further later. In this context, the balance field and the associated methods (getBalance, deposit, withdraw) are integral components of the Account class.

Public Interface:

The class provides a public interface (getBalance, deposit, withdraw) through which other parts of the program can interact with the Account object. Class users don’t need to know the internal details of how the balance is stored or manipulated; they interact with the public methods.

Controlled Access:

By keeping the balance field private, the class can control how it is accessed and modified. The class can enforce rules and validation (like checking for non-negative amounts in deposit and withdrawal) to ensure that the object’s state remains valid.

In short, data hiding in this example is achieved by making the balance field private, encapsulating it within the Account class, and providing a controlled public interface for interacting with the object. This helps maintain a clear separation between the internal implementation details and the external usage of the class.

Data Abstractions

Data Abstraction involves concealing the internal implementation details and emphasizing a set of service offerings. An example of this is an ATM GUI screen. Instead of exposing the intricate workings behind the scenes, the user interacts with a simplified interface that provides specific services. This abstraction allows users to utilize the functionality without needing to understand or interact with the complex internal processes.

Encapsulation

Encapsulation is the binding of data members and methods (behavior) into a single unit, namely a class. It encompasses both data hiding and abstraction. In encapsulation, the internal workings of a class, including its data and methods, are encapsulated or enclosed within the class itself. This means that the implementation details are hidden from external entities, and users interact with the class through a defined interface. The combination of data hiding and abstraction in encapsulation contributes to the organization and security of an object-oriented program.

Java
public class Person {
    private String name;
    private int age;

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

    // Getter for name
    public String getName() {
        return name;
    }

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

    // Getter for age
    public int getAge() {
        return age;
    }

    // Setter for age
    public void setAge(int age) {
        if (age > 0) {
            this.age = age;
        } else {
            System.out.println("Invalid age");
        }
    }
}

 

The above class encapsulates the data (name and age) and the methods that operate on that data. Users of the Person class can access the information through the getters and modify it through the setters, but they don’t have direct access to the internal fields.

Using encapsulation in this way helps to control access to the internal state of the Person object allows for validation and additional logic in the setters, and provides a clean and understandable interface for interacting with Person objects.

Tightly Encapsulated Class

A tightly encapsulated class is a class that enforces strict data hiding by declaring all of its data members (attributes) as private. This means that the data members can only be accessed and modified within the class itself, and not directly from other classes. This helps to protect the integrity of the data and prevent it from being unintentionally or maliciously modified.

Java
// Superclass (Parent class)
class Animal {
    private String species;

    // Constructor
    public Animal(String species) {
        this.species = species;
    }

    // Getter for species
    public String getSpecies() {
        return species;
    }
}

// Subclass (Child class)
class Dog extends Animal {
    private String breed;

    // Constructor
    public Dog(String species, String breed) {
        super(species);
        this.breed = breed;
    }

    // Getter for breed
    public String getBreed() {
        return breed;
    }
}

public class EncapsulationExample {
    public static void main(String[] args) {
        // Creating an instance of Dog
        Dog myDog = new Dog("Canine", "Labrador");

        // Accessing information through getters
        System.out.println("Species: " + myDog.getSpecies());
        System.out.println("Breed: " + myDog.getBreed());
    }
}

 

This example demonstrates a tightly encapsulated class structure where both the superclass (Animal) and the subclass (Dog) have private variables and provide getters to access those variables. This ensures that the internal state of objects is not directly accessible from outside the class hierarchy, promoting information hiding and encapsulation.

Inheritance (IS-A Relationships)

An IS-A relationship, also known as inheritance, is a fundamental concept in object-oriented programming (OOP) that allows a class to inherit the properties and methods of another class. This is achieved using the extends keyword in Java.

The main advantage of using IS-A relationships is code reusability. By inheriting from a parent class, a subclass can automatically acquire all of the parent class’s methods and attributes. This eliminates the need to recode these methods and attributes in the subclass, which can save a significant amount of time and effort.

Additionally, inheritance promotes code modularity and maintainability. By organizing classes into a hierarchical structure, inheritance makes it easier to understand the relationships between classes and to manage changes to the codebase. When a change is made to a parent class, those changes are automatically reflected in all of its subclasses, which helps to ensure that the code remains consistent and up-to-date.

Java
public class P {
    public void m1() {
        System.out.println("m1");
    }
}

public class C extends P {
    public void m2() {
        System.out.println("m2");
    }
}

 

There are two classes: P (parent class) and C (child class).

The child class C extends the parent class P, indicating an IS-A relationship, and it uses the extends keyword for inheritance.

Case 1: Parent class cannot called child methods

Java
P p1 = new P();
p1.m1();   // Calls m1 from class P

p1.m2(); // Results in a compilation error, as m2 is not defined in class P

 

Case 2: Child class called Parent and its own method, if it extends the Parent class.

Java
C c1 = new C();
c1.m1();   // Calls m1 from class P (inherited)
c1.m2();   // Calls m2 from class C

 

Case 3: Parent reference can hold child object but by using this it only calls parent methods, child-specific can’t be called

Java
P p2 = new C();
p2.m1();   // Calls m1 from class P (inherited)

p2.m2(); // Results in a compilation error, as m2 is not defined in class P

 

Case 4: Child class reference can not hold parent class object

Java
C c2 = new P(); // Not possible, results in a compilation error

 

In short, this example demonstrates the basic principles of inheritance, polymorphism, and the limitations on method access based on the type of reference used. The use of extends signifies that C is a subclass of P, inheriting its properties and allowing for code reusability.

Multiple Inheritance

Java doesn’t support multiple inheritance in classes, meaning that a class can extend only one class at a time; extending multiple classes simultaneously is not allowed. This restriction is in place to avoid the ambiguity problems that arise in the case of multiple inheritance.

In multiple inheritance, if a class A extends both B and C, and both B and C have a method with the same name, it creates ambiguity regarding which method should be inherited. To prevent such ambiguity, Java allows only single inheritance for classes.

However, it’s important to note that Java supports multilevel inheritance. For instance, if class A extends class B, and class B extends Object (the default superclass for all Java classes), then it is considered multilevel inheritance, not multiple inheritance.

In the case of interfaces, Java supports multiple inheritance because interfaces provide only method signatures without implementation. Therefore, a class can implement multiple interfaces with the same method name, and the implementing class must provide the method implementations. This avoids the ambiguity problem associated with multiple inheritance in classes.

Cyclic inheritance is not allowed in Java. Cyclic inheritance occurs when a class extends itself or when there is a circular reference, such as class A extends B and class B extends A. Java prohibits such cyclic inheritance to maintain the integrity and clarity of the class hierarchy.

HAS-A relationships

HAS-A relationships, also known as composition or aggregation, represent a type of association between classes where one class contains a reference to another class. This relationship indicates that an object of the containing class “has” or owns an object of the contained class.

Consider a Car class that contains an Engine object. This represents a HAS-A relationship, as the Car “has” an Engine.

Java
class Engine {
  // Engine specific functionality in m1 method
}

class Car {
  Engine e = new Engine();

  void start() {
    e.m1();
  }
}

 

In this case, we say that “Car HAS-A Engine reference.”

Composition vs. Aggregation

Composition and aggregation are two types of HAS-A relationships that differ in the strength of the association between the classes:

Composition: 

Composition signifies a strong association between classes. In composition, one class, known as the container object, contains another class, referred to as the contained object. An example is the relationship between a University (container object) and a Department (contained object). In composition, the existence of the contained object depends on the container object. Without an existing University object, a Department object doesn’t exist.

Java
class University {
  Department department = new Department();
}

class Department {
  // Department-specific functionality
}

 

Here, University a class might contain an Department object. This represents a composition relationship, as they (Department) can not exist without the University.

Aggregation: 

Aggregation represents a weaker association between classes. An example is the relationship between a Department (container object) and Professors (contained object). In aggregation, the existence of the contained object doesn’t entirely depend on the container object. Professors may exist independently of any specific Department.

Java
class Department {
  List<Professor> professors = new ArrayList<>();
}

class Professor {
  // Professor-specific functionality
}

 

Here, Department class might contain a list of Professors. This represents an aggregation relationship, as they (Professors) can exist without the Department.

When to Use HAS-A Relationships

When choosing between IS-A (inheritance) and HAS-A relationships, consider the following guideline: if you need the entire functionality of a class, opt for IS-A relationships. On the other hand, if you only require specific functionality, choose HAS-A relationships.

HAS-A relationships, also known as compositions or aggregations, don’t use specific keywords like “extends” in IS-A relationships. Instead, the “new” keyword is used to create an instance of the contained class. HAS-A relationships are often employed for reusability purposes, allowing classes to be composed or aggregated to enhance flexibility and modularity in the codebase.

HAS-A relationships are a fundamental concept in object-oriented programming that allows you to model complex relationships between objects. Understanding the distinction between composition and aggregation and when to use HAS-A vs. IS-A relationships is crucial for designing effective object-oriented software.

Method Overloading

Before exploring polymorphism, it’s essential to understand method signature and related concepts.

Method Signature

A method signature is a concise representation of a method, encompassing its name and the data types of its parameters. It does not include the method’s return type. The compiler primarily uses the method signature to identify and differentiate methods during method calls.

Here’s an example of a method signature:

Java
public static int m1(int i, float f)

 

This signature indicates a method named m1 that takes two parameters: int i and float f, and returns an int.

Method Overloading

Method overloading refers to the concept of having multiple methods with the same name but different parameter signatures within a class. This allows for methods to perform similar operations with different data types or a different number of arguments.

Consider the following methods:

Java
public void m1(int i) {
  // Method implementation
}

public int m1(float f) {
  // Method implementation
}

 

These two methods are overloaded because they share the same name (m1) but have different parameter signatures.

Method Resolution

Method resolution is the process by which the compiler determines the specific method to be invoked when a method call is encountered. The compiler primarily relies on the method signature to identify the correct method.

In the case of method overloading, the compiler resolves the method call based on the reference types of the arguments provided. This means that the method with the parameter types matching the argument types is chosen for execution.

Compile-Time Polymorphism

Method overloading is also known as compile-time polymorphism, static polymorphism, or early binding polymorphism. This is because the method to be invoked is determined during compilation, based on the method signature and argument types.

Method Overloading Loopholes and Ambiguities

Method overloading is a powerful feature of object-oriented programming that allows multiple methods with the same name to exist within a class, provided they have different parameter types. However, this flexibility can also lead to potential loopholes and ambiguities that can cause unexpected behavior or compiler errors.

Case 1: Implicit Type Promotion

Java employs implicit type promotion, where a value of a smaller data type is automatically converted to a larger data type during method invocation. This can lead to unexpected method calls if the compiler promotes an argument to a type that matches an overloaded method.

For instance, in the below code:

Java
public class Test {
    public void m1(int i) {
        System.out.println("int-arg");
    }

    public void m1(float f) {
        System.out.println("float-arg");
    }

    public static void main(String[] args) {
        Test t1 = new Test();
        t1.m1(10);     // Output: int-arg
        t1.m1(10.5f);   // Output: float-arg
        t1.m1('a');     // Output: int-arg
        t1.m1(10L);     // Output: float-arg
        t1.m1(10.5);  // Compilation Error: cannot find symbol method m1(double) in Test class
    }
}

 

byteshortintlongfloatdouble

charintlongfloatdouble

The provided code calls a specific method if the exact argument types match. However, if an exact match is not found, the arguments are promoted to the next level, and this process continues until all checks are completed.

Calling t1.m1(10l) results in the “float-arg” output because long is automatically promoted to float. However, calling t1.m1(10.5) causes a compiler error because there’s no m1(double) method. This highlights the potential for implicit type promotion to lead to unexpected method calls.

Case 2: Inheritance and Method Resolution

In Java, inheritance plays a role in method resolution. If a class inherits multiple methods with the same name from its parent classes, the compiler determines the method to invoke based on the reference type of the object.

Consider the following example:

Java
public void m1(String s) {
    System.out.println("String-Version");
}

public void m1(Object o) {
    System.out.println("Object-Version");
}

 

If we call these overloaded methods:

Java
public static void main(String[] args) {
        Test t1 = new Test();
        t1.m1("Amol Pawar");            // Output: String-Version
        t1.m1(new Object());      // Output: Object-Version
        t1.m1(null);              // Output: String-Version
}

 

In the case of overloading with String and Object, when a String argument is passed, the method with the String parameter is chosen. However, if null is passed, the compiler chooses the String version because String extends Object.

Case 3: Ambiguity with String and StringBuffer

When passing null to overloaded methods that accept both String and StringBuffer, a compiler error occurs: “reference to m1() is ambiguous”. This is because null can be considered both a String and a StringBuffer, leading to ambiguity in method resolution.

Case 4: Ambiguity with Different Order of Arguments

If two overloaded methods have the same parameter types but in different orders, a compiler error occurs if only one argument is passed. This is because the compiler cannot determine the intended method without both arguments.

For instance, if methods m1(int, float) and m1(float, int) exist, passing only an int or float value will result in a compiler error.

Java
public void m1(int i, float f) { ... }
public void m1(float f, int i) { ... }

 

If we pass only an int or float value, a compilation error occurs because the compiler cannot decide which method to call.

Case 5: Varargs Method Priority

In the case of varargs methods, if a general method and a varargs method are present, the general method gets priority. Varargs has the least priority in method resolution. This is because var-args methods were introduced in Java 1.5, while general methods have been available since Java 1.0.

Case 6: Method Resolution and Runtime Object

Method resolution in method overloading is based on the reference type of the object, not the runtime object. This means that if a subclass object is passed as a reference to its superclass, the method defined in the superclass will be invoked, even if the actual object is a subclass instance.

For example, if Class Monkey extends Animal and m1(Animal) and m1(Monkey) methods exist, passing an Animal reference that holds a Monkey object will invoke the m1(Animal) method.

Method Overriding

Method overriding is a mechanism in object-oriented programming where, if dissatisfied with the implementation of a method in the parent class, a child class provides its own implementation with the same method signature.

In the context of method overriding:

  • The method in the parent class is referred to as the overridden method.
  • The method in the child class providing its own implementation is referred to as the overriding method.
Java
class Parent {
    void marry() {
        System.out.println("Parent's choice");
    }
}

class Child extends Parent {
    @Override
    void marry() {
        System.out.println("Child's choice");
    }
}

 

Java
Parent p = new Parent();
p.marry();  // calls the parent class method

Child c = new Child();
c.marry();  // calls the child class method

Parent pc = new Child();
pc.marry();  // calls the child class method; runtime polymorphism in action

 

In the last example, even though the reference is of type Parent, the JVM checks at runtime whether the actual object is of type Child. If so, it calls the overridden method in the child class.

Method Resolution in Overriding

Method resolution in method overriding always takes place at runtime, and it is handled by the Java Virtual Machine (JVM). The JVM checks if the runtime object has any overriding method. If it does, the overriding method is called; otherwise, the superclass method is invoked.

Here are a few important points to remember:

  • Method resolution in method overriding always takes place at runtime by the JVM.
  • This phenomenon is known as runtime polymorphism, dynamic binding, or late binding.
  • The method called is determined by the actual runtime type of the object rather than the reference type.

This dynamic method resolution allows for flexibility and extensibility in the code, as it enables the use of different implementations of the same method based on the actual type of the object at runtime.

Rules for Method Overriding

Here are the rules and considerations regarding method overriding in Java:

Method Signature

The method name and argument types must be the same in both the parent and child class.

Return Type

  • The return type should be the same in the parent and child classes.
  • Co-variant return types are allowed from Java 1.5 onwards. This means the child method can have the same or a subtype of the return type in the parent method.
  • For example, if the parent method returns an object, the child method can return a more specific type like String or StringBuffer. Similarly, if the parent method returns a type like Number, the child methods can return more specific types like Integer, Float, or Double. This makes Java methods more expressive and versatile.
  • Co-variant return types are not applicable to primitive types.
Java
// Valid co-variant return type
class Parent {
    Object m1() { ... }
}
class Child extends Parent {
    String m1() { ... }
}

 

Private and Final Methods

  • Private methods in the parent class can be used in the child class with exactly the same private method based on requirements. This is valid but is not considered method overriding. Method overriding concept is not applicable to private methods.
  • Final methods cannot be overridden in the child class. A final method has a constant implementation that cannot be changed.

Abstract Methods

Abstract methods in an abstract class must be overridden in the child class. Non-abstract methods in the parent class can also be overridden in the child class, but if overridden, the child class must be declared abstract.

Modifiers

There are no restrictions on abstract, synchronized, strictfp, and native modifiers in method overriding.

Scope of Access Modifiers

  • While overriding, you cannot reduce the scope of access modifiers. You can, however, increase the scope. The order of accessibility is private < default < protected < public.
  • Method overriding is not applicable to private methods. Private methods are only accessible within the class in which they are defined.
  • In public methods, you cannot reduce the scope. However, in protected methods, you can reduce the scope to protected or public. Similarly, in default methods, you can reduce the scope to default, protected, or public.
  • For example, if the parent method is public, the child method can be public or protected but not private.
Java
class Parent {
    // Public method in the parent class
    public void display() {
        System.out.println("Public method in the Parent class");
    }
}

class Child extends Parent {
    // Valid override: Increasing the scope from public to protected
    protected void display() {
        System.out.println("Protected method in the Child class");
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
        child.display(); // Outputs: Protected method in the Child class
    }
}

 

In this example, the display method in the Child class overrides the display method in the Parent class. The access level is increased from public to protected, which is allowed during method overriding.

These rules ensure that method overriding maintains consistency, adheres to the principles of object-oriented programming, and prevents unintended side effects.

Why we can’t reduce scope in method overriding?

The principle of not reducing the scope in method overriding is tied to the concept of substitutability and the Liskov Substitution Principle, which is one of the SOLID principles in object-oriented design.

When you override a method in a subclass, it’s essential to maintain compatibility with the superclass. If a client code is using a reference to the superclass to access an object of the subclass, it should be able to rely on the same level of accessibility for the overridden method. Reducing the scope could potentially break this contract.

Let’s break down the reasons:

  1. Substitutability: Method overriding is a way of providing a specific implementation in a subclass that is substitutable for the implementation in the superclass. Substitutability implies that wherever an object of the superclass is expected, you should be able to use an object of the subclass without altering the correctness of the program.
  2. Client Expectations: Clients (other parts of the code using the class hierarchy) expect a certain level of accessibility for methods. Reducing the scope could lead to unexpected behavior for client code that relies on the superclass interface.
  3. Security and Encapsulation: Allowing a subclass to reduce the scope of a method could potentially violate the encapsulation principle, as it might expose implementation details that were intended to be private.

Consider the following example:

Java
class Parent {
    public void doSomething() {
        // implementation
    }
}

class Child extends Parent {
    // This would break substitutability and client expectations
    // as the method becomes less accessible
    private void doSomething() {
        // overridden implementation
    }
}

If you were able to reduce the scope in the child class, code that expects a Parent reference might not be able to access doSomething, violating the contract expected from a subclass.

In short, not allowing a reduction in scope during method overriding is a design choice to ensure that the principle of substitutability is maintained and client code expectations are not violated.

Additional Rules for Method Overriding

Come back to our discussion and continuing with the few more rules for method overriding in Java:

Checked and Unchecked Exceptions

In the case of checked exceptions, the child class must always throw the same checked exception as thrown by the parent class method or its subclass. However, this rule is not applicable to unchecked exceptions, so there are no restrictions in that case.

Static Methods

A non-static method cannot override a static method, and a static method cannot override a non-static method. Static methods are associated with the class itself, not with individual objects, and their resolution is based on the class name, not the object reference.

Attempting to override a static method with a non-static method or vice versa results in a compiler error because it violates the principle of static methods being bound to classes, not objects.

Method Hiding with Static Methods

  • If a static method is used with the same signature in the child class, it is not considered method overriding; instead, it is method hiding. This is because the static method resolution is based on the class name, not the object reference. In method hiding, the method resolution is always taken care of by the compiler based on the reference type of the parent class.

Example:

Java
class Parent {
    static void method() { ... }
}
class Child extends Parent {
    static void method() { ... } // It's method hiding, not overriding
}

In this case, if we use Parent reference to call the method, the compiler resolves it based on the reference type.

This is different from dynamic method overriding, where the method resolution is determined at runtime based on the actual object type.

Varargs Method Overloading

When a varargs method is used in the parent class, such as m1(int... x), it means you can pass any number of arguments, including no arguments (m1()). If you attempt to use the same varargs method in the child class, it is considered overloading, not overriding. Overloading occurs when you provide a different method in the child class, either with a different number or type of parameters.

Example:

Java
class Parent {
    void m1(int... x) { ... }
}

class Child extends Parent {
    // Overloading, not overriding
    void m1(int x, int y) { ... }
}

 

Overriding Not Applicable to Variables

Method overriding is a concept that applies to methods, not variables. Variables are resolved at compile time based on the reference type, and this remains the same regardless of whether the reference is to a parent class or a child class.

Static and non-static variables behave similarly in this regard. The static or non-static nature of a variable does not affect the concept of method overriding.

Java
class Parent {
    int x = 10;
}

class Child extends Parent {
    int x = 20; // Variable in Child, not overridden
}

 

In this case, if you use Parent reference to access the variable, the compiler resolves it based on the reference type.

Method Overloading Vs Method Overriding

Method Overloading Method Overriding
Method overloading occurs when two or more methods in the same class have the same name but different parameters (number, type, or order). Method overriding occurs when a subclass provides a specific implementation for a method that is already defined in its superclass.
Method overloading is determined at compile-time based on the method signature (name and parameter types). Method overriding is determined at runtime based on the actual type of the object.
The return type may or may not be different. Overloading is not concerned with the return type. The return type must be the same or a subtype of the return type in the superclass.
The access modifier can be different for overloaded methods. The overridden method cannot be more restrictive in terms of access; it can be the same or less restrictive.
Overloading can occur in the same class or its subclasses. Overriding occurs in a subclass that inherits from a superclass.

 

Polymorphism

Polymorphism, characterized by a single name representing multiple forms, encompasses method overloading, where the same name is used with different method signatures, and method overriding, where the same name is employed with distinct method implementations in both child and parent classes.

Additionally, the utilization of a parent reference to encapsulate a child object is demonstrated, such as a List reference being able to hold objects of ArrayList, LinkedList, Stack, and Vector. When the runtime object is uncertain, employing a parent reference to accommodate the object is recommended.

Java
List<String> myList = new ArrayList<>();
List<String> anotherList = new LinkedList<>();

Difference between P p = new C() and C c = new C()

  • P p = new C():
    • This uses polymorphism, where a parent reference (P) is used to hold a child object (C). The type of reference (P) determines which methods can be called on the object.
    • Only methods defined in the parent class (P) are accessible through the reference. If there are overridden methods in the child class (C), the overridden implementations are called at runtime.
  • C c = new C():
    • This creates an object of the child class (C) and uses a reference of the same type (C). This allows access to both the methods defined in the child class and those inherited from the parent class.

In short, the difference lies in the type of reference used, affecting the visibility of methods and the level of polymorphism achieved. Using a parent reference (P p = new C()) enhances flexibility and allows for interchangeable objects, while using a child reference (C c = new C()) provides access to all methods defined in both the parent and child classes.

Polymorphism Types

There are two main types of polymorphism:

Static polymorphism (Compile-time polymorphism/Early binding)

Static polymorphism occurs when the compiler determines which method to call based on the method signature, which is the method name and the number and type of its parameters. This type of polymorphism is also known as compile-time polymorphism or early binding because the compiler resolves the method call at compile time.

Examples – Method Overloading and Method Hiding
Dynamic polymorphism (Run-time polymorphism/Late binding)

Dynamic polymorphism occurs when the method to call is determined at runtime based on the dynamic type of the object. This means that the same method call can have different results depending on the actual object that is called upon. This type of polymorphism is also known as run-time polymorphism or late binding because the compiler does not determine the method call until runtime.

Example – Method Overriding

Three Pillars of Object-Oriented Programming (OOP)

The three pillars of object-oriented programming (OOP) are encapsulation, polymorphism, and inheritance. These three concepts form the foundation of OOP and are essential for designing well-structured, maintainable, and scalable software applications.

Encapsulation – Security: Encapsulation involves bundling data and the methods that operate on that data into a single unit, known as a class. It enhances security by restricting access to certain components, allowing for better control and maintenance of the code.

Polymorphism – Flexibility: Polymorphism provides flexibility by allowing objects of different types to be treated as objects of a common type. This can be achieved through method overloading and overriding, enabling code to adapt to various data types and structures.

Inheritance – Reusability: Inheritance allows a new class (subclass or derived class) to inherit attributes and behaviors from an existing class (base class or parent class). This promotes code reuse, as common functionality can be defined in a base class and inherited by multiple derived classes, reducing redundancy and enhancing maintainability.

Conclusion

Java’s Object-Oriented Programming, built upon encapsulation, inheritance, polymorphism, and abstraction, establishes a robust framework for crafting well-organized and efficient code. Proficiency in these principles is indispensable, whether you’re embarking on your coding journey or an experienced developer. This blog has covered essential aspects of Object-Oriented Programming (OOP). Nevertheless, there are pivotal advanced OOP features yet to be explored, and we intend to address them comprehensively in our forthcoming article.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!