Java - Static Factory Methods and Autoboxing
While Java's focus on performance made the primitive types inevitable in the language, its commitment to strong backward compatibility made primitives continue to remain in Java. Thus, the divide between primitive types and reference types (objects) continues to exist. This means conversions between primitive and reference types are inevitable. Over time, Java's support for easing these conversions has evolved and improved.
Versions before Java 5 did not have automatic boxing and unboxing features. For example, the following would lead to a compile error in Java 1.4
public class BoxingDemo {
public static void main(String[] args) {
int number = 10;
Integer integer = number;
}
}
Note how the compiler complains that int
and java.lang.Interger
are incompatible.
root@temp:~/code# javac BoxingDemo.java
BoxingDemo.java:4: incompatible types
found : int
required: java.lang.Integer
Integer integer = number;
^
1 error
If you compile the same code with the Java 5 compiler, it will compile successfully.
The obvious choice in versions up to Java 1.4 to convert a primitive to an instance of its respective wrapper type was to make use of the constructor provided by the wrapper class.
public class BoxingDemo {
public static void main(String[] args) {
int number = 10;
Integer integer = new Integer(number);
}
}
However, when Java 5 added the autoboxing feature, it was a language feature without any changes to the JVM. The compiler would detect the conversions between primitives and their corresponding wrapper types and generate the bytecode accordingly. At this point, if one were to guess what code the compiler would have generated - we would think it would be similar to the code above, invoking the constructor. However, Java added a set of valueOf
methods to all the wrapper classes.
To understand the reason behind valueOf
methods, let's understand what static factory methods are capable of. The famous "Effective Java" book describes static factory methods and suggests developers consider using them instead of the constructors.
One of the benefits of the static factory methods is developers can name them as they want so that the developers who use them can understand the context better. Since constructors are bound to have the class name, the developers cannot get creative with the names of constructors.
Also, one can have multiple static factory methods with the same signature but different names, which is impossible to achieve with the constructors. One benefit is that a customised strategy can be used to construct the object—for example, a mutable or immutable structure. In a typical implementation of this strategy, we would return one of the subtypes from the static factory methods.
Another advantage of static factory methods, as mentioned by "Effective Java", is that, unlike constructors, these methods need not always create new objects. This provides a great opportunity to cache the objects and return them as required. Modern Java takes advantage of this heavily. While in the initial days of Java (starting in the mid-1990s), the commonly used heap sizes were in MBs, it is very common for modern Java deployments to require several GBs of memory. Hence, spending a few MBs to improve the throughput and accommodate more requests with the same set of resources is worth sparing. This is one of the main reasons Java shifted to static factory methods.
The code developer writes:
public class BoxingDemo {
public static void main(String[] args) {
int number = 10;
Integer integer = number;
}
}
Java treats it as follows:
public class BoxingDemo {
public static void main(String[] args) {
int number = 10;
Integer integer = Integer.valueOf(number);
}
}
By default, Java caches Integer
values from -128 to 127. Since the number 10 falls in that range, there will only be one Integer object for value 10. One can customise the JVM to cache larger values.
Similarly, Java caches all possible values for Byte type, which range from -128 to 127. Boolean values TRUE
and FALSE
are cached too. Short type caches values from -128 to 127. Long type, too, caches the values in the same range. Character type caches the standard 7-bit ASCII characters.
Now that we understand how caching benefits wouldn't have been possible without static factory methods and with autoboxing, the Java compiler gets the control to generate the code using these factory methods. Still, there are also disadvantages to this magic conversion.
Consider the following code.
import java.util.ArrayList;
import java.util.List;
public class BoxingDemo {
public static void main(String[] args) {
int number = 10;
final List<Integer> integers = new ArrayList<>();
integers.add(number); //autoboxing happens here
// integers.add(Integer.valueOf(number)); - this is what the above line means
}
}
While adding an item to the List
variable integers
, the number
primitive gets converted into an object of type Integer
. A beginner Java programmer might probably think that List
can accept a value of a primitive kind too.
Over the years, I have seen the use of static factory methods in core Java libraries steadily increase. In this post, several static factory methods related to the file IO are mentioned.