1. Overview
In this tutorial, we’ll take a look at the Guava Cache implementation – basic usage, eviction policies, refreshing the cache and some interesting bulk operations.
Finally, we will take a look at the using the removal notifications the cache is able to send out.
2. How to Use Guava Cache
Let’s start with a simple example – let’s cache the uppercase form of String instances.
First, we’ll create the CacheLoader – 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 is no value in the cache for our “hello” key – and so the value is computed and cached.
Also note that we’re using the getUnchecked() operation – this 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, the oldest items will be evicted.
In the following code, we limit the cache size to 3 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 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
Beside using size to evict old records, we can use time. In the following example, we 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 2ms of being stored:
@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. Week Keys
Next, let’s see how to make our cache keys have weak references – allowing the garbage collector to collect cache key that are not referenced elsewhere.
By default, both cache keys and values have strong references but we can make our cache store the keys using weak references using weakKeys() as in the following example:
@Test public void whenWeekKeyHasNoRef_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 allow garbage collector to collect our cached values by using softValues() as in the following example:
@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: Many soft references may affect the system performance – it’s preferred 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 you try to load a null value – as it doesn’t make any sense to cache a null.
But if null value means something in your code, then you can make good use of the Optional class as in the following example:
@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, let’s see how to refresh our cache values. We can refresh our cache automatically using refreshAfterWrite().
In the following example, the cache is refreshed automatically every 1 minute:
@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); }
Note: You can refresh specific record manually using refresh(key).
8. Preload the Cache
We can insert multiple records in our cache using putAll() method. In the following example, we 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, you need to take some actions when a record is removed from the cache; so, let’s discuss RemovalNotification.
We can register a RemovalListener to get notifications of a record being removed. We also have access to the cause of the removal – via the getCause() method.
In the following sample, a RemovalNotification is recieved when the forth element in the cache 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 is thread-safe
- you can insert values manually into the cache using put(key,value)
- you can measure your cache performance using CacheStats ( hitRate(), missRate(), ..)
11. Conclusion
We went through a lot of usecases of the Guava Cache in this tutorial – from simple usage to eviction of elements, refresh and preload of the cache and removal notifications.