WeakValueHashMap.java Source code

Java tutorial

Introduction

Here is the source code for WeakValueHashMap.java

Source

/*
 * 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.
 */

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;

import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

/**
 * A WeakValueHashMap is implemented as a HashMap that maps keys to
 * WeakValues.  Because we don't have access to the innards of the
 * HashMap, we have to wrap/unwrap value objects with WeakValues on
 * every operation.  Fortunately WeakValues are small, short-lived
 * objects, so the added allocation overhead is tolerable. This
 * implementaton directly extends java.util.HashMap.
 *
 * @author  Markus Fuchs
 * @see     java.util.HashMap
 * @see     java.lang.ref.WeakReference
 */

public class WeakValueHashMap extends HashMap {

    /* Reference queue for cleared WeakValues */
    private ReferenceQueue queue = new ReferenceQueue();

    /**
     * Returns the number of key-value mappings in this map.<p>
     * @return the number of key-value mappings in this map.
     */
    public int size() {
        // delegate to entrySet, as super.size() also counts WeakValues
        return entrySet().size();
    }

    /**
     * Returns <tt>true</tt> if this map contains no key-value mappings.<p>
     * @return <tt>true</tt> if this map contains no key-value mappings.
     */
    public boolean isEmpty() {
        return size() == 0;
    }

    /**
     * Returns <tt>true</tt> if this map contains a mapping for the specified
     * key.<p>
     * @param key key whose presence in this map is to be tested
     * @return <tt>true</tt> if this map contains a mapping for the specified
     * key.
     */
    public boolean containsKey(Object key) {
        // need to clean up gc'ed values before invoking super method
        processQueue();
        return super.containsKey(key);
    }

    /**
      * Returns <tt>true</tt> if this map maps one or more keys to the
      * specified value.<p>
      * @param value value whose presence in this map is to be tested
      * @return <tt>true</tt> if this map maps one or more keys to this value.
      */
    public boolean containsValue(Object value) {
        return super.containsValue(WeakValue.create(value));
    }

    /**
     * Gets the value for the given key.<p>
     * @param key key whose associated value, if any, is to be returned
     * @return the value to which this map maps the specified key.
     */
    public Object get(Object key) {
        // We don't need to remove garbage collected values here;
        // if they are garbage collected, the get() method returns null;
        // the next put() call with the same key removes the old value
        // automatically so that it can be completely garbage collected
        return getReferenceObject((WeakReference) super.get(key));
    }

    /**
     * Puts a new (key,value) into the map.<p>
     * @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 null
     * if there was no mapping for key or the value has been garbage
     * collected by the garbage collector.
     */
    public Object put(Object key, Object value) {
        // If the map already contains an equivalent key, the new key
        // of a (key, value) pair is NOT stored in the map but the new
        // value only. But as the key is strongly referenced by the
        // map, it can not be removed from the garbage collector, even
        // if the key becomes weakly reachable due to the old
        // value. So, it isn't necessary to remove all garbage
        // collected values with their keys from the map before the
        // new entry is made. We only clean up here to distribute
        // clean up calls on different operations.
        processQueue();

        WeakValue oldValue = (WeakValue) super.put(key, WeakValue.create(key, value, queue));
        return getReferenceObject(oldValue);
    }

    /**
     * Removes key and value for the given key.<p>
     * @param key key whose mapping is to be removed from the map.
     * @return previous value associated with specified key, or null
     * if there was no mapping for key or the value has been garbage
     * collected by the garbage collector.
     */
    public Object remove(Object key) {
        return getReferenceObject((WeakReference) super.remove(key));
    }

    /**
     * A convenience method to return the object held by the
     * weak reference or <code>null</code> if it does not exist.
     */
    private final Object getReferenceObject(WeakReference ref) {
        return (ref == null) ? null : ref.get();
    }

    /**
     * Removes all garbage collected values with their keys from the map.
     * Since we don't know how much the ReferenceQueue.poll() operation
     * costs, we should not call it every map operation.
     */
    private void processQueue() {
        WeakValue wv = null;

        while ((wv = (WeakValue) this.queue.poll()) != null) {
            // "super" is not really necessary but use it
            // to be on the safe side
            super.remove(wv.key);
        }
    }

    /* -- Helper classes -- */

    /**
     * We need this special class to keep the backward reference from
     * the value to the key, so that we are able to remove the key if
     * the value is garbage collected.
     */
    private static class WeakValue extends WeakReference {
        /**
         * It's the same as the key in the map. We need the key to remove
         * the value if it is garbage collected.
         */
        private Object key;

        private WeakValue(Object value) {
            super(value);
        }

        /**
         * Creates a new weak reference without adding it to a
         * ReferenceQueue.
         */
        private static WeakValue create(Object value) {
            if (value == null)
                return null;
            else
                return new WeakValue(value);
        }

        private WeakValue(Object key, Object value, ReferenceQueue queue) {
            super(value, queue);
            this.key = key;
        }

        /**
         * Creates a new weak reference and adds it to the given queue.
         */
        private static WeakValue create(Object key, Object value, ReferenceQueue queue) {
            if (value == null)
                return null;
            else
                return new WeakValue(key, value, queue);
        }

