/* * 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.AbstractMap; import java.util.Arrays; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import org.geotools.util.logging.Logging; /** * A hashtable-based {@link Map} implementation with <em>weak values</em>. An entry in a * {@code WeakValueHashMap} will automatically be removed when its value is no longer * in ordinary use. This class is similar to the standard {@link java.util.WeakHashMap} * class provided in J2SE, except that weak references are hold on values instead of keys. * <p> * The {@code WeakValueHashMap} class is thread-safe. * * @param <K> The class of key elements. * @param <V> The class of value elements. * * @since 2.0 * * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) * * @see java.util.WeakHashMap * @see WeakHashSet */ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { /** * 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; /** * An entry in the {@link WeakValueHashMap}. This is a weak reference * to a value together with a strong reference to a key. */ private final class Entry extends WeakReference<V> implements Map.Entry<K,V> { /** * The key. */ K key; /** * 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 K key, final V value, final Entry next, final int index) { super(value, WeakCollectionCleaner.DEFAULT.referenceQueue); this.key = key; this.next = next; this.index = index; } /** * Returns the key corresponding to this entry. */ public K getKey() { return key; } /** * Returns the value corresponding to this entry. */ public V getValue() { return get(); } /** * Replaces the value corresponding to this entry with the specified value. */ public V setValue(final V value) { if (value != null) { throw new UnsupportedOperationException(); } V old = get(); clear(); return old; } /** * Clear the reference. The {@link WeakCollectionCleaner} requires that this method is * overridden in order to remove this entry from the enclosing hash map. */ @Override public void clear() { super.clear(); removeEntry(this); key = null; } /** * Compares the specified object with this entry for equality. */ @Override public boolean equals(final Object other) { if (other instanceof Map.Entry) { final Map.Entry that = (Map.Entry) other; return Utilities.equals(this.getKey(), that.getKey()) && Utilities.equals(this.getValue(), that.getValue()); } return false; } /** * Returns the hash code value for this map entry. */ @Override public int hashCode() { final Object val = get(); return (key==null ? 0 : key.hashCode()) ^ (val==null ? 0 : val.hashCode()); } } /** * Table of weak references. */ private Entry[] table; /** * 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; /** * Creates a {@code WeakValueHashMap}. */ public WeakValueHashMap() { this(MIN_CAPACITY); } /** * Creates a {@code WeakValueHashMap} of the requested size and default load factor. * * @param initialSize The initial size. */ public WeakValueHashMap(final int initialSize) { newEntryTable(initialSize); 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. */ private void newEntryTable(final int size) { // table = new Entry[size]; table = (Entry[]) Array.newInstance(Entry.class, size); } /** * Creates a new {@code WeakValueHashMap} populated with the contents of the provied map. * * @param map Initial contents of the {@code WeakValueHashMap}. */ public WeakValueHashMap(final Map<K,V> map) { this(Math.round(map.size() / LOAD_FACTOR) + 1); putAll(map); } /** * 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}. */ } /** * Rehashs {@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; // On retient 'next' tout de suite car sa valeur va changer... final Object key = e.key; if (key != null) { final int index=(key.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(WeakValueHashMap.class.getName()); record.setLoggerName(logger.getName()); logger.log(record); } assert valid(); } /** * Checks if this {@code WeakValueHashMap} 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 number of key-value mappings in this map. */ @Override public synchronized int size() { assert valid(); return count; } /** * Returns {@code true} if this map maps one or more keys to this value. * * @param value value whose presence in this map is to be tested. * @return {@code true} if this map maps one or more keys to this value. */ @Override public synchronized boolean containsValue(final Object value) { return super.containsValue(value); } /** * Returns {@code true} if this map contains a mapping for the specified key. * * @param key key whose presence in this map is to be tested. * @return {@code true} if this map contains a mapping for the specified key. * @throws NullPointerException If key is {@code null}. */ @Override public boolean containsKey(final Object key) { return get(key) != null; } /** * Returns the value to which this map maps the specified key. Returns * {@code null} if the map contains no mapping for this key. * * @param key Key whose associated value is to be returned. * @return The value to which this map maps the specified key. * @throws NullPointerException if the key is {@code null}. */ @Override public synchronized V get(final Object key) { assert WeakCollectionCleaner.DEFAULT.isAlive(); assert valid() : count; final int index = (key.hashCode() & 0x7FFFFFFF) % table.length; for (Entry e=table[index]; e!=null; e=e.next) { if (key.equals(e.key)) { return e.get(); } } return null; } /** * Implementation of {@link #put} and {@link #remove} operations. */ private synchronized V intern(final K key, final V value) { assert WeakCollectionCleaner.DEFAULT.isAlive(); assert valid() : count; /* * Check if {@code obj} is already contained in this * {@code WeakValueHashMap}. If yes, clear it. */ V oldValue = null; final int hash = key.hashCode() & 0x7FFFFFFF; int index = hash % table.length; for (Entry e=table[index]; e!=null; e=e.next) { if (key.equals(e.key)) { oldValue = e.get(); e.clear(); } } if (value != null) { if (count >= threshold) { rehash(true); index = hash % table.length; } table[index] = new Entry(key, value, table[index], index); count++; } assert valid(); return oldValue; } /** * Associates the specified value with the specified key in this map. * The value is associated using a {@link WeakReference}. * * @param key key with which the specified value is to be associated. * @param value value to be associated with the specified key. * @return previous value associated with specified key, or {@code null} * if there was no mapping for key. * * @throws NullPointerException if the key or the value is {@code null}. */ @Override public V put(final K key, final V value) { if (value == null) { throw new NullPointerException("Null value not allowed"); // TODO: localize this message. } return intern(key, value); } /** * Removes the mapping for this key from this map if present. * * @param key key whose mapping is to be removed from the map. * @return previous value associated with specified key, or {@code null} * if there was no entry for key. */ @Override @SuppressWarnings("unchecked") public V remove(final Object key) { return intern((K) key, null); } /** * Removes all of the elements from this map. */ @Override public synchronized void clear() { Arrays.fill(table, null); count = 0; } /** * Returns a set view of the mappings contained in this map. * Each element in this set is a {@link java.util.Map.Entry}. * The current implementation thrown {@link UnsupportedOperationException}. * * @return a set view of the mappings contained in this map. */ @Override public Set<Map.Entry<K,V>> entrySet() { throw new UnsupportedOperationException(); } }