Table of contents
1.
Introduction
2.
What is a Virtual Function in Java?
3.
How to Use Virtual Functions in Java
4.
Virtual Function Example
5.
Virtual Function with Interface
6.
Pure Virtual Function
7.
Run-time Polymorphism
8.
Frequently Asked Questions
8.1.
Can we override a static method in Java?
8.2.
Can we declare a virtual function as final?
8.3.
Is it mandatory to use the @Override annotation when overriding a method?
9.
Conclusion
Last Updated: Nov 18, 2024
Easy

Virtual Function in Java

Author Gaurav Gandhi
0 upvote
Career growth poll
Do you think IIT Guwahati certified course can help you in your career?

Introduction

In object-oriented programming, virtual functions play a very important role in enabling polymorphism and dynamic behavior in Java. A virtual function is a member function that is declared within a base class and can be overridden by derived classes. This powerful concept helps developers to create flexible and extensible code structures where the behavior of objects can be determined at runtime based on their actual type. With the help of virtual functions, programmers can achieve runtime polymorphism, which is a fundamental principle of object-oriented design. 

Virtual Function in Java

In this article, we will discuss the concept of virtual functions in Java, understand how they are used, and look into different examples to understand their implementation.

What is a Virtual Function in Java?

In Java, a virtual function is a method that is declared in a base class & can be overridden by its derived classes. When a method is declared as virtual, the compiler determines which version of the method to call based on the actual type of the object at runtime, rather than the type of the reference variable. This behavior is known as dynamic method dispatch or runtime polymorphism.

To declare a virtual function in Java, you simply define the method in the base class & use the same method signature in the derived classes. The `@Override` annotation is used to indicate that a method in a derived class is intended to override a method in the base class. This annotation helps catch potential errors if the method signature doesn't match the base class method.

By default, all non-static methods in Java are considered virtual, which means they can be overridden by derived classes. However, there are a few exceptions, like:

1. Final methods: Methods declared as `final` cannot be overridden by derived classes.
 

2. Private methods: Private methods are not visible to derived classes & therefore cannot be overridden.
 

3. Static methods: Static methods belong to the class itself & cannot be overridden, but they can be hidden by declaring a method with the same name in a derived class.


The main advantage of virtual functions is that they give polymorphism, which allows objects of different derived classes to be treated as objects of the base class. This makes code more flexible and modular, as an object's behavior can be determined at runtime based on its actual type.

How to Use Virtual Functions in Java

To use virtual functions in Java, follow these steps:

1. Declare the base class: Create a base class that contains the virtual function. The virtual function should be declared with the desired accessibility (public, protected, or default) & return type.

public class Animal {
    public void makeSound() {
        System.out.println("The animal makes a sound");
    }
}


2. Create derived classes: Create one or more derived classes that extend the base class. These classes will override the virtual function defined in the base class.

public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow");
    }
}

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


3. Use polymorphism: Create objects of the derived classes & assign them to variables of the base class type. This allows you to treat objects of different derived classes as objects of the base class.

Animal animal1 = new Cat();
Animal animal2 = new Dog();


4. Call the virtual function: Call the virtual function on the objects. The appropriate overridden method will be called based on the actual type of the object at runtime.

animal1.makeSound(); // Output: Meow
animal2.makeSound(); // Output: Woof

Virtual Function Example

Consider a scenario where we have a base class called `Shape` and two derived classes, `Circle` and `Rectangle`, which override the virtual function `calculateArea()`.

public class Shape {
    public double calculateArea() {
        return 0;
    }
}

public class Circle extends Shape {
    private double radius;


    public Circle(double radius) {
        this.radius = radius;
    }


    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

public class Rectangle extends Shape {
    private double length;
    private double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    @Override
    public double calculateArea() {
        return length * width;
    }
}


In this example, the `Shape` class serves as the base class and declares the virtual function `calculateArea()`. The `Circle` and `Rectangle` classes extend the `Shape` class and override the `calculateArea()` method to provide their own implementations for calculating the area based on their specific properties.

Now, let's see how we can use these classes and use the power of virtual functions:

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


        System.out.println("Area of shape1: " + shape1.calculateArea());
        System.out.println("Area of shape2: " + shape2.calculateArea());
    }
}


Output:

Area of shape1: 78.53981633974483
Area of shape2: 24.0


In the `main` method, we create objects of the `Circle` and `Rectangle` classes and assign them to variables of the `Shape` type. This is possible because both `Circle` and `Rectangle` inherit from the `Shape` class.

When we call the `calculateArea()` method on `shape1` and `shape2`, the appropriate overridden method is invoked which is based on the object's actual type at runtime. This shows the power of virtual functions and runtime polymorphism, where an object's behavior is determined by its actual type rather than the type of the reference variable.

Virtual Function with Interface

In Java, an interface is a collection of abstract methods that can be implemented by classes. Since all methods in an interface are implicitly abstract & public, they are inherently virtual. When a class implements an interface, it must provide an implementation for all the methods declared in the interface.

Let's take an example where we have an interface called `Drawable` with a virtual function `draw()` and two classes, `Circle` and `Rectangle`, that implement the interface.

