Java, being a statically-typed language, often requires explicit type conversions and careful handling of primitive types and their corresponding wrapper classes. However, certain features introduced in later versions of Java, particularly in the java.lang
package, aim to simplify the process of working with primitive types and their object counterparts. In this article, we’ll delve into four such features: autoboxing, auto-unboxing, widening, and varargs methods.
Autoboxing and Auto-Unboxing
Autoboxing and auto-unboxing were introduced in Java 5 as a means to automatically convert between primitive types and their corresponding wrapper classes (Integer
, Double
, Boolean
, etc.) without explicit intervention from the programmer.
Autoboxing
Autoboxing refers to the automatic conversion from a primitive type to its corresponding wrapper object by the compiler.
For example, when we write: Integer I = 10
, the compiler converts the int
to Integer
automatically through autoboxing.
After compilation, the above line becomes: Integer I = Integer.valueOf(10);
. Internally, autoboxing is always implemented using .valueOf()
.
So, Integer I = Integer.valueOf(10);
is what the compiler executes at the time of autoboxing.
Auto-Unboxing
AutoUnboxing refers to the automatic conversion of a wrapper object to its corresponding primitive type by the compiler.
For example:
Integer I = new Integer(10);
int i = I; // Compiler converts Integer to int automatically by autounboxing
After compilation, the above line becomes:
int i = I.intValue();
Internally, the autounboxing concept is implemented using xxxValue()
methods.
The conversion can be summarized as follows:
- Primitive value ———– Autoboxing[valueOf()] ——————-> Wrapper object
- Wrapper object ————- AutoUnboxing[xxxValue()] —————-> Primitive value
Here’s an example demonstrating autoboxing and autounboxing in code:
class Test {
static Integer I = 10; // AutoBoxing
public static void main(String[] args) {
int i = I; // AutoUnboxing
m1(i);
}
public static void m1(Integer K) { // AutoBoxing
int m = K; // AutoUnboxing
System.out.println(m); // Output: 10
}
}
Just because of autoboxing and autounboxing, we can use wrapper class objects and primitives interchangeably from Java 1.5 onwards.
Example 2:
class Test {
static Integer I = 0;
public static void main(String[] args) {
int m = I;
System.out.println(m); // Output: 0
}
}
However, if the wrapper object is null when performing autoboxing, it will result in a NullPointerException:
class Test {
static Integer I;
public static void main(String[] args) {
int m = I;
System.out.println(m); // NullPointerException
}
}
To avoid NullPointerException in such cases, ensure that the wrapper object is not null before performing autounboxing.
Lets see one more example for another use case
Integer X = 10;
Integer Y = X;
X++;
System.out.println(X); // Output: 11
System.out.println(Y); // Output: 10
System.out.println(X == Y); // Output: false
Explanation:
Integer X = 10;
: This line creates anInteger
objectX
with the value10
.Integer Y = X;
: Here, a reference to the sameInteger
object thatX
points to is assigned toY
.X++;
: This increments the value ofX
by 1. SinceInteger
objects are immutable, a newInteger
object with the value11
is created, andX
now points to this new object.System.out.println(X);
: Prints the value ofX
, which is11
.System.out.println(Y);
: Prints the value ofY
, which still holds the original value ofX
, i.e.,10
.System.out.println(X == Y);
: Compares the references ofX
andY
, which are pointing to different objects after the increment operation, hence it returnsfalse
.
Note: All wrapper classes are immutable, meaning once we create a wrapper object, we cannot modify its value. If we attempt to perform any changes to it, a new object will be created instead. This is why a new Integer
object with the value 11
is created when we increment X
.
Few more examples:
Example 1
Integer x = new Integer(10); // x => 10
Integer y = new Integer(10); // y => 10
System.out.println(x == y); // Output: false
x
and y
are referencing different Integer
objects, even though they hold the same value 10
. Therefore, the comparison using ==
returns false
.
Example 2
Integer x = new Integer(10); // x => 10
Integer y = 10; // y => 10
System.out.println(x == y); // Output: false
x
refers to a new Integer
object with the value 10
, while y
refers to an Integer
object from the Integer cache. Though they hold the same value, they are different objects, hence false
is printed.
Example 3
Integer x = 10; // x, y => 10
Integer y = 10;
System.out.println(x == y); // Output: true
Both x
and y
refer to the same Integer
object with the value 10
. In this case, they are referencing the same cached object, so the comparison using ==
returns true
.
Example 4
Integer x = 100; // x, y => 100
Integer y = 100;
System.out.println(x == y); // Output: true
Similar to the previous case, x
and y
reference the same Integer
object with the value 100
, which is within the range of the Integer cache. Hence, true
is printed.
Example 5
Integer x = 1000; // x => 1000
Integer y = 1000; // y => 1000
System.out.println(x == y); // Output: false
The values 1000
are outside the range of the Integer cache (-128 to 127), so new Integer
objects are created for both x
and y
, resulting in false
when compared using ==
.
To facilitate autoboxing, Java internally maintains a buffer of wrapper objects upon the loading of wrapper classes. This buffer strategy optimizes memory usage by reusing existing objects. When an object is required, the JVM checks if it’s already present in the buffer. If so, the existing object is utilized; otherwise, a new object is created.
The buffer concept is available only within specific ranges for each wrapper type:
Byte
: Always available.Short
: Ranges from -128 to 127.Integer
: Ranges from -128 to 127.Long
: Ranges from -128 to 127.Character
: Ranges from 0 to 127.Boolean
: Always available.
See below example usage which demonstrating buffer behavior
Integer x = 127;
Integer y = 127;
System.out.println(x == y); // Output: true
Integer x = 128;
Integer y = 128;
System.out.println(x == y); // Output: false
Boolean x = false;
Boolean y = false;
System.out.println(x == y); // Output: true
Double x = 10.0;
Double y = 10.0;
System.out.println(x == y); // Output: false
Internally, autoboxing is implemented using valueOf()
, so the buffer concept applies to it as well.
Examples using valueOf()
:
Integer x = new Integer(10);
Integer y = new Integer(10);
System.out.println(x == y); // Output: false
Integer x = 10;
Integer y = 10;
System.out.println(x == y); // Output: true
Integer x = Integer.valueOf(10);
Integer y = Integer.valueOf(10);
System.out.println(x == y); // Output: true
Integer x = 10;
Integer y = Integer.valueOf(10);
System.out.println(x == y); // Output: true
Widening Methods
Let’s delve into overloading with respect to autoboxing, widening, and varargs, but befor that let’s see what is widening exactly.
Widening refers to the automatic conversion of a smaller data type to a larger data type in the hierarchy. In Java, the widening conversion follows the hierarchy: byte -> short -> char -> int -> long -> float -> double.
In the given example:
class Test {
public static void m1(Integer I) {
System.out.println("Autoboxing");
}
public static void m1(long l) {
System.out.println("Widening");
}
public static void main(String args[]) {
int x = 10;
m1(x);
}
}
Explanation:
- We have two overloaded methods
m1
: one accepts anInteger
parameter, and the other accepts along
parameter. - In the
main
method, we have anint
variablex
. - When we call
m1(x)
, theint
valuex
undergoes widening to match thelong
parameter, as widening is preferred over autoboxing. - Therefore, the output will be “Widening”.
This happens because widening is applicable from Java 1.0 version and gets priority over autoboxing, which was introduced later in Java 1.5 version.
Varargs Methods
Varargs, short for variable-length arguments, allow methods to accept a variable number of arguments of a specified type. This feature simplifies the creation of methods that operate on a variable number of inputs.
Syntax
The syntax for declaring a varargs parameter is to use an ellipsis (...
) after the parameter type.
public void exampleMethod(String... strings) {
// Method body
}
public void printValues(String... values) {
for (String value : values) {
System.out.println(value);
}
}
// Invoking the method
printValues("Hello", "World"); // Output: Hello\nWorld
printValues("Java", "is", "awesome"); // Output: Java\nis\nawesome
Now, Let’s see overloading with respect to varargs and widening.
class Test {
public static void m1(Int... x) {
System.out.println("Varargs method");
}
public static void m1(long l) {
System.out.println("Widening");
}
public static void main(String args[]) {
int x = 10;
m1(x);
}
}
- We have two overloaded methods
m1
: one accepts a varargs parameter of typeInt
, and the other accepts along
parameter. - In the
main
method, we have anint
variablex
. - When we call
m1(x)
, theint
valuex
undergoes widening to match thelong
parameter, as widening is preferred over varargs methods. - Therefore, the output will be “Widening” (as widening is from 1.0v to it gets first chance instead of Autoboxing).
Let’s see overloading with respect to varargs, autoboxing, and widening
class Test {
public static void m1(Int... x) {
System.out.println("Varargs method");
}
public static void m1(Integer I) {
System.out.println("Autoboxing");
}
public static void main(String args[]) {
int x = 10;
m1(x);
}
}
- We have two overloaded methods
m1
: one accepts a varargs parameter of typeInt
, and the other accepts anInteger
parameter. - In the
main
method, we have anint
variablex
. - When we call
m1(x)
, theint
valuex
undergoes autoboxing to match theInteger
parameter, as autoboxing is preferred over varargs methods. - Therefore, the output will be “Autoboxing”.
This happens because, while resolving overloaded methods, the compiler gives priority to widening, then autoboxing, and finally varargs methods. Since autoboxing has a higher priority than varargs, it gets selected in this scenario.
Order Priority -> 1. Widening 2. Autoboxing 3. var-arg method
Compilation Error (CE)
In some cases compiler error occurs, Let’s see why we face a compilation error (CE).
class Test {
public static void m1(Long l) {
System.out.println("Long");
}
public static void main (String args[]) {
int x = 10;
m1(x);
}
}
Compilation Error: m1(java.lang.Long) in Test cannot be applied to (int)
Here, compiler error occurs because there’s no direct conversion path from int
to Long
involving both autoboxing and widening.
- In the
m1
method, the parameter type isLong
. - In the
main
method, an int variablex
is declared and initialized with the value 10. - When calling
m1(x)
, the compiler attempts to find a suitable method to match the argumentx
, which is of typeint
. - In Java, there are no implicit conversions from
int
toLong
that involve both autoboxing and widening. Althoughint
can be widened tolong
and then autoboxed toLong
, this kind of conversion sequence is not supported by the compiler. - Therefore, the compiler reports a compilation error because there is no suitable overload of
m1
that accepts anint
argument directly or through a sequence of implicit conversions involving autoboxing and widening. - To resolve the error, you can either change the type of
x
toLong
or provide a suitable overload ofm1
that accepts anint
argument or its equivalent types.
This limitation arises because after widening, autoboxing is not implemented in Java. However, after autoboxing, widening is internally implemented. This means that widening followed by autoboxing is not supported directly, leading to the compilation error.
What will happen in case of Java Objects
class Test {
public static void m1(Object o) {
System.out.println("Object version");
}
public static void main(String[] args) {
int x = 10;
m1(x);
}
}
The output will be “Object version”. As we know autoboxing then widening is possible in java
Explanation:
- The method
m1(Object o)
expects a parameter of typeObject
. - In the
main
method, we have anint
variablex
. - When
m1(x)
is called, autoboxing is applied to convertint
toInteger
. - Then, widening is applied to convert
Integer
toObject
, as widening is applicable in Java. - Therefore, the method
m1(Object o)
is invoked with the argumentInteger
, and the output will be “Object Version”.
Additionally, the statements:
Object o = 10; // is perfectly valid
Number n = 10; // is also perfectly valid
These statements are valid because of autoboxing. In Java, autoboxing allows automatic conversion between primitive types and their corresponding wrapper classes (e.g., int
to Integer
, double
to Double
, etc.). Since Integer
is a subclass of Object
, and Integer
is a subclass of Number
, the assignments Object o = 10;
and Number n = 10;
are perfectly valid.
Conclusion
Autoboxing, auto-unboxing, widening, and varargs methods are features introduced in Java to simplify the handling of primitive types and method parameterization. They enhance code readability, reduce boilerplate, and provide more flexibility in method invocation. However, it’s essential to understand their implications and usage patterns to leverage them effectively in Java programming.