Reputation: 18163
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
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
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