1. Introduction

In programming, the concept of polymorphism plays a pivotal role, offering flexibility and versatility akin to its biological counterpart. Just as in nature where a species can manifest in various physical forms, in object-oriented programming (OOP), polymorphism empowers objects to exhibit multiple behaviors. Let's explore the essence of polymorphism and its manifestation in Java.

1.1 Overview of Polymorphism

The provided image illustrates two animals of the same species, each embodying a distinct form. In scientific terms, polymorphism occurs when diverse physical forms or types coexist within a species. In the programming realm, particularly in OOP, polymorphism refers to an object's ability to take on multiple forms.

A common scenario in polymorphism involves using a parent class reference to refer to a child class object. Consider the following code snippet:

P par_chi = new C();
par_chi.m1();

In this example, the parent class reference (P) is employed to reference a child class object named par_chi, and m1 is a method of class P.

It's crucial to understand that in Java, any object capable of passing more than one IS-A test is considered polymorphic. The IS-A relationship signifies that when one class inherits from another class, it establishes an IS-A relationship. Consequently, all Java objects inherently possess polymorphic characteristics, passing the IS-A test for their own type and the class object.

Accessing an object in Java is only possible through a reference variable. In the aforementioned code snippet, par_chi is referred to as the reference variable. Notably, a reference variable can have only one type, and once declared, its type remains unchangeable. While the reference variable can be reassigned to other objects, it is imperative to note that the type of the reference variable dictates the methods it can invoke on the object.

1.2 Types of Polymorphism in Java

In Java, polymorphism manifests in two primary forms: runtime polymorphism (dynamic polymorphism) and compile-time polymorphism (static polymorphism). Achieving polymorphism in Java is made possible through method overloading and method overriding.

Now, let's delve deeper into these facets of polymorphism to comprehend how they contribute to the versatility and dynamism of Java programming.

2. Exploring Polymorphism in Java

Polymorphism, a prominent feature of Object-Oriented Programming (OOP), empowers developers to execute a single action in diverse ways. In Java, polymorphism manifests in two distinct forms: compile-time polymorphism and runtime polymorphism. Before delving into polymorphism, it's essential to comprehend the disparities between compile-time and runtime in the context of Java.

2.1 Compile-time vs. Runtime in Java

Java, being a high-level programming language, necessitates the translation of source code into machine code for a computer to comprehend. Compile time refers to the phase during which the source code is transformed into bytecode (e.g., from .java to .class). The compiler scrutinizes the syntax, semantics, and type of the code during compile time. On the other hand, runtime signifies a program's lifecycle when it is in execution. The table below illustrates the distinctions between compile time and runtime:

Aspect Compile Time Runtime
Conversion Source code to bytecode Bytecode to machine code
Checks Syntax, semantics, and type Program execution

Now that we have clarified compile time and runtime, let's explore the two types of polymorphism in Java.

2.2 Types of Polymorphism in Java

Java exhibits polymorphism through two mechanisms: method overloading and method overriding. Before delving into these concepts, let's understand polymorphism through a simple example.

2.2.1 Example – Polymorphism in Java

Consider a scenario where we have a generic class Animal with a method sound(). Since this class is generic, we cannot provide specific implementations like 'Moo,' 'Meow,' 'Chirp,' or 'Roar.' Instead, a generic message is given in class Animal. Now, suppose we have two child classes: Cat and Bird, both extending Animal. Both child classes possess a method named sound() with different outputs. For Cat, the output is 'Meow,' and for Bird, the output is 'Chirp.'

class Animal {
  void sound() {
    System.out.println("Animal is making a sound");
  }
}

class Cat extends Animal {
  void sound() {
    System.out.println("Meow");
  }
}

class Bird extends Animal {
  void sound() {
    System.out.println("Chirp");
  }
}

In this example, despite both child classes having the same method (sound()), they produce different outputs. This exemplifies polymorphism, allowing a single action to be performed in different ways. Calling the generic sound() method for each animal would not make sense, as each animal has a distinct sound. Thus, the action performed by this method is contingent on the type of object.

This simple example lays the groundwork for understanding polymorphism in Java, paving the way for a more in-depth exploration of method overloading and method overriding.

3. Runtime Polymorphism and Method Overriding in Java

In Java, runtime polymorphism, also known as dynamic polymorphism, is achieved through method overriding. Method overriding involves declaring a method in the child class that is already present in the parent class. This allows the child class to provide its own implementation for a method already provided by the parent class. The method in the parent class is referred to as the overridden method, while the method in the child class is termed the overriding method.

As illustrated in the previous example (Example – Polymorphism in Java), the sound() method was defined with multiple implementations in different subclasses. The determination of which sound() method to call is made at runtime, exemplifying runtime polymorphism.

To further elucidate runtime polymorphism and method overriding, let's examine a comprehensive code example:

class Animal {
  void sound() {
    System.out.println("Animal is making a sound");
  }
}

class Cat extends Animal {
  void sound() {
    System.out.println("Meow");
  }
}

class Bird extends Animal {
  void sound() {
    System.out.println("Chirp");
  }
}

class Polymorphism {
  public static void main(String[] args) {
    Animal mySound;

    mySound = new Cat();
    mySound.sound();

    mySound = new Bird();
    mySound.sound();
  }
}

In this code, we declare a method sound() in the Animal class and provide different implementations in the Cat and Bird subclasses. The main method demonstrates runtime polymorphism by using a reference variable of the parent class (Animal) to call the sound() method on both Cat and Bird objects.

Since the reference variable points to the child object, and the child method overrides the parent class method, the child method is invoked at runtime. This dynamic method invocation, determined by the Java Virtual Machine (JVM), exemplifies the essence of runtime polymorphism. The provided example concurrently illustrates the concept of method overriding.

4. Compile-time Polymorphism and Method Overloading in Java

In Java, compile-time polymorphism, also known as static polymorphism, is achieved through method overloading. Method overloading allows multiple methods to share the same name but have different parameters. This enables the same operation to be performed on different data types or a different number of parameters.

Consider the following example of compile-time polymorphism with method overloading:

class my_math {
  int add_integer(int x, int y) {
    return x + y;
  }

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

class Overload {
  public static void main(String[] args) {
    my_math obj = new my_math();
    System.out.println(obj.add_integer(2, 3));
    System.out.println(obj.add_double(2.55, 3.325));
  }
}

In the above code, the my_math class contains two methods with different names (add_integer and add_double) that add numbers of different types (int and double). This is a typical example of method overloading for performing the same operation on different data types.

To demonstrate method overloading more effectively, let's refactor the code by overloading the addition method to work for both int and double:

class my_math {
  int addition(int x, int y) {
    return x + y;
  }

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

class Overload {
  public static void main(String[] args) {
    my_math obj = new my_math();
    System.out.println(obj.addition(2, 3));
    System.out.println(obj.addition(2.55, 3.325));
  }
}

Here, the addition() method is overloaded with two versions—one that takes two int parameters and another that takes two double parameters. The method to be called is determined by the arguments passed during the method invocation, and this determination occurs at compile-time, hence the term compile-time polymorphism.

End Of Article

End Of Article