Java optional parameters
When you design a method in a Java class, some parameters may be optional for its execution. No matter it is inside a DTO, a fat model domain object, or a simple stateless service class, optional method parameters are common.
From this article you will learn how to handle optional parameters in Java. We’ll focus on regular method, class constructors with optional fields, and quickly look at bad practices of the discussed topic. We’ll stop for a moment to look at Java 8 Optional and assess if it fits our needs.
Let’s get started.
1. Optional method parameters
You can tackle Java optional parameters in method in several different ways. I’ll walk you through from the simplest to more complex.
1.1. @Nullable annotation
Why just don’t pass the null around? It’s a simple solution which doesn’t require any extra work. You don’t have any object which is required as one of 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.
User createUser(String name, @Nullable Email email) { // ... }
Don’t you agree this method declaration is self-explanatory?
While simple, the issue with null passing approach 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.2. Optional lists
Instead of null, we can sometime 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 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 code base.
And you know what? Java already has empty instances of all collections which you can use. You’ll find them in the Collections utility class.
User createUser(String name, List<Rights> rights) { // ... }
import java.util.Collections; // ... create("bob", Collections.emptyList());
1.3. Null object pattern
The concept of empty collections also applies to other classes. An empty collection is just a regular collection with zero elements. By the same token, you can think about other objects in your applications.
The Null object is a special instance of a class which represents 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:
public class User { public static final User EMPTY = new User("", Collections.emptyList()); private final String name; private final List<Rights> rights; public User(String name, List<Rights> rights) { Objects.requireNonNull(name); Objects.requireNonNull(rights); this.name = name; this.rights = rights; } // ... }
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 which extends the previous one:
public class AnonymousUser extends User { public static final AnonymousUser INSTANCE = new AnonymousUser(); private AnonymousUser() { super("", Collections.emptyList()); } @Override public void changeName(String newName) { throw new AuthenticationException("Only authenticated user 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.4. Method overloading
If your design a method with optional parameters, you can expose overloaded versions of that method. Each method should accept only parameters which are required.
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 overloaded method. In other words, you hide the default values for optional parameters from method’s callers.
User createUser(String name) { this.createUser(name, Email.EMPTY); } User createUser(String name, Email email) { Objects.requireNonNull(name); Objects.requireNonNull(rights); // ... }
The overloaded methods can call each other but it’s not mandatory. You can implement each method independently if it’s more convenient. However, usually you validate all parameters and put the logic inside the method with the longest parameter list.
It’s worth mentioning that method overloading is widely used inside the standard Java library. When you learn how to design APIs, learn from people with greater experience.
1.5. Parameter Object pattern
The majority of developers agree that when the list of method parameters grows too long it become hard to read. Usually, you handle the issue with the Parameter Object pattern. The parameter object is a named container class which groups all method parameters.
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.
Let’s see how do we solve this more general problem with …
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.
If you agree, you should check out the Builder pattern.
2.1. Builder pattern
Let’s consider a class with multiple optional fields:
class ProgrammerProfile { // required field private final String name; // optional fields private final String blogUrl; private final String twitterHandler; private final String githubUrl; public ProgrammerProfile(String name) { Objects.requireNonNull(name); this.name = name; // other fields assignment... } // getters }
If you created a constructors to cover all possible combination with optional parameters, you would end up with a quite overwhelming list.
How to avoid multiple constructors? Use a builder class.
You usually implement the builder as an inner class of the class it suppose to build. That way, both classes have access to their private members.
Take a look at a builder for the class from the previous example:
class ProgrammerProfile { // fields, getters, ... private ProgrammerProfile(Builder builder) { Objects.requireNonNull(builder.name); name = builder.name; blogUrl = builder.blogUrl; twitterHandler = builder.twitterHandler; githubUrl = builder.githubUrl; } public static Builder newBuilder() { return new Builder(); } static final class Builder { private String name; private String blogUrl; private String twitterHandler; private String githubUrl; private Builder() {} public Builder withName(String val) { name = val; return this; } public Builder withBlogUrl(String val) { blogUrl = val; return this; } public Builder withTwitterHandler(String val) { twitterHandler = val; return this; } public Builder withGithubUrl(String val) { githubUrl = val; return this; } public ProgrammerProfile build() { return new ProgrammerProfile(this); } } }
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.
It’s a pretty simple technique once you think about it.
The client code of that builder which sets only a selected optional parameter may look as follows:
ProgrammerProfile.newBuilder() .withName("Daniel") .withBlogUrl("www.dolszewski.com/blog/") .build();
With the builder, you can create all possible combinations with optional parameters of the object.
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:
ProgrammerProfile.newBuilder() .withBlogUrl("www.dolszewski.com/blog/") .withTwitterHandler("daolszewski") .build();
The compiler won’t report any error. You’ll realize the problem with the missing required parameter only at the runtime.
So how do you tackle the issue?
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 ProgrammerProfile { // ... 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; } // ... } }
2.3. Builder class generation
You may think that builders require 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 which allow to generate class builders. IntelliJ users can check the InnerBuilder plugin, while Eclipse fans can take a look at Spart Builder Generator. You can also find alternative plugins in the official repositories.
If you use the project Lombok, it also simplify working with class builders. You can check this short introduction to Lombok builders if you need a place to start.
3. Java optional parameters anti-patterns
While browsing the web in search for approaches to work with Java optional parameters, you can find a few additional suggestions than we already covered. Let me explain why you should consider them as wrong approaches.
3.1. Maps
Technically speaking, method’s input is a set of key-value pairs. In Java, we have a standard built-in data structure which matches this description – the Map.
The compile won’t prevent you from using HashMap<String, Object> as a container for all optional method parameters but your common sense should.
Although you can put anything in such HashMap, it’s wrong idea. This approach is hard to understand, unreadable and will quickly become your maintenance nightmare.
Still not convinced?
You should definitely consider switching your career to a JavaScript developer. It’s much easier to cry in the company.
3.2. Java varargs
Just to be clear, there’s absolutely nothing wrong in using Java varargs. If you don’t know with how many arguments your method will be called, varargs is a perfect fit.
But using varargs as a container for a single value, which might be present or not, is a misuse. Such declaration allows to call a method with more optional values than expected. We discussed much more descriptive approaches for handling a single optional parameters.
3.3. Why not Optional as method argument?
Finally, the most controversial approach – Java 8 Optional as a method input. I’ve already written a post about Optional use cases in which I also covered method parameters. Let me extend what you can find there.
Memory usage
When you create an instance of the Optional class, you have to allocate the memory for it. While the empty optional instance accessed with Optional.empty() is a reusable singleton (just like empty collections we’ve already discussed), non empty instances will occupy the operating memory.
Wrapping objects using Optional factory methods just for the purpose of calling a method which will immediately unwrap them doesn’t make sense if you compare this approach with other possibilities.
Yet, nowadays Garbage Collectors handle short-lived objects very well. The memory allocation isn’t a big deal. Do we have any other cons?
Coding with reader in mind
What about code readability?
createProfile("Daniel", Optional.of("www.dolszewski.com/blog/"), Optional.of("daolszewski"), Optional.of("https://github.com/danielolszewski"));
Maybe it’s just a matter of personal preference but for many developers multiple Optional factory calls are distracting. The noise in the code for the reader. But again, it’s just a matter of taste. Let’s find something more convincing.
Java langauge architect opinion
Brian Goetz, who is Java language architect at Oracle once stated that Optional was added to the standard library with methods’ results in mind, not their inputs.
But software developers are rebels and don’t like listening to authorities. This argument may also seems weak. We have to go deeper.
Does Optional solve optional parameter problem?
If you have a method declaration like this:
doSomethingWith(Optional<Object> parameter);
How many possible inputs should you expect?
The parameter can be either a wrapped value or the empty optional instance. So the answer is 2, right?
Wrong.
The real answer is 3 because you can also pass the null as the argument. You should have a limited trust to inputs when you don’t know who will be the caller of your API.
In that case, before processing the parameter you should check if the Optional isn’t equal to the null and then if the value is present. Quite complicated, don’t you agree?
I pretty sure I have not exhausted the topic yet. As this is one of potential holy wars of Java programming, you should form your own opinion. If you want to add something to the list of arguments against Optional as a method parameter, please share your thoughts in the comments. It’s more than welcome.
Conclusion
Let’s recap what we’ve learned. Java doesn’t have a possibility to set a default value to method parameters. The language provides us we many other alternatives for handling optional parameters.
These alternatives include the @Nullable annotation, null objects, method overloading, and the Parameter Object pattern. We also familiarize with class builders for objects with multiple optional fields. Lastly, we reviewed common bad practices and potential misuses.
If you find the article helpful, I’ll be grateful for sharing it with your followers. I’d also love to know your thoughts, all comments are welcome.
Published on Java Code Geeks with permission by Daniel Olszewski, partner at our JCG program. See the original article here: Java optional parameters Opinions expressed by Java Code Geeks contributors are their own. |