1

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?

3
  • NB if your cache is meant to be shared consider using a ConcurrentHashMap Commented May 13, 2016 at 7:28
  • 1
    I don't believe that there is a better approach except if you use Map<String, Optional<String>> but indeed it will fill up your cache of empty values but is it a problem? Knowing that a key has no value is also a valuable info. Imagine that your application tries to access 10 000 times to the same key that has no value, it will spend its time to compute this key instead of getting it from the cache Commented May 13, 2016 at 7:35
  • Yes, this is a valid point and in my view primarily use-case dependent - is a one-time empty value OK to fail on further requests or does the situation leading to the empty value may change and the re-calculation is needed at a later point so the cache needs manual emptying. Commented May 13, 2016 at 7:40

2 Answers 2

3

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.

Sign up to request clarification or add additional context in comments.

4 Comments

I get the point in storing "invalid" values and therefore I mark this solution as "accepted". As mentioned in my comment above, this solution sadly is not 100% feasible for use-cases where one does not want to store invalid values and rather have them re-calculated again at a later moment when an external situation has changed which will lead to a valid value without having the need of clearing the cache manually or inventing an expire time.
Supplier.get does not take a parameter. But if you need to access the key inside the lambda expression, there is no reason to capture the value of the key variable from the surrounding scope when you get the same key as parameter…
@Holger - spot on, I was thinking it was a function rather than a supplier. Updated appropriately.
@Smutje: if you want to keep the option of the supplier returning empty Optionals and enforce recalculation of empty Optionals, you can keep supplier’s type as Supplier<Optional<String>> and use return mapping.compute(key, (k,v) -> v!=null&&v.isPresent()? v: supplier.get());
1

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

2 Comments

Yes, but the re-invention took my like 5 minutes and less time to even write this SO post :-) But I will look into Guava for future use.
@Smutje sure, I can put some wheels on a wooden box and re-invent a porsche in a shorter time than it takes them to build the original, if you get my drift. Don't get me wrong, your approach is fine for most cases, but it will die under heavy load.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.