cfml.parsing.util.SequencedHashMap Maven / Gradle / Ivy
/*
* The Apache Software License, Version 2
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* .
*
*/
/**
* This is a modified version of org.apache.commons.collections.SequencedHashMap
* that uses com.nary.util.HashMap for storing entries (instead of java.util.Map)
* and that supports case-sensitive or case-insensitive keys.
*/
package cfml.parsing.util;
import java.io.Externalizable;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.NoSuchElementException;
import java.util.ConcurrentModificationException;
/**
* A map of objects whose mapping entries are sequenced based on the order in which they were added. This data structure
* has fast O(1) search time, deletion time, and insertion time.
*
*
* 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)}).
*
*
* This class is not thread safe. When a thread safe implementation is required, use
* {@link Collections#synchronizedMap(Map)} as it is documented, or use explicit synchronization controls.
*
* @since 2.0
* @author Michael A. Smith
* @author Daniel Rall
* @author Henning P. Schmiedehausen
*/
public class SequencedHashMap implements CaseSensitiveMap, 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 {
// 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
// implementated, 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 static final long serialVersionUID = 1L;
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()
@Override
public Object getKey() {
return this.key;
}
// per Map.Entry.getValue()
@Override
public Object getValue() {
return this.value;
}
// per Map.Entry.setValue()
@Override
public Object setValue(Object value) {
Object oldValue = this.value;
this.value = value;
return oldValue;
}
@Override
public int hashCode() {
// implemented per api docs for Map.Entry.hashCode()
return ((getKey() == null ? 0 : getKey().hashCode()) ^ (getValue() == null ? 0 : getValue().hashCode()));
}
@Override
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())));
}
@Override
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
* sentinal has a null
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 FastMap 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 and case-sensitive keys.
**/
public SequencedHashMap() {
sentinel = createSentinel();
entries = new FastMap();
}
/**
* Construct a new sequenced hash map with default initial size and load factor and specified case-sensitivity.
**/
public SequencedHashMap(boolean caseSensitive) {
sentinel = createSentinel();
entries = new FastMap(caseSensitive);
}
/**
* Construct a new sequenced hash map with the specified initial size and default load factor and case-sensitive
* keys.
*
* @param initialSize
* the initial size for the hash table
*
* @see FastMap#FastMap(int)
**/
public SequencedHashMap(int initialSize) {
sentinel = createSentinel();
entries = new FastMap(initialSize);
}
/**
* 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);
}
public SequencedHashMap(Map m, boolean caseSensitive) {
this(caseSensitive);
putAll(m);
}
@Override
public boolean isCaseSensitive() {
return entries.isCaseSensitive();
}
/**
* Removes an internal entry from the linked list. This does not remove it from the underlying map.
**/
private static 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()}.
*/
@Override
public int size() {
// use the underlying Map's size since size is not maintained here.
return entries.size();
}
/**
* Implements {@link Map#isEmpty()}.
*/
@Override
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)}.
*/
@Override
public boolean containsKey(Object key) {
// pass on to underlying map implementation
return entries.containsKey(key);
}
/**
* Implements {@link Map#containsValue(Object)}.
*/
@Override
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)}.
*/
@Override
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
* entrySet().iterator().next()
, but this method provides an optimized implementation.
*
* @return The first entry in the sequence, or null
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
* getFirst().getKey()
, but this method provides a slightly optimized implementation.
*
* @return The first key in the sequence, or null
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 convient 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
* getFirst().getValue()
, but this method provides a slightly optimized implementation.
*
* @return The first value in the sequence, or null
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 convient 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:
*
*
* Object obj = null;
* Iterator iter = entrySet().iterator();
* while (iter.hasNext()) {
* obj = iter.next();
* }
* return (Map.Entry) obj;
*
*
* 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 null
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
* getLast().getKey()
, but this method provides a slightly optimized implementation.
*
* @return The last key in the sequence, or null
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 convient 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
* getLast().getValue()
, but this method provides a slightly optimized implementation.
*
* @return The last value in the sequence, or null
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 convient way to avoid test for an empty list)
return sentinel.prev.getValue();
}
/**
* Implements {@link Map#put(Object, Object)}.
*/
@Override
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)}.
*/
@Override
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.
*
* @exception NullPointerException
* if t
is null
**/
@Override
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()}.
*/
@Override
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)}.
*/
@Override
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()}.
*/
@Override
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.
**/
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
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()}.
*/
@Override
public Set keySet() {
return new AbstractSet() {
// required impls
@Override
public Iterator iterator() {
return new OrderedIterator(KEY);
}
@Override
public boolean remove(Object o) {
Entry e = SequencedHashMap.this.removeImpl(o);
return (e != null);
}
// more efficient impls than abstract set
@Override
public void clear() {
SequencedHashMap.this.clear();
}
@Override
public int size() {
return SequencedHashMap.this.size();
}
@Override
public boolean isEmpty() {
return SequencedHashMap.this.isEmpty();
}
@Override
public boolean contains(Object o) {
return SequencedHashMap.this.containsKey(o);
}
};
}
/**
* Implements {@link Map#values()}.
*/
@Override
public Collection values() {
return new AbstractCollection() {
// required impl
@Override
public Iterator iterator() {
return new OrderedIterator(VALUE);
}
@Override
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
@Override
public void clear() {
SequencedHashMap.this.clear();
}
@Override
public int size() {
return SequencedHashMap.this.size();
}
@Override
public boolean isEmpty() {
return SequencedHashMap.this.isEmpty();
}
@Override
public boolean contains(Object o) {
return SequencedHashMap.this.containsValue(o);
}
};
}
/**
* Implements {@link Map#entrySet()}.
*/
@Override
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
@Override
public Iterator iterator() {
return new OrderedIterator(ENTRY);
}
@Override
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
@Override
public void clear() {
SequencedHashMap.this.clear();
}
@Override
public int size() {
return SequencedHashMap.this.size();
}
@Override
public boolean isEmpty() {
return SequencedHashMap.this.isEmpty();
}
@Override
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 {@link #KEY},
* {@link #VALUE}, or {@link #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. Essientially, if
* this value is negative (i.e. the bit specified by {@link #REMOVED_MASK} is set), the current position has
* been removed. If positive, remove can still be called.
**/
private static final long serialVersionUID = 1L;
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 returnType
which must be either {@link #KEY},
* {@link #VALUE}, or {@link #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 true
if there are more elements left to be returned from the iterator;
* false
otherwise.
**/
@Override
public boolean hasNext() {
return pos.next != sentinel;
}
/**
* Returns the next element from the iterator.
*
* @return the next element from the iterator.
*
* @exception NoSuchElementException
* if there are no more elements in the iterator.
*
* @exception ConcurrentModificationException
* if a modification occurs in the underlying map.
**/
@Override
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("bad iterator type: " + returnType);
}
}
/**
* Removes the last element returned from the {@link #next()} method from the sequenced map.
*
* @exception 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.
*
* @exception ConcurrentModificationException
* if a modification occurs in the underlying map.
**/
@Override
public void remove() {
if ((returnType & REMOVED_MASK) != 0) {
throw new IllegalStateException("remove() must follow next()");
}
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 clone()
'd. The cloned object maintains the same sequence.
*
* @return A clone of this instance.
*
* @exception CloneNotSupportedException
* if clone is not supported by a subclass.
*/
@Override
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 FastMap();
// 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
*
* @exception ArrayIndexOutOfBoundsException
* if the specified index is < 0
or >
the size of the map.
**/
private Map.Entry getEntry(int index) {
Entry pos = sentinel;
if (index < 0) {
throw new ArrayIndexOutOfBoundsException(index + " < 0");
}
// 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(index + " >= " + (i + 1));
}
return pos.next;
}
/**
* Returns the key at the specified index.
*
* @exception ArrayIndexOutOfBoundsException
* if the index
is < 0
or >
the size of the map.
*/
public Object get(int index) {
return getEntry(index).getKey();
}
/**
* Returns the value at the specified index.
*
* @exception ArrayIndexOutOfBoundsException
* if the index
is < 0
or >
the size of the map.
*/
public Object getValue(int index) {
return getEntry(index).getValue();
}
/**
* Returns the index of the specified key.
*/
public int indexOf(Object key) {
Entry e = (Entry) entries.get(key);
int pos = 0;
while (e.prev != sentinel) {
pos++;
e = e.prev;
}
return pos;
}
/**
* Returns a key iterator.
*/
public Iterator iterator() {
return keySet().iterator();
}
/**
* Returns the last index of the specified key.
*/
public int lastIndexOf(Object key) {
// keys in a map are guarunteed 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.
*
*
* 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 coressponding the key
, or null
if none existed.
*
* @exception ArrayIndexOutOfBoundsException
* if the index
is < 0
or >
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
*/
@Override
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
*/
@Override
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;
}