I thought about it after seeing "Write a null case using Optional type without using if statement".
There is an interface like this
public interface Foo {
public abstract String foo();
}
public interface Bar {
public abstract String bar();
}
public interface Buzz {
public abstract String buzz();
}
Each
Optional<Foo> foo = FooImpl.getInstance();
Optional<Bar> bar = BarImpl.getInstance();
Optional<Buzz> buzz = BuzzImpl.getInstance();
And so on. It looks for non-empty objects in sequence, bar if foo is empty, buzz if bar is empty, and so on, and calls the method of the first non-empty object to take the value. If all are empty, set an appropriate default value.
String value;
if(foo.isPresent()){
value = foo.foo();
} else if(bar.isPresent()){
value = bar.bar();
} else if(buzz.isPresent()){
value = buzz.buzz();
} else {
value = "empty";
}
It's an old way of writing. You can write almost the same with the ternary operator.
String value =
foo.isPresent() ? foo.foo() :
bar.isPresent() ? bar.bar() :
buzz.isPresent() ? buzz.buzz() : "empty";
Do you like which one you choose?
Optional#orElse() / orElseGet() If you write it obediently with orElse (), it will look like this.
String value =
foo.map(Foo::foo).orElse(
bar.map(Bar::bar).orElse(
buzz.map(Buzz::buzz).orElse("empty")));
This is not unreadable, but the more you check, the deeper the nesting becomes, so the question is how to indent.
String value = foo.map(Foo::foo).orElse(
bar.map(Bar::bar).orElse(
buzz.map(Buzz::buzz).orElse("empty")));
Or
String value = foo.map(Foo::foo)
.orElse(bar.map(Bar::bar)
.orElse(buzz.map(Buzz::buzz)
.orElse("empty")));
Is it better to have plastic surgery like this? Similarly for orElseGet ()
String value = foo.map(Foo::foo).orElseGet(() ->
bar.map(Bar::bar).orElseGet(() ->
buzz.map(Buzz::buzz).orElseGet(() ->
"empty")));
Or
String value = foo.map(Foo::foo)
.orElseGet(() -> bar.map(Bar::bar)
.orElseGet(() -> buzz.map(Buzz::buzz)
.orElseGet(() -> "empty")));
If so, I think I can't read it ... If this kind of writing becomes popular as an idiom, I feel that this is fine. However, I cannot deny the complicated feeling without syntactic sugar.
Let's change the way of thinking and think about whether it can be treated declaratively. For the sake of clarity, let's first consider a single type.
Optional<Foo> foo1 = FooImpl.getInstance();
Optional<Foo> foo2 = FooImpl.getInstance();
Optional<Foo> foo3 = FooImpl.getInstance();
String value = Stream.of(foo1, foo2, foo3)
.filter(Optional::isPresent)
//If you write in one line.map(o -> o.get().foo())
.map(Optional::get)
.map(Foo::foo)
.findFirst()
.orElse("empty");
This is easy, isn't it? If you can modify the original interfaces (Foo, Bar, Buzz) and it seems reasonable to have a common super interface for them, then such a fix would be a good idea. Since findFirst () is a short circuit operation, the mapping is done only for the first object matched by the filter, and no unnecessary value is retrieved.
If you want to handle different types as they are, do you need to map them to a common type before creating a stream?
String value = Stream.of(foo.map(Foo::foo), bar.map(Bar::bar), buzz.map(Buzz::buzz))
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.orElse("empty");
The drawback of this method is that the end method call is still made at the time of mapping. Bar # bar () and Buzz # buzz () should be avoided if they are costly operations. Some kind of wrapping is required to delay the mapping ...
//You need to specify the type to pass in lambda
String value = Stream.<Supplier<Optional<String>>>of(
() -> foo.map(Foo::foo),
() -> bar.map(Bar::bar),
() -> buzz.map(Buzz::buzz))
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.orElseGet(() -> "empty");
It's a delicate place.
Generalize
@SafeVarargs
public static <R> R findFirstOrElseGet(
Supplier<R> defaultSupplier,
Supplier<Optional<R>>... suppliers) {
return Stream.of(suppliers)
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.orElseGet(defaultSupplier);
}
If you prepare such a method
String value = findFirstOrElseGet(
() -> "empty",
() -> foo.map(Foo::foo),
() -> bar.map(Bar::bar),
() -> buzz.map(Buzz::buzz));
That's fine, but it may be an exaggeration for what you want to do anyway.
Note that the processing using these Optional # map () is different from the version of if-else and ternary operator, and even when Foo # foo () etc. returns null, the value specified under orElse () is the same. I will return. In other words, in terms of if
String value = null;
if(foo.isPresent()){
value = foo.foo();
}
if(value == null && bar.isPresent()){
value = bar.bar();
}
if(value == null && buzz.isPresent()){
value = buzz.buzz();
}
if(value == null) {
value = "empty";
}
Please note that the process is close to.
――If-else / The ternary operator is a low level, but know-how is pervasive at present, and it will be almost unnecessary to squeeze within the team. --Optional # orElse () / orElseGet () nesting is a bit inferior to the if-else / ternary operator in terms of readability, but you can do it without worrying about nulls until the end. --Declarative processing in Stream is powerful when type conversion is not costly.
I would like to choose an implementation that seems to be correct on a case-by-case basis.
Recommended Posts