rafael_del nero
Java Developer

Method overloading in the JVM

how-to
Aug 23, 202411 mins
JavaProgramming LanguagesSoftware Development

Learn how and why Java developers use method overloading, then test your learning against the Java virtual machine itself.

Method overloading is a programming technique that allows developers to use the same method name multiple times in the same class, but with different parameters. Because of the word overloading, developers sometimes assume this technique means overloading the system, but that’s not true. In programming, method overloading means using the same method name with different parameters. 

Method overloading in Java

This article focuses on the mechanics of method overloading, or how the JVM processes overloaded methods:

  • Example of an overloaded method
  • Method overloading and primitive types
  • Why use method overloading?
  • What method overloading isn’t
  • How the JVM compiles overloaded methods

Listing 1 shows a single method whose parameters differ in number, type, and order.

Listing 1. Three types of method overloading


Number of parameters:
public class Calculator {
  	void calculate(int number1, int number2) { }
  	void calculate(int number1, int number2, int number3) { }
}

Type of parameters:
public class Calculator {
  	void calculate(int number1, int number2) { }
  	void calculate(double number1, double number2) { }
}

Order of parameters:
public class Calculator {
  	void calculate(double number1, int number2) { }
  	void calculate(int number1, double number2) { }
}

Method overloading and primitive types

In Listing 1, you see the primitive types int and double. We’ll work more with these and other types, so take a minute to review the primitive types in Java.

Table 1. Primitive types in Java

Type Range Default Size Example literals
boolean  true or false  false  1 bit  true, false
byte  -128 .. 127  0  8 bits  1, -90, 128
char  Unicode character or 0 to 65,536  u0000  16 bits  ‘a’, ‘u0031’, ‘201’, ‘n’, 4
short  -32,768 .. 32,767  0  16 bits  1, 3, 720, 22,000
int  -2,147,483,648 .. 2,147,483,647  0  32 bits  -2, -1, 0, 1, 9
long  -9,223,372,036,854,775,808 to  9,223,372,036,854,775,807  0  64 bits  -4000L, -900L, 10L, 700L
float  3.40282347 x 1038, 1.40239846 x 10-45  0.0  32 bits  1.67e200f, -1.57e-207f, .9f, 10.4F
double

 1.7976931348623157 x 10308,  4.9406564584124654 x 10-324

 0.0  64 bits  1.e700d, -123457e, 37e1d

Why should I use method overloading?

Overloading makes your code cleaner and easier to read, and it may also help you avoid bugs in your programs.

In contrast to Listing 1, imagine a program where you had multiple calculate() methods with names like calculate1, calculate2, calculate3 … not good, right? Overloading the calculate() method lets you use the same method name while only changing what needs to change: the parameters. It’s also very easy to find overloaded methods because they are grouped together in your code.

What overloading isn’t

Be aware that changing a variable’s name is not overloading. The following code won’t compile:


public class Calculator {
	
	void calculate(int firstNumber, int secondNumber){}

void calculate(int secondNumber, int thirdNumber){}

}

You also can’t overload a method by changing the return type in the method signature. The following code also won’t compile:


public class Calculator {
    double calculate(int number1, int number2){return 0.0;}
    long calculate(int number1, int number2){return 0;}
}

Constructor overloading

Note that you can overload a constructor the same way you would a method:


public class Calculator {
	private int number1;
	private int number2;

	public Calculator(int number1) {this.number1 = number1;}
	
public Calculator(int number1, int number2) {
		this.number1 = number1;
		this.number2 = number2;
}

}

Take the method overloading challenge!

Carefully review the following code.

Listing 2. Advanced method overloading challenge


public class AdvancedOverloadingChallenge3 {
  static String x = "";
  public static void main(String... doYourBest) {
     executeAction(1);
     executeAction(1.0);
     executeAction(Double.valueOf("5"));
     executeAction(1L);
    
     System.out.println(x);
  }
  static void executeAction(int ... var) {x += "a"; }
  static void executeAction(Integer var) {x += "b"; }
  static void executeAction(Object var)  {x += "c"; }
  static void executeAction(short var)   {x += "d"; }
  static void executeAction(float var)   {x += "e"; }
  static void executeAction(double var)  {x += "f"; }
}

What do you think the output of this code will be?

    befe bfce efce aecf

The answer to the challenge in Listing 2 is option 3, efce.

In order to understand what happened, we need to go a little deeper with method overloading.

How the JVM compiles overloaded methods

In order to understand what happened in Listing 2, you need to know some things about how the JVM compiles overloaded methods.

First of all, the JVM is intelligently lazy: it will always exert the least possible effort to execute a method. Thus, when you are thinking about how the JVM handles overloading, keep in mind three important compiler techniques:

  1. Widening
  2. Boxing (autoboxing and unboxing)
  3. Varargs

If you’ve never encountered these three techniques, a few examples should help make them clear. Note that the JVM executes them in the order given.

Here is an example of widening:


int primitiveIntNumber = 5;
 double primitiveDoubleNumber = primitiveIntNumber ;

This is the order of the primitive types when widened:

A diagram of the order of primitive types when widened. Rafael del Nero

Figure 1. The order of primitive types when widenened

Here is an example of autoboxing:


int primitiveIntNumber = 7;
 Integer wrapperIntegerNumber = primitiveIntNumber;

Note what happens behind the scenes when this code is compiled:


