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;
}

      

+3


source to share


3 answers


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.

+2


source


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.

0


source


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

.

0


source







All Articles