Why Should Java 8's Optional Not Be Used in Arguments

Why should Java 8's Optional not be used in arguments

Oh, those coding styles are to be taken with a bit of salt.

  1. (+) Passing an Optional result to another method, without any semantic analysis; leaving that to the method, is quite alright.
  2. (-) Using Optional parameters causing conditional logic inside the methods is literally contra-productive.
  3. (-) Needing to pack an argument in an Optional, is suboptimal for the compiler, and does an unnecessary wrapping.
  4. (-) In comparison to nullable parameters Optional is more costly.
  5. (-) The risk of someone passing the Optional as null in actual parameters.

In general: Optional unifies two states, which have to be unraveled. Hence better suited for result than input, for the complexity of the data flow.

Is it a good practice to use Optional as an attribute in a class?

Java 8's Optional was mainly intended for return values from methods, and not for properties of Java classes, as described in Optional in Java SE 8:

Of course, people will do what they want. But we did have a clear intention when adding this feature, and it was not to be a general purpose Maybe or Some type, as much as many people would have liked us to do so. Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result", and using null for such was overwhelmingly likely to cause errors.

The key here is the focus on use as a return type. The class is definitively not intended for use as a property of a Java Bean. Witness to this is that Optional does not implement Serializable, which is generally necessary for widespread use as a property of an object.

Optional T as a Record Parameter in Java

I would not attempt to explain why the current version of IntelliJ doesn't issue a warning for a record having optional fields (that's a question for developers of the IDE).

In this post, I'm addressing the question regarding recommended practices of using Optional, in particular with Java 16 Records.

Does the practice of not using Optional<T> types as parameters/fields not apply to record parameters

Firstly, Optional is not intended to be used as a field type, for that reason Optional doesn't implement Serializable (see). Secondly, records are data-carriers, their fields are final. Hence, if a record gets initialized with optionals that are empty, they would stay empty throughout all its time-span.

Usage of Optional

Here's a quote from the answer by @StuartMarks, Java and OpenJDK developer, regarding what Optional is meant to be used for:

The primary use of Optional is as follows:

Optional is intended to
provide a limited mechanism for library method return types where
there is a clear need to represent "no result," and where using null
for that is overwhelmingly likely to cause errors.

The only valid usage for Optional is returning it from a method where it was originated. And the caller should immediately unpack the optional (API offers plenty of methods for that). But if you're passing the optional object around and storing it somewhere, then what you're doing isn't a good practice.

Optional is not meant for

Optional is not meant to be used:

  • As a field type;
  • As a type of the method parameter;
  • To be stored in a Collection;
  • Or utilized to perform null-checks. Substituting explicit null-check with Optional.ofNullable() is an antipattern.

Here's a quote from the answer by @Brian Goetz, Java Language Architect
(Should Java 8 getters return optional type?):

Of course, people will do what they want...

For example, you probably should never use it for something that
returns an array of results, or a list of results; instead return an
empty array or list. You should almost never use it as a field
of something or a method parameter.

Also, have a look at this answer by StuartMarks, here's a small quote:

having an Optional in a class field or in a data structure, is considered a misuse of the API. First, it goes against the main design goal of Optional as stated at the top. Second, it doesn't add any value.

Records are Transparent carriers for Immutable data

An object with Optional fields forces the one who deals with it always take into consideration that the object obtained via a getter isn't a value, but potentially empty optional, which might throw NoSuchElementException if you blindly invoke get() on it.

Also, having fields of Optional type in a Record is in contradiction with the general concept of Records.

Here's a definition of Record from the JEP 395:

records, which are classes that act as transparent carriers for immutable data. Records can be thought of as nominal tuples.

A record with Optional fields is no longer transparent, because we're not able to get a value directly from it via accessor method.

Since record fields are immutable, it doesn't make sense to store potentially empty optionals as record properties, it almost the same as storing null-references. Because fields in a record can't be changed, an empty optional will remain empty, and some of your tuples might not contain useful data at all.

There's no advantage in passing around optionals, storing them inside records in order to find out at a later point in time that they don't contain the actual data.

Instead, you have to extract a value (if present) from an Optional object obtained from somewhere right on the spot before creating a record.

How do I use optional parameters in Java?

There are several ways to simulate optional parameters in Java:

  1. Method overloading.

    void foo(String a, Integer b) {
    //...
    }

    void foo(String a) {
    foo(a, 0); // here, 0 is a default value for b
    }

    foo("a", 2);
    foo("a");

One of the limitations of this approach is that it doesn't work if you have two optional parameters of the same type and any of them can be omitted.

  1. Varargs.

a) All optional parameters are of the same type:

    void foo(String a, Integer... b) {
Integer b1 = b.length > 0 ? b[0] : 0;
Integer b2 = b.length > 1 ? b[1] : 0;
//...
}

foo("a");
foo("a", 1, 2);

b) Types of optional parameters may be different:

    void foo(String a, Object... b) {
Integer b1 = 0;
String b2 = "";
if (b.length > 0) {
if (!(b[0] instanceof Integer)) {
throw new IllegalArgumentException("...");
}
b1 = (Integer)b[0];
}
if (b.length > 1) {
if (!(b[1] instanceof String)) {
throw new IllegalArgumentException("...");
}
b2 = (String)b[1];
//...
}
//...
}

foo("a");
foo("a", 1);
foo("a", 1, "b2");

The main drawback of this approach is that if optional parameters are of different types you lose static type checking. Furthermore, if each parameter has the different meaning you need some way to distinguish them.

  1. Nulls. To address the limitations of the previous approaches you can allow null values and then analyze each parameter in a method body:

    void foo(String a, Integer b, Integer c) {
    b = b != null ? b : 0;
    c = c != null ? c : 0;
    //...
    }

    foo("a", null, 2);

