com.samskivert.util.HashIntMap Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of samskivert Show documentation
Show all versions of samskivert Show documentation
A collection of Java utilities.
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2011 Michael Bayne, et al.
//
// 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; either version 2.1 of the License, or
// (at your option) any later version.
//
// 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.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.util;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Set;
import com.samskivert.annotation.ReplacedBy;
/**
* An int map is like a regular map, but with integers as keys. We avoid
* the annoyance of having to create integer objects every time we want to
* lookup or insert values. The hash int map is an int map that uses a
* hashtable mechanism to store its key/value mappings.
*/
@ReplacedBy(value="java.util.Map",
reason="Boxing shouldn't be a major concern. It's probably better to stick to " +
"standard classes rather than worry about a tiny memory or performance gain.")
public class HashIntMap extends AbstractMap
implements IntMap, Cloneable, Serializable
{
/**
* The default number of buckets to use for the hash table.
*/
public final static int DEFAULT_BUCKETS = 16;
/**
* The default load factor.
*/
public final static float DEFAULT_LOAD_FACTOR = 1.75f;
/**
* Constructs an empty hash int map with the specified number of hash
* buckets.
*/
public HashIntMap (int buckets, float loadFactor)
{
// force the capacity to be a power of 2
int capacity = 1;
while (capacity < buckets) {
capacity <<= 1;
}
_buckets = createBuckets(capacity);
_loadFactor = loadFactor;
}
/**
* Constructs an empty hash int map with the default number of hash
* buckets.
*/
public HashIntMap ()
{
this(DEFAULT_BUCKETS, DEFAULT_LOAD_FACTOR);
}
@Override
public int size ()
{
return _size;
}
@Override
public boolean containsKey (Object key)
{
return (key instanceof Integer) && containsKey(((Integer)key).intValue());
}
// documentation inherited
public boolean containsKey (int key)
{
return (null != getImpl(key));
}
@Override
public boolean containsValue (Object o)
{
for (Record bucket : _buckets) {
for (Record r = bucket; r != null; r = r.next) {
if (ObjectUtil.equals(r.value, o)) {
return true;
}
}
}
return false;
}
@Override
public V get (Object key)
{
return (key instanceof Integer) ? get(((Integer)key).intValue()) : null;
}
// documentation inherited
public V get (int key)
{
Record rec = getImpl(key);
return (rec == null) ? null : rec.value;
}
@Override
public V put (Integer key, V value)
{
return put(key.intValue(), value);
}
// documentation inherited
public V put (int key, V value)
{
// check to see if we've passed our load factor, if so: resize
ensureCapacity(_size + 1);
int index = keyToIndex(key);
Record rec = _buckets[index];
// either we start a new chain
if (rec == null) {
_buckets[index] = new Record(key, value);
_size++; // we're bigger
return null;
}
// or we replace an element in an existing chain
Record prev = rec;
for (; rec != null; rec = rec.next) {
if (rec.key == key) {
V ovalue = rec.value;
rec.value = value; // we're not bigger
return ovalue;
}
prev = rec;
}
// or we append it to this chain
prev.next = new Record(key, value);
_size++; // we're bigger
return null;
}
@Override
public V remove (Object key)
{
return (key instanceof Integer) ? remove(((Integer)key).intValue()) : null;
}
// documentation inherited
public V remove (int key)
{
Record removed = removeImpl(key, true);
return (removed == null) ? null : removed.value;
}
/**
* Locate the record with the specified key.
*/
protected Record getImpl (int key)
{
for (Record rec = _buckets[keyToIndex(key)]; rec != null; rec = rec.next) {
if (rec.key == key) {
return rec;
}
}
return null;
}
/**
* Remove an element with optional checking to see if we should shrink.
* When this is called from our iterator, checkShrink==false to avoid booching the buckets.
*/
protected Record removeImpl (int key, boolean checkShrink)
{
int index = keyToIndex(key);
// go through the chain looking for a match
for (Record prev = null, rec = _buckets[index]; rec != null; rec = rec.next) {
if (rec.key == key) {
if (prev == null) {
_buckets[index] = rec.next;
} else {
prev.next = rec.next;
}
_size--;
if (checkShrink) {
checkShrink();
}
return rec;
}
prev = rec;
}
return null;
}
// documentation inherited
public void putAll (IntMap t)
{
// if we can, avoid creating Integer objects while copying
for (IntEntry entry : t.intEntrySet()) {
put(entry.getIntKey(), entry.getValue());
}
}
@Override
public void clear ()
{
// abandon all of our hash chains (the joy of garbage collection)
for (int ii = _buckets.length - 1; ii >= 0; ii--) {
_buckets[ii] = null;
}
// zero out our size
_size = 0;
}
/**
* Ensure that the hash can comfortably hold the specified number
* of elements. Calling this method is not necessary, but can improve
* performance if done prior to adding many elements.
*/
public void ensureCapacity (int minCapacity)
{
int size = _buckets.length;
while (minCapacity > (int) (size * _loadFactor)) {
size *= 2;
}
if (size != _buckets.length) {
resizeBuckets(size);
}
}
/**
* Turn the specified key into an index.
*/
protected final int keyToIndex (int key)
{
// we lift the hash-fixing function from HashMap because Sun
// wasn't kind enough to make it public
key += ~(key << 9);
key ^= (key >>> 14);
key += (key << 4);
key ^= (key >>> 10);
return key & (_buckets.length - 1);
}
/**
* Check to see if we want to shrink the table.
*/
protected void checkShrink ()
{
if ((_buckets.length > DEFAULT_BUCKETS) &&
(_size < (int) (_buckets.length * _loadFactor * .125))) {
resizeBuckets(Math.max(DEFAULT_BUCKETS, _buckets.length >> 1));
}
}
/**
* Resize the hashtable.
*
* @param newsize MUST be a power of 2.
*/
protected void resizeBuckets (int newsize)
{
Record[] oldbuckets = _buckets;
_buckets = createBuckets(newsize);
// we shuffle the records around without allocating new ones
int index = oldbuckets.length;
while (index-- > 0) {
Record oldrec = oldbuckets[index];
while (oldrec != null) {
Record newrec = oldrec;
oldrec = oldrec.next;
// always put the newrec at the start of a chain
int newdex = keyToIndex(newrec.key);
newrec.next = _buckets[newdex];
_buckets[newdex] = newrec;
}
}
}
@Override
public Set> entrySet ()
{
return new AbstractSet>() {
@Override public int size () {
return _size;
}
@Override public Iterator> iterator () {
return new MapEntryIterator();
}
};
}
// documentation inherited
public Set> intEntrySet ()
{
return new AbstractSet>() {
@Override public int size () {
return _size;
}
@Override public Iterator> iterator () {
return new IntEntryIterator();
}
};
}
protected abstract class RecordIterator
implements Iterator
{
public boolean hasNext ()
{
// if we're pointing to an entry, we're good
if (_record != null) {
return true;
}
// search backward through the buckets looking for the next non-empty hash chain
while (_index-- > 0) {
if ((_record = _buckets[_index]) != null) {
return true;
}
}
// found no non-empty hash chains, we're done
return false;
}
public Record nextRecord ()
{
// if we're not pointing to an entry, search for the next
// non-empty hash chain
if (_record == null) {
if (!hasNext()) {
throw new NoSuchElementException();
}
}
// keep track of the last thing we returned, our next record, and return
_last = _record;
_record = _record.next;
return _last;
}
public void remove ()
{
if (_last == null) {
throw new IllegalStateException();
}
// remove the record the hard way, avoiding any major changes to the buckets
HashIntMap.this.removeImpl(_last.key, false);
_last = null;
}
protected int _index = _buckets.length;
protected Record _record, _last;
}
protected class IntEntryIterator extends RecordIterator>
{
public IntEntry next () {
return nextRecord();
}
}
protected class MapEntryIterator extends RecordIterator>
{
public Entry next () {
return nextRecord();
}
}
// documentation inherited from interface IntMap
public IntSet intKeySet ()
{
// damn Sun bastards made the 'keySet' variable with default access, so we can't share it
if (_keySet == null) {
_keySet = new AbstractIntSet() {
public Interator interator () {
return new AbstractInterator () {
public boolean hasNext () {
return i.hasNext();
}
public int nextInt () {
return i.next().getIntKey();
}
@Override public void remove () {
i.remove();
}
private Iterator> i = intEntrySet().iterator();
};
}
@Override public int size () {
return HashIntMap.this.size();
}
@Override public boolean contains (int t) {
return HashIntMap.this.containsKey(t);
}
@Override public boolean remove (int value) {
Record removed = removeImpl(value, true);
return (removed != null);
}
};
}
return _keySet;
}
@Override
public Set keySet ()
{
return intKeySet();
}
/**
* Returns an interation over the keys of this hash int map.
*/
public Interator keys ()
{
return intKeySet().interator();
}
/**
* Returns an iteration over the elements (values) of this hash int
* map.
*/
public Iterator elements ()
{
return values().iterator();
}
@Override
public HashIntMap clone ()
{
try {
@SuppressWarnings("unchecked")
HashIntMap result = (HashIntMap) super.clone();
result._keySet = null;
Record[] buckets = result._buckets = result._buckets.clone();
for (int ii = buckets.length - 1; ii >= 0; ii--) {
if (buckets[ii] != null) {
buckets[ii] = buckets[ii].clone();
}
}
return result;
} catch (CloneNotSupportedException cnse) {
throw new AssertionError(cnse); // won't happen; we're Cloneable
}
}
/**
* Save the state of this instance to a stream (i.e., serialize it).
*/
private void writeObject (ObjectOutputStream s)
throws IOException
{
// write out number of buckets
s.writeInt(_buckets.length);
s.writeFloat(_loadFactor);
// write out size (number of mappings)
s.writeInt(_size);
// write out keys and values
for (IntEntry entry : intEntrySet()) {
s.writeInt(entry.getIntKey());
s.writeObject(entry.getValue());
}
}
/**
* Reconstitute the HashIntMap instance from a stream (i.e.,
* deserialize it).
*/
private void readObject (ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// read in number of buckets and allocate the bucket array
_buckets = createBuckets(s.readInt());
_loadFactor = s.readFloat();
// read in size (number of mappings)
int size = s.readInt();
// read the keys and values
for (int i=0; i[] createBuckets (int size)
{
@SuppressWarnings("unchecked") Record[] recs = (Record[])new Record>[size];
return recs;
}
protected static class Record
implements Cloneable, IntEntry
{
public Record next;
public int key;
public V value;
public Record (int key, V value)
{
this.key = key;
this.value = value;
}
public Integer getKey ()
{
return Integer.valueOf(key);
}
public int getIntKey ()
{
return key;
}
public V getValue ()
{
return value;
}
public V setValue (V value)
{
V ovalue = this.value;
this.value = value;
return ovalue;
}
@Override public boolean equals (Object o)
{
if (o instanceof IntEntry>) {
IntEntry> that = (IntEntry>)o;
return (this.key == that.getIntKey()) &&
ObjectUtil.equals(this.value, that.getValue());
} else if (o instanceof Entry,?>) {
Entry,?> that = (Entry,?>)o;
return (this.getKey().equals(that.getKey())) &&
ObjectUtil.equals(this.value, that.getValue());
} else {
return false;
}
}
@Override public int hashCode ()
{
return key ^ ((value == null) ? 0 : value.hashCode());
}
@Override public String toString ()
{
return key + "=" + StringUtil.toString(value);
}
@Override public Record clone ()
{
try {
@SuppressWarnings("unchecked")
Record result = (Record) super.clone();
// value is not cloned
if (result.next != null) {
result.next = result.next.clone();
}
return result;
} catch (CloneNotSupportedException cnse) {
throw new AssertionError(cnse); // won't happen; we are Cloneable.
}
}
}
protected Record[] _buckets;
protected int _size;
protected float _loadFactor;
/** A stateless view of our keys, so we re-use it. */
protected transient volatile IntSet _keySet = null;
/** Change this if the fields or inheritance hierarchy ever changes
* (which is extremely unlikely). We override this because I'm tired
* of serialized crap not working depending on whether I compiled with
* jikes or javac. */
private static final long serialVersionUID = 1;
}