blob: ef80d157dc30e692cb1d1cc4f72b64fc3e52dbf6 [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.directory.shared.ldap.util;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import org.apache.commons.collections.KeyValue;
import org.apache.directory.shared.i18n.I18n;
/**
* A map of objects whose mapping entries are sequenced based on the order in
* which they were added. This data structure has fast <i>O(1)</i> search time,
* deletion time, and insertion time.
* <p>
* Although this map is sequenced, it cannot implement {@link java.util.List}
* because of incompatible interface definitions. The remove methods in List and
* Map have different return values (see: {@link java.util.List#remove(Object)}
* and {@link java.util.Map#remove(Object)}).
* <p>
* This class is not thread safe. When a thread safe implementation is required,
* use {@link java.util.Collections#synchronizedMap(Map)} as it is documented,
* or use explicit synchronization controls.
*
* @since Commons Collections 2.0
* @version $Revision$ $Date$
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
*/
public class SequencedHashMap implements Map, Cloneable, Externalizable
{
/**
* {@link java.util.Map.Entry} that doubles as a node in the linked list of
* sequenced mappings.
*/
private static class Entry implements Map.Entry, KeyValue
{
// Note: This class cannot easily be made clonable. While the actual
// implementation of a clone would be simple, defining the semantics is
// difficult. If a shallow clone is implemented, then entry.next.prev !=
// entry, which is unintuitive and probably breaks all sorts of
// assumptions
// in code that uses this implementation. If a deep clone is
// implemented, then what happens when the linked list is cyclical (as
// is
// the case with SequencedHashMap)? It's impossible to know in the clone
// when to stop cloning, and thus you end up in a recursive loop,
// continuously cloning the "next" in the list.
private final Object key;
private Object value;
// package private to allow the SequencedHashMap to access and
// manipulate
// them.
Entry next = null;
Entry prev = null;
public Entry(Object key, Object value)
{
this.key = key;
this.value = value;
}
// per Map.Entry.getKey()
public Object getKey()
{
return this.key;
}
// per Map.Entry.getValue()
public Object getValue()
{
return this.value;
}
// per Map.Entry.setValue()
public Object setValue( Object value )
{
Object oldValue = this.value;
this.value = value;
return oldValue;
}
/**
* Compute the instance's hash code
* @return the instance's hash code
*/
public int hashCode()
{
// implemented per api docs for Map.Entry.hashCode()
return ( ( getKey() == null ? 0 : getKey().hashCode() ) ^ ( getValue() == null ? 0 : getValue().hashCode() ) );
}
public boolean equals( Object obj )
{
if ( obj == null )
return false;
if ( obj == this )
return true;
if ( !( obj instanceof Map.Entry ) )
return false;
Map.Entry other = ( Map.Entry ) obj;
// implemented per api docs for Map.Entry.equals(Object)
return ( ( getKey() == null ? other.getKey() == null : getKey().equals( other.getKey() ) ) && ( getValue() == null ? other
.getValue() == null
: getValue().equals( other.getValue() ) ) );
}
public String toString()
{
return "[" + getKey() + "=" + getValue() + "]";
}
}
/**
* Construct an empty sentinel used to hold the head (sentinel.next) and the
* tail (sentinel.prev) of the list. The sentinel has a <code>null</code>
* key and value.
*/
private static final Entry createSentinel()
{
Entry s = new Entry( null, null );
s.prev = s;
s.next = s;
return s;
}
/**
* Sentinel used to hold the head and tail of the list of entries.
*/
private Entry sentinel;
/**
* Map of keys to entries
*/
private HashMap entries;
/**
* Holds the number of modifications that have occurred to the map,
* excluding modifications made through a collection view's iterator (e.g.
* entrySet().iterator().remove()). This is used to create a fail-fast
* behavior with the iterators.
*/
private transient long modCount = 0;
/**
* Construct a new sequenced hash map with default initial size and load
* factor.
*/
public SequencedHashMap()
{
sentinel = createSentinel();
entries = new HashMap();
}
/**
* Construct a new sequenced hash map with the specified initial size and
* default load factor.
*
* @param initialSize
* the initial size for the hash table
* @see HashMap#HashMap(int)
*/
public SequencedHashMap(int initialSize)
{
sentinel = createSentinel();
entries = new HashMap( initialSize );
}
/**
* Construct a new sequenced hash map with the specified initial size and
* load factor.
*
* @param initialSize
* the initial size for the hash table
* @param loadFactor
* the load factor for the hash table.
* @see HashMap#HashMap(int,float)
*/
public SequencedHashMap(int initialSize, float loadFactor)
{
sentinel = createSentinel();
entries = new HashMap( initialSize, loadFactor );
}
/**
* Construct a new sequenced hash map and add all the elements in the
* specified map. The order in which the mappings in the specified map are
* added is defined by {@link #putAll(Map)}.
*/
public SequencedHashMap(Map m)
{
this();
putAll( m );
}
/**
* Removes an internal entry from the linked list. This does not remove it
* from the underlying map.
*/
private void removeEntry( Entry entry )
{
entry.next.prev = entry.prev;
entry.prev.next = entry.next;
}
/**
* Inserts a new internal entry to the tail of the linked list. This does
* not add the entry to the underlying map.
*/
private void insertEntry( Entry entry )
{
entry.next = sentinel;
entry.prev = sentinel.prev;
sentinel.prev.next = entry;
sentinel.prev = entry;
}
// per Map.size()
/**
* Implements {@link Map#size()}.
*/
public int size()
{
// use the underlying Map's size since size is not maintained here.
return entries.size();
}
/**
* Implements {@link Map#isEmpty()}.
*/
public boolean isEmpty()
{
// for quick check whether the map is entry, we can check the linked
// list
// and see if there's anything in it.
return sentinel.next == sentinel;
}
/**
* Implements {@link Map#containsKey(Object)}.
*/
public boolean containsKey( Object key )
{
// pass on to underlying map implementation
return entries.containsKey( key );
}
/**
* Implements {@link Map#containsValue(Object)}.
*/
public boolean containsValue( Object value )
{
// unfortunately, we cannot just pass this call to the underlying map
// because we are mapping keys to entries, not keys to values. The
// underlying map doesn't have an efficient implementation anyway, so
// this
// isn't a big deal.
// do null comparison outside loop so we only need to do it once. This
// provides a tighter, more efficient loop at the expense of slight
// code duplication.
if ( value == null )
{
for ( Entry pos = sentinel.next; pos != sentinel; pos = pos.next )
{
if ( pos.getValue() == null )
return true;
}
}
else
{
for ( Entry pos = sentinel.next; pos != sentinel; pos = pos.next )
{
if ( value.equals( pos.getValue() ) )
return true;
}
}
return false;
}
/**
* Implements {@link Map#get(Object)}.
*/
public Object get( Object o )
{
// find entry for the specified key object
Entry entry = ( Entry ) entries.get( o );
if ( entry == null )
return null;
return entry.getValue();
}
/**
* Return the entry for the "oldest" mapping. That is, return the Map.Entry
* for the key-value pair that was first put into the map when compared to
* all the other pairings in the map. This behavior is equivalent to using
* <code>entrySet().iterator().next()</code>, but this method provides an
* optimized implementation.
*
* @return The first entry in the sequence, or <code>null</code> if the
* map is empty.
*/
public Map.Entry getFirst()
{
// sentinel.next points to the "first" element of the sequence -- the
// head
// of the list, which is exactly the entry we need to return. We must
// test
// for an empty list though because we don't want to return the
// sentinel!
return ( isEmpty() ) ? null : sentinel.next;
}
/**
* Return the key for the "oldest" mapping. That is, return the key for the
* mapping that was first put into the map when compared to all the other
* objects in the map. This behavior is equivalent to using
* <code>getFirst().getKey()</code>, but this method provides a slightly
* optimized implementation.
*
* @return The first key in the sequence, or <code>null</code> if the map
* is empty.
*/
public Object getFirstKey()
{
// sentinel.next points to the "first" element of the sequence -- the
// head
// of the list -- and the requisite key is returned from it. An empty
// list
// does not need to be tested. In cases where the list is empty,
// sentinel.next will point to the sentinel itself which has a null key,
// which is exactly what we would want to return if the list is empty (a
// nice convenient way to avoid test for an empty list)
return sentinel.next.getKey();
}
/**
* Return the value for the "oldest" mapping. That is, return the value for
* the mapping that was first put into the map when compared to all the
* other objects in the map. This behavior is equivalent to using
* <code>getFirst().getValue()</code>, but this method provides a
* slightly optimized implementation.
*
* @return The first value in the sequence, or <code>null</code> if the
* map is empty.
*/
public Object getFirstValue()
{
// sentinel.next points to the "first" element of the sequence -- the
// head
// of the list -- and the requisite value is returned from it. An empty
// list does not need to be tested. In cases where the list is empty,
// sentinel.next will point to the sentinel itself which has a null
// value,
// which is exactly what we would want to return if the list is empty (a
// nice convenient way to avoid test for an empty list)
return sentinel.next.getValue();
}
/**
* Return the entry for the "newest" mapping. That is, return the Map.Entry
* for the key-value pair that was first put into the map when compared to
* all the other pairings in the map. The behavior is equivalent to:
*
* <pre>
* Object obj = null;
* Iterator iter = entrySet().iterator();
* while ( iter.hasNext() )
* {
* obj = iter.next();
* }
* return ( Map.Entry ) obj;
* </pre>
*
* However, the implementation of this method ensures an O(1) lookup of the
* last key rather than O(n).
*
* @return The last entry in the sequence, or <code>null</code> if the map
* is empty.
*/
public Map.Entry getLast()
{
// sentinel.prev points to the "last" element of the sequence -- the
// tail
// of the list, which is exactly the entry we need to return. We must
// test
// for an empty list though because we don't want to return the
// sentinel!
return ( isEmpty() ) ? null : sentinel.prev;
}
/**
* Return the key for the "newest" mapping. That is, return the key for the
* mapping that was last put into the map when compared to all the other
* objects in the map. This behavior is equivalent to using
* <code>getLast().getKey()</code>, but this method provides a slightly
* optimized implementation.
*
* @return The last key in the sequence, or <code>null</code> if the map
* is empty.
*/
public Object getLastKey()
{
// sentinel.prev points to the "last" element of the sequence -- the
// tail
// of the list -- and the requisite key is returned from it. An empty
// list
// does not need to be tested. In cases where the list is empty,
// sentinel.prev will point to the sentinel itself which has a null key,
// which is exactly what we would want to return if the list is empty (a
// nice convenient way to avoid test for an empty list)
return sentinel.prev.getKey();
}
/**
* Return the value for the "newest" mapping. That is, return the value for
* the mapping that was last put into the map when compared to all the other
* objects in the map. This behavior is equivalent to using
* <code>getLast().getValue()</code>, but this method provides a slightly
* optimized implementation.
*
* @return The last value in the sequence, or <code>null</code> if the map
* is empty.
*/
public Object getLastValue()
{
// sentinel.prev points to the "last" element of the sequence -- the
// tail
// of the list -- and the requisite value is returned from it. An empty
// list does not need to be tested. In cases where the list is empty,
// sentinel.prev will point to the sentinel itself which has a null
// value,
// which is exactly what we would want to return if the list is empty (a
// nice convenient way to avoid test for an empty list)
return sentinel.prev.getValue();
}
/**
* Implements {@link Map#put(Object, Object)}.
*/
public Object put( Object key, Object value )
{
modCount++;
Object oldValue = null;
// lookup the entry for the specified key
Entry e = ( Entry ) entries.get( key );
// check to see if it already exists
if ( e != null )
{
// remove from list so the entry gets "moved" to the end of list
removeEntry( e );
// update value in map
oldValue = e.setValue( value );
// Note: We do not update the key here because its unnecessary. We
// only
// do comparisons using equals(Object) and we know the specified key
// and
// that in the map are equal in that sense. This may cause a problem
// if
// someone does not implement their hashCode() and/or equals(Object)
// method properly and then use it as a key in this map.
}
else
{
// add new entry
e = new Entry( key, value );
entries.put( key, e );
}
// assert(entry in map, but not list)
// add to list
insertEntry( e );
return oldValue;
}
/**
* Implements {@link Map#remove(Object)}.
*/
public Object remove( Object key )
{
Entry e = removeImpl( key );
return ( e == null ) ? null : e.getValue();
}
/**
* Fully remove an entry from the map, returning the old entry or null if
* there was no such entry with the specified key.
*/
private Entry removeImpl( Object key )
{
Entry e = ( Entry ) entries.remove( key );
if ( e == null )
return null;
modCount++;
removeEntry( e );
return e;
}
/**
* Adds all the mappings in the specified map to this map, replacing any
* mappings that already exist (as per {@link Map#putAll(Map)}). The order
* in which the entries are added is determined by the iterator returned
* from {@link Map#entrySet()} for the specified map.
*
* @param t
* the mappings that should be added to this map.
* @throws NullPointerException
* if <code>t</code> is <code>null</code>
*/
public void putAll( Map t )
{
Iterator iter = t.entrySet().iterator();
while ( iter.hasNext() )
{
Map.Entry entry = ( Map.Entry ) iter.next();
put( entry.getKey(), entry.getValue() );
}
}
/**
* Implements {@link Map#clear()}.
*/
public void clear()
{
modCount++;
// remove all from the underlying map
entries.clear();
// and the list
sentinel.next = sentinel;
sentinel.prev = sentinel;
}
/**
* Implements {@link Map#equals(Object)}.
*/
public boolean equals( Object obj )
{
if ( obj == null )
return false;
if ( obj == this )
return true;
if ( !( obj instanceof Map ) )
return false;
return entrySet().equals( ( ( Map ) obj ).entrySet() );
}
/**
* Implements {@link Map#hashCode()}.
* @return the instance's hash code
*/
public int hashCode()
{
return entrySet().hashCode();
}
/**
* Provides a string representation of the entries within the map. The
* format of the returned string may change with different releases, so this
* method is suitable for debugging purposes only. If a specific format is
* required, use {@link #entrySet()}.{@link Set#iterator() iterator()} and
* iterate over the entries in the map formatting them as appropriate.
*/
public String toString()
{
StringBuffer buf = new StringBuffer();
buf.append( '[' );
for ( Entry pos = sentinel.next; pos != sentinel; pos = pos.next )
{
buf.append( pos.getKey() );
buf.append( '=' );
buf.append( pos.getValue() );
if ( pos.next != sentinel )
{
buf.append( ',' );
}
}
buf.append( ']' );
return buf.toString();
}
/**
* Implements {@link Map#keySet()}.
*/
public Set keySet()
{
return new AbstractSet()
{
// required impls
public Iterator iterator()
{
return new OrderedIterator( KEY );
}
public boolean remove( Object o )
{
Entry e = SequencedHashMap.this.removeImpl( o );
return ( e != null );
}
// more efficient impls than abstract set
public void clear()
{
SequencedHashMap.this.clear();
}
public int size()
{
return SequencedHashMap.this.size();
}
public boolean isEmpty()
{
return SequencedHashMap.this.isEmpty();
}
public boolean contains( Object o )
{
return SequencedHashMap.this.containsKey( o );
}
};
}
/**
* Implements {@link Map#values()}.
*/
public Collection values()
{
return new AbstractCollection()
{
// required impl
public Iterator iterator()
{
return new OrderedIterator( VALUE );
}
public boolean remove( Object value )
{
// do null comparison outside loop so we only need to do it
// once. This
// provides a tighter, more efficient loop at the expense of
// slight
// code duplication.
if ( value == null )
{
for ( Entry pos = sentinel.next; pos != sentinel; pos = pos.next )
{
if ( pos.getValue() == null )
{
SequencedHashMap.this.removeImpl( pos.getKey() );
return true;
}
}
}
else
{
for ( Entry pos = sentinel.next; pos != sentinel; pos = pos.next )
{
if ( value.equals( pos.getValue() ) )
{
SequencedHashMap.this.removeImpl( pos.getKey() );
return true;
}
}
}
return false;
}
// more efficient impls than abstract collection
public void clear()
{
SequencedHashMap.this.clear();
}
public int size()
{
return SequencedHashMap.this.size();
}
public boolean isEmpty()
{
return SequencedHashMap.this.isEmpty();
}
public boolean contains( Object o )
{
return SequencedHashMap.this.containsValue( o );
}
};
}
/**
* Implements {@link Map#entrySet()}.
*/
public Set entrySet()
{
return new AbstractSet()
{
// helper
private Entry findEntry( Object o )
{
if ( o == null )
return null;
if ( !( o instanceof Map.Entry ) )
return null;
Map.Entry e = ( Map.Entry ) o;
Entry entry = ( Entry ) entries.get( e.getKey() );
if ( entry != null && entry.equals( e ) )
return entry;
else
return null;
}
// required impl
public Iterator iterator()
{
return new OrderedIterator( ENTRY );
}
public boolean remove( Object o )
{
Entry e = findEntry( o );
if ( e == null )
return false;
return SequencedHashMap.this.removeImpl( e.getKey() ) != null;
}
// more efficient impls than abstract collection
public void clear()
{
SequencedHashMap.this.clear();
}
public int size()
{
return SequencedHashMap.this.size();
}
public boolean isEmpty()
{
return SequencedHashMap.this.isEmpty();
}
public boolean contains( Object o )
{
return findEntry( o ) != null;
}
};
}
// constants to define what the iterator should return on "next"
private static final int KEY = 0;
private static final int VALUE = 1;
private static final int ENTRY = 2;
private static final int REMOVED_MASK = 0x80000000;
private class OrderedIterator implements Iterator
{
/**
* Holds the type that should be returned from the iterator. The value
* should be either KEY, VALUE, or ENTRY. To save a tiny bit of memory,
* this field is also used as a marker for when remove has been called
* on the current object to prevent a second remove on the same element.
* Essentially, if this value is negative (i.e. the bit specified by
* REMOVED_MASK is set), the current position has been removed. If
* positive, remove can still be called.
*/
private int returnType;
/**
* Holds the "current" position in the iterator. When pos.next is the
* sentinel, we've reached the end of the list.
*/
private Entry pos = sentinel;
/**
* Holds the expected modification count. If the actual modification
* count of the map differs from this value, then a concurrent
* modification has occurred.
*/
private transient long expectedModCount = modCount;
/**
* Construct an iterator over the sequenced elements in the order in
* which they were added. The {@link #next()} method returns the type
* specified by <code>returnType</code> which must be either KEY,
* VALUE, or ENTRY.
*/
public OrderedIterator(int returnType)
{
// // Since this is a private inner class, nothing else should have
// // access to the constructor. Since we know the rest of the outer
// // class uses the iterator correctly, we can leave of the
// following
// // check:
// if(returnType >= 0 && returnType <= 2) {
// throw new IllegalArgumentException("Invalid iterator type");
// }
// Set the "removed" bit so that the iterator starts in a state
// where
// "next" must be called before "remove" will succeed.
this.returnType = returnType | REMOVED_MASK;
}
/**
* Returns whether there is any additional elements in the iterator to
* be returned.
*
* @return <code>true</code> if there are more elements left to be
* returned from the iterator; <code>false</code> otherwise.
*/
public boolean hasNext()
{
return pos.next != sentinel;
}
/**
* Returns the next element from the iterator.
*
* @return the next element from the iterator.
* @throws NoSuchElementException
* if there are no more elements in the iterator.
* @throws ConcurrentModificationException
* if a modification occurs in the underlying map.
*/
public Object next()
{
if ( modCount != expectedModCount )
{
throw new ConcurrentModificationException();
}
if ( pos.next == sentinel )
{
throw new NoSuchElementException();
}
// clear the "removed" flag
returnType = returnType & ~REMOVED_MASK;
pos = pos.next;
switch ( returnType )
{
case KEY:
return pos.getKey();
case VALUE:
return pos.getValue();
case ENTRY:
return pos;
default:
// should never happen
throw new Error( I18n.err( I18n.ERR_04425, returnType ) );
}
}
/**
* Removes the last element returned from the {@link #next()} method
* from the sequenced map.
*
* @throws IllegalStateException
* if there isn't a "last element" to be removed. That is,
* if {@link #next()} has never been called, or if
* {@link #remove()} was already called on the element.
* @throws ConcurrentModificationException
* if a modification occurs in the underlying map.
*/
public void remove()
{
if ( ( returnType & REMOVED_MASK ) != 0 )
{
throw new IllegalStateException( I18n.err( I18n.ERR_04426 ) );
}
if ( modCount != expectedModCount )
{
throw new ConcurrentModificationException();
}
SequencedHashMap.this.removeImpl( pos.getKey() );
// update the expected mod count for the remove operation
expectedModCount++;
// set the removed flag
returnType = returnType | REMOVED_MASK;
}
}
// APIs maintained from previous version of SequencedHashMap for backwards
// compatibility
/**
* Creates a shallow copy of this object, preserving the internal structure
* by copying only references. The keys and values themselves are not
* <code>clone()</code>'d. The cloned object maintains the same sequence.
*
* @return A clone of this instance.
* @throws CloneNotSupportedException
* if clone is not supported by a subclass.
*/
public Object clone() throws CloneNotSupportedException
{
// yes, calling super.clone() silly since we're just blowing away all
// the stuff that super might be doing anyway, but for motivations on
// this, see:
// http://www.javaworld.com/javaworld/jw-01-1999/jw-01-object.html
SequencedHashMap map = ( SequencedHashMap ) super.clone();
// create new, empty sentinel
map.sentinel = createSentinel();
// create a new, empty entry map
// note: this does not preserve the initial capacity and load factor.
map.entries = new HashMap();
// add all the mappings
map.putAll( this );
// Note: We cannot just clone the hashmap and sentinel because we must
// duplicate our internal structures. Cloning those two will not clone
// all
// the other entries they reference, and so the cloned hash map will not
// be
// able to maintain internal consistency because there are two objects
// with
// the same entries. See discussion in the Entry implementation on why
// we
// cannot implement a clone of the Entry (and thus why we need to
// recreate
// everything).
return map;
}
/**
* Returns the Map.Entry at the specified index
*
* @throws ArrayIndexOutOfBoundsException
* if the specified index is <code>&lt; 0</code> or
* <code>&gt;</code> the size of the map.
*/
private Map.Entry getEntry( int index )
{
Entry pos = sentinel;
if ( index < 0 )
{
throw new ArrayIndexOutOfBoundsException( I18n.err( I18n.ERR_04427, index ) );
}
// loop to one before the position
int i = -1;
while ( i < ( index - 1 ) && pos.next != sentinel )
{
i++;
pos = pos.next;
}
// pos.next is the requested position
// if sentinel is next, past end of list
if ( pos.next == sentinel )
{
throw new ArrayIndexOutOfBoundsException( I18n.err( I18n.ERR_04428, index, ( i + 1 ) ) );
}
return pos.next;
}
/**
* Gets the key at the specified index.
*
* @param index
* the index to retrieve
* @return the key at the specified index, or null
* @throws ArrayIndexOutOfBoundsException
* if the <code>index</code> is <code>&lt; 0</code> or
* <code>&gt;</code> the size of the map.
*/
public Object get( int index )
{
return getEntry( index ).getKey();
}
/**
* Gets the value at the specified index.
*
* @param index
* the index to retrieve
* @return the value at the specified index, or null
* @throws ArrayIndexOutOfBoundsException
* if the <code>index</code> is <code>&lt; 0</code> or
* <code>&gt;</code> the size of the map.
*/
public Object getValue( int index )
{
return getEntry( index ).getValue();
}
/**
* Gets the index of the specified key.
*
* @param key
* the key to find the index of
* @return the index, or -1 if not found
*/
public int indexOf( Object key )
{
Entry e = ( Entry ) entries.get( key );
if ( e == null )
{
return -1;
}
int pos = 0;
while ( e.prev != sentinel )
{
pos++;
e = e.prev;
}
return pos;
}
/**
* Gets an iterator over the keys.
*
* @return an iterator over the keys
*/
public Iterator iterator()
{
return keySet().iterator();
}
/**
* Gets the last index of the specified key.
*
* @param key
* the key to find the index of
* @return the index, or -1 if not found
*/
public int lastIndexOf( Object key )
{
// keys in a map are guaranteed to be unique
return indexOf( key );
}
/**
* Returns a List view of the keys rather than a set view. The returned list
* is unmodifiable. This is required because changes to the values of the
* list (using {@link java.util.ListIterator#set(Object)}) will effectively
* remove the value from the list and reinsert that value at the end of the
* list, which is an unexpected side effect of changing the value of a list.
* This occurs because changing the key, changes when the mapping is added
* to the map and thus where it appears in the list.
* <p>
* An alternative to this method is to use {@link #keySet()}
*
* @see #keySet()
* @return The ordered list of keys.
*/
public List sequence()
{
List l = new ArrayList( size() );
Iterator iter = keySet().iterator();
while ( iter.hasNext() )
{
l.add( iter.next() );
}
return Collections.unmodifiableList( l );
}
/**
* Removes the element at the specified index.
*
* @param index
* The index of the object to remove.
* @return The previous value corresponding the <code>key</code>, or
* <code>null</code> if none existed.
* @throws ArrayIndexOutOfBoundsException
* if the <code>index</code> is <code>&lt; 0</code> or
* <code>&gt;</code> the size of the map.
*/
public Object remove( int index )
{
return remove( get( index ) );
}
// per Externalizable.readExternal(ObjectInput)
/**
* Deserializes this map from the given stream.
*
* @param in
* the stream to deserialize from
* @throws IOException
* if the stream raises it
* @throws ClassNotFoundException
* if the stream raises it
*/
public void readExternal( ObjectInput in ) throws IOException, ClassNotFoundException
{
int size = in.readInt();
for ( int i = 0; i < size; i++ )
{
Object key = in.readObject();
Object value = in.readObject();
put( key, value );
}
}
/**
* Serializes this map to the given stream.
*
* @param out
* the stream to serialize to
* @throws IOException
* if the stream raises it
*/
public void writeExternal( ObjectOutput out ) throws IOException
{
out.writeInt( size() );
for ( Entry pos = sentinel.next; pos != sentinel; pos = pos.next )
{
out.writeObject( pos.getKey() );
out.writeObject( pos.getValue() );
}
}
// add a serial version uid, so that if we change things in the future
// without changing the format, we can still deserialize properly.
private static final long serialVersionUID = 3380552487888102930L;
}