Smutje
Smutje

Reputation: 18163

Java 8 java.util.Map#computeIfAbsent with java.util.Optional

Suppose I have a cache implemented as java.util.Map which stores (arbitrary) values for keys. As the values are not mandatorily present, the cache returns an java.util.Optional and is able to be provided with a java.util.function.Supplier to calculate the value for a given non-existing key.

My first naive approach was

public class Cache0 {

    private final Map<String, String> mapping = new HashMap<>();

    public Optional<String> get(String key, Supplier<Optional<String>> supplier) {
        final Optional<String> valueOptional;

        if (this.mapping.containsKey(key)) {
            final String value = this.mapping.get(key);

            valueOptional = Optional.of(value);
        } else {
            valueOptional = supplier.get();

            if (valueOptional.isPresent()) {
                this.mapping.put(key, valueOptional.get());
            }
        }

        return valueOptional;
    }
}

but I found this very inelegant and as I learned about java.util.Map#computeIfAbsent I changed the code to the following

public class Cache1 {

    private final Map<String, String> mapping = new HashMap<>();

    public Optional<String> get(String key, Supplier<Optional<String>> supplier) {
        final String value = this.mapping.computeIfAbsent(key, absentKey -> this.getValue(supplier));

        return Optional.ofNullable(value);
    }

    private String getValue(Supplier<Optional<String>> supplier) {
        return supplier.get()
                .orElse(null);
    }
}

but what now bothers me is the redundant use of java.util.Optional#ofNullable in combination with the null result of the getValue method which is needed to provide java.util.Map#computeIfAbsent with the "default" value not to be inserted into the map.

In an ideal situation, something like the following would be possible

public class Cache2 {

    private final Map<String, String> mapping = new HashMap<>();

    public Optional<String> get(String key, Supplier<Optional<String>> supplier) {
        return this.mapping.computeIfAbsent(key, absentKey -> supplier.get());
    }
}

where java.util.Map#computeIfAbsent would skip the insertion if the second parameter represents an empty java.util.Optional and returns an java.util.Optional#empty instead but unfortunately the use of java.util.Optional#empty as "default" insert value for java.util.Map#computeIfAbsent is not supported and the code does not compile.

A further possibility would be to store a mapping of String to java.util.Optional but then the java.util.Map would store the java.util.Optional#empty as value contradicting my use-case again to be forced to store invalid mappings and removing/replacing them by hand later.

public class Cache3 {

    private final Map<String, Optional<String>> mapping = new HashMap<>();

    public Optional<String> get(String key, Supplier<Optional<String>> supplier) {
        return this.mapping.computeIfAbsent(key, absentKey -> supplier.get());
    }
}

Is anyone aware of a better approach to handle this kind of use-case or do I have to fall back to my implementation of Cache1?

Upvotes: 1

Views: 10907

Answers (2)

Michael Anderson
Michael Anderson

Reputation: 73520

To do this kind of thing I usually use an Optional in my map - this way map.get()!=null means I've cached the access and map.get().isPresent() tells me if a sensible value was returned.

In this case I'd use a Suplier<String> that returns null when the value is not present. Then the implementation would look like this:

public class Cache {
  private final Map<String, Optional<String>> mapping = new HashMap<>();

  public Optional<String> get(String key, Suplier<String> supplier) {
    return mapping.computeIfAbsent(key, 
         unused -> Optional.ofNullable(supplier.get()) );
  }
}

Absent keys do get inserted into the map, but marked as missing.

Upvotes: 3

Sean Patrick Floyd
Sean Patrick Floyd

Reputation: 299048

It sounds to me like you are re-inventing a Guava LoadingCache (read here about Guava Caches). While this is definitely an interesting programming exercise, the existing solution is time-proven, can be configured to your needs and works under extremely heavy load.

An example definition would be:

Cache<Key, Value> cache = CacheBuilder.newBuilder()
    .maximumSize(1000)
    .build(); // look Ma, no CacheLoader
...
try {
  // If the key wasn't in the "easy to compute" group, we need to
  // do things the hard way.
  cache.get(key, new Callable<Value>() {
    @Override
    public Value call() throws AnyException {
      return doThingsTheHardWay(key);
    }
  });
} catch (ExecutionException e) {
  throw new OtherException(e.getCause());
}

This is somewhat equivalent to your usage scenario, i.e. the calculation can be different on a per-key level. Usually, you don't need this, so you'd prefer a stored calculation method inside the cache:

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .maximumSize(1000)
       .build(
           new CacheLoader<Key, Graph>() {
             public Graph load(Key key) throws AnyException {
               return createExpensiveGraph(key);
             }
           });

...
try {
  return graphs.get(key);
} catch (ExecutionException e) {
  throw new OtherException(e.getCause());
}

Upvotes: 1

Related Questions