Java Optional Parameters
In this post, we feature a comprehensive article about the Java Optional Parameters. When you design a method in a Java class, some parameters may be optional for its execution.
1. Java Optional Parameters
You can tackle Java optional parameters in a method in several different ways. We will see them in the following:
1.1 Mutability with accessors
Using standard getters and setters is a simple way to work with an object that has optional instance parameters. We use a default constructor with mandatory parameters to create the object then invoke the setter methods to set the value of each optional parameter as needed. We can set the default values for parameters within a constructor, if necessary:
ChemicalPotion.java
public class ChemicalPotion { private String name; // required private int materialA; public ChemicalPotion (String name) { this.name = name; } public String getName() { return name; } public int getMaterialA() { return materialA; } public void setMaterialA(int materialA) { this.materialA= materialA; } // other getters and setters }
This approach is the ubiquitous JavaBeans pattern and is likely the simplest strategy available for working with optional parameters. There are, unfortunately, serious drawbacks to use this approach, especially if thread safety is a concern. The use of this pattern requires that the object is mutable since we can change it after its creation.
Since the creation of the instance and setting of its state are decoupled and do not occur atomically, it’s possible that the instance could be used before it’s in a valid state. In a sense, we’re splitting the construction of the object over multiple calls.
You can consider this pattern when thread safety and creating a robust API isn’t a primary concern.
1.2 @Nullable annotation
You can just pass the null. It’s a simple solution that doesn’t require any extra work. You don’t have any object which is required as one of the method’s parameters? No problem. Just pass the null and the compiler is happy.
The issue here is readability. How does the programmer who calls a method knows if he can safely pass the null? For which parameters nulls are acceptable and which are mandatory?
To make it clear that the null is a valid input, you can use a @Nullable annotation.
passing the null
Student createStudent(String name, @Nullable Email email) { // ... }
Although null passing approach looks simple, the issue with it is that it can easily get out of control. The team may quickly start overusing it and make the code base hard to maintain with plenty of null check conditions.
There are other options, though.
1.3 Optional lists
Instead of null, we can sometimes create an empty representation of a class. Think about Java collections. If a method accepts a list or a map, you should never use nulls as the input.
An empty collection is always better than null because in the majority of cases it doesn’t require any special treatment.
You may wonder why you should waste memory to create empty collections. After all, null doesn’t cost you anything.
Your doubts are justified. Fortunately, there’s a simple solution.
You shouldn’t create a new instance of a collection whenever you need an empty representative. Reuse the same instance across the codebase. Java already has empty instances of all collections which you can use. You’ll find them in the Collections utility class.
passing a List
Student createStudent(String name, List courses) { // ... } . . . import java.util.Collections; // ... createStudent("John", Collections.emptyList());
1.4 Null object pattern
The Null object is a special instance of a class that represents a missing value. If some method expects an object as a parameter, you can always pass the Null object representation without worry it will cause an unexpected exception at the runtime.
You can implement the Null object pattern in two ways.
For simple value objects, a default instance with predefined values assigned to properties is enough. Usually, you expose this Null object as a constant so you can reuse it multiple times. For instance:
Student.java
public class Student { public static final User EMPTY = new Student("", Collections.emptyList()); private final String name; private final List courses; public Student(String name, List courses) { Objects.requireNonNull(name); Objects.requireNonNull(courses); this.name = name; this.courses = courses; } // ... }
If your Null object also needs to mimic some behavior exposed via methods, a simple instance may not work. In that case, you should extend the class and override such methods.
Here is an example that extends the previous one:
AnonymousStudnet.java
public class AnonymousStudnet extends Student{ public static final AnonymousStudnet INSTANCE = new AnonymousStudnet (); private AnonymousStudnet() { super("", Collections.emptyList()); } @Override public void changeName(String newName) { throw new AuthenticationException("Only authenticated student can change the name"); } }
A dedicated Null object class allows you to put many corner cases in a single place which makes maintenance much more pleasant.
1.5 Method overloading
The idea here is that we start with a method that only takes the required parameters. We provide an additional method that takes a single optional parameter. We then provide yet another method that takes two of these parameters, and so on.
The methods which take fewer parameters supply default values for the more verbose signatures.
With this approach, you don’t have to expect that the caller will provide default values for optional parameters. You pass the defaults on your own inside the overloaded method. In other words, you hide the default values for optional parameters from the method’s callers.
Method Overloading
Student createStudent(String name) { this.createStudent(name, Email.EMPTY); } Student createStudent(String name, Email email) { Objects.requireNonNull(name); Objects.requireNonNull(rights); // ... }
See the example below:
ChemicalPotion.java
public class ChemicalPotion { private String name; // required private int materialA; // in mg private int materialB; // in mg private int materialC; // in mg // instance fields public ChemicalPotion(String name) { this(name, 0); } public ChemicalPotion(String name, int materialA) { this(name, materialA, 0); } public ChemicalPotion(String name, int materialA, int materialB) { this(name, materialA, materialB, 0); } public ChemicalPotion(String name, int materialA, int materialB, int materialC) { this.name = name; this.materialA= materialA; this.materialB= materialB; this.materialC= materialC; } }
The simplicity and familiarity of the method overloading approach make it a good choice for use cases with a small number of optional parameters.
The main downside of using this approach is that it does not scale well – as the number of parameters increases. This only gets worse with the fact that our optional parameters are of the same type. Clients could easily order the parameters wrongly – such a mistake would not be noticed by the compiler and would likely result in a subtle bug at runtime.
1.6 Parameter Object pattern
Most of the developers agree that when the list of method parameters grows too long it becomes hard to read. Usually, you handle the issue with the Parameter Object pattern. The parameter object is a named container class that groups all method parameters.
Now the question is: Does it solve the problem of optional method parameters?
No. It doesn’t. It just moves the problem to the constructor of the parameter object.
In the following section, we will describe the Optional constructor parameters.
2. Optional constructor parameters
In the perspective of the problem with optional parameters, simple constructors don’t differ from regular member methods. You can successfully use all the techniques we’ve already discussed also with constructors.
However, when the list of constructor parameters is getting longer and many of them are optional parameters, applying constructor overloading may seem cumbersome. Let’s see what is Builder pattern.
2.1 Builder pattern
One of the main advantages of the builder pattern is that it scales well with large numbers of optional and mandatory parameters.
If you created a constructor to cover all possible combinations with optional parameters, you would end up with a quite overwhelming list.
In our example here, we require the mandatory parameter in the constructor of the builder. We expose all of the optional parameters in the rest of the builder’s API.
You usually implement the builder as an inner class of the class it supposes to build. That way, both classes have access to their private members. Take a look at the following example:
StudentProfile.java
class StudentProfile { // required field private final String name; // optional fields private final String email; private final String birthDate; private final String githubUrl; private StudentProfile (Builder builder) { Objects.requireNonNull(builder.name); name = builder.name; email= builder.email; birthDate= builder.birthDate; githubUrl = builder.githubUrl; } public static Builder newBuilder() { return new Builder(); } static final class Builder { private String name; private String email; private String birthDate; private String githubUrl; private Builder() {} public StudentProfile.Builder withName(String val) { name = val; return this; } public StudentProfile.Builder withEmail(String val) { email= val; return this; } public StudentProfile.Builder withBirthDate(String val) { birthDate= val; return this; } public StudentProfile.Builder withGithubUrl(String val) { githubUrl = val; return this; } public StudentProfile build() { return new StudentProfile(this); } } }
Another advantage is that it’s much more difficult to make a mistake when setting values for optional parameters. We have explicit methods for each optional parameter, and we don’t expose callers to bugs that can arise due to calling methods with parameters that are in the wrong order.
Instead of a public constructor, we only expose one single static factory method for the inner builder class. The private constructor (which the builder calls in the build() method) uses a builder instance to assign all fields and verifies if all required values are present.
The client code of that builder which sets only a selected optional parameter may look as follows:
Builder code for StudentProfile
StudentProfile.newBuilder() .withName("John") .withEmail("john.uni@gmail.com") .build();
With the builder, you can create all possible combinations with optional parameters of the object.
And also for the ChemicalPotion
example, we can have something like this:
Builder code for ChemicalPotion
ChemicalPotionWithBuildermyPotion = new ChemicalPotionWithBuilder.ChemicalPotionBuilder("Energetic Potion") .withMaterialA(5000) .withMaterialB(2000) .withMaterialC(3000) .build();
2.2 Compile time safe class builder
Unfortunately, just by look at the methods of the builder from the previous paragraph, you can’t really tell which parameters are optional and which are required. What is more, without knowing you can omit the required parameters by accident.
Check out the following example of the incorrectly used builder:
Builder code for StudentProfile
StudentProfile.newBuilder() .withEmail("john.uni@gmail.com") .withBirthDate("2012-01-05") .build();
The compiler won’t report any error. You’ll realize the problem with the missing required parameter only at the runtime.
You need to slightly modify the builder factory method so that you can call it only with required parameters and left builder methods only for optional parameters.
Here’s all you have to change:
class StudentProfile { // ... public static Builder newBuilder(String name) { return new Builder(name); } public static final class Builder { private final String name; // ... private Builder(String name) { this.name = name; } // ... } }
You may think that builders require a hell of a lot of code. Don’t worry. You don’t have to type all that code on your own. All popular Java IDEs have plugins that allow generating class builders. IntelliJ users can check the InnerBuilder plugin.
3. Summary
In this article, we’ve looked at a variety of strategies for working with optional parameters in Java, such as method overloading, the builder pattern, and the strategy of allowing callers to supply null values.
We highlighted the relative strengths and weaknesses of each strategy and provided usage for each.
4. More articles
- Java Array – java.util.Arrays Example (with Video)
- Java List Example
- Unreachable Statement Java Error – How to resolve it
- What is null in Java
- Integer Java Class Example
- Java String Class Example (with video)
- Convert int to string Java Example (with video)
- Java Constructor Example (with video)
- Java Set Example (with video)
- Printf Java Example (with video)
- Java API Tutorial
5. Download the source code
You can download the full source code of this example here: Java Optional Parameters
Last updated on Aug. 11th, 2021