You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by nf...@apache.org on 2025/03/21 20:58:47 UTC
(camel) 01/01: CAMEL-21888: camel-core - Compress changes only if needed
This is an automated email from the ASF dual-hosted git repository.
nfilotto pushed a commit to branch essobedo/CAMEL-21888/no-longer-compress-changes
in repository https://gitbox.apache.org/repos/asf/camel.git
commit fa1ef4553c557254e66422ef98c08dc05b586f84
Author: Nicolas Filotto <ni...@qlik.com>
AuthorDate: Fri Mar 21 21:58:12 2025 +0100
CAMEL-21888: camel-core - Compress changes only if needed
---
.../camel/support/cache/SimpleLRUCacheTest.java | 142 +++++++++++
.../apache/camel/support/cache/SimpleLRUCache.java | 270 +++++++++++++++++----
2 files changed, 366 insertions(+), 46 deletions(-)
diff --git a/core/camel-core/src/test/java/org/apache/camel/support/cache/SimpleLRUCacheTest.java b/core/camel-core/src/test/java/org/apache/camel/support/cache/SimpleLRUCacheTest.java
index cb1a17ee60d..58e44a4b215 100644
--- a/core/camel-core/src/test/java/org/apache/camel/support/cache/SimpleLRUCacheTest.java
+++ b/core/camel-core/src/test/java/org/apache/camel/support/cache/SimpleLRUCacheTest.java
@@ -17,6 +17,7 @@
package org.apache.camel.support.cache;
import java.util.ArrayList;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
@@ -60,9 +61,12 @@ class SimpleLRUCacheTest {
assertNull(map.put("1", "One"));
assertEquals(1, map.size());
assertEquals(1, map.getQueueSize());
+ assertEquals("One", map.get("1"));
map.entrySet().iterator().next().setValue("bar");
assertEquals(1, map.size());
assertEquals(2, map.getQueueSize());
+ assertEquals("bar", map.get("1"));
+ assertThrows(NullPointerException.class, () -> map.entrySet().iterator().next().setValue(null));
}
@Test
@@ -79,6 +83,80 @@ class SimpleLRUCacheTest {
map.put("1", "B");
assertEquals(1, map.size());
assertEquals(2, map.getQueueSize());
+ map.put("2", "A");
+ assertEquals(2, map.size());
+ assertEquals(3, map.getQueueSize());
+ map.put("3", "A");
+ assertEquals(3, map.size());
+ assertEquals(4, map.getQueueSize());
+ map.put("4", "A");
+ assertEquals(3, map.size());
+ assertEquals(3, map.getQueueSize());
+ }
+
+ @Test
+ void size() {
+ assertEquals(0, map.size());
+ assertNull(map.put("1", "One"));
+ assertEquals(1, map.size());
+ }
+
+ @Test
+ void isEmpty() {
+ assertTrue(map.isEmpty());
+ assertNull(map.put("1", "One"));
+ assertFalse(map.isEmpty());
+ map.remove("1");
+ assertTrue(map.isEmpty());
+ }
+
+ @Test
+ void containsKey() {
+ assertFalse(map.containsKey("1"));
+ assertNull(map.put("1", "One"));
+ assertTrue(map.containsKey("1"));
+ map.remove("1");
+ assertFalse(map.containsKey("1"));
+ assertThrows(NullPointerException.class, () -> map.containsKey(null));
+ }
+
+ @Test
+ void containsValue() {
+ assertFalse(map.containsValue("One"));
+ assertNull(map.put("1", "One"));
+ assertTrue(map.containsValue("One"));
+ map.remove("1");
+ assertFalse(map.containsValue("One"));
+ assertThrows(NullPointerException.class, () -> map.containsValue(null));
+ }
+
+ @Test
+ void remove() {
+ assertTrue(map.isEmpty());
+ map.remove("1");
+ assertTrue(map.isEmpty());
+ assertNull(map.put("1", "One"));
+ assertFalse(map.isEmpty());
+ map.remove("1");
+ assertTrue(map.isEmpty());
+ assertThrows(NullPointerException.class, () -> map.remove(null));
+ }
+
+ @Test
+ void removeWithValue() {
+ assertTrue(map.isEmpty());
+ map.remove("1", "One");
+ assertTrue(map.isEmpty());
+ assertNull(map.put("1", "One"));
+ assertFalse(map.isEmpty());
+ map.remove("1", "Two");
+ assertFalse(map.isEmpty());
+ map.remove("2", "One");
+ assertFalse(map.isEmpty());
+ map.remove("1", "One");
+ assertTrue(map.isEmpty());
+ assertThrows(NullPointerException.class, () -> map.remove(null, "A"));
+ assertThrows(NullPointerException.class, () -> map.remove("A", null));
}
@Test
@@ -101,6 +179,54 @@ class SimpleLRUCacheTest {
assertEquals(1, consumed.size());
assertTrue(map.containsKey("2"));
assertEquals("Two v2", map.get("2"));
+ assertThrows(NullPointerException.class, () -> map.put("A", null));
+ assertThrows(NullPointerException.class, () -> map.put(null, "A"));
+ }
+
+ @Test
+ void putAll() {
+ assertEquals(0, map.size());
+ Map<String, String> data = new LinkedHashMap<>();
+ data.put("1", "One");
+ data.put("2", "Two");
+ data.put("3", "Three");
+ map.putAll(data);
+ assertEquals(3, map.size());
+ assertEquals(0, consumed.size());
+ data.clear();
+ data.put("4", "Four");
+ data.put("5", "Five");
+ map.putAll(data);
+ assertEquals(3, map.size());
+ assertEquals(2, consumed.size());
+ assertFalse(map.containsKey("1"));
+ assertFalse(map.containsKey("2"));
+ assertTrue(consumed.contains("One"));
+ assertTrue(consumed.contains("Two"));
+ assertThrows(NullPointerException.class, () -> map.putAll(null));
+ }
+
+ @Test
+ void clear() {
+ assertEquals(0, map.size());
+ map.putAll(Map.of("1", "One", "2", "Two", "3", "Three"));
+ assertEquals(3, map.size());
+ map.clear();
+ assertEquals(0, map.size());
+ }
+
+ @Test
+ void replaceAll() {
+ map.replaceAll((k, v) -> v + " v2");
+ assertEquals(0, map.size());
+ map.putAll(Map.of("1", "One", "2", "Two", "3", "Three"));
+ assertEquals(3, map.size());
+ map.replaceAll((k, v) -> v + " v2");
+ assertEquals(3, map.size());
+ assertEquals("One v2", map.get("1"));
+ assertEquals("Two v2", map.get("2"));
+ assertEquals("Three v2", map.get("3"));
+ assertThrows(NullPointerException.class, () -> map.replaceAll(null));
}
@Test
@@ -128,6 +254,8 @@ class SimpleLRUCacheTest {
assertEquals(2, consumed.size());
assertFalse(map.containsKey("2"));
assertTrue(consumed.contains("Two"));
+ assertThrows(NullPointerException.class, () -> map.putIfAbsent("A", null));
+ assertThrows(NullPointerException.class, () -> map.putIfAbsent(null, "A"));
}
@Test
@@ -164,6 +292,8 @@ class SimpleLRUCacheTest {
assertEquals("Five", map.computeIfAbsent("5", k -> null));
assertEquals(3, map.size());
assertEquals(2, consumed.size());
+ assertThrows(NullPointerException.class, () -> map.computeIfAbsent(null, k -> null));
+ assertThrows(NullPointerException.class, () -> map.computeIfAbsent("A", null));
}
@Test
@@ -189,6 +319,8 @@ class SimpleLRUCacheTest {
assertEquals(2, map.size());
assertEquals(0, consumed.size());
assertFalse(map.containsKey("1"));
+ assertThrows(NullPointerException.class, () -> map.computeIfPresent(null, (k, v) -> null));
+ assertThrows(NullPointerException.class, () -> map.computeIfPresent("A", null));
}
@Test
@@ -215,6 +347,8 @@ class SimpleLRUCacheTest {
assertEquals(2, map.size());
assertEquals(1, consumed.size());
assertFalse(map.containsKey("2"));
+ assertThrows(NullPointerException.class, () -> map.compute(null, (k, v) -> null));
+ assertThrows(NullPointerException.class, () -> map.compute("A", null));
}
@Test
@@ -238,6 +372,9 @@ class SimpleLRUCacheTest {
assertNull(map.merge("2", "V2", (v1, v2) -> null));
assertEquals(2, map.size());
assertEquals(1, consumed.size());
+ assertThrows(NullPointerException.class, () -> map.merge("A", "B", null));
+ assertThrows(NullPointerException.class, () -> map.merge("A", null, (v1, v2) -> null));
+ assertThrows(NullPointerException.class, () -> map.merge(null, "A", (v1, v2) -> null));
}
@Test
@@ -259,6 +396,8 @@ class SimpleLRUCacheTest {
assertEquals("Three v2", map.get("3"));
assertEquals(3, map.size());
assertEquals(0, consumed.size());
+ assertThrows(NullPointerException.class, () -> map.replace("A", null));
+ assertThrows(NullPointerException.class, () -> map.replace(null, "A"));
}
@Test
@@ -285,6 +424,9 @@ class SimpleLRUCacheTest {
assertEquals("Three v2", map.get("3"));
assertEquals(3, map.size());
assertEquals(0, consumed.size());
+ assertThrows(NullPointerException.class, () -> map.replace("A", "B", null));
+ assertThrows(NullPointerException.class, () -> map.replace("A", null, "B"));
+ assertThrows(NullPointerException.class, () -> map.replace(null, "A", "B"));
}
@Test
diff --git a/core/camel-support/src/main/java/org/apache/camel/support/cache/SimpleLRUCache.java b/core/camel-support/src/main/java/org/apache/camel/support/cache/SimpleLRUCache.java
index 7c1b76e601a..9a0510a5839 100644
--- a/core/camel-support/src/main/java/org/apache/camel/support/cache/SimpleLRUCache.java
+++ b/core/camel-support/src/main/java/org/apache/camel/support/cache/SimpleLRUCache.java
@@ -16,7 +16,7 @@
*/
package org.apache.camel.support.cache;
-import java.util.Collections;
+import java.util.Collection;
import java.util.Deque;
import java.util.HashSet;
import java.util.Map;
@@ -25,12 +25,14 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
+import java.util.stream.Collectors;
/**
* {@code SimpleLRUCache} is a simple implementation of a cache of type Least Recently Used . The implementation doesn't
@@ -40,7 +42,7 @@ import java.util.function.Function;
* @param <K> type of the key
* @param <V> type of the value
*/
-public class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
+public class SimpleLRUCache<K, V> implements Map<K, V> {
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
@@ -54,7 +56,7 @@ public class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
/**
* The lock to prevent the addition of changes during the swap of queue of changes.
*/
- private final ReadWriteLock swapLock = new ReentrantReadWriteLock();
+ private final ReadWriteLock lock = new ReentrantReadWriteLock();
/**
* The maximum cache size.
*/
@@ -62,17 +64,20 @@ public class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
/**
* The last changes recorded.
*/
- private final AtomicReference<Deque<Entry<K, V>>> lastChanges = new AtomicReference<>(new ConcurrentLinkedDeque<>());
+ private final AtomicReference<Deque<Entry<K, ValueHolder<V>>>> lastChanges
+ = new AtomicReference<>(new ConcurrentLinkedDeque<>());
/**
* The function to call when an entry is evicted.
*/
private final Consumer<V> evict;
+ private final AtomicLong sequence = new AtomicLong();
+ private final Map<K, ValueHolder<V>> delegate;
public SimpleLRUCache(int initialCapacity, int maximumCacheSize, Consumer<V> evicted) {
- super(initialCapacity, DEFAULT_LOAD_FACTOR);
if (maximumCacheSize <= 0) {
throw new IllegalArgumentException("The maximum cache size must be greater than 0");
}
+ this.delegate = new ConcurrentHashMap<>(initialCapacity, DEFAULT_LOAD_FACTOR);
this.maximumCacheSize = maximumCacheSize;
this.evict = Objects.requireNonNull(evicted);
}
@@ -84,20 +89,66 @@ public class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
* @param mappingFunction the mapping function to apply.
* @return the result of the mapping function.
*/
- private V addChange(OperationContext<K, V> context, Function<? super K, ? extends V> mappingFunction) {
+ private ValueHolder<V> addChange(OperationContext<K, V> context, Function<? super K, ? extends V> mappingFunction) {
K key = context.key;
V value = mappingFunction.apply(key);
if (value == null) {
return null;
}
- Entry<K, V> entry = Map.entry(key, value);
- swapLock.readLock().lock();
- try {
- lastChanges.get().add(entry);
- } finally {
- swapLock.readLock().unlock();
+ ValueHolder<V> holder = newValue(value);
+ lastChanges.get().add(Map.entry(key, holder));
+ return holder;
+ }
+
+ @Override
+ public int size() {
+ return delegate.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return delegate.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return delegate.containsKey(key);
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ if (value == null)
+ throw new NullPointerException();
+ return delegate.values().stream()
+ .map(ValueHolder::get)
+ .anyMatch(v -> Objects.equals(v, value));
+ }
+
+ @Override
+ public V get(Object key) {
+ return extractValue(delegate.get(key));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean remove(Object key, Object value) {
+ if (key == null || value == null) {
+ throw new NullPointerException();
+ }
+ K keyK = (K) key;
+ try (OperationContext<K, V> context = new OperationContext<>(this, keyK)) {
+ delegate.compute(
+ keyK,
+ (k, v) -> {
+ V extractedValue = extractValue(v);
+ if (Objects.equals(value, extractedValue)) {
+ context.result = extractedValue;
+ return null;
+ }
+ return v;
+ });
+ return context.result != null;
}
- return value;
}
@Override
@@ -106,26 +157,31 @@ public class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
throw new NullPointerException();
}
try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
- super.compute(
+ delegate.compute(
key,
(k, v) -> {
- context.result = v;
+ context.result = extractValue(v);
return addChange(context, x -> value);
});
return context.result;
}
}
+ @Override
+ public V remove(Object key) {
+ return extractValue(delegate.remove(key));
+ }
+
@Override
public V putIfAbsent(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException();
}
try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
- super.compute(
+ delegate.compute(
key,
(k, v) -> {
- context.result = v;
+ context.result = extractValue(v);
if (v != null) {
return v;
}
@@ -141,7 +197,7 @@ public class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
throw new NullPointerException();
}
try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
- return super.computeIfAbsent(key, k -> addChange(context, mappingFunction));
+ return extractValue(delegate.computeIfAbsent(key, k -> addChange(context, mappingFunction)));
}
}
@@ -151,7 +207,8 @@ public class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
throw new NullPointerException();
}
try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
- return super.computeIfPresent(key, (k, v) -> addChange(context, x -> remappingFunction.apply(x, v)));
+ return extractValue(delegate.computeIfPresent(key,
+ (k, v) -> addChange(context, x -> remappingFunction.apply(x, extractValue(v)))));
}
}
@@ -161,7 +218,8 @@ public class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
throw new NullPointerException();
}
try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
- return super.compute(key, (k, v) -> addChange(context, x -> remappingFunction.apply(x, v)));
+ return extractValue(
+ delegate.compute(key, (k, v) -> addChange(context, x -> remappingFunction.apply(x, extractValue(v)))));
}
}
@@ -171,12 +229,12 @@ public class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
throw new NullPointerException();
}
try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
- return super.compute(
+ return extractValue(delegate.compute(
key,
(k, oldValue) -> {
- V newValue = (oldValue == null) ? value : remappingFunction.apply(oldValue, value);
+ V newValue = (oldValue == null) ? value : remappingFunction.apply(oldValue.get(), value);
return addChange(context, x -> newValue);
- });
+ }));
}
}
@@ -186,12 +244,13 @@ public class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
throw new NullPointerException();
}
try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
- super.computeIfPresent(
+ delegate.computeIfPresent(
key,
(k, v) -> {
- if (Objects.equals(oldValue, v)) {
- context.result = addChange(context, x -> newValue);
- return context.result;
+ if (Objects.equals(oldValue, extractValue(v))) {
+ ValueHolder<V> result = addChange(context, x -> newValue);
+ context.result = extractValue(result);
+ return result;
}
return v;
});
@@ -205,10 +264,10 @@ public class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
throw new NullPointerException();
}
try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
- super.computeIfPresent(
+ delegate.computeIfPresent(
key,
(k, v) -> {
- context.result = v;
+ context.result = extractValue(v);
return addChange(context, x -> value);
});
return context.result;
@@ -222,16 +281,48 @@ public class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
}
}
+ @Override
+ public void clear() {
+ lock.writeLock().lock();
+ try {
+ delegate.clear();
+ lastChanges.getAndSet(new ConcurrentLinkedDeque<>());
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
+ @Override
+ public Set<K> keySet() {
+ return delegate.keySet();
+ }
+
+ @Override
+ public Collection<V> values() {
+ return delegate.values().stream().map(ValueHolder::get).toList();
+ }
+
@Override
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
+ if (function == null) {
+ throw new NullPointerException();
+ }
for (Entry<? extends K, ? extends V> e : entrySet()) {
- replace(e.getKey(), e.getValue(), function.apply(e.getKey(), e.getValue()));
+ K key = e.getKey();
+ V value = e.getValue();
+ try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
+ delegate.computeIfPresent(
+ key,
+ (k, v) -> addChange(context, x -> function.apply(x, value)));
+ }
}
}
@Override
public Set<Entry<K, V>> entrySet() {
- return Collections.unmodifiableSet(super.entrySet());
+ return delegate.entrySet().stream()
+ .map(entry -> new CacheEntry<>(this, entry.getKey(), entry.getValue().get()))
+ .collect(Collectors.toUnmodifiableSet());
}
/**
@@ -272,24 +363,29 @@ public class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
/**
* @return the oldest existing change.
*/
- private Entry<K, V> nextOldestChange() {
+ private Entry<K, ValueHolder<V>> nextOldestChange() {
return lastChanges.get().poll();
}
/**
- * Removes duplicates from the queue of changes.
+ * Removes duplicates from the queue of changes if the queue is full.
*/
- private void compressChanges() {
- Deque<Entry<K, V>> newChanges = new ConcurrentLinkedDeque<>();
- Deque<Entry<K, V>> currentChanges;
- swapLock.writeLock().lock();
+ private void compressChangesIfNeeded() {
+ Deque<Entry<K, ValueHolder<V>>> newChanges;
+ Deque<Entry<K, ValueHolder<V>>> currentChanges;
+ lock.writeLock().lock();
try {
- currentChanges = lastChanges.getAndSet(newChanges);
+ if (isQueueFull()) {
+ newChanges = new ConcurrentLinkedDeque<>();
+ currentChanges = lastChanges.getAndSet(newChanges);
+ } else {
+ return;
+ }
} finally {
- swapLock.writeLock().unlock();
+ lock.writeLock().unlock();
}
Set<K> keys = new HashSet<>();
- Entry<K, V> entry;
+ Entry<K, ValueHolder<V>> entry;
while ((entry = currentChanges.pollLast()) != null) {
if (keys.add(entry.getKey())) {
newChanges.addFirst(entry);
@@ -297,6 +393,14 @@ public class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
}
}
+ private ValueHolder<V> newValue(V value) {
+ return new ValueHolder<>(sequence.incrementAndGet(), value);
+ }
+
+ private V extractValue(ValueHolder<V> holder) {
+ return holder == null ? null : holder.get();
+ }
+
/**
* The internal context of all write operations.
*/
@@ -317,21 +421,21 @@ public class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
OperationContext(SimpleLRUCache<K, V> cache, K key) {
this.cache = cache;
this.key = key;
+ cache.lock.readLock().lock();
}
@Override
public void close() {
+ cache.lock.readLock().unlock();
if (cache.evictionNeeded() && cache.eviction.compareAndSet(false, true)) {
try {
do {
- cache.compressChanges();
+ cache.compressChangesIfNeeded();
if (cache.isCacheFull()) {
- Entry<K, V> oldest = cache.nextOldestChange();
- if (oldest != null && cache.remove(oldest.getKey(), oldest.getValue())) {
- cache.evict.accept(oldest.getValue());
+ Entry<K, ValueHolder<V>> oldest = cache.nextOldestChange();
+ if (oldest != null && cache.delegate.remove(oldest.getKey(), oldest.getValue())) {
+ cache.evict.accept(oldest.getValue().get());
}
- } else {
- break;
}
} while (cache.evictionNeeded());
} finally {
@@ -340,4 +444,78 @@ public class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
}
}
}
+
+ /**
+ * A cache value holder that is identified by its id represented by the revision number.
+ *
+ * @param <V> the type of the value
+ */
+ private static class ValueHolder<V> {
+ private final long revision;
+ private final V value;
+
+ ValueHolder(long revision, V value) {
+ this.revision = revision;
+ this.value = value;
+ }
+
+ V get() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass())
+ return false;
+ ValueHolder<?> that = (ValueHolder<?>) o;
+ return revision == that.revision;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(revision);
+ }
+ }
+
+ /**
+ * A modifiable cache entry.
+ *
+ * @param <K> the type of the key
+ * @param <V> the type of the value
+ */
+ private static class CacheEntry<K, V> implements Entry<K, V> {
+
+ private final K key;
+ private V val;
+ /**
+ * The underlying cache.
+ */
+ private final SimpleLRUCache<K, V> cache;
+
+ CacheEntry(SimpleLRUCache<K, V> cache, K key, V value) {
+ this.cache = cache;
+ this.key = key;
+ this.val = value;
+ }
+
+ @Override
+ public K getKey() {
+ return key;
+ }
+
+ @Override
+ public V getValue() {
+ return val;
+ }
+
+ @Override
+ public V setValue(V value) {
+ if (value == null)
+ throw new NullPointerException();
+ V v = val;
+ val = value;
+ cache.put(key, value);
+ return v;
+ }
+ }
}