Polymorphic overloading in a nutshell

Since overloading (also known as “ad-hoc” polymorphism) is a core concept of Object-Oriented Programming (OOP), I’m sure you are familiar with Java method overloading, so I’ll not insist in the basic theory of this concept.Also, I’m aware that some people don’t agree that overloading can be a form of polymorphism, but that is another topic that will not tackle here.We will be more practical, and we will jump into a suite of quizzes meant to highlight some interesting aspects of overloading. More precisely, we will discuss type dominance. So, let’s tackle the first quiz (wordie is an initially empty string):

static void kaboom(byte b) { wordie += “a”;} 
static void kaboom(short s) { wordie += “b”;} 
kaboom(1);

What will happen? If you answered that the compiler will point out that there is no suitable method found for kaboom(1) then you’re right. The compiler looks for a method that gets an integer argument, kaboom(int). Ok, that was easy! Next one:

static void kaboom(byte b) { wordie += “a”;} 
static void kaboom(short s) { wordie += “b”;}
static void kaboom(long l) { wordie += “d”;}  
static void kaboom(Integer i) { wordie += “i”;} 
kaboom(1);

We know that the first two kaboom() are useless. How about kaboom(long) and kaboom(Integer)? You are right, the kaboom(long) will be called. If we remove kaboom(long) then kaboom(Integer) is called.

In primitives overloading, the compiler starts by searching for a one-to-one match. If this attempt fails then the compiler search for an overloading flavor taking a primitive broader domain than the primitive current domain (for instance, for an int, it looks for int, long, float, or double). If this fails as well then the compiler checks for overloading taking boxed types (Integer, Float, and so on).

Following the previous statements let’s have this one:

static void kaboom(Integer i) { wordie += “i”;} 
static void kaboom(Long l) { wordie += “j”;} 
kaboom(1);

This time, wordie will be i. The kaboom(Integer) is called since there is no kaboom(int/long/float/double). If we had a kaboom(double) then that method has higher precedence than kaboom(Integer). Interesting, right?! On the other hand, if we remove kaboom(Integer) then don’t expect that the kaboom(Long) will be called. Any other kaboom(boxed type) with a broader/narrow domain than Integer will not be called. This is happening because the compiler follows the inheritance path based on an IS-A relationship, so after kaboom(Integer) it looks for kaboom(Number), since Integer is a Number.

In boxed types overloading, the compiler start by searching for a one-to-one match. If this attempt fails then the compiler will not consider any overloading flavor taking a boxed type with a broader domain than the current domain (of course, a narrow domain is ignored as well). It looks for Number as being the superclass of all boxed types. If Number is not found, the compiler goes up in the hierarchy reaching the java.lang.Object which is the end of the road.

Ok, let’s complicate things a little bit:

static void kaboom(Object… ov) { wordie += “o”;} 
static void kaboom(Number n) { wordie += “p”;} 
static void kaboom(Number… nv) { wordie += “q”;}   
kaboom(1);

So, which method will be called this time? I know, you think of kaboom(Number), right? At least, my simple logic pushes me to think that this is a common-sense choice. And it is correct!If we remove kaboom(Number) then the compiler will call the varargs method, kaboom(Number…). This makes sense since kaboom(1) uses a single argument, so kaboom(Number) should have higher precedence than kaboom(Number…). This logic reverses if we call kaboom(1,2,3), since the kaboom(Number) is no longer representing a valid overloading for this call, and kaboom(Number…) is the right choice.But, this logic applies because Number is the superclass of all boxed classes (Integer, Double, Float, and so on).How about now?

static void kaboom(Object… ov) { wordie += “o”;} 
static void kaboom(File… fv) { wordie += “s”;} 
kaboom(1);

This time, the compiler will “bypass” kaboom(File…) and will call the kaboom(Object…). Based on the same logic, a call of kaboom(1, 2, 3) will call kaboom(Object…) since there is no kaboom(Number…).

In overloading, if the call has a single argument then the method with a single argument has higher precedence than its varargs counterpart. On the other part, if the call has more arguments of the same type then the varargs method is called since the one-argument method is not suitable anymore. When the call has a single argument but only the varargs overloading is available then this method is called.

This leads us to the following example:

static void kaboom(Number… nv) { wordie += “q”;} 
static void kaboom(File… fv) { wordie += “s”;} 
kaboom();

This time, kaboom() has no arguments and the compiler cannot find a unique match. This means that the reference to kaboom() is ambiguous since both methods match (kaboom(java.lang.Number…) in modern.challenge.Main and method kaboom(java.io.File…) in modern.challenge.Main).In the bundled code, you can play even more with polymorphic overloading and test your knowledge. Moreover, try to challenge yourself and introduce generics in the equation as well.

Erasure vs. overloading

Ok, now based on the previous experience, check out this code:

void print(List<A> listOfA) {
  System.out.println(“Printing A: ” + listOfA);
}
 
void print(List<B> listofB) {
  System.out.println(“Printing B: ” + listofB);
}

What will happen? Well, this is a case where overloading and type erasure collide. The type erasure will replace List<A> with List<Object> and List<B> with List<Object> as well. So, overloading is not possible and we get an error as, name clash: print(java.util.List<modern.challenge.B>) and print(java.util.List<modern.challenge.A>) have the same erasure.In order to solve this issue, we can add a dummy argument to one of these two methods:

void print(List<A> listOfA, Void… v) {
  System.out.println(“Printing A: ” + listOfA);
}

Now, we can have the same call for both methods:

new Main().print(List.of(new A(), new A()));
new Main().print(List.of(new B(), new B()));

Done! You can practice these examples in the bundled code.

Leave a Reply

Your email address will not be published. Required fields are marked *