Mastering Autoboxing and Auto-Unboxing: A Comprehensive Guide for Java Developers

Table of Contents

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:

Java
Integer I = new Integer(10);
int i = I;  // Compiler converts Integer to int automatically by autounboxing

After compilation, the above line becomes:

Java
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:

Java
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:

Java
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:

Java
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

Java
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:

  1. Integer X = 10;: This line creates an Integer object X with the value 10.
  2. Integer Y = X;: Here, a reference to the same Integer object that X points to is assigned to Y.
  3. X++;: This increments the value of X by 1. Since Integer objects are immutable, a new Integer object with the value 11 is created, and X now points to this new object.
  4. System.out.println(X);: Prints the value of X, which is 11.
  5. System.out.println(Y);: Prints the value of Y, which still holds the original value of X, i.e., 10.
  6. System.out.println(X == Y);: Compares the references of X and Y, which are pointing to different objects after the increment operation, hence it returns false.

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

Java
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

Java
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

Java
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

Java
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

Java
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

Java
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():

Java
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:

Java
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 an Integer parameter, and the other accepts a long parameter.
  • In the main method, we have an int variable x.
  • When we call m1(x), the int value x undergoes widening to match the long 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.

Java
public void exampleMethod(String... strings) {
    // Method body
}
Java
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.

Java
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 type Int, and the other accepts a long parameter.
  • In the main method, we have an int variable x.
  • When we call m1(x), the int value x undergoes widening to match the long 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

Java
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 type Int, and the other accepts an Integer parameter.
  • In the main method, we have an int variable x.
  • When we call m1(x), the int value x undergoes autoboxing to match the Integer 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).

Java
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.

  1. In the m1 method, the parameter type is Long.
  2. In the main method, an int variable x is declared and initialized with the value 10.
  3. When calling m1(x), the compiler attempts to find a suitable method to match the argument x, which is of type int.
  4. In Java, there are no implicit conversions from int to Long that involve both autoboxing and widening. Although int can be widened to long and then autoboxed to Long, this kind of conversion sequence is not supported by the compiler.
  5. Therefore, the compiler reports a compilation error because there is no suitable overload of m1 that accepts an int argument directly or through a sequence of implicit conversions involving autoboxing and widening.
  6. To resolve the error, you can either change the type of x to Long or provide a suitable overload of m1 that accepts an int 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

Java
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 type Object.
  • In the main method, we have an int variable x.
  • When m1(x) is called, autoboxing is applied to convert int to Integer.
  • Then, widening is applied to convert Integer to Object, as widening is applicable in Java.
  • Therefore, the method m1(Object o) is invoked with the argument Integer, and the output will be “Object Version”.

Additionally, the statements:

Java
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.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!