[COLLECTIONS-696] AbstractReferenceMap made easier for subclassing; PR

#51.
This commit is contained in:
Maxim Solodovnik 2018-09-19 09:09:10 -06:00 committed by Gary Gregory
parent ad442e3c73
commit 2374711606
2 changed files with 86 additions and 6 deletions

View File

@ -400,13 +400,15 @@ public abstract class AbstractReferenceMap<K, V> extends AbstractHashedMap<K, V>
HashEntry<K, V> previous = null;
HashEntry<K, V> entry = data[index];
while (entry != null) {
if (((ReferenceEntry<K, V>) entry).purge(ref)) {
ReferenceEntry<K, V> refEntry = (ReferenceEntry<K, V>) entry;
if (refEntry.purge(ref)) {
if (previous == null) {
data[index] = entry.next;
} else {
previous.next = entry.next;
}
this.size--;
refEntry.onPurge();
return;
}
previous = entry;
@ -721,12 +723,18 @@ public abstract class AbstractReferenceMap<K, V> extends AbstractHashedMap<K, V>
throw new Error();
}
/**
* This is the callback for custom "after purge" logic
*/
protected void onPurge() {
}
/**
* Purges the specified reference
* @param ref the reference to purge
* @return true or false
*/
boolean purge(final Reference<?> ref) {
protected boolean purge(final Reference<?> ref) {
boolean r = parent.keyType != ReferenceStrength.HARD && key == ref;
r = r || parent.valueType != ReferenceStrength.HARD && value == ref;
if (r) {
@ -736,7 +744,7 @@ public abstract class AbstractReferenceMap<K, V> extends AbstractHashedMap<K, V>
if (parent.valueType != ReferenceStrength.HARD) {
((Reference<?>) value).clear();
} else if (parent.purgeValues) {
value = null;
nullValue();
}
}
return r;
@ -750,6 +758,13 @@ public abstract class AbstractReferenceMap<K, V> extends AbstractHashedMap<K, V>
protected ReferenceEntry<K, V> next() {
return (ReferenceEntry<K, V>) next;
}
/**
* This method can be overriden to provide custom logic to purge value
*/
protected void nullValue() {
value = null;
}
}
//-----------------------------------------------------------------------
@ -1073,4 +1088,13 @@ public abstract class AbstractReferenceMap<K, V> extends AbstractHashedMap<K, V>
protected boolean isKeyType(final ReferenceStrength type) {
return this.keyType == type;
}
/**
* Provided protected read-only access to the value type.
* @param type the type to check against.
* @return true if valueType has the specified type
*/
protected boolean isValueType(final ReferenceStrength type) {
return this.valueType == type;
}
}

View File

@ -21,15 +21,20 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import junit.framework.Test;
import java.util.function.Consumer;
import org.apache.commons.collections4.BulkTest;
import org.apache.commons.collections4.map.AbstractHashedMap.HashEntry;
import org.apache.commons.collections4.map.AbstractReferenceMap.ReferenceEntry;
import org.apache.commons.collections4.map.AbstractReferenceMap.ReferenceStrength;
import junit.framework.Test;
/**
* Tests for ReferenceMap.
*
@ -255,6 +260,40 @@ public class ReferenceMapTest<K, V> extends AbstractIterableMapTest<K, V> {
}
}
public void testCustomPurge() {
List<Integer> expiredValues = new ArrayList<>();
@SuppressWarnings("unchecked")
final Consumer<Integer> consumer = (Consumer<Integer> & Serializable) v -> expiredValues.add(v);
final Map<Integer, Integer> map = new ReferenceMap<Integer, Integer>(ReferenceStrength.WEAK, ReferenceStrength.HARD, false) {
private static final long serialVersionUID = 1L;
@Override
protected ReferenceEntry<Integer, Integer> createEntry(HashEntry<Integer, Integer> next, int hashCode, Integer key, Integer value) {
return new AccessibleEntry<>(this, next, hashCode, key, value, consumer);
}
};
for (int i = 100000; i < 100010; i++) {
map.put(Integer.valueOf(i), Integer.valueOf(i));
}
int iterations = 0;
int bytz = 2;
while (true) {
System.gc();
if (iterations++ > 50 || bytz < 0) {
fail("Max iterations reached before resource released.");
}
map.isEmpty();
if (!expiredValues.isEmpty()) {
break;
}
// create garbage:
@SuppressWarnings("unused")
final byte[] b = new byte[bytz];
bytz = bytz * 2;
}
assertFalse("Value should be stored", expiredValues.isEmpty());
}
/**
* Test whether after serialization the "data" HashEntry array is the same size as the original.<p>
*
@ -292,4 +331,21 @@ public class ReferenceMapTest<K, V> extends AbstractIterableMapTest<K, V> {
}
}
private static class AccessibleEntry<K, V> extends ReferenceEntry<K, V> {
final AbstractReferenceMap<K, V> parent;
final Consumer<V> consumer;
public AccessibleEntry(final AbstractReferenceMap<K, V> parent, final HashEntry<K, V> next, final int hashCode, final K key, final V value, final Consumer<V> consumer) {
super(parent, next, hashCode, key, value);
this.parent = parent;
this.consumer = consumer;
}
@Override
protected void onPurge() {
if (parent.isValueType(ReferenceStrength.HARD)) {
consumer.accept(getValue());
}
}
}
}