In my last article, Methods in Java, we explored various aspects of methods, including access modifiers and static specifiers. However, we didn’t cover method overloading. This article will focus on method overloading and the nuances you may encounter on exam day.
Method Overloading
Method overloading, also known as compile-time polymorphism, enables us to define multiple methods with the same name but different parameter lists. It is called compile-time polymorphism because the compiler determines the appropriate method to call based on the method signatures at compile time.
Method Lookup
Determining the right method to call can sometimes be confusing, especially in cases involving autoboxing, varargs, and primitive types. Let’s explore these cases in detail.
Overloading and Widening Primitive Conversion.
Java always looks for an exact match first. If no exact match is found, Java will look for methods with wider data types in a specific order (ie, the wider data int
types of int are long
>float
> double
.)
Consider the following example:
public class MathUtils {
public static long add(long a, long b) {
System.out.println("long version being called");
return a + b;
}
public static float add(float a, float b) {
System.out.println("float version being called");
return a + b;
}
}
public class Main {
public static void main(String[] args) {
System.out.println(MathUtils.add(3, 5));
}
}
In this example, calling add(3, 5)
will invoke the add(long, long)
method since there is no exact match for add(int, int)
, and long
is the next wider type.
Overloading & AutoBoxing
Consider the following example:
public class MathUtils {
public static int add(int a, int b) {
System.out.println("int version being called");
return a + b;
}
public static int add(Integer a, Integer b) {
System.out.println("Integer version being called");
return a + b;
}
}
public class Main {
public static void main(String[] args) {
System.out.println(MathUtils.add(3, 5));
}
}
In this example, calling add(3, 5)
will invoke the add(int, int)
method. Although Java can autobox primitives into their wrapper classes, it prioritizes an exact match with primitive types. If the add(int, int)
method did not exist, Java would then box the int
arguments and call add(Integer, Integer)
— provided that there are no methods such as add(long, long)
, add(float, float)
, or add(double, double)
, which would take precedence over add(Integer, Integer)
.
Overloading and Varargs
Let’s consider an example involving varargs:
public class MathUtils {
public static int add(int... x) {
System.out.println("varargs version being called");
int s = 0;
for (int i : x) {
s += i;
}
return s;
}
}
public class Main {
public static void main(String[] args) {
System.out.println(MathUtils.add(3, 5));
}
}
In this example, calling add(3, 5)
will invoke the add(int...)
method, since there is no add(int, int)
, add(long, long)
, add(float, float)
, add(double, double)
, or add(Integer, Integer)
.
Consider this code:
public class MathUtils {
public static int add(int... x) {
System.out.println("varargs version being called");
int s = 0;
for (int i : x) {
s += i;
}
return s;
}
public static int add(int[] x) {
System.out.println("array version being called");
int s = 0;
for (int i : x) {
s += i;
}
return s;
}
}
This code will not compile because, in Java, varargs are essentially syntactic sugar for arrays. Defining both add(int...)
and add(int[])
is considered a duplicate method signature by the compiler.
General Workflow to Resolve Overloaded Method Call
When the compiler resolves overloaded method calls, it follows these steps:
- Exact Match: The compiler first looks for an exact match of the method signature that matches the provided argument types exactly.
- Match Found:
add(int, int)
- Match Found:
- Widening Primitive Conversion: If no exact match is found, the compiler looks for methods that can be called by widening the primitive types.
- int to long: Converts the
int
arguments tolong
. - Match Found:
add(long, long)
- int to float: Converts the
int
arguments tofloat
. - Match Found:
add(float, float)
- int to double: Converts the
int
arguments todouble
. - Match Found:
add(double, double)
- int to long: Converts the
- Boxing Conversion: If no suitable widening conversion is found, the compiler looks for methods where the primitive types can be boxed to their corresponding wrapper classes.
- int to Integer: Converts the
int
arguments toInteger
. - Match Found:
add(Integer, Integer)
- int to Integer: Converts the
- Varargs: If no exact match, widening, or boxing conversion is found, the compiler will consider methods with varargs.
- int… x: Matches any number of
int
arguments. - Match Found:
add(int...)
- int… x: Matches any number of
- Widening Reference Conversion: If still no match, the compiler looks for methods that can be called by widening the reference types (e.g., from a subclass to a superclass).
- int to Object: Converts the
int
arguments toObject
. Match Found:add(Object, Object)
- int to Object: Converts the
- Ambiguity and Errors: If there are multiple equally specific methods, the compiler will throw an ambiguity error. If no suitable method is found, the compiler will throw a “method not found” error.
Conclusion
Java follows a specific order of precedence, starting with the most specific match and moving to less specific matches if necessary. By understanding this workflow, you can better anticipate which method will be called in your overloaded methods.