blob: fb493633c2a138d1f3224856adfd261aa04d0446 [file] [log] [blame]
/*
* Copyright 2007 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;
import com.google.gwt.core.client.JavaScriptObject;
/**
* Implementation of Map interface based on a hash table. <a
* href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/HashMap.html">[Sun
* docs]</a>
*
* @param <K> key type
* @param <V> value type
*/
public class HashMap<K, V> extends AbstractMap<K, V> {
/*
* Implementation notes:
*
* String keys are stored in a separate map from non-String keys. String keys
* are mapped to their values via a JS associative map, stringMap. String keys
* could collide with intrinsic properties (like watch, constructor) so we
* prepend each key with a ':' inside of stringMap.
*
* Integer keys are used to index all non-string keys. A key's hashCode is the
* index in hashCodeMap which should contain that key. Since several keys may
* have the same hash, each value in hashCodeMap is actually an array
* containing all entries whose keys share the same hash.
*/
private final class EntrySet extends AbstractSet<Entry<K, V>> {
@Override
public void clear() {
HashMap.this.clear();
}
@Override
public boolean contains(Object o) {
if (o instanceof Map.Entry) {
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
Object key = entry.getKey();
if (HashMap.this.containsKey(key)) {
Object value = HashMap.this.get(key);
return Utility.equalsWithNullCheck(entry.getValue(), value);
}
}
return false;
}
@Override
public Iterator<Entry<K, V>> iterator() {
return new EntrySetIterator();
}
@Override
public boolean remove(Object entry) {
if (contains(entry)) {
Object key = ((Map.Entry<?, ?>) entry).getKey();
HashMap.this.remove(key);
return true;
}
return false;
}
@Override
public int size() {
return HashMap.this.size();
}
}
/**
* Iterator for <code>EntrySetImpl</code>.
*/
private final class EntrySetIterator implements Iterator<Entry<K, V>> {
private final Iterator<Map.Entry<K, V>> iter;
private Map.Entry<K, V> last = null;
/**
* Constructor for <code>EntrySetIterator</code>.
*/
public EntrySetIterator() {
List<Map.Entry<K, V>> list = new ArrayList<Map.Entry<K, V>>();
if (nullSlotLive) {
MapEntryImpl<K, V> entryImpl = new MapEntryImpl<K, V>(null, nullSlot);
list.add(entryImpl);
}
addAllStringEntries(stringMap, list);
addAllHashEntries(hashCodeMap, list);
this.iter = list.iterator();
}
public boolean hasNext() {
return iter.hasNext();
}
public Map.Entry<K, V> next() {
return last = iter.next();
}
public void remove() {
if (last == null) {
throw new IllegalStateException("Must call next() before remove().");
} else {
iter.remove();
HashMap.this.remove(last.getKey());
last = null;
}
}
}
private static native void addAllHashEntries(JavaScriptObject hashCodeMap,
Collection<?> dest) /*-{
for (var hashCode in hashCodeMap) {
// sanity check that it's really an integer
if (hashCode == parseInt(hashCode)) {
var array = hashCodeMap[hashCode];
for (var i = 0, c = array.length; i < c; ++i) {
dest.@java.util.Collection::add(Ljava/lang/Object;)(array[i]);
}
}
}
}-*/;
private static native void addAllStringEntries(JavaScriptObject stringMap,
Collection<?> dest) /*-{
for (var key in stringMap) {
// only keys that start with a colon ':' count
if (key.charCodeAt(0) == 58) {
var value = stringMap[key];
var entry = @java.util.MapEntryImpl::create(Ljava/lang/Object;Ljava/lang/Object;)(key.substring(1), value);
dest.@java.util.Collection::add(Ljava/lang/Object;)(entry);
}
}
}-*/;
/**
* Returns true if hashCodeMap contains any Map.Entry whose value is Object
* equal to <code>value</code>.
*/
private static native boolean containsHashValue(JavaScriptObject hashCodeMap,
Object value) /*-{
for (var hashCode in hashCodeMap) {
// sanity check that it's really one of ours
if (hashCode == parseInt(hashCode)) {
var array = hashCodeMap[hashCode];
for (var i = 0, c = array.length; i < c; ++i) {
var entry = array[i];
var entryValue = entry.@java.util.Map$Entry::getValue()();
if (@java.util.Utility::equalsWithNullCheck(Ljava/lang/Object;Ljava/lang/Object;)(value, entryValue)) {
return true;
}
}
}
}
return false;
}-*/;
/**
* Returns true if stringMap contains any key whose value is Object equal to
* <code>value</code>.
*/
private static native boolean containsStringValue(JavaScriptObject stringMap,
Object value) /*-{
for (var key in stringMap) {
// only keys that start with a colon ':' count
if (key.charCodeAt(0) == 58) {
var entryValue = stringMap[key];
if (@java.util.Utility::equalsWithNullCheck(Ljava/lang/Object;Ljava/lang/Object;)(value, entryValue)) {
return true;
}
}
}
return false;
}-*/;
/**
* A map of integral hashCodes onto entries.
*/
private transient JavaScriptObject hashCodeMap;
/**
* This is the slot that holds the value associated with the "null" key.
*/
private transient V nullSlot;
private transient boolean nullSlotLive;
private int size;
/**
* A map of Strings onto values.
*/
private transient JavaScriptObject stringMap;
{
clearImpl();
}
public HashMap() {
}
public HashMap(int ignored) {
// This implementation of HashMap has no need of initial capacities.
this(ignored, 0);
}
public HashMap(int ignored, float alsoIgnored) {
// This implementation of HashMap has no need of load factors or capacities.
if (ignored < 0 || alsoIgnored < 0) {
throw new IllegalArgumentException(
"initial capacity was negative or load factor was non-positive");
}
}
public HashMap(Map<? extends K, ? extends V> toBeCopied) {
this.putAll(toBeCopied);
}
@Override
public void clear() {
clearImpl();
}
public Object clone() {
return new HashMap<K, V>(this);
}
@Override
public boolean containsKey(Object key) {
return (key == null) ? nullSlotLive : (!(key instanceof String) ? hasHashValue(
key, key.hashCode()) : hasStringValue((String) key));
}
@Override
public boolean containsValue(Object value) {
if (nullSlotLive && Utility.equalsWithNullCheck(nullSlot, value)) {
return true;
} else if (containsStringValue(stringMap, value)) {
return true;
} else if (containsHashValue(hashCodeMap, value)) {
return true;
}
return false;
}
@Override
public Set<Map.Entry<K, V>> entrySet() {
return new EntrySet();
}
@Override
public V get(Object key) {
return (key == null) ? nullSlot : (!(key instanceof String) ? getHashValue(
key, key.hashCode()) : getStringValue((String) key));
}
@Override
public V put(K key, V value) {
return (key == null) ? putNullSlot(value) : (!(key instanceof String)
? putHashValue(key, value, key.hashCode()) : putStringValue(
(String) key, value));
}
@Override
public V remove(Object key) {
return (key == null) ? removeNullSlot() : (!(key instanceof String) ? removeHashValue(
key, key.hashCode()) : removeStringValue((String) key));
}
@Override
public int size() {
return size;
}
private void clearImpl() {
hashCodeMap = JavaScriptObject.createArray();
stringMap = JavaScriptObject.createObject();
nullSlotLive = false;
nullSlot = null;
size = 0;
}
/**
* Returns the Map.Entry whose key is Object equal to <code>key</code>,
* provided that <code>key</code>'s hash code is <code>hashCode</code>;
* or <code>null</code> if no such Map.Entry exists at the specified
* hashCode.
*/
private native V getHashValue(Object key, int hashCode) /*-{
var array = this.@java.util.HashMap::hashCodeMap[hashCode];
if (array) {
for (var i = 0, c = array.length; i < c; ++i) {
var entry = array[i];
var entryKey = entry.@java.util.Map$Entry::getKey()();
if (@java.util.Utility::equalsWithNullCheck(Ljava/lang/Object;Ljava/lang/Object;)(key, entryKey)) {
return entry.@java.util.Map$Entry::getValue()();
}
}
}
return null;
}-*/;
/**
* Returns the value for the given key in the stringMap. Returns
* <code>null</code> if the specified key does not exist.
*/
private native V getStringValue(String key) /*-{
return (_ = this.@java.util.HashMap::stringMap[':' + key]) == null ? null : _ ;
}-*/;
/**
* Returns true if the a key exists in the hashCodeMap that is Object equal to
* <code>key</code>, provided that <code>key</code>'s hash code is
* <code>hashCode</code>.
*/
private native boolean hasHashValue(Object key, int hashCode) /*-{
var array = this.@java.util.HashMap::hashCodeMap[hashCode];
if (array) {
for (var i = 0, c = array.length; i < c; ++i) {
var entry = array[i];
var entryKey = entry.@java.util.Map$Entry::getKey()();
if (@java.util.Utility::equalsWithNullCheck(Ljava/lang/Object;Ljava/lang/Object;)(key, entryKey)) {
return true;
}
}
}
return false;
}-*/;
/**
* Returns true if the given key exists in the stringMap.
*/
private native boolean hasStringValue(String key) /*-{
return this.@java.util.HashMap::stringMap[':' + key] !== undefined ;
}-*/;
/**
* Sets the specified key to the specified value in the hashCodeMap. Returns
* the value previously at that key. Returns <code>null</code> if the
* specified key did not exist.
*/
private native V putHashValue(K key, V value, int hashCode) /*-{
var array = this.@java.util.HashMap::hashCodeMap[hashCode];
if (array) {
for (var i = 0, c = array.length; i < c; ++i) {
var entry = array[i];
var entryKey = entry.@java.util.Map$Entry::getKey()();
if (@java.util.Utility::equalsWithNullCheck(Ljava/lang/Object;Ljava/lang/Object;)(key, entryKey)) {
// Found an exact match, just update the existing entry
var previous = entry.@java.util.Map$Entry::getValue()();
entry.@java.util.Map$Entry::setValue(Ljava/lang/Object;)(value);
return previous;
}
}
} else {
array = this.@java.util.HashMap::hashCodeMap[hashCode] = [];
}
var entry = @java.util.MapEntryImpl::create(Ljava/lang/Object;Ljava/lang/Object;)(key, value);
array.push(entry);
++this.@java.util.HashMap::size;
return null;
}-*/;
private V putNullSlot(V value) {
V result = nullSlot;
nullSlot = value;
if (!nullSlotLive) {
nullSlotLive = true;
++size;
}
return result;
}
/**
* Sets the specified key to the specified value in the stringMap. Returns the
* value previously at that key. Returns <code>null</code> if the specified
* key did not exist.
*/
private native V putStringValue(String key, V value) /*-{
key = ':' + key;
var result = this.@java.util.HashMap::stringMap[key];
this.@java.util.HashMap::stringMap[key] = value;
return (result === undefined) ?
(++this.@java.util.HashMap::size, null) : result;
}-*/;
/**
* Removes the pair whose key is Object equal to <code>key</code> from
* <code>hashCodeMap</code>, provided that <code>key</code>'s hash code
* is <code>hashCode</code>. Returns the value that was associated with the
* removed key, or null if no such key existed.
*/
private native V removeHashValue(Object key, int hashCode) /*-{
var array = this.@java.util.HashMap::hashCodeMap[hashCode];
if (array) {
for (var i = 0, c = array.length; i < c; ++i) {
var entry = array[i];
var entryKey = entry.@java.util.Map$Entry::getKey()();
if (@java.util.Utility::equalsWithNullCheck(Ljava/lang/Object;Ljava/lang/Object;)(key, entryKey)) {
if (array.length == 1) {
// remove the whole array
delete this.@java.util.HashMap::hashCodeMap[hashCode];
} else {
// splice out the entry we're removing
array.splice(i, 1);
}
--this.@java.util.HashMap::size;
return entry.@java.util.Map$Entry::getValue()();
}
}
}
return null;
}-*/;
private V removeNullSlot() {
V result = nullSlot;
nullSlot = null;
if (nullSlotLive) {
nullSlotLive = false;
--size;
}
return result;
}
/**
* Removes the specified key from the stringMap and returns the value that was
* previously there. Returns <code>null</code> if the specified key
* does not exist.
*/
private native V removeStringValue(String key) /*-{
key = ':' + key;
var result = this.@java.util.HashMap::stringMap[key];
return (result === undefined) ? null :
(--this.@java.util.HashMap::size,
delete this.@java.util.HashMap::stringMap[key],
result);
}-*/;
}