Why is java Map.merge not passing the provider?
I want there to be a method in java that allows me to change the value if it exists, or insert it if it doesn't. Similar to merge, but:
- I want to pass a value provider, not a value, to avoid creating it when not required.
- If this value exists, I don't want to re-insert or delete it, just get its methods using the container.
I had to write this. The problem with the writing itself is that the parallel maps version is not trivial
public static <K, V> V putOrConsume(Map<K, V> map, K key, Supplier<V> ifAbsent, Consumer<V> ifPresent) {
V val = map.get(key);
if (val != null) {
ifPresent.accept(val);
} else {
map.put(key, ifAbsent.get());
}
return val;
}
source to share
The best "standard" way to achieve it is using the compute () function:
Map<String, String> map = new HashMap<>();
BiFunction<String, String, String> convert = (k, v) -> v == null ? "new_" + k : "old_" + v;
map.compute("x", convert);
map.compute("x", convert);
System.out.println(map.get("x")); //prints old_new_x
Now, let's say you have your Supplier and Consumer and would like to follow the DRY principle. Then you can use a simple function combinator:
Map<String, String> map = new HashMap<>();
Supplier<String> ifAbsent = () -> "new";
Consumer<String> ifPresent = System.out::println;
BiFunction<String, String, String> putOrConsume = (k, v) -> {
if (v == null) return ifAbsent.get();
ifPresent.accept(v);
return v;
};
map.compute("x", putOrConsume); //nothing
map.compute("x", putOrConsume); //prints "new"
Obviously, you could write a combinatorial function that takes a supplier and a consumer and returns a BiFunction to make the above code even more general.
The downside to this suggested approach is the extra call to map.put () even if you are just consuming the value, i.e. it will be slightly slower by the time the key is found. Good news: the map implementation will simply replace the value without creating a new node. That is, no new objects will be created or garbage collected. In most cases, such compromises are justified.
map.compute(...)
and map.putIfAbsent(...)
much more powerful than the rather specialized ones offered by putOrConsume (...). It's so asymmetrical that I would really go over the reasons why you need it in code.
source to share
You can achieve what you want Map.compute
with a trivial helper method and also with a local class to know if your provider has been used ifAbsent
:
public static <K, V> V putOrConsume(
Map<K, V> map,
K key,
Supplier<V> ifAbsent,
Consumer<V> ifPresent) {
class AbsentSupplier implements Supplier<V> {
boolean used = false;
public V get() {
used = true;
return ifAbsent.get();
}
}
AbsentSupplier absentSupplier = new AbsentSupplier();
V computed = map.compute(
key,
(k, v) -> v == null ?
absentSupplier.get() :
consumeAndReturn(v, ifPresent));
return absentSupplier.used ? null : computed;
}
private static <V> V consumeAndReturn(V v, Consumer<V> consumer) {
consumer.accept(v);
return v;
}
The tricky part is that you've used a provider ifAbsent
to return either null
an existing, consumed value.
The helper method simply adapts the user ifPresent
to behave like a unary operator that consumes the given value and returns it.
source to share
different from others, you also use a method Map.compute
and combine Function
with standard / static methods of the interface to make your code more readable . eg:
Using
//only consuming if value is present
Consumer<V> action = ...;
map.compute(key,ValueMapping.ifPresent(action));
//create value if value is absent
Supplier<V> supplier = ...;
map.compute(key,ValueMapping.ifPresent(action).orElse(supplier));
//map value from key if value is absent
Function<K,V> mapping = ...;
map.compute(key,ValueMapping.ifPresent(action).orElse(mapping));
//orElse supports short-circuit feature
map.compute(key,ValueMapping.ifPresent(action)
.orElse(supplier)
.orElse(() -> fail("it should not be called "+
"if the value computed by the previous orElse")));
<T> T fail(String message) {
throw new AssertionError(message);
}
ValueMapping
interface ValueMapping<T, R> extends BiFunction<T, R, R> {
default ValueMapping<T, R> orElse(Supplier<R> other) {
return orElse(k -> other.get());
}
default ValueMapping<T, R> orElse(Function<T, R> other) {
return (k, v) -> {
R result = this.apply(k, v);
return result!=null ? result : other.apply(k);
};
}
static <T, R> ValueMapping<T, R> ifPresent(Consumer<R> action) {
return (k, v) -> {
if (v!=null) {
action.accept(v);
}
return v;
};
}
}
Note
I used Objects.isNull
in ValueMapping
the previous version. and @Holger point out that this is a case of overuse and should replace it with a simpler condition it != null
.
source to share