blob: c4b404f7759c40560ffb1d58f437429dc0df6d49 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.util.collection;
import java.util.Arrays;
import java.util.Objects;
import java.util.Iterator;
import java.util.AbstractSet;
import java.lang.reflect.Array;
import org.apache.sis.util.Debug;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.Workaround;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.NullArgumentException;
import static org.apache.sis.util.collection.WeakEntry.*;
/**
* 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 the elements stored in this set are arrays like {@code int[]}, {@code float[]} or
* {@code Object[]}, then the hash code computations and the comparisons are performed using
* the static {@code hashCode(a)} and {@code equals(a1, a2)} methods defined in the {@link Arrays}
* class.</p>
*
* <div class="section">Optimizing memory use in factory implementations</div>
* The {@code WeakHashSet} class has a {@link #get(Object)} method that is not part of the
* {@link java.util.Set} interface. This {@code get} method retrieves an entry from this set
* that is equals to the supplied object. The {@link #unique(Object)} method combines a
* {@code get} followed by a {@code add} operation if the specified object was not in the set.
* This is similar in spirit to the {@link String#intern()} method. The following example shows
* a convenient way to use {@code WeakHashSet} as an internal pool of immutable objects:
*
* {@preformat java
* private final WeakHashSet<Foo> pool = new WeakHashSet<Foo>(Foo.class);
*
* public Foo create(String definition) {
* Foo created = new Foo(definition);
* return pool.unique(created);
* }
* }
*
* Thus, {@code WeakHashSet} can be used inside a factory to prevent creating duplicate immutable objects.
*
* <div class="section">Thread safety</div>
* The same {@code WeakHashSet} instance can be safely used by many threads without synchronization on the part of
* the caller. But if a sequence of two or more method calls need to appear atomic from other threads perspective,
* then the caller can synchronize on {@code this}.
*
* @author Martin Desruisseaux (MPO, IRD, Geomatys)
* @version 0.3
*
* @param <E> the type of elements in the set.
*
* @see java.util.WeakHashMap
*
* @since 0.3
* @module
*/
public class WeakHashSet<E> extends AbstractSet<E> implements CheckedContainer<E> {
/**
* A weak reference to an element. This is an element in a linked list.
* When the reference is disposed, it is removed from the enclosing set.
*/
private final class Entry extends WeakEntry<E> {
/**
* Constructs a new weak reference.
*/
Entry(final E obj, final Entry next, final int hash) {
super(obj, next, hash);
}
/**
* Invoked by {@link org.apache.sis.internal.system.ReferenceQueueConsumer}
* for removing the reference from the enclosing collection.
*/
@Override
public void dispose() {
super.clear();
removeEntry(this);
}
}
/**
* Table of weak references.
*/
private Entry[] table;
/**
* Number of non-null elements in {@link #table}. This is used for determining
* when {@link WeakEntry#rehash(WeakEntry[], int, String)} needs to be invoked.
*/
private int count;
/**
* The type of the elements in this set.
*/
private final Class<E> elementType;
/**
* {@code true} if the elements in this set may be arrays. If the elements can not
* be arrays, then we can avoid the calls to the costly {@link Utilities} methods.
*/
private final boolean mayContainArrays;
/**
* The last time when {@link #table} was not in need for rehash. When the garbage collector
* collected a lot of elements, we will wait a few seconds before rehashing {@link #table}
* in case lot of news elements are going to be added. Without this field, we noticed many
* "reduce", "expand", "reduce", "expand", <i>etc.</i> cycles.
*/
private transient long lastTimeNormalCapacity;
/**
* Creates a {@code WeakHashSet} for elements of the specified type.
*
* @param type the type of the element to be included in this set.
*/
public WeakHashSet(final Class<E> type) {
elementType = type;
mayContainArrays = type.isArray() || type.equals(Object.class);
lastTimeNormalCapacity = System.nanoTime();
/*
* Workaround for the "generic array creation" compiler error.
* Otherwise we would use the commented-out line instead.
*/
@SuppressWarnings("unchecked")
@Workaround(library="JDK", version="1.7")
final Entry[] table = (Entry[]) Array.newInstance(Entry.class, MIN_CAPACITY);
// table = new Entry[size];
this.table = table;
}
/**
* Returns the type of elements in this set.
*/
@Override
public Class<E> getElementType() {
return elementType;
}
/**
* Invoked by {@link Entry} when an element has been collected by the garbage
* collector. This method removes the weak reference from the {@link #table}.
*
* @param toRemove the entry to remove from this map.
*/
private synchronized void removeEntry(final Entry toRemove) {
assert isValid();
final int capacity = table.length;
if (toRemove.removeFrom(table, toRemove.hash % capacity)) {
count--;
assert isValid();
if (count < lowerCapacityThreshold(capacity)) {
final long currentTime = System.nanoTime();
if (currentTime - lastTimeNormalCapacity > REHASH_DELAY) {
table = (Entry[]) WeakEntry.rehash(table, count, "remove");
lastTimeNormalCapacity = currentTime;
assert isValid();
}
}
}
}
/**
* Checks if this {@code WeakHashSet} is valid. This method counts the number of elements and
* compares it to {@link #count}. This method is invoked in assertions only.
*
* @return whether {@link #count} matches the expected value.
*/
@Debug
private boolean isValid() {
if (!Thread.holdsLock(this)) {
throw new AssertionError();
}
if (count > upperCapacityThreshold(table.length)) {
throw new AssertionError(count);
}
return count(table) == count;
}
/**
* Returns the count of element in this set.
*
* @return number of elements in this set.
*/
@Override
public synchronized int size() {
assert isValid();
return count;
}
/**
* 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 element element to be added to this set.
* @return {@code true} if this set did not already contain the specified element.
* @throws NullArgumentException if the given object is {@code null}.
*/
@Override
public synchronized boolean add(final E element) throws NullArgumentException {
ArgumentChecks.ensureNonNull("element", element);
return intern(element, ADD) == null;
}
/**
* Removes a single instance of the specified element from this set, if it is present
* Null values are considered never present.
*
* @param element element to be removed from this set, if present. Can be {@code null}.
* @return {@code true} if the set contained the specified element.
*/
@Override
public synchronized boolean remove(final Object element) {
return intern(element, REMOVE) != null;
}
/**
* Returns an object equals to the specified object, if present. If this set doesn't
* contain any object equals to {@code element}, then this method returns {@code null}.
* Null values are considered never present.
*
* @param element the element to get.
* @return an element equals to the given one if already presents in the set,
* or {@code null} otherwise.
*
* @see #unique(Object)
*/
public synchronized E get(final Object element) {
return intern(element, GET);
}
/**
* Returns {@code true} if this set contains the specified element.
* Null values are considered never present.
*
* @param element object to be checked for containment in this set. Can be {@code null}.
* @return {@code true} if this set contains the specified element.
*/
@Override
public synchronized boolean contains(final Object element) {
return intern(element, GET) != null;
}
/**
* Returns an object equals to {@code element} if such an object already exist in this
* {@code WeakHashSet}. Otherwise, adds {@code element} to this {@code WeakHashSet}.
* This method is functionally equivalents to the following code:
*
* {@preformat java
* if (element != null) {
* T current = get(element);
* if (current != null) {
* return current;
* } else {
* add(element);
* }
* }
* return element;
* }
*
* @param <T> the type of the element to get. Can be {@code null}.
* @param element the element to get or to add in the set if not already presents,
* or {@code null} if the given element was null.
* @return an element equals to the given one if already presents in the set,
* or the given {@code object} otherwise.
*/
public synchronized <T extends E> T unique(final T element) {
/*
* There is no way to make sure that this operation is really safe.
* We have to trust the Object.equals(Object) method to be strict
* about the type of compared objects.
*/
return (T) intern(element, INTERN);
}
// Arguments for the {@link #intern} method.
/** The "remove" operation. */ private static final int REMOVE = -1;
/** The "get" operation. */ private static final int GET = 0;
/** The "add" operation. */ private static final int ADD = +1;
/** The "intern" operation. */ private static final int INTERN = +2;
/**
* Implementation of the {@link #add(Object)}, {@link #remove(Object)}, {@link #get(Object)},
* {@link #contains(Object)} and {@link #unique(Object)} methods.
*/
private E intern(final Object obj, final int operation) {
assert isValid();
if (obj != null) {
/*
* Check if the object is already contained in this
* WeakHashSet. If yes, return the existing element.
*/
Entry[] table = this.table;
final int hash = (mayContainArrays ? Utilities.deepHashCode(obj) : obj.hashCode()) & HASH_MASK;
int index = hash % table.length;
for (Entry e=table[index]; e!=null; e=(Entry) e.next) {
final E candidate = e.get();
if (mayContainArrays ? Objects.deepEquals(candidate, obj) : obj.equals(candidate)) {
if (operation == REMOVE) {
e.dispose();
}
return candidate;
}
// 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 needs to be rehashed, and add {@code obj} to the table.
*/
if (++count >= lowerCapacityThreshold(table.length)) {
if (count > upperCapacityThreshold(table.length)) {
this.table = table = (Entry[]) rehash(table, count, "add");
index = hash % table.length;
}
lastTimeNormalCapacity = System.nanoTime();
}
final E element = elementType.cast(obj);
table[index] = new Entry(element, table[index], hash);
assert isValid();
if (operation == INTERN) {
return element;
}
}
}
return 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 references. Consequently, no
* object reclamation will occur as long as a reference to this array is hold.
*
* @return all elements in this set.
*/
@Override
public synchronized E[] toArray() {
assert isValid();
@SuppressWarnings("unchecked")
final E[] elements = (E[]) Array.newInstance(elementType, count);
int index = 0;
for (Entry el : table) {
while (el != null) {
if ((elements[index] = el.get()) != null) {
index++;
}
el = (Entry) el.next;
}
}
return ArraysExt.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.
*
* @return an iterator over all elements in this set.
*/
@Override
public Iterator<E> iterator() {
return Arrays.asList(toArray()).iterator();
}
}