Integer wrapperIntegerNumber = Integer.valueOf(primitiveIntNumber);

Here’s unboxing:


Integer wrapperIntegerNumber = 7;
 int primitiveIntNumber= wrapperIntegerNumber;

Now consider what happens behind the scenes when this code is compiled:


int primitiveIntNumber = wrapperIntegerNumber.intValue();

Finally, here’s an example of varargs; note that varargs is always the last to be executed:


execute(int… numbers){}

You might wonder what the syntax is for. Basically, varargs is used for variable arguments. It’s an array of values specified by three dots (…) We can pass however many int numbers we want to this method.

For example:


execute(1,3,4,6,7,8,8,6,4,6,88...); // We could continue…

Varargs is very handy because the values can be passed directly to the method. If we were using arrays, we would have to instantiate the array with the values.

Widening: A practical example

When we pass the number 1 directly to the executeAction method, the JVM automatically treats it as an int. That’s why the number doesn’t go to the executeAction(short var) method.

Similarly, if we pass the number 1.0, the JVM automatically recognizes that number as a double.

Of course, the number 1.0 could also be a float, but the type is pre-defined. That’s why the executeAction(double var) method is invoked in Listing 2.

When we use the Double wrapper type, there are two possibilities: either the wrapper number could be unboxed to a primitive type, or it could be widened into an Object. (Remember that every class in Java extends the Object class.) In that case, the JVM chooses to wided the Double type to an Object because it takes less effort than unboxing would,  as I explained before.

The last number we pass is 1L, and because we’ve specified the variable type this time, it is long.

Common mistakes with overloading

By now you’ve probably figured out that things can get tricky with method overloading, so let’s consider a few of the challenging scenarios you may encounter.

Autoboxing with wrappers

Java is a strongly typed programming language, and when we use autoboxing with wrappers there are some things we have to keep in mind. For one thing, the following code won’t compile:


int primitiveIntNumber = 7;
Double wrapperNumber = primitiveIntNumber;

Autoboxing will only work with the double type because what happens when you compile this code is the same as the following:


Double number = Double.valueOf(primitiveIntNumber);

The above code will compile. The first int type will be widened to double and then it will be boxed to Double. But when autoboxing, there is no type widening and the constructor from Double.valueOf will receive a double, not an int. In this case, autoboxing would only work if we applied a cast, like so:


Double wrapperNumber = (double) primitiveIntNumber;

Remember that Integer cannot be Long and Float cannot be Double. There is no inheritance. Each of these types—Integer, Long, Float, and Double—is a Number and an Object.

When in doubt, just remember that wrapper numbers can be widened to Number or Object. (There is a lot more to explore about wrappers but I will leave it for another post.)

Hard-coded number types in the JVM

When we don’t specify a type to a number, the JVM will do it for us. If we use the number 1 directly in the code, the JVM will create it as an int. If you try to pass 1 directly to a method that is receiving a short, it won’t compile.

For example:


class Calculator {
        public static void main(String… args) {
              // This method invocation will not compile
        	  // Yes, 1 could be char, short, byte but the JVM creates it as an int
              calculate(1);
        } 

        void calculate(short number) {}
  
  }

The same rule will be applied when using the number 1.0; although it could be a float, the JVM will treat this number as a double:


  class Calculator {
        public static void main(String… args) {
               // This method invocation will not compile
               // Yes, 1 could be float but the JVM creates it as double
               calculate(1.0);
        } 

        void calculate(float number) {}  
  }

Another common mistake is to think that the Double or any other wrapper type would be better suited to the method that is receiving a double. In fact, it takes less effort for the JVM to widen the Double wrapper to an Object instead of unboxing it to a double primitive type.

To sum up, when used directly in Java code, 1 will be int and 1.0 will be double. Widening is the laziest path to execution, boxing or unboxing comes next, and the last operation will always be varargs.

What to remember about overloading

Overloading is a very powerful technique for scenarios where you need the same method name with different parameters. It’s a useful technique because having the right name in your code makes a big difference for readability. Rather than duplicate the method and add clutter to your code, you may simply overload it. Doing this keeps your code clean and easy to read, and it reduces the risk that duplicate methods will break some part of the system.

What to keep in mind: When overloading a method the JVM will make the least effort possible; this is the order of the laziest path to execution:

  • First is widening
  • Second is boxing
  • Third is Varargs

What to watch out for: Tricky situations will arise from declaring a number directly: 1 will be int and 1.0 will be double.

Also remember that you can declare these types explicitly using the syntax of 1F or 1f for a float or 1D or 1d for a double.

That concludes our introduction to the JVM’s role in method overloading. It is important to realize that the JVM is inherently lazy, and will always follow the laziest path to execution.

Video challenge! Debugging method overloading

Debugging is one of the easiest ways to fully absorb programming concepts while also improving your code. In this video you can follow along while I debug and explain the method overloading challenge:

Learn more about Java

rafael_del nero
Java Developer

Rafael del Nero is a Java Champion and Oracle Ace, creator of the Java Challengers initiative, and a quiz master in the Oracle Dev Gym. Rafael is the author of "Java Challengers" and "Golden Lessons." He believes there are many techniques involved in creating high-quality software that developers are often unaware of. His purpose is to help Java developers use better programming practices to code quality software for stress-free projects with fewer bugs.

More from this author