public interface Drawable {
    void draw();
}


public class Circle implements Drawable {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}
public class Rectangle implements Drawable {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}


In this example, the `Drawable` interface declares the `draw()` method, which is implicitly virtual. The `Circle` and `Rectangle` classes implement the `Drawable` interface and provide their own implementations for the `draw()` method.

Now, let's see how we can use the interface & the implementing classes:

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

        drawable1.draw();
        drawable2.draw();
    }
}


Output:

Drawing a circle
Drawing a rectangle


In the `main` method, we create objects of the `Circle` and `Rectangle` classes and assign them to variables of the `Drawable` type. This is possible because both `Circle` and `Rectangle` implement the `Drawable` interface.

When we call the `draw()` method on `drawable1` & `drawable2`, the appropriate implementation of the method is invoked based on the actual type of the object at runtime. This demonstrates how virtual functions work with interfaces in Java.

Pure Virtual Function

In Java, there is no explicit concept of pure virtual functions like in C++. However, we can achieve a similar behavior using abstract classes & abstract methods.

An abstract class is a class that cannot be instantiated & may contain abstract methods. An abstract method is a method that is declared without an implementation & must be overridden by the derived classes. Any class that extends an abstract class must provide an implementation for all the abstract methods declared in the abstract class.

Let’s discuss an example (that we took earlier also) where we have an abstract class called `Animal` with an abstract method `makeSound()` and two derived classes, `Cat` and `Dog`, that extend the abstract class and provide their own implementations for the `makeSound()` method.

public abstract class Animal {
    public abstract void makeSound();
}
public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow");
    }
}
public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Woof");
    }
}


In this example, the `Animal` class is declared as an abstract class using the `abstract` keyword. The `makeSound()` method is declared as an abstract method, which means it doesn't have an implementation in the abstract class. The `Cat` & `Dog` classes extend the `Animal` class & provide their own implementations for the `makeSound()` method.

Now, let's see how we can use the abstract class & the derived classes:

public class Main {
    public static void main(String[] args) {
        Animal animal1 = new Cat();
        Animal animal2 = new Dog();
        animal1.makeSound();
        animal2.makeSound();
    }
}


Output:

Meow
Woof


In the `main` method, we create objects of the `Cat` and `Dog` classes and assign them to variables of the `Animal` type. This is possible because both `Cat` and `Dog` extend the `Animal` class.

When we call the `makeSound()` method on `animal1` and `animal2`, the appropriate implementation of the method is invoked based on the actual type of the object at runtime. This demonstrates how pure virtual functions can be simulated using abstract classes and abstract methods in Java.

Run-time Polymorphism

Run-time polymorphism, also known as dynamic polymorphism, is a fundamental concept in object-oriented programming that allows objects to exhibit different behaviors based on their actual type at runtime. In Java, run-time polymorphism is achieved through the use of virtual functions & method overriding.

Let's consider an example that shows run-time polymorphism using a base class called `Shape` & two derived classes, `Circle` & `Rectangle`.

public class Shape {
    public void draw() {
        System.out.println("Drawing a shape");
    }
}
public class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}
public class Rectangle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}


In this example, the `Shape` class has a virtual function `draw()`, which is overridden by the `Circle` & `Rectangle` classes. Each derived class provides its own implementation of the `draw()` method.

Now, let's see how run-time polymorphism works:

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

        shape1.draw();
        shape2.draw();
    }
}

 

Output:

Drawing a circle
Drawing a rectangle


In the `main` method, we create objects of the `Circle` and `Rectangle` classes and assign them to variables of the `Shape` type. At compile time, the compiler sees the variables as references of the `Shape` type.

However, at runtime, when the `draw()` method is called on `shape1` and `shape2`, the actual type of the objects is determined, and the appropriate overridden `draw()` method is invoked. This demonstrates run-time polymorphism, where the behavior of an object is determined by its actual type at runtime rather than the type of the reference variable.

Note: Run-time polymorphism gives more flexibility and extensibility in the codebase. It enables us to write generic code that can work with objects of different derived classes, without needing to know the specific type of the object at compile-time. This promotes code reuse, modularity, and maintainability.

Frequently Asked Questions

Can we override a static method in Java?

No, static methods cannot be overridden in Java. They are associated with the class itself, not the instances of the class.

Can we declare a virtual function as final?

No, a virtual function cannot be declared as final. The final keyword prevents a method from being overridden in derived classes.

Is it mandatory to use the @Override annotation when overriding a method?

No, using the @Override annotation is not mandatory, but it is considered a good practice. It helps catch errors if the method signature doesn't match the base class method.

Conclusion

In this article, we discussed the concept of virtual functions in Java and how they enable run-time polymorphism. We learned that virtual functions are declared in a base class and can be overridden by derived classes. We also saw how to use virtual functions with abstract classes, interfaces, and in the context of run-time polymorphism. Virtual functions are a powerful mechanism for creating flexible, extensible, and modular code structures in Java, which allows objects to exhibit different behaviors based on their actual type at runtime.

You can also check out our other blogs on Code360.

Live masterclass