blob: a7080d863372874d3f78d3f3160659403531873a [file] [log] [blame]
/*
* Copyright 2008 Google Inc.
*
* Licensed 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 java.util;
/**
* Hash table implementation of the Map interface with predictable iteration
* order. <a
* href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/LinkedHashMap.html">[Sun
* docs]</a>
*
* @param <K> key type.
* @param <V> value type.
*/
public class LinkedHashMap<K, V> extends HashMap<K, V> implements Map<K, V> {
/**
* The entry we use includes next/prev pointers for a doubly-linked circular
* list with a head node. This reduces the special cases we have to deal with
* in the list operations.
*
* Note that we duplicate the key from the underlying hash map so we can find
* the eldest entry. The alternative would have been to modify HashMap so more
* of the code was directly usable here, but this would have added some
* overhead to HashMap, or to reimplement most of the HashMap code here with
* small modifications. Paying a small storage cost only if you use
* LinkedHashMap and minimizing code size seemed like a better tradeoff
*/
private class ChainEntry extends MapEntryImpl<K, V> {
private transient ChainEntry next;
private transient ChainEntry prev;
public ChainEntry() {
this(null, null);
}
public ChainEntry(K key, V value) {
super(key, value);
next = prev = null;
}
/**
* Add this node to the end of the chain.
*/
public void addToEnd() {
ChainEntry tail = head.prev;
// Chain is valid.
assert (head != null && tail != null);
// This entry is not in the list.
assert (next == null) && (prev == null);
// Update me.
prev = tail;
next = head;
tail.next = head.prev = this;
}
/**
* Remove this node from any list it may be a part of.
*/
public void remove() {
next.prev = prev;
prev.next = next;
next = prev = null;
}
}
private final class EntrySet extends AbstractSet<Map.Entry<K, V>> {
private final class EntryIterator implements Iterator<Entry<K, V>> {
// The last entry that was returned from this iterator.
private ChainEntry last;
// The next entry to return from this iterator.
private ChainEntry next;
public EntryIterator() {
next = head.next;
}
public boolean hasNext() {
return next != head;
}
public Map.Entry<K, V> next() {
if (next == head) {
throw new NoSuchElementException();
}
last = next;
next = next.next;
return last;
}
public void remove() {
if (last == null) {
throw new IllegalStateException("No current entry");
}
last.remove();
map.remove(last.getKey());
last = null;
}
}
@Override
public void clear() {
LinkedHashMap.this.clear();
}
@Override
public boolean contains(Object o) {
if (!(o instanceof Map.Entry)) {
return false;
}
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
Object key = entry.getKey();
if (LinkedHashMap.this.containsKey(key)) {
Object value = LinkedHashMap.this.get(key);
return Utility.equalsWithNullCheck(entry.getValue(), value);
}
return false;
}
@Override
public Iterator<Map.Entry<K, V>> iterator() {
return new EntryIterator();
}
@Override
public int size() {
return map.size();
}
}
// True if we should use the access order (ie, for LRU caches) instead of
// insertion order.
private transient boolean accessOrder;
/*
* The head of the LRU/insert order chain, which is a doubly-linked circular
* list. The key and value of head should never be read.
*
* The most recently inserted/accessed node is at the end of the chain, ie.
* chain.prev.
*/
private final transient ChainEntry head = new ChainEntry();
/*
* The hashmap that keeps track of our entries and the chain. Note that we
* duplicate the key here to eliminate changes to HashMap and minimize the
* code here, at the expense of additional space.
*/
private final transient HashMap<K, ChainEntry> map = new HashMap<K, ChainEntry>();
{
// Initialize the empty linked list.
head.prev = head;
head.next = head;
}
public LinkedHashMap() {
}
public LinkedHashMap(int ignored) {
super(ignored);
}
public LinkedHashMap(int ignored, float alsoIgnored) {
super(ignored, alsoIgnored);
}
public LinkedHashMap(int ignored, float alsoIgnored, boolean accessOrder) {
super(ignored, alsoIgnored);
this.accessOrder = accessOrder;
}
public LinkedHashMap(Map<? extends K, ? extends V> toBeCopied) {
this.putAll(toBeCopied);
}
@Override
public void clear() {
map.clear();
head.prev = head;
head.next = head;
}
@Override
public boolean containsKey(Object key) {
return map.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
ChainEntry node = head.next;
while (node != head) {
if (Utility.equalsWithNullCheck(node.getValue(), value)) {
return true;
}
node = node.next;
}
return false;
}
@Override
public Set<Map.Entry<K, V>> entrySet() {
return new EntrySet();
}
@Override
public V get(Object key) {
ChainEntry entry = map.get(key);
if (entry != null) {
recordAccess(entry);
return entry.getValue();
}
return null;
}
@Override
public V put(K key, V value) {
ChainEntry old = map.get(key);
if (old == null) {
ChainEntry newEntry = new ChainEntry(key, value);
map.put(key, newEntry);
newEntry.addToEnd();
ChainEntry eldest = head.next;
if (removeEldestEntry(eldest)) {
eldest.remove();
map.remove(eldest.getKey());
}
return null;
} else {
V oldValue = old.getValue();
old.setValue(value);
recordAccess(old);
return oldValue;
}
}
@Override
public V remove(Object key) {
ChainEntry entry = map.remove(key);
if (entry != null) {
entry.remove();
return entry.getValue();
}
return null;
}
@Override
public int size() {
return map.size();
}
@SuppressWarnings("unused")
protected boolean removeEldestEntry(Entry<K, V> eldest) {
return false;
}
private void recordAccess(ChainEntry entry) {
if (accessOrder) {
// Move to the tail of the chain on access.
entry.remove();
entry.addToEnd();
}
}
}