Object-Oriented Programming has different concepts allowing developers to build logical code. One of these concepts is polymorphism. But what is polymorphism?
Polymorphism is one of the core concepts of object-oriented programming (OOP) that describes situations in which something occurs in several different forms. In computer science, polymorphism describes the concept that you can access objects of different types through the same interface. Each type can provide its own independent implementation of this interface.
You can perform a simple test to know whether an object is polymorphic. If the object successfully passes multiple is-a or instanceof tests, it’s polymorphic. As described in our post about inheritance, all Java classes extend the class Object. Due to this, all objects in Java are polymorphic because they pass at least two instanceof checks.
Java supports 2 types of polymorphism:
Like many other OOP languages, Java allows you to implement multiple methods within the same class that use the same name. But, Java uses a different set of parameters called method overloading and represents a static form of polymorphism.
The parameter sets have to differ in at least one of the following three criteria:
In most cases, these overloaded methods provide a different but very similar functionality.
Due to the different sets of parameters, each method has a different signature. That signature allows the compiler to identify which method to call and binds it to the method call. This approach is popular and it is known as static binding or static polymorphism.
Let’s take a look at an example.
Let’s use the same CoffeeMachine project as we used in the previous posts of this series. You can clone it at Github.
The BasicCoffeeMachine class implements two methods with the name brewCoffee. The first one accepts one parameter of type CoffeeSelection. The other method accepts two parameters, a CoffeeSelection and an int.
public class BasicCoffeeMachine {
// ...
public Coffee brewCoffee(CoffeeSelection selection) throws CoffeeException {
switch (selection) {
case FILTER_COFFEE:
return brewFilterCoffee();
default:
throw new CoffeeException(
"CoffeeSelection ["+selection+"] not supported!");
}
}
public List brewCoffee(CoffeeSelection selection, int number) throws CoffeeException {
List coffees = new ArrayList(number);
for (int i=0; i<number; i++) {
coffees.add(brewCoffee(selection));
}
return coffees;
}
// ...
}
When you call one of these methods, the provided set of parameters identifies the method which has to be called.
In the following code snippet, we’ll call the method only with a CoffeeSelection object. At compile time, the Java compiler binds this method call to the brewCoffee(CoffeeSelection selection) method.
BasicCoffeeMachine coffeeMachine = createCoffeeMachine();
coffeeMachine.brewCoffee(CoffeeSelection.FILTER_COFFEE);
If we change this code and call the brewCoffee method with a CoffeeSelection object and an int, the compiler binds the method call to the other brewCoffee(CoffeeSelection selection, int number) method.
BasicCoffeeMachine coffeeMachine = createCoffeeMachine();
List coffees = coffeeMachine.brewCoffee(CoffeeSelection.ESPRESSO, 2);
This form of polymorphism doesn’t allow the compiler to determine the executed method. The JVM needs to do that at runtime.
Within an inheritance hierarchy, a subclass can override a method of its superclass, enabling the developer of the subclass to customize or completely replace the behavior of that method.
Doing so also creates a form of polymorphism. Both methods implemented by the super- and subclasses share the same name and parameters. However, they provide different functionality.
Let’s take a look at another example from the CoffeeMachine project.
The BasicCoffeeMachine class is the superclass of the PremiumCoffeeMachine class.
Both classes provide an implementation of the brewCoffee(CoffeeSelection selection) method.
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class BasicCoffeeMachine extends AbstractCoffeeMachine {
protected Map beans;
protected Grinder grinder;
protected BrewingUnit brewingUnit;
public BasicCoffeeMachine(Map beans) {
super();
this.beans = beans;
this.grinder = new Grinder();
this.brewingUnit = new BrewingUnit();
this.configMap.put(CoffeeSelection.FILTER_COFFEE, new Configuration(30, 480));
}
public List brewCoffee(CoffeeSelection selection, int number) throws CoffeeException {
List coffees = new ArrayList(number);
for (int i=0; i<number; i++) {
coffees.add(brewCoffee(selection));
}
return coffees;
}
public Coffee brewCoffee(CoffeeSelection selection) throws CoffeeException {
switch (selection) {
case FILTER_COFFEE:
return brewFilterCoffee();
default:
throw new CoffeeException("CoffeeSelection ["+selection+"] not supported!");
}
}
private Coffee brewFilterCoffee() {
Configuration config = configMap.get(CoffeeSelection.FILTER_COFFEE);
// grind the coffee beans
GroundCoffee groundCoffee = this.grinder.grind(this.beans.get(CoffeeSelection.FILTER_COFFEE), config.getQuantityCoffee());
// brew a filter coffee
return this.brewingUnit.brew(CoffeeSelection.FILTER_COFFEE, groundCoffee, config.getQuantityWater());
}
public void addBeans(CoffeeSelection selection, CoffeeBean newBeans) throws CoffeeException {
CoffeeBean existingBeans = this.beans.get(selection);
if (existingBeans != null) {
if (existingBeans.getName().equals(newBeans.getName())) {
existingBeans.setQuantity(existingBeans.getQuantity() + newBeans.getQuantity());
} else {
throw new CoffeeException("Only one kind of beans supported for each CoffeeSelection.");
}
} else {
this.beans.put(selection, newBeans);
}
}
}
import java.util.Map;
public class PremiumCoffeeMachine extends BasicCoffeeMachine {
public PremiumCoffeeMachine(Map beans) {
// call constructor in superclass
super(beans);
// add configuration to brew espresso
this.configMap.put(CoffeeSelection.ESPRESSO, new Configuration(8, 28));
}
private Coffee brewEspresso() {
Configuration config = configMap.get(CoffeeSelection.ESPRESSO);
// grind the coffee beans
GroundCoffee groundCoffee = this.grinder.grind(this.beans.get(CoffeeSelection.ESPRESSO), config.getQuantityCoffee());
// brew an espresso
return this.brewingUnit.brew(
CoffeeSelection.ESPRESSO, groundCoffee, config.getQuantityWater());
}
public Coffee brewCoffee(CoffeeSelection selection) throws CoffeeException {
if (selection == CoffeeSelection.ESPRESSO)
return brewEspresso();
else
return super.brewCoffee(selection);
}
}
If you read our post about the OOP concept inheritance, you already know the two implementations of the brewCoffee method. The BasicCoffeeMachine only supports the CoffeeSelection.FILTER_COFFEE. The brewCoffee method of the PremiumCoffeeMachine class adds support for CoffeeSelection.ESPRESSO.
If the action gets called with any other CoffeeSelection, it uses the keyword super to delegate the call to the superclass.
Sometimes, you want to use an inheritance hierarchy in your project. To do this, you must answer the question, which method will the JVM call?
The answer manifests during runtime because it depends on the object on which the method gets called. The type of the reference, which you can see in your code, is irrelevant. You need to distinguish three general scenarios:
Let’s delve a bit further …
The first scenario is pretty simple. When you instantiate a BasicCoffeeMachine object and store it in a variable of type BasicCoffeeMachine, the JVM will call the brewCoffee method on the BasicCoffeeMachine class. So, you can only brew a CoffeeSelection.FILTER_COFFEE.
// create a Map of available coffee beans
Map beans = new HashMap();
beans.put(CoffeeSelection.FILTER_COFFEE,
new CoffeeBean("My favorite filter coffee bean", 1000));
// instantiate a new CoffeeMachine object
BasicCoffeeMachine coffeeMachine = new BasicCoffeeMachine(beans);
Coffee coffee = coffeeMachine.brewCoffee(CoffeeSelection.FILTER_COFFEE);
The second scenario is similar. But this time, we instantiate a PremiumCoffeeMachine and reference it as a PremiumCoffeeMachine. In this case, the JVM calls the brewCoffee method of the PremiumCoffeeMachine class, which adds support for CoffeeSelection.ESPRESSO.
// create a Map of available coffee beans
Map beans = new HashMap();
beans.put(CoffeeSelection.FILTER_COFFEE,
new CoffeeBean("My favorite filter coffee bean", 1000));
beans.put(CoffeeSelection.ESPRESSO,
new CoffeeBean("My favorite espresso bean", 1000));
// instantiate a new CoffeeMachine object
PremiumCoffeeMachine coffeeMachine = new PremiumCoffeeMachine(beans);
Coffee coffee = coffeeMachine.brewCoffee(CoffeeSelection.ESPRESSO);
This is the most interesting scenario and the main reason why we explain dynamic polymorphism in such detail.
When you instantiate a PremiumCoffeeMachine object and assign it to the BasicCoffeeMachine coffeeMachine variable, the object is still a PremiumCoffeeMachine object. It just looks like a BasicCoffeeMachine.The compiler doesn’t see that in the code, and you can only use the methods provided by the BasicCoffeeMachine class. If you call the brewCoffee method on the coffeeMachine variable, the JVM recognizes it as an object of the PremiumCoffeeMachine type. Then JVM executes the overridden method. This is known as late binding.
// create a Map of available coffee beans
Map beans = new HashMap();
beans.put(CoffeeSelection.FILTER_COFFEE,
new CoffeeBean("My favorite filter coffee bean", 1000));
// instantiate a new CoffeeMachine object
BasicCoffeeMachine coffeeMachine = new PremiumCoffeeMachine(beans);
Coffee coffee = coffeeMachine.brewCoffee(CoffeeSelection.ESPRESSO);
Polymorphism is one of the core concepts in OOP languages and describes the concept of using different classes with the same interface. Each of these classes can provide its implementation of the interface.
Java supports two kinds of polymorphism. You can overload a method with different sets of parameters. The compiler statically binds the method call to a specific method, which we know as static polymorphism.
Within an inheritance hierarchy, a subclass can override a method of its superclass. The JVM will always call the overridden method if you instantiate the subclass. Even if you cast the subclass to its superclass, the result will be the same. That is dynamic polymorphism.In building robust software, you need to write flawless codes. Try Netreo’s free code profiler, Prefix, to write better code faster every time. Prefix now provides OpenTelementry data ingestion, which works with .NET, Java, PHP, Node.js, Ruby, Python, C++, Erlang/Elixer, Go, Rust and Swift.
If you would like to be a guest contributor to the Stackify blog please reach out to stackify@stackify.com