0
# Functional Interfaces
1
2
Caffeine provides several functional interfaces that enable customization of cache behavior including value loading, removal notifications, entry weighing, custom expiration policies, and time/scheduling abstractions.
3
4
## CacheLoader Interface
5
6
The `CacheLoader` interface enables automatic value computation for loading caches.
7
8
```java { .api }
9
@FunctionalInterface
10
public interface CacheLoader<K, V> extends AsyncCacheLoader<K, V> {
11
// Primary loading method
12
V load(K key) throws Exception;
13
14
// Bulk loading method (optional override)
15
default Map<? extends K, ? extends V> loadAll(Set<? extends K> keys) throws Exception {
16
throw new UnsupportedOperationException();
17
}
18
19
// Inherited async methods
20
default CompletableFuture<V> asyncLoad(K key, Executor executor) throws Exception;
21
default CompletableFuture<? extends Map<? extends K, ? extends V>> asyncLoadAll(
22
Set<? extends K> keys, Executor executor) throws Exception;
23
default CompletableFuture<V> asyncReload(K key, V oldValue, Executor executor) throws Exception;
24
25
// Static factory method for bulk loading
26
static <K, V> CacheLoader<K, V> bulk(
27
Function<? super Set<? extends K>, ? extends Map<? extends K, ? extends V>> mappingFunction);
28
}
29
```
30
31
### Basic CacheLoader Implementation
32
33
```java
34
// Simple lambda-based loader
35
CacheLoader<String, String> simpleLoader = key -> "loaded_" + key.toUpperCase();
36
37
LoadingCache<String, String> cache = Caffeine.newBuilder()
38
.maximumSize(1000)
39
.build(simpleLoader);
40
41
String value = cache.get("test"); // Returns "loaded_TEST"
42
```
43
44
### Advanced CacheLoader with Error Handling
45
46
```java
47
CacheLoader<String, UserData> userLoader = new CacheLoader<String, UserData>() {
48
@Override
49
public UserData load(String userId) throws Exception {
50
// Simulate database lookup with potential failures
51
if (userId.startsWith("invalid_")) {
52
throw new IllegalArgumentException("Invalid user ID: " + userId);
53
}
54
55
if (userId.equals("slow_user")) {
56
Thread.sleep(2000); // Simulate slow operation
57
}
58
59
return new UserData(userId, "User " + userId, "user" + userId + "@example.com");
60
}
61
62
@Override
63
public Map<String, UserData> loadAll(Set<? extends String> userIds) throws Exception {
64
Map<String, UserData> result = new HashMap<>();
65
66
// Efficient bulk loading from database
67
List<UserData> users = database.batchFetchUsers(new ArrayList<>(userIds));
68
for (UserData user : users) {
69
result.put(user.getId(), user);
70
}
71
72
return result;
73
}
74
};
75
76
LoadingCache<String, UserData> userCache = Caffeine.newBuilder()
77
.maximumSize(10000)
78
.expireAfterWrite(Duration.ofMinutes(30))
79
.recordStats()
80
.build(userLoader);
81
82
// Single load
83
UserData user = userCache.get("user123");
84
85
// Bulk load - uses efficient loadAll implementation
86
Map<String, UserData> users = userCache.getAll(Set.of("user1", "user2", "user3"));
87
```
88
89
### CacheLoader Bulk Factory Method
90
91
```java
92
// Create a bulk-only loader using the static factory method
93
CacheLoader<String, Product> bulkProductLoader = CacheLoader.bulk(productIds -> {
94
// Only implement bulk loading - single load delegates to bulk
95
return productService.fetchProductsMap(productIds)
96
.stream()
97
.collect(Collectors.toMap(Product::getId, p -> p));
98
});
99
100
LoadingCache<String, Product> productCache = Caffeine.newBuilder()
101
.maximumSize(5000)
102
.expireAfterWrite(Duration.ofHours(1))
103
.build(bulkProductLoader);
104
105
// Both single and bulk loads work efficiently
106
Product single = productCache.get("prod123"); // Delegates to bulk load with single key
107
Map<String, Product> bulk = productCache.getAll(Set.of("prod1", "prod2", "prod3"));
108
```
109
110
## AsyncCacheLoader Interface
111
112
The `AsyncCacheLoader` interface provides asynchronous value loading for async caches.
113
114
```java { .api }
115
@FunctionalInterface
116
public interface AsyncCacheLoader<K, V> {
117
// Primary async loading method
118
CompletableFuture<? extends V> asyncLoad(K key, Executor executor) throws Exception;
119
120
// Bulk async loading (optional override)
121
default CompletableFuture<? extends Map<? extends K, ? extends V>> asyncLoadAll(
122
Set<? extends K> keys, Executor executor) throws Exception {
123
throw new UnsupportedOperationException();
124
}
125
126
// Async reload for refresh operations (optional override)
127
default CompletableFuture<? extends V> asyncReload(
128
K key, V oldValue, Executor executor) throws Exception {
129
return asyncLoad(key, executor);
130
}
131
132
// Static factory methods for bulk loading
133
static <K, V> AsyncCacheLoader<K, V> bulk(
134
Function<? super Set<? extends K>, ? extends Map<? extends K, ? extends V>> mappingFunction);
135
static <K, V> AsyncCacheLoader<K, V> bulk(
136
BiFunction<? super Set<? extends K>, ? super Executor,
137
? extends CompletableFuture<? extends Map<? extends K, ? extends V>>> mappingFunction);
138
}
139
```
140
141
### AsyncCacheLoader Implementation
142
143
```java
144
AsyncCacheLoader<String, String> asyncLoader = (key, executor) -> {
145
return CompletableFuture.supplyAsync(() -> {
146
// Simulate async computation
147
try {
148
Thread.sleep(100);
149
} catch (InterruptedException e) {
150
Thread.currentThread().interrupt();
151
throw new RuntimeException(e);
152
}
153
return "async_loaded_" + key;
154
}, executor);
155
};
156
157
AsyncLoadingCache<String, String> asyncCache = Caffeine.newBuilder()
158
.maximumSize(1000)
159
.buildAsync(asyncLoader);
160
161
CompletableFuture<String> future = asyncCache.get("test");
162
String value = future.join(); // "async_loaded_test"
163
```
164
165
### AsyncCacheLoader with Bulk Loading
166
167
```java
168
AsyncCacheLoader<String, ProductData> productLoader = new AsyncCacheLoader<String, ProductData>() {
169
@Override
170
public CompletableFuture<ProductData> asyncLoad(String productId, Executor executor) {
171
return CompletableFuture.supplyAsync(() -> {
172
return productService.fetchProduct(productId);
173
}, executor);
174
}
175
176
@Override
177
public CompletableFuture<Map<String, ProductData>> asyncLoadAll(
178
Set<? extends String> productIds, Executor executor) {
179
return CompletableFuture.supplyAsync(() -> {
180
// Efficient bulk loading
181
return productService.batchFetchProducts(new ArrayList<>(productIds))
182
.stream()
183
.collect(Collectors.toMap(ProductData::getId, p -> p));
184
}, executor);
185
}
186
187
@Override
188
public CompletableFuture<ProductData> asyncReload(
189
String productId, ProductData oldValue, Executor executor) {
190
return CompletableFuture.supplyAsync(() -> {
191
// Custom refresh logic - may use old value for optimization
192
if (oldValue.getLastModified().isAfter(Instant.now().minus(Duration.ofMinutes(5)))) {
193
return oldValue; // Recent enough, no need to refresh
194
}
195
return productService.fetchProduct(productId);
196
}, executor);
197
}
198
};
199
```
200
201
### AsyncCacheLoader Bulk Factory Methods
202
203
```java
204
// Synchronous bulk loader
205
AsyncCacheLoader<String, Product> syncBulkLoader = AsyncCacheLoader.bulk(
206
productIds -> {
207
// Synchronous bulk loading - will be wrapped in CompletableFuture
208
return productService.fetchProductsSync(productIds)
209
.stream()
210
.collect(Collectors.toMap(Product::getId, p -> p));
211
}
212
);
213
214
// Asynchronous bulk loader with executor
215
AsyncCacheLoader<String, Product> asyncBulkLoader = AsyncCacheLoader.bulk(
216
(productIds, executor) -> {
217
return CompletableFuture.supplyAsync(() -> {
218
// Long-running bulk operation using provided executor
219
return productService.fetchProductsBatch(productIds)
220
.stream()
221
.collect(Collectors.toMap(Product::getId, p -> p));
222
}, executor);
223
}
224
);
225
226
AsyncLoadingCache<String, Product> bulkCache = Caffeine.newBuilder()
227
.maximumSize(10_000)
228
.buildAsync(asyncBulkLoader);
229
230
// Efficient bulk loading through factory method
231
CompletableFuture<Map<String, Product>> productsFuture =
232
bulkCache.getAll(Set.of("prod1", "prod2", "prod3"));
233
```
234
235
## RemovalListener Interface
236
237
The `RemovalListener` interface provides notifications when entries are removed from the cache.
238
239
```java { .api }
240
@FunctionalInterface
241
public interface RemovalListener<K, V> {
242
void onRemoval(K key, V value, RemovalCause cause);
243
}
244
```
245
246
### Basic RemovalListener
247
248
```java
249
RemovalListener<String, String> basicListener = (key, value, cause) -> {
250
System.out.println("Removed: " + key + " -> " + value + " (cause: " + cause + ")");
251
};
252
253
Cache<String, String> cache = Caffeine.newBuilder()
254
.maximumSize(100)
255
.removalListener(basicListener)
256
.build();
257
258
cache.put("key1", "value1");
259
cache.invalidate("key1"); // Prints: Removed: key1 -> value1 (cause: EXPLICIT)
260
```
261
262
### Advanced RemovalListener with Resource Cleanup
263
264
```java
265
RemovalListener<String, DatabaseConnection> connectionListener = (key, connection, cause) -> {
266
System.out.println("Connection removed: " + key + " (cause: " + cause + ")");
267
268
if (connection != null) {
269
try {
270
connection.close();
271
System.out.println("Connection closed for: " + key);
272
} catch (Exception e) {
273
System.err.println("Failed to close connection for " + key + ": " + e.getMessage());
274
}
275
}
276
277
// Log different removal causes
278
switch (cause) {
279
case EXPIRED:
280
metricsCollector.increment("cache.connection.expired");
281
break;
282
case SIZE:
283
metricsCollector.increment("cache.connection.evicted");
284
System.out.println("WARNING: Connection evicted due to cache size limit");
285
break;
286
case EXPLICIT:
287
metricsCollector.increment("cache.connection.manual_removal");
288
break;
289
case COLLECTED:
290
metricsCollector.increment("cache.connection.gc_collected");
291
System.out.println("Connection garbage collected: " + key);
292
break;
293
}
294
};
295
296
Cache<String, DatabaseConnection> connectionCache = Caffeine.newBuilder()
297
.maximumSize(50)
298
.expireAfterAccess(Duration.ofMinutes(30))
299
.removalListener(connectionListener)
300
.build();
301
```
302
303
### RemovalListener vs EvictionListener
304
305
```java
306
// RemovalListener - called asynchronously for ALL removals
307
RemovalListener<String, String> removalListener = (key, value, cause) -> {
308
System.out.println("REMOVAL: " + key + " (cause: " + cause + ")");
309
// Heavy operations like logging, cleanup, external notifications
310
slowExternalService.notifyRemoval(key, value, cause);
311
};
312
313
// EvictionListener - called synchronously only for evictions
314
RemovalListener<String, String> evictionListener = (key, value, cause) -> {
315
if (cause.wasEvicted()) {
316
System.out.println("EVICTION: " + key + " (cause: " + cause + ")");
317
// Lightweight operations only - affects cache performance
318
quickMetrics.recordEviction(cause);
319
}
320
};
321
322
Cache<String, String> cache = Caffeine.newBuilder()
323
.maximumSize(100)
324
.removalListener(removalListener) // Async, all removals
325
.evictionListener(evictionListener) // Sync, evictions only
326
.build();
327
```
328
329
## Weigher Interface
330
331
The `Weigher` interface calculates custom weights for cache entries used in weight-based eviction.
332
333
```java { .api }
334
@FunctionalInterface
335
public interface Weigher<K, V> {
336
int weigh(K key, V value);
337
338
// Static factory methods
339
static <K, V> Weigher<K, V> singletonWeigher();
340
static <K, V> Weigher<K, V> boundedWeigher(Weigher<K, V> delegate);
341
}
342
```
343
344
### String-Based Weigher
345
346
```java
347
Weigher<String, String> stringWeigher = (key, value) -> {
348
// Weight based on string length
349
return key.length() + value.length();
350
};
351
352
Cache<String, String> stringCache = Caffeine.newBuilder()
353
.maximumWeight(1000)
354
.weigher(stringWeigher)
355
.build();
356
357
stringCache.put("short", "a"); // weight: 6
358
stringCache.put("medium_key", "data"); // weight: 14
359
stringCache.put("very_long_key_name", "very_long_value_content"); // weight: 43
360
```
361
362
### Complex Object Weigher
363
364
```java
365
Weigher<String, CacheableObject> objectWeigher = (key, obj) -> {
366
int baseWeight = key.length();
367
368
if (obj == null) {
369
return baseWeight;
370
}
371
372
// Calculate weight based on object characteristics
373
int objectWeight = obj.getSerializedSize();
374
375
// Add extra weight for resource-intensive objects
376
if (obj.hasLargeData()) {
377
objectWeight *= 2;
378
}
379
380
// Ensure minimum weight
381
return Math.max(baseWeight + objectWeight, 1);
382
};
383
384
Cache<String, CacheableObject> objectCache = Caffeine.newBuilder()
385
.maximumWeight(100_000)
386
.weigher(objectWeigher)
387
.recordStats()
388
.build();
389
```
390
391
### Dynamic Weigher with Validation
392
393
```java
394
Weigher<String, byte[]> byteArrayWeigher = (key, data) -> {
395
// Validate inputs
396
if (key == null || data == null) {
397
return 1; // Minimum weight for null values
398
}
399
400
// Base weight from key
401
int keyWeight = key.length();
402
403
// Data weight
404
int dataWeight = data.length;
405
406
// Apply scaling factor for very large objects
407
if (dataWeight > 10_000) {
408
dataWeight = 10_000 + (dataWeight - 10_000) / 10;
409
}
410
411
int totalWeight = keyWeight + dataWeight;
412
413
// Ensure weight is positive and reasonable
414
return Math.max(totalWeight, 1);
415
};
416
```
417
418
### Bounded Weigher for Safety
419
420
```java
421
// Potentially unsafe weigher that might return negative values
422
Weigher<String, String> unsafeWeigher = (key, value) -> {
423
// Hypothetical calculation that could go negative
424
return key.length() - value.length();
425
};
426
427
// Wrap with boundedWeigher to ensure non-negative weights
428
Weigher<String, String> safeWeigher = Weigher.boundedWeigher(unsafeWeigher);
429
430
Cache<String, String> safeCache = Caffeine.newBuilder()
431
.maximumWeight(1000)
432
.weigher(safeWeigher) // Automatically validates weights >= 0
433
.build();
434
435
// This would throw IllegalArgumentException if unsafe weigher returned negative value
436
safeCache.put("long_key", "short"); // Negative weight prevented by boundedWeigher
437
```
438
439
## Expiry Interface
440
441
The `Expiry` interface enables custom expiration policies based on entry characteristics and access patterns.
442
443
```java { .api }
444
public interface Expiry<K, V> {
445
long expireAfterCreate(K key, V value, long currentTime);
446
long expireAfterUpdate(K key, V value, long currentTime, long currentDuration);
447
long expireAfterRead(K key, V value, long currentTime, long currentDuration);
448
449
// Static factory methods
450
static <K, V> Expiry<K, V> creating(BiFunction<K, V, Duration> expireAfterCreate);
451
static <K, V> Expiry<K, V> accessing(BiFunction<K, V, Duration> expireAfterCreate,
452
TriFunction<K, V, Duration, Duration> expireAfterRead);
453
static <K, V> Expiry<K, V> writing(BiFunction<K, V, Duration> expireAfterCreate,
454
TriFunction<K, V, Duration, Duration> expireAfterUpdate);
455
}
456
```
457
458
### Dynamic Expiry Based on Value Characteristics
459
460
```java
461
Expiry<String, String> dynamicExpiry = new Expiry<String, String>() {
462
@Override
463
public long expireAfterCreate(String key, String value, long currentTime) {
464
// Different expiration based on key pattern
465
if (key.startsWith("temp_")) {
466
return Duration.ofMinutes(5).toNanos();
467
} else if (key.startsWith("session_")) {
468
return Duration.ofHours(2).toNanos();
469
} else if (value.length() > 1000) {
470
// Large values expire sooner
471
return Duration.ofMinutes(15).toNanos();
472
} else {
473
return Duration.ofHours(1).toNanos();
474
}
475
}
476
477
@Override
478
public long expireAfterUpdate(String key, String value, long currentTime, long currentDuration) {
479
// Reset to creation expiration on update
480
return expireAfterCreate(key, value, currentTime);
481
}
482
483
@Override
484
public long expireAfterRead(String key, String value, long currentTime, long currentDuration) {
485
// Extend expiration for frequently accessed items
486
if (key.startsWith("hot_")) {
487
return currentDuration + Duration.ofMinutes(10).toNanos();
488
}
489
// No change for other items
490
return currentDuration;
491
}
492
};
493
494
Cache<String, String> dynamicCache = Caffeine.newBuilder()
495
.maximumSize(1000)
496
.expireAfter(dynamicExpiry)
497
.build();
498
```
499
500
### Simplified Expiry with Factory Methods
501
502
```java
503
// Creation-only expiry
504
Expiry<String, UserSession> creationExpiry = Expiry.creating((key, session) -> {
505
// Different expiration based on user role
506
return session.isAdminUser() ? Duration.ofHours(8) : Duration.ofHours(2);
507
});
508
509
// Access-based expiry
510
Expiry<String, String> accessExpiry = Expiry.accessing(
511
(key, value) -> Duration.ofMinutes(30), // Initial expiration
512
(key, value, duration) -> Duration.ofMinutes(45) // Extended on access
513
);
514
515
// Write-based expiry
516
Expiry<String, String> writeExpiry = Expiry.writing(
517
(key, value) -> Duration.ofHours(1), // Initial expiration
518
(key, value, duration) -> Duration.ofHours(2) // Extended on update
519
);
520
```
521
522
## Ticker Interface
523
524
The `Ticker` interface provides time source abstraction for testing and custom time handling.
525
526
```java { .api }
527
@FunctionalInterface
528
public interface Ticker {
529
long read();
530
531
static Ticker systemTicker();
532
static Ticker disabledTicker();
533
}
534
```
535
536
### Custom Ticker for Testing
537
538
```java
539
public class ManualTicker implements Ticker {
540
private volatile long nanos = 0;
541
542
@Override
543
public long read() {
544
return nanos;
545
}
546
547
public void advance(Duration duration) {
548
nanos += duration.toNanos();
549
}
550
551
public void advance(long time, TimeUnit unit) {
552
nanos += unit.toNanos(time);
553
}
554
}
555
556
// Use in tests
557
ManualTicker ticker = new ManualTicker();
558
Cache<String, String> testCache = Caffeine.newBuilder()
559
.maximumSize(100)
560
.expireAfterWrite(Duration.ofMinutes(10))
561
.ticker(ticker)
562
.build();
563
564
testCache.put("key", "value");
565
566
// Simulate time passage
567
ticker.advance(Duration.ofMinutes(15));
568
testCache.cleanUp(); // Triggers expiration
569
570
String value = testCache.getIfPresent("key"); // null - expired
571
```
572
573
## Scheduler Interface
574
575
The `Scheduler` interface controls background maintenance scheduling.
576
577
```java { .api }
578
@FunctionalInterface
579
public interface Scheduler {
580
Future<?> schedule(Executor executor, Runnable command, long delay, TimeUnit unit);
581
582
static Scheduler forScheduledExecutorService(ScheduledExecutorService scheduledExecutorService);
583
static Scheduler systemScheduler();
584
static Scheduler disabledScheduler();
585
}
586
```
587
588
### Custom Scheduler Implementation
589
590
```java
591
public class CustomScheduler implements Scheduler {
592
private final ScheduledExecutorService scheduler =
593
Executors.newScheduledThreadPool(2, r -> {
594
Thread t = new Thread(r, "caffeine-maintenance");
595
t.setDaemon(true);
596
return t;
597
});
598
599
@Override
600
public Future<?> schedule(Executor executor, Runnable command, long delay, TimeUnit unit) {
601
// Add logging for maintenance operations
602
Runnable wrappedCommand = () -> {
603
System.out.println("Running cache maintenance");
604
long start = System.nanoTime();
605
try {
606
command.run();
607
} finally {
608
long duration = System.nanoTime() - start;
609
System.out.println("Maintenance completed in " +
610
TimeUnit.NANOSECONDS.toMillis(duration) + "ms");
611
}
612
};
613
614
return scheduler.schedule(wrappedCommand, delay, unit);
615
}
616
}
617
618
Cache<String, String> scheduledCache = Caffeine.newBuilder()
619
.maximumSize(1000)
620
.expireAfterAccess(Duration.ofMinutes(30))
621
.scheduler(new CustomScheduler())
622
.build();
623
```
624
625
## Interner Interface
626
627
The `Interner` interface provides object interning functionality similar to `String.intern()` for any immutable type.
628
629
```java { .api }
630
@FunctionalInterface
631
public interface Interner<E> {
632
E intern(E sample);
633
634
static <E> Interner<E> newStrongInterner();
635
static <E> Interner<E> newWeakInterner();
636
}
637
```
638
639
### Strong Interner
640
641
Strong interners retain strong references to interned instances, preventing garbage collection.
642
643
```java
644
Interner<String> strongInterner = Interner.newStrongInterner();
645
646
// Intern strings
647
String s1 = strongInterner.intern("hello");
648
String s2 = strongInterner.intern(new String("hello"));
649
String s3 = strongInterner.intern("hello");
650
651
// All return the same instance
652
assert s1 == s2;
653
assert s2 == s3;
654
assert s1 == s3;
655
656
// Memory efficient for frequently used immutable objects
657
Interner<ImmutableConfig> configInterner = Interner.newStrongInterner();
658
ImmutableConfig config1 = configInterner.intern(new ImmutableConfig("prod", "database"));
659
ImmutableConfig config2 = configInterner.intern(new ImmutableConfig("prod", "database"));
660
assert config1 == config2; // Same instance
661
```
662
663
### Weak Interner
664
665
Weak interners use weak references, allowing garbage collection when no other references exist.
666
667
```java
668
Interner<String> weakInterner = Interner.newWeakInterner();
669
670
// Intern strings with weak references
671
String s1 = weakInterner.intern("temporary");
672
String s2 = weakInterner.intern(new String("temporary"));
673
assert s1 == s2;
674
675
// After gc, instances may be collected
676
System.gc();
677
// Subsequent interns may return different instances if previous ones were collected
678
679
// Useful for reducing memory usage of temporary objects
680
Interner<RequestKey> keyInterner = Interner.newWeakInterner();
681
RequestKey key1 = keyInterner.intern(new RequestKey("user123", "action"));
682
RequestKey key2 = keyInterner.intern(new RequestKey("user123", "action"));
683
assert key1 == key2; // Same instance while both are reachable
684
```
685
686
### Performance Comparison
687
688
```java
689
public class InternerPerformanceExample {
690
public void demonstrateInterning() {
691
Interner<String> strongInterner = Interner.newStrongInterner();
692
Interner<String> weakInterner = Interner.newWeakInterner();
693
694
// Strong interner - better for frequently accessed objects
695
long start = System.nanoTime();
696
for (int i = 0; i < 10000; i++) {
697
String interned = strongInterner.intern("common_value_" + (i % 100));
698
// Process interned string
699
}
700
long strongTime = System.nanoTime() - start;
701
702
// Weak interner - better for memory-constrained scenarios
703
start = System.nanoTime();
704
for (int i = 0; i < 10000; i++) {
705
String interned = weakInterner.intern("common_value_" + (i % 100));
706
// Process interned string
707
}
708
long weakTime = System.nanoTime() - start;
709
710
System.out.println("Strong interner time: " + strongTime / 1_000_000 + "ms");
711
System.out.println("Weak interner time: " + weakTime / 1_000_000 + "ms");
712
}
713
}
714
```
715
716
### Best Practices
717
718
**When to use Strong Interner:**
719
- Frequently accessed immutable objects
720
- Objects with long lifetimes
721
- Performance-critical scenarios
722
- Small to medium number of unique instances
723
724
**When to use Weak Interner:**
725
- Large number of potentially unique instances
726
- Memory-constrained environments
727
- Temporary objects that can be garbage collected
728
- Scenarios where canonical instances may not always be needed
729
730
**Thread Safety:**
731
Both strong and weak interners are thread-safe and can be safely accessed from multiple threads concurrently.