Polymorphism in Java — Method Overloading

Let’s try to model a person object, and we see that the person has different roles to perform. He becomes an employee to fend for himself, his role changes as he becomes a brother, husband, father, etc. These diverse roles expect distinct behavior. His behavior changes as an employee of a company, and his behavior would be different as a son. These distinct behaviors of an object can be emulated using polymorphism in object-oriented programming. The word “poly” means many, and “morphs” denotes something having a specified character. Thus, polymorphism refers to an object exhibiting multiple forms or behaviors.

Java supports two types of polymorphism viz. compile-time polymorphism and runtime polymorphism. Object binds or resolves at the time of compilation in compile-time polymorphism, whereas binding happens at runtime in runtime polymorphism. Compile-time polymorphism is implemented using method overloading, and that will be our topic of discussion.

Method Overloading

Overloaded methods let us reuse the same method name in a class, but with different arguments (and optionally, a different return type). In a way, we are making the job of method-caller a little easier by exposing the same method name for performing similar actions on different forms.

There are a few important points we need to keep in mind while using method overloading because there are many cases where we might confuse overriding with overloading. The rules of overloading are pretty simple:

  • The argument list MUST be changed. There is no way around this rule. If we don’t change the argument list, we can’t have method overloading. Simple!
  • The return type CAN be changed. If we keep the argument list unchanged and change only the return type of a method, we will get a compilation error
  • Access modifiers CAN be different
  • We CAN declare new or broader checked exceptions.

Apart from these rules, remember that method overloading is possible in the same class or in its sub-class. Think of overloading as creating a new method altogether but with the already used method name. Look at the following example:

PerimeterOfShapes.java

public class PerimeterOfShapes {

    /* Perimeter of a circle */
    public double perimeter(int radius) {
	return 2 * Math.PI * radius;
    }

    /* Perimeter of a rectangle */
    public double perimeter(int length, int breadth) {
	return 2 * (length + breadth);
    }

    /* Perimeter of a triangle */
    public double perimeter(int sideOne, int sideTwo, int sideThree) {
	return sideOne + sideTwo + sideThree;
    }
}

Client.java

public class Client {
   public static void main(String[] args) {
	PerimeterOfShapes shapes = new PerimeterOfShapes();
	double perimeterOfCircle = shapes.perimeter(12);
	double perimeterOfRectangle = shapes.perimeter(12, 14);
	double perimeterOfTriangle = shapes.perimeter(12, 15, 14);
   }
}

In the above example, we see that we have a method named perimeter() with a different argument list that gets invoked depending on the parameters passed during the method invocation. Things become particularly intriguing when we have overloading with var-args and boxing. We will check those conditions in the next section.


Before we proceed further, we need to know three concepts: widening, var-args, and autoboxing:

  • Widening: When an exact argument match is not found, the JVM uses the method with the smallest argument that is wider than the parameter. For example: if the call is getSomething(5) and there is no method definition such as getSomething(int a), but we do have getSomething(long a) then the variable a with int value 5 is widened to long, and the calls get routed to method with the declaration getSomething(long a)
  • Autoboxing: It is a phenomenon of implicit conversion from primitive types to their typed variants like Integer, Float, etc.
  • Var-args: These are used to pass a variable number of arguments to a method.

Things become particularly difficult when we encounter a combination of widening, boxing, and var-args in our method definitions, and the method calls can satisfy or yield results in all of the above combinations.

Widening & Boxing

Consider the following example.

ConfusingClass.java

class ConfusingClass {
	public static void go(Short x) {
		System.out.println("Short");
	}

	public static void go(long x) {
		System.out.println("long");
	}

	public static void main(String[] args) {
		short i = 5;
		go(i);
	}
}

What will the output of the above code? If you look closely, both the implementations of the method go() run without any errors when invoked separately. In the absence of go(Short x) implementation, primitive short value 5 can be widened to primitive long value and go(long x) gets invoked. Similarly, in the absence of go(long x) implementation, primitive short value 5 can be boxed to Short and go(Short x) gets invoked. What happens if we have both?

The output would be longIt is so because the compiler chooses to widen before boxing. That’s rule number one: widening always beats boxing. This choice was in view of backward compatibility; widening capability already existed, and a new method with boxing capability should not cause problems to the existing code.

Widening & Var-args

Consider the following example.

ConfusingClass.java

class ConfusingClass {
	public static void go(short... x) {
		System.out.println("Short");
	}

	public static void go(long x, long y) {
		System.out.println("long");
	}

	public static void main(String[] args) {
		short i = 5;
		go(i, i);
	}
}

Keeping everything we discussed in mind, we see that now the clash is between a method with variable arguments and widening. What will the output of the above code?

