1. Overview
In this tutorial, we’ll focus on the Guava Cache implementation, including basic usage, eviction policies, refreshing the cache, and some interesting bulk operations.
Finally, we’ll discuss how to use the removal notifications the cache is able to send out.
2. How to Use Guava Cache
Let’s start with a simple example of caching the uppercase form of String instances.
First, we’ll create the CacheLoader, which is used to compute the value stored in the cache. From this, we’ll use the handy CacheBuilder to build our cache using the given specifications:
@Test
public void whenCacheMiss_thenValueIsComputed() {
CacheLoader<String, String> loader;
loader = new CacheLoader<String, String>() {
@Override
public String load(String key) {
return key.toUpperCase();
}
};
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder().build(loader);
assertEquals(0, cache.size());
assertEquals("HELLO", cache.getUnchecked("hello"));
assertEquals(1, cache.size());
}
Notice how there’s no value in the cache for our “hello” key, so the value is computed and cached.
Also note that we’re using the getUnchecked() operation, which computes and loads the value into the cache if it doesn’t already exist.
3. Eviction Policies
Every cache needs to remove values at some point. Let’s discuss the mechanism of evicting values out of the cache using different criteria.
3.1. Eviction by Size
We can limit the size of our cache using maximumSize(). If the cache reaches the limit, it evicts the oldest items.
In the following code, we’ll limit the cache size to three records:
@Test
public void whenCacheReachMaxSize_thenEviction() {
CacheLoader<String, String> loader;
loader = new CacheLoader<String, String>() {
@Override
public String load(String key) {
return key.toUpperCase();
}
};
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder().maximumSize(3).build(loader);
cache.getUnchecked("first");
cache.getUnchecked("second");
cache.getUnchecked("third");
cache.getUnchecked("forth");
assertEquals(3, cache.size());
assertNull(cache.getIfPresent("first"));
assertEquals("FORTH", cache.getIfPresent("forth"));
}
3.2. Eviction by Weight
We can also limit the cache size using a custom weight function. In the following code, we’ll use the length as our custom weight function:
@Test
public void whenCacheReachMaxWeight_thenEviction() {
CacheLoader<String, String> loader;
loader = new CacheLoader<String, String>() {
@Override
public String load(String key) {
return key.toUpperCase();
}
};
Weigher<String, String> weighByLength;
weighByLength = new Weigher<String, String>() {
@Override
public int weigh(String key, String value) {
return value.length();
}
};
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder()
.maximumWeight(16)
.weigher(weighByLength)
.build(loader);
cache.getUnchecked("first");
cache.getUnchecked("second");
cache.getUnchecked("third");
cache.getUnchecked("last");
assertEquals(3, cache.size());
assertNull(cache.getIfPresent("first"));
assertEquals("LAST", cache.getIfPresent("last"));
}
Note: the cache may remove more than one record to leave room for a new large one.
3.3. Eviction by Time
In addition to using size to evict old records, we can use time. In the following example, we’ll customize our cache to remove records that have been idle for 2ms:
@Test
public void whenEntryIdle_thenEviction()
throws InterruptedException {
CacheLoader<String, String> loader;
loader = new CacheLoader<String, String>() {
@Override
public String load(String key) {
return key.toUpperCase();
}
};
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder()
.expireAfterAccess(2,TimeUnit.MILLISECONDS)
.build(loader);
cache.getUnchecked("hello");
assertEquals(1, cache.size());
cache.getUnchecked("hello");
Thread.sleep(300);
cache.getUnchecked("test");
assertEquals(1, cache.size());
assertNull(cache.getIfPresent("hello"));
}
We can also evict records based on their total live time. In the following example, the cache will remove the records after they’ve been stored for 2ms:
@Test
public void whenEntryLiveTimeExpire_thenEviction()
throws InterruptedException {
CacheLoader<String, String> loader;
loader = new CacheLoader<String, String>() {
@Override
public String load(String key) {
return key.toUpperCase();
}
};
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder()
.expireAfterWrite(2,TimeUnit.MILLISECONDS)
.build(loader);
cache.getUnchecked("hello");
assertEquals(1, cache.size());
Thread.sleep(300);
cache.getUnchecked("test");
assertEquals(1, cache.size());
assertNull(cache.getIfPresent("hello"));
}
4. Weak Keys
Next, we’ll demonstrate how to make our cache keys have weak references, allowing the garbage collector to collect cache keys that aren’t referenced elsewhere.
By default, both cache keys and values have strong references, but we can make our cache store the keys using weak references by using weakKeys():
@Test
public void whenWeakKeyHasNoRef_thenRemoveFromCache() {
CacheLoader<String, String> loader;
loader = new CacheLoader<String, String>() {
@Override
public String load(String key) {
return key.toUpperCase();
}
};
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder().weakKeys().build(loader);
}
5. Soft Values
We can also allow the garbage collector to collect our cached values by using softValues():
@Test
public void whenSoftValue_thenRemoveFromCache() {
CacheLoader<String, String> loader;
loader = new CacheLoader<String, String>() {
@Override
public String load(String key) {
return key.toUpperCase();
}
};
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder().softValues().build(loader);
}
Note: too many soft references may affect the system performance, so the preferred option is to use maximumSize().
6. Handle null Values
Now let’s see how to handle cache null values. By default, Guava Cache will throw exceptions if we try to load a null value, as it doesn’t make any sense to cache a null.
But if a null value means something in our code, then we can make good use of the Optional class:
@Test
public void whenNullValue_thenOptional() {
CacheLoader<String, Optional<String>> loader;
loader = new CacheLoader<String, Optional<String>>() {
@Override
public Optional<String> load(String key) {
return Optional.fromNullable(getSuffix(key));
}
};
LoadingCache<String, Optional<String>> cache;
cache = CacheBuilder.newBuilder().build(loader);
assertEquals("txt", cache.getUnchecked("text.txt").get());
assertFalse(cache.getUnchecked("hello").isPresent());
}
private String getSuffix(final String str) {
int lastIndex = str.lastIndexOf('.');
if (lastIndex == -1) {
return null;
}
return str.substring(lastIndex + 1);
}
7. Refresh the Cache
Next, we’ll learn how to refresh our cache values.
7.1. Manual Refresh
We can refresh a single key manually with the help of LoadingCache.refresh(key):
String value = loadingCache.get("key");
loadingCache.refresh("key");
This will force the CacheLoader to load the new value for the key.
Until the new value is successfully loaded, the previous value of the key will be returned by the get(key).
7.2. Automatic Refresh
We can also use CacheBuilder.refreshAfterWrite(duration) to automatically refresh cached values:
@Test
public void whenLiveTimeEnd_thenRefresh() {
CacheLoader<String, String> loader;
loader = new CacheLoader<String, String>() {
@Override
public String load(String key) {
return key.toUpperCase();
}
};
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder()
.refreshAfterWrite(1,TimeUnit.MINUTES)
.build(loader);
}
It’s important to understand that refreshAfterWrite(duration) only makes a key eligible for the refresh after the specified duration. The value will actually be refreshed only when a corresponding entry is queried by get(key).
8. Preload the Cache
We can insert multiple records in our cache using the putAll() method. In the following example, we’ll add multiple records into our cache using a Map:
@Test
public void whenPreloadCache_thenUsePutAll() {
CacheLoader<String, String> loader;
loader = new CacheLoader<String, String>() {
@Override
public String load(String key) {
return key.toUpperCase();
}
};
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder().build(loader);
Map<String, String> map = new HashMap<String, String>();
map.put("first", "FIRST");
map.put("second", "SECOND");
cache.putAll(map);
assertEquals(2, cache.size());
}
9. RemovalNotification
Sometimes we need to take action when a record is removed from the cache, so we’ll discuss RemovalNotification.
We can register a RemovalListener to get notifications of records being removed. We also have access to the cause of the removal via the getCause() method.
In the following example, a RemovalNotification is received when the fourth element in the cache is removed because of its size:
@Test
public void whenEntryRemovedFromCache_thenNotify() {
CacheLoader<String, String> loader;
loader = new CacheLoader<String, String>() {
@Override
public String load(final String key) {
return key.toUpperCase();
}
};
RemovalListener<String, String> listener;
listener = new RemovalListener<String, String>() {
@Override
public void onRemoval(RemovalNotification<String, String> n){
if (n.wasEvicted()) {
String cause = n.getCause().name();
assertEquals(RemovalCause.SIZE.toString(),cause);
}
}
};
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder()
.maximumSize(3)
.removalListener(listener)
.build(loader);
cache.getUnchecked("first");
cache.getUnchecked("second");
cache.getUnchecked("third");
cache.getUnchecked("last");
assertEquals(3, cache.size());
}
10. Notes
Finally, here are a few additional quick notes about the Guava cache implementation:
- it’s thread-safe
- we can insert values into the cache manually using put(key,value)
- we can measure our cache performance using CacheStats ( hitRate(), missRate(), ..)
11. Conclusion
In this article, we explored a lot of use cases for Guava Cache. The topics we discussed include simple use, evicting elements, refreshing and preloading the cache, as well as removal notifications.
As usual, all of the examples can be found over on GitHub.