/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2001-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.util;
import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.util.AbstractSet;
import java.util.Arrays;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.geotools.resources.XArray;
import org.geotools.util.logging.Logging;
/**
* A set of objects hold by weak references. An entry in a {@code WeakHashSet}
* will automatically be removed when it is no longer in ordinary use. More precisely,
* the presence of an entry will not prevent the entry from being discarded by the
* garbage collector, that is, made finalizable, finalized, and then reclaimed.
* When an entry has been discarded it is effectively removed from the set, so
* this class behaves somewhat differently than other {@link java.util.Set} implementations.
* <p>
* If you would like to use {@code WeakHashSet} as inside a factory to prevent creating
* duplicate immutable objects, please look at the {@link CanonicalSet} subclass.
* <p>
* The {@code WeakHashSet} class is thread-safe.
*
* @param <E> The type of elements in the set.
*
* @since 2.0
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*
* @see java.util.WeakHashMap
*/
public class WeakHashSet<E> extends AbstractSet<E> implements CheckedCollection<E> {
/**
* Minimal capacity for {@link #table}.
*/
private static final int MIN_CAPACITY = 7;
/**
* Load factor. Control the moment
* where {@link #table} must be rebuild.
*/
private static final float LOAD_FACTOR = 0.75f;
/**
* A weak reference to an element.
*/
private final class Entry extends WeakReference<E> {
/**
* The next entry, or {@code null} if there is none.
*/
Entry next;
/**
* Index for this element in {@link #table}. This index
* must be updated at every {@link #rehash} call.
*/
int index;
/**
* Constructs a new weak reference.
*/
Entry(final E obj, final Entry next, final int index) {
super(obj, WeakCollectionCleaner.DEFAULT.referenceQueue);
this.next = next;
this.index = index;
}
/**
* Clear the reference.
*/
@Override
public void clear() {
super.clear();
removeEntry(this);
}
}
/**
* Table of weak references.
*/
private Entry[] table;
/**
* The type of the elements in this set.
*/
private final Class<E> type;
/**
* Number of non-nul elements in {@link #table}.
*/
private int count;
/**
* The next size value at which to resize. This value should
* be <code>{@link #table}.length*{@link #loadFactor}</code>.
*/
private int threshold;
/**
* The timestamp when {@link #table} was last rehashed. This information
* is used to avoid too early table reduction. When the garbage collector
* collected a lot of elements, we will wait at least 20 seconds before
* rehashing {@link #table}. Too early table reduction leads to many cycles
* like "reduce", "expand", "reduce", "expand", etc.
*/
private long lastRehashTime;
/**
* Number of millisecond to wait before to rehash
* the table for reducing its size.
*/
private static final long HOLD_TIME = 20*1000L;
/**
* Constructs a {@code WeakHashSet}.
*
* @deprecated Use {@link WeakHashSet(Class)}.
*/
@SuppressWarnings("unchecked")
public WeakHashSet() {
this((Class) Object.class);
}
/**
* Constructs a {@code WeakHashSet}.
*
* @param type The type of the element to be included in this set.
*
* @since 2.5
*/
public WeakHashSet(final Class<E> type) {
this.type = type;
newEntryTable(MIN_CAPACITY);
threshold = Math.round(table.length*LOAD_FACTOR);
lastRehashTime = System.currentTimeMillis();
}
/**
* Sets the {@link #table} array to the specified size. The content of the old array is lost.
*
* @todo Use the commented line instead if a future Java version supports generic arrays.
*/
@SuppressWarnings("unchecked")
private void newEntryTable(final int size) {
// table = new Entry[size];
table = (Entry[]) Array.newInstance(Entry.class, size);
}
/**
* Returns the element type.
*
* @since 2.5
*/
public Class<E> getElementType() {
return type;
}
/**
* Invoked by {@link Entry} when an element has been collected by the garbage
* collector. This method will remove the weak reference from {@link #table}.
*/
private synchronized void removeEntry(final Entry toRemove) {
assert valid() : count;
final int i = toRemove.index;
// Index 'i' may not be valid if the reference 'toRemove'
// has been already removed in a previous rehash.
if (i < table.length) {
Entry prev = null;
Entry e = table[i];
while (e != null) {
if (e == toRemove) {
if (prev != null) {
prev.next = e.next;
} else {
table[i] = e.next;
}
count--;
assert valid();
// If the number of elements has dimunished
// significatively, rehash the table.
if (count <= threshold/4) {
rehash(false);
}
// We must not continue the loop, since
// variable 'e' is no longer valid.
return;
}
prev = e;
e = e.next;
}
}
assert valid();
/*
* If we reach this point, its mean that reference 'toRemove' has not
* been found. This situation may occurs if 'toRemove' has already been
* removed in a previous run of {@link #rehash}.
*/
}
/**
* Rehash {@link #table}.
*
* @param augmentation
* {@code true} if this method is invoked for augmenting {@link #table},
* or {@code false} if it is invoked for making the table smaller.
*/
private void rehash(final boolean augmentation) {
assert Thread.holdsLock(this);
assert valid();
final long currentTime = System.currentTimeMillis();
final int capacity = Math.max(Math.round(count/(LOAD_FACTOR/2)), count+MIN_CAPACITY);
if (augmentation ? (capacity<=table.length) :
(capacity>=table.length || currentTime-lastRehashTime<HOLD_TIME))
{
return;
}
lastRehashTime = currentTime;
final Entry[] oldTable = table;
newEntryTable(capacity);
threshold = Math.round(capacity*LOAD_FACTOR);
for (int i=0; i<oldTable.length; i++) {
for (Entry old=oldTable[i]; old!=null;) {
final Entry e=old;
old = old.next; // We keep 'next' right now because its value will change.
final E obj_e = e.get();
if (obj_e != null) {
final int index = (obj_e.hashCode() & 0x7FFFFFFF) % table.length;
e.index = index;
e.next = table[index];
table[index]=e;
} else {
count--;
}
}
}
final Logger logger = Logging.getLogger("org.geotools.util");
final Level level = Level.FINEST;
if (logger.isLoggable(level)) {
final LogRecord record = new LogRecord(level,
"Rehash from " + oldTable.length + " to " + table.length);
record.setSourceMethodName(augmentation ? "unique" : "remove");
record.setSourceClassName(WeakHashSet.class.getName());
record.setLoggerName(logger.getName());
logger.log(record);
}
assert valid();
}
/**
* Checks if this {@code WeakHashSet} is valid. This method counts the
* number of elements and compare it to {@link #count}. If the check fails,
* the number of elements is corrected (if we didn't, an {@link AssertionError}
* would be thrown for every operations after the first error, which make
* debugging more difficult). The set is otherwise unchanged, which should
* help to get similar behaviour as if assertions hasn't been turned on.
*/
private boolean valid() {
int n = 0;
for (int i=0; i<table.length; i++) {
for (Entry e=table[i]; e!=null; e=e.next) {
n++;
}
}
if (n!=count) {
count = n;
return false;
} else {
return true;
}
}
/**
* Returns the count of element in this set.
*/
public synchronized int size() {
assert valid();
return count;
}
/**
* Returns {@code true} if this set contains the specified element.
*
* @param obj Object to be checked for containment in this set.
* @return {@code true} if this set contains the specified element.
*/
@Override
public synchronized boolean contains(final Object obj) {
return obj != null && intern(type.cast(obj), GET) != null;
}
/**
* Removes a single instance of the specified element from this set, if it is present
*
* @param obj element to be removed from this set, if present.
* @return {@code true} if the set contained the specified element.
*/
@Override
public synchronized boolean remove(final Object obj) {
return intern(type.cast(obj), REMOVE) != null;
}
/**
* Adds the specified element to this set if it is not already present.
* If this set already contains the specified element, the call leaves
* this set unchanged and returns {@code false}.
*
* @param obj Element to be added to this set.
* @return {@code true} if this set did not already contain the specified element.
*/
@Override
public synchronized boolean add(final E obj) {
return intern(obj, ADD) == null;
}
// Arguments for the {@link #intern} method.
/** The "remove" operation. */ static final int REMOVE = -1;
/** The "get" operation. */ static final int GET = 0;
/** The "add" operation. */ static final int ADD = +1;
/** The "intern" operation. */ static final int INTERN = +2;
/**
* Returns an object equals to {@code obj} if such an object already
* exist in this {@code WeakHashSet}. Otherwise, add {@code obj}
* to this {@code WeakHashSet}. This method is equivalents to the
* following code:
*
* <blockquote><pre>
* if (object!=null) {
* final Object current = get(object);
* if (current != null) {
* return current;
* } else {
* add(object);
* }
* }
* return object;
* </pre></blockquote>
*/
final <T extends E> T intern(final T obj, final int operation) {
assert Thread.holdsLock(this);
assert WeakCollectionCleaner.DEFAULT.isAlive();
assert valid() : count;
if (obj != null) {
assert obj.equals(obj) : obj;
/*
* Check if {@code obj} is already contained in this
* {@code WeakHashSet}. If yes, returns the element.
*/
final int hash = obj.hashCode() & 0x7FFFFFFF;
int index = hash % table.length;
for (Entry e=table[index]; e!=null; e=e.next) {
final E candidate = e.get();
if (candidate != null) {
if (candidate.equals(obj)) {
if (operation == REMOVE) {
e.clear();
}
assert candidate.getClass().equals(obj.getClass()) : candidate;
@SuppressWarnings("unchecked")
final T result = (T) candidate;
return result;
}
}
// Do not remove the null element; lets ReferenceQueue do its job
// (it was a bug to remove element here as an "optimization")
}
if (operation >= ADD) {
/*
* Check if the table need to be rehashed,
* and add {@code obj} to the table.
*/
if (count >= threshold) {
rehash(true);
index = hash % table.length;
}
table[index] = new Entry(obj, table[index], index);
count++;
}
}
assert valid();
return (operation == INTERN) ? obj : null;
}
/**
* Removes all of the elements from this set.
*/
@Override
public synchronized void clear() {
Arrays.fill(table, null);
count = 0;
}
/**
* Returns a view of this set as an array. Elements will be in an arbitrary
* order. Note that this array contains strong reference. Consequently, no
* object reclamation will occurs as long as a reference to this array is hold.
*/
@Override
public synchronized E[] toArray() {
assert valid();
@SuppressWarnings("unchecked")
final E[] elements = (E[]) Array.newInstance(type, count);
int index = 0;
for (int i=0; i<table.length; i++) {
for (Entry el=table[i]; el!=null; el=el.next) {
if ((elements[index]=el.get()) != null) {
index++;
}
}
}
return XArray.resize(elements, index);
}
/**
* Returns an iterator over the elements contained in this collection.
* No element from this set will be garbage collected as long as a
* reference to the iterator is hold.
*/
@Override
public Iterator<E> iterator() {
return Arrays.asList(toArray()).iterator();
}
}