The output would be longIt is so because the compiler chooses to widen before using var-args. That’s rule number two: widening always beats var-argsOnce again, the reason is backward compatibility, even though each invocation will require some sort of conversion, the compiler will choose the older style before it adapts the newer style of var-args, keeping existing code more robust.

Boxing & Var-args

Now the clash is between boxing & var-args. Consider the following example and try to guess the output.

ConfusingClass.java

class ConfusingClass {
	public static void go(short... x) {
		System.out.println("Var-args");
	}

	public static void go(Short x, Short y) {
		System.out.println("Boxing");
	}

	public static void main(String[] args) {
		short i = 5;
		go(i, i);
	}
}

The output now would be Boxing. This gives us rule number three: boxing always beats var-argsA var-args method is more like a catch-all argument sort of method, in terms of what invocations it can handle and such a catch-all method should always be used as a last resort.

Can we widen reference variables? Yes. We most certainly can, and the widening here is only applicable through inheritance i.e. if the IS-A test succeeds. In other words, if a method expects Animal object, we can very well pass a Dog object(assuming Dog is a sub-class of Animal). It is to be noted that we cannot widen from one type to another unrelated type. So we cannot widen from Short to Integer or from Integer to Float, as these classes are completely unrelated, and there is no IS-A relationship.


Till now, we dealt with invocations involving only one conversion. What happens when the compiler has to make multiple conversions? We will deal with such examples in this section.

Combining Widening and Boxing

Consider the following example:

class ConfusingClassToWidenAndBox {
	public static void go(Long x) {
       System.out.println(“Long”);
    }

	public static void main(String[] args) {
		short b = 5;
		go(b);
	}
}

What would be the output of the above code?

There will be a compilation error saying, “The method go(Long) in the type ConfusingClassToWidenAndBox is not applicable for the arguments (short)”. We now have rule number four: We cannot widen and box a variable or variables to match the declaration.

Consider an inverted version of this example where we first box and then try to widen.

class ConfusingClassToBoxAndWiden {
	public static void go(Object x) {
		System.out.println("Long");
	}

	public static void main(String[] args) {
		short b = 5;
		go(b);
	}
}

Strangely, we get the output as Long. The compiler first coverts primitive short value to wrapper class Short using boxing and then widens Short to Object. So, why didn’t the compiler try to do a similar thing in case of a widen-and-then-box example? In that example, the compiler converts a primitive short value to Short object using boxing, but now Short cannot be converted to Long because the IS-A test fails, and widening cannot be done. This establishes a new rule, so rule number five says that we can box and then widen a variable or variables to match the declaration.

Consider this code and guess the output:

class ConfusingClass {
	public static void go(Integer x) {
		System.out.println("Integer");
	}

	public static void main(String[] args) {
		short b = 5;
		go(b);
	}
}

There will be a compilation error as now the compiler boxes the primitive short to Short object and then tries to convert Short to Integer(which is an invalid operation). The following code, however, will work:

class ConfusingClass {
	public static void go(Number x) {
		System.out.println("Long");
	}

	public static void main(String[] args) {
		short b = 5;
		go(b);
	}
}

This works and prints Long because Short object after widening can be converted to Number since Number is a superclass of Short.

Combining Var-args With Widening/Boxing

What happens when we attempt to combine var-args with either widening or boxing? Now consider the following example and guess the output:

class Vararg {
	public static void widenFirstThenUseVararg(long... x) {
		System.out.println("widenFirstThenUseVararg...");
	}

	public static void boxFirstThenUseVararg(Integer... x) {
		System.out.println("boxFirstThenUseVararg...");
	}

	public static void main(String[] args) {
		int i = 5;
		widenFirstThenUseVararg(i, i);
		boxFirstThenUseVararg(i, i);
	}
}

Where do you think there will be a compilation error? Nowhere. It is a legal code and produces the following output:

Output:
widenFirstThenUseVararg
boxFirstThenUseVararg

So, that brings us to the sixth rule that says we can successfully combine var-args with either widening or boxing.

That’s it! 🙂

Let’s have a short overview of whatever we have discussed until now. The rules for overloading methods using widening, boxing, and var-args:

  • In the case of a primitive widening, the “smallest” method argument is chosen.
  • We CANNOT widen from one wrapper type to another (i.e. we cannot convert Integer to Long as IS-A test fails)
  • Widening always beats boxing
  • Widening always beats var-args
  • Boxing always beats var-args
  • We CANNOT widen and then box. (An int can’t become a Long.)
  • We can box and then widen. (An int can become Object or Number, via Integer.)
  • We can combine var-args with either widening or boxing

Phew! Thanks for reading.


Posted

in

Comments

Leave a Reply

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