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.