Now all arguments values must be provided, but the default ones may be null.

  1. Optional class. This approach is similar to nulls, but uses Java 8 Optional class for parameters that have a default value:

    void foo(String a, Optional bOpt) {
    Integer b = bOpt.isPresent() ? bOpt.get() : 0;
    //...
    }

    foo("a", Optional.of(2));
    foo("a", Optional.absent());

    Optional makes a method contract explicit for a caller, however, one may find such signature too verbose.

    Update: Java 8 includes the class java.util.Optional out-of-the-box, so there is no need to use guava for this particular reason in Java 8. The method name is a bit different though.

  2. Builder pattern. The builder pattern is used for constructors and is implemented by introducing a separate Builder class:

    class Foo {
    private final String a;
    private final Integer b;

    Foo(String a, Integer b) {
    this.a = a;
    this.b = b;
    }

    //...
    }

    class FooBuilder {
    private String a = "";
    private Integer b = 0;

    FooBuilder setA(String a) {
    this.a = a;
    return this;
    }

    FooBuilder setB(Integer b) {
    this.b = b;
    return this;
    }

    Foo build() {
    return new Foo(a, b);
    }
    }

    Foo foo = new FooBuilder().setA("a").build();
  3. Maps. When the number of parameters is too large and for most of the default values are usually used, you can pass method arguments as a map of their names/values:

    void foo(Map<String, Object> parameters) {
    String a = "";
    Integer b = 0;
    if (parameters.containsKey("a")) {
    if (!(parameters.get("a") instanceof Integer)) {
    throw new IllegalArgumentException("...");
    }
    a = (Integer)parameters.get("a");
    }
    if (parameters.containsKey("b")) {
    //...
    }
    //...
    }

    foo(ImmutableMap.<String, Object>of(
    "a", "a",
    "b", 2,
    "d", "value"));

    In Java 9, this approach became easier:

    @SuppressWarnings("unchecked")
    static <T> T getParm(Map<String, Object> map, String key, T defaultValue) {
    return (map.containsKey(key)) ? (T) map.get(key) : defaultValue;
    }

    void foo(Map<String, Object> parameters) {
    String a = getParm(parameters, "a", "");
    int b = getParm(parameters, "b", 0);
    // d = ...
    }

    foo(Map.of("a","a", "b",2, "d","value"));

Please note that you can combine any of these approaches to achieve a desirable result.

Should I use Java 8's Optional in my wrapper class or null when not using some attributes?

If you want to point out the fact that some of these are nullable, and the rest of the code makes use of streams/optionals etc., you can make the getters return optionals:

Optional<String> getName() {
return Optional.ofNullable(name);
}

Optional<String> getSurname() ...

etc.

You shouldn't have actual field types as Optionals. One reason being these are not serializable.

But generally the use case you described is probably more suitable for some boolean discriminatory method like isSocialSecurityProvided() that you can later use like this:

if (sw.isSocialSecutiryProvided()) {
// do something with
sw.getSocialSecurity();
} else {
//do domething with
sw.getName();
// and with
sw.getSurname();
}

Even if the whole method looks like something below, one could argue that naming it properly provides better readability of the code:

public boolean isSocialSecurityProvided() {
return socialSecurity != null;
}

Problem about argument of Java 8 OptionalInt.of

1) Optional's map() requires a mapper Function that returns a ? extends U, so it is allowed to return an OptionalInt. Therefore it accepts OptionalInt::of. And you can pass an Integer to OptionalInt.of() due to auto-unboxing.

2) Optional's flatMap() requires a mapper Function that returns an Optional<U>. OptionalInt is not an Optional, so you can't pass OptionalInt::of to it.

Call function in Java Optional ifPresent with OrElse

  1. The base of the code is not compilable. Stream#forEach returns void, therefore you cannot perform Stream#filter on that. Use either Stream#peek (please, read this) or Stream#map.

    Arrays.stream(fields)
    .peek(field -> field.setAccessible(true))
    .filter(field -> !field.get(oldRow).equals(field.get(newRow))
    ...

    Better use an appropriate method to avoid Stream#map/Stream#peek as of Java 9 (big thanks to @Slaw's comment):

    Arrays.stream(fields)
    .filter(field -> field.trySetAccessible() && !rowsAreEqual(field, oldRow, newRow))
    ...
  2. The method Field#get throws an exception that must be handled. The Stream API is not suitable for it, but you can create a wrapper to avoid the boilerplate.

    private static boolean rowsAreEqual(Field field, Row oldRow, Row newRow) {
    try {
    return field.get(oldRow).equals(field.get(newRow));
    } catch (IllegalAccessException e) {
    log.warn("Unexpected error", e);
    return false;
    }
    }
    Arrays.stream(fields)
    .peek(field -> !field.setAccessible(true))
    .filter(field -> rowsAreEqual(field, oldRow, newRow))
    ...

    Notice, that the outer try-catch is not needed.

  3. The argument of Optional#isPresent is a Consumer<T> and you put there void. Although the Consumer#accept has a void return type, it must be called first. You need to pass the implementation represented by the Consumer<T> and not its result:

    Arrays.stream(fields)
    .peek(field -> field.setAccessible(true))
    .filter(field -> rowsAreEqual(field, oldRow, newRow))
    .findAny()
    .ifPresentOrElse(
    field -> newRow.updateACell(),
    () -> newRow.udpateACell("new value"));


Related Topics



Leave a reply



Submit