        /**
         * A WeakValue is equal to another WeakValue iff they both refer
         * to objects that are, in turn, equal according to their own
         * equals methods.
         */
        public boolean equals(Object obj) {
            if (this == obj)
                return true;

            if (!(obj instanceof WeakValue))
                return false;

            Object ref1 = this.get();
            Object ref2 = ((WeakValue) obj).get();

            if (ref1 == ref2)
                return true;

            if ((ref1 == null) || (ref2 == null))
                return false;

            return ref1.equals(ref2);
        }

        /**
         *
         */
        public int hashCode() {
            Object ref = this.get();

            return (ref == null) ? 0 : ref.hashCode();
        }
    }

    /** 
     * Internal class for entries. This class wraps/unwraps the
     * values of the Entry objects returned from the underlying map.
     */
    private class Entry implements Map.Entry {
        private Map.Entry ent;
        private Object value; /* Strong reference to value, so that the
                              GC will leave it alone as long as this
                              Entry exists */

        Entry(Map.Entry ent, Object value) {
            this.ent = ent;
            this.value = value;
        }

        public Object getKey() {
            return ent.getKey();
        }

        public Object getValue() {
            return value;
        }

        public Object setValue(Object value) {
            // This call changes the map. Please see the comment on 
            // the put method for the correctness remark.
            Object oldValue = this.value;
            this.value = value;
            ent.setValue(WeakValue.create(getKey(), value, queue));
            return oldValue;
        }

        private boolean valEquals(Object o1, Object o2) {
            return (o1 == null) ? (o2 == null) : o1.equals(o2);
        }

        public boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry) o;
            return (valEquals(ent.getKey(), e.getKey()) && valEquals(value, e.getValue()));
        }

        public int hashCode() {
            Object k;
            return ((((k = ent.getKey()) == null) ? 0 : k.hashCode()) ^ ((value == null) ? 0 : value.hashCode()));
        }

    }

    /**
     * Internal class for entry sets to unwrap/wrap WeakValues stored
     * in the map.
     */
    private class EntrySet extends AbstractSet {

        public Iterator iterator() {
            // remove garbage collected elements
            processQueue();

            return new Iterator() {
                Iterator hashIterator = hashEntrySet.iterator();
                Entry next = null;

                public boolean hasNext() {
                    if (hashIterator.hasNext()) {
                        // since we removed garbage collected elements,
                        // we can simply return the next entry.
                        Map.Entry ent = (Map.Entry) hashIterator.next();
                        WeakValue wv = (WeakValue) ent.getValue();
                        Object v = (wv == null) ? null : wv.get();
                        next = new Entry(ent, v);
                        return true;
                    }
                    return false;
                }

                public Object next() {
                    if ((next == null) && !hasNext())
                        throw new NoSuchElementException();
                    Entry e = next;
                    next = null;
                    return e;
                }

                public void remove() {
                    hashIterator.remove();
                }

            };
        }

        public boolean isEmpty() {
            return !(iterator().hasNext());
        }

        public int size() {
            int j = 0;
            for (Iterator i = iterator(); i.hasNext(); i.next())
                j++;
            return j;
        }

        public boolean remove(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry) o;
            Object ek = e.getKey();
            Object ev = e.getValue();
            Object hv = WeakValueHashMap.this.get(ek);
            if (hv == null) {
                // if the map's value is null, we have to check, if the
                // entry's value is null and the map contains the key
                if ((ev == null) && WeakValueHashMap.this.containsKey(ek)) {
                    WeakValueHashMap.this.remove(ek);
                    return true;
                } else {
                    return false;
                }
                // otherwise, simply compare the values
            } else if (hv.equals(ev)) {
                WeakValueHashMap.this.remove(ek);
                return true;
            }

            return false;
        }

        public int hashCode() {
            int h = 0;
            for (Iterator i = hashEntrySet.iterator(); i.hasNext();) {
                Map.Entry ent = (Map.Entry) i.next();
                Object k;
                WeakValue wv = (WeakValue) ent.getValue();
                if (wv == null)
                    continue;
                h += ((((k = ent.getKey()) == null) ? 0 : k.hashCode()) ^ wv.hashCode());
            }
            return h;
        }

    }

    // internal helper variable, because we can't access
    // entrySet from the superclass inside the EntrySet class
    private Set hashEntrySet = null;
    // stores the EntrySet instance
    private Set entrySet = null;

    /**
     * Returns a <code>Set</code> view of the mappings in this map.<p>
     * @return a <code>Set</code> view of the mappings in this map.
     */
    public Set entrySet() {
        if (entrySet == null) {
            hashEntrySet = super.entrySet();
            entrySet = new EntrySet();
        }
        return entrySet;
    }

    // stores the value collection
    private transient Collection values = null;

    /**
     * Returns a <code>Collection</code> view of the values contained
     * in this map.<p>
     * @return a <code>Collection</code> view of the values contained
     * in this map.
     */
    public Collection values() {
        // delegates to entrySet, because super method returns
        // WeakValues instead of value objects
        if (values == null) {
            values = new AbstractCollection() {
                public Iterator iterator() {
                    return new Iterator() {
                        private Iterator i = entrySet().iterator();

                        public boolean hasNext() {
                            return i.hasNext();
                        }

                        public Object next() {
                            return ((Entry) i.next()).getValue();
                        }

                        public void remove() {
                            i.remove();
                        }
                    };
                }

                public int size() {
                    return WeakValueHashMap.this.size();
                }

                public boolean contains(Object v) {
                    return WeakValueHashMap.this.containsValue(v);
                }
            };
        }
        return values;
    }

}