1. Overview
Java constructors are the default mechanism for getting fully-initialized class instances. After all, they provide all the infrastructure required for injecting dependencies, either manually or automatically.
Even so, in a few specific use cases, it’s preferable to resort to static factory methods for achieving the same result.
In this tutorial, we’ll be highlighting the pros and cons of using static factory methods vs plain old Java constructors.
2. Advantages of Static Factory Methods Over Constructors
In an object-oriented language like Java, what could be wrong with constructors? Overall, nothing. Even so, the famous Joshua Bloch’s Effective Java Item 1 clearly states:
“Consider static factory methods instead of constructors”
While this isn’t a silver bullet, here are the most compelling reasons that sustain this approach:
- Constructors don’t have meaningful names, so they are always restricted to the standard naming convention imposed by the language. Static factory methods can have meaningful names, hence explicitly conveying what they do
- Static factory methods can return the same type that implements the method(s), a subtype, and also primitives, so they offer a more flexible range of returning types
- Static factory methods can encapsulate all the logic required for pre-constructing fully initialized instances, so they can be used for moving this additional logic out of constructors. This prevents constructors from performing further tasks, others than just initializing fields
- Static factory methods can be controlled-instanced methods, with the Singleton pattern being the most glaring example of this feature
3. Static Factory Methods in the JDK
There are plenty of examples of static factory methods in the JDK that showcase many of the advantages outlined above. Let’s explore some of them.
3.1. The String Class
Because of the well-known String interning, it’s very unlikely we’ll use the String class constructor to create a new String object. Even so, this is perfectly legal:
String value = new String("Baeldung");
In this case, the constructor will create a new String object, which is the expected behavior.
Alternatively, if we want to create a new String object using a static factory method, we can use some of the following implementations of the valueOf() method:
String value1 = String.valueOf(1);
String value2 = String.valueOf(1.0L);
String value3 = String.valueOf(true);
String value4 = String.valueOf('a');
There are several overloaded implementations of valueOf(). Each one will return a new String object, depending on the type of the argument passed to the method (e.g. int, long, boolean, char, and so forth).
The name expresses pretty clearly what the method does. It also sticks to a well-established standard in the Java ecosystem for naming static factory methods.
3.2. The Optional Class
Another neat example of static factory methods in the JDK is the Optional class. This class implements a few factory methods with pretty meaningful names, including empty(), of(), and ofNullable():
Optional<String> value1 = Optional.empty();
Optional<String> value2 = Optional.of("Baeldung");
Optional<String> value3 = Optional.ofNullable(null);
3.3. The Collections Class
Quite possibly the most representative example of static factory methods in the JDK is the Collections class. This is a non-instantiable class that implements only static methods.
Many of these are factory methods that also return collections, after applying to the supplied collection some type of algorithm.
Here are some typical examples of the class’ factory methods:
Collection syncedCollection = Collections.synchronizedCollection(originalCollection);
Set syncedSet = Collections.synchronizedSet(new HashSet());
List<Integer> unmodifiableList = Collections.unmodifiableList(originalList);
Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(originalMap);
The number of static factory methods in the JDK is really extensive, so we’ll keep the list of examples short for brevity’s sake.
Nevertheless, the above examples should give us a clear idea of how ubiquitous static factory methods are in Java.
4. Custom Static Factory Methods
Of course, we can implement our own static factory methods. But when is it really worth doing so, instead of creating class instances via plain constructors?
Let’s see a simple example.
Let’s consider this naive User class:
public class User {
private final String name;
private final String email;
private final String country;
public User(String name, String email, String country) {
this.name = name;
this.email = email;
this.country = country;
}
// standard getters / toString
}
In this case, there’re no visible warnings to indicate that a static factory method could be better than the standard constructor.
What if we want that all the User instances get a default value for the country field?
If we initialize the field with a default value, we’d have to refactor the constructor too, hence making the design more rigid.
We can use a static factory method instead:
public static User createWithDefaultCountry(String name, String email) {
return new User(name, email, "Argentina");
}
Here’s how we’d get a User instance with a default value assigned to the country field:
User user = User.createWithDefaultCountry("John", "[email protected]");
5. Moving Logic out of Constructors
Our User class could quickly rot into a flawed design if we decide to implement features that would require adding further logic to the constructor (alarm bells should be sounding off by this time).
Let’s suppose that we want to provide the class with the ability for logging the time at which every User object is created.
If we just put this logic into the constructor, we’d be breaking the Single Responsibility Principle. We would end up with a monolithic constructor that does a lot more than initialize fields.
We can keep our design clean with a static factory method:
public class User {
private static final Logger LOGGER = Logger.getLogger(User.class.getName());
private final String name;
private final String email;
private final String country;
// standard constructors / getters
public static User createWithLoggedInstantiationTime(
String name, String email, String country) {
LOGGER.log(Level.INFO, "Creating User instance at : {0}", LocalTime.now());
return new User(name, email, country);
}
}
Here’s how we’d create our improved User instance:
User user
= User.createWithLoggedInstantiationTime("John", "[email protected]", "Argentina");
6. Instance-Controlled Instantiation
As shown above, we can encapsulate chunks of logic into static factory methods before returning fully-initialized User objects. And we can do this without polluting the constructor with the responsibility of performing multiple, unrelated tasks.
For instance, suppose we want to make our User class a Singleton. We can achieve this by implementing an instance-controlled static factory method:
public class User {
private static volatile User instance = null;
// other fields / standard constructors / getters
public static User getSingletonInstance(String name, String email, String country) {
if (instance == null) {
synchronized (User.class) {
if (instance == null) {
instance = new User(name, email, country);
}
}
}
return instance;
}
}
The implementation of the getSingletonInstance() method is thread-safe, with a small performance penalty, due to the synchronized block.
In this case, we used lazy initialization to demonstrate the implementation of an instance-controlled static factory method.
It’s worth mentioning, however, that the best way to implement a Singleton is with a Java enum type, as it’s both serialization-safe and thread-safe. For the full details on how to implement Singletons using different approaches, please check this article.
As expected, getting a User object with this method looks very similar to the previous examples:
User user = User.getSingletonInstance("John", "[email protected]", "Argentina");
7. Conclusion
In this article, we explored a few use cases where static factory methods can be a better alternative to using plain Java constructors.
Moreover, this refactoring pattern is so tightly rooted to a typical workflow that most IDEs will do it for us.
Of course, Apache NetBeans, IntelliJ IDEA, and Eclipse will perform the refactoring in slightly different ways, so please make sure first to check your IDE documentation.
As with many other refactoring patterns, we should use static factory methods with due caution, and only when it’s worth the trade-off between producing more flexible and clean designs and the cost of having to implement additional methods.
As usual, all the code samples shown in this article are available over on GitHub.