/*
* Copyright 2014-2017 Real Logic Ltd.
*
* 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 org.agrona.collections;
import org.agrona.BitUtil;
import org.agrona.generation.DoNotSub;
import java.util.*;
import java.util.function.IntFunction;
import static java.util.Objects.requireNonNull;
import static org.agrona.collections.CollectionUtil.validateLoadFactor;
/**
* {@link java.util.Map} implementation specialised for int keys using open addressing and
* linear probing for cache efficient access.
*
* @param <V> type of values stored in the {@link java.util.Map}
*/
public class Int2ObjectHashMap<V>
implements Map<Integer, V>
{
private final float loadFactor;
@DoNotSub private int resizeThreshold;
@DoNotSub private int size;
private int[] keys;
private Object[] values;
private final ValueCollection<V> valueCollection;
private final KeySet keySet;
private final EntrySet<V> entrySet;
public Int2ObjectHashMap()
{
this(8, Hashing.DEFAULT_LOAD_FACTOR);
}
/**
* Construct a new map allowing a configuration for initial capacity and load factor.
*
* @param initialCapacity for the backing array
* @param loadFactor limit for resizing on puts
*/
public Int2ObjectHashMap(
@DoNotSub final int initialCapacity,
final float loadFactor)
{
validateLoadFactor(loadFactor);
this.loadFactor = loadFactor;
/* @DoNotSub */ final int capacity = BitUtil.findNextPositivePowerOfTwo(initialCapacity);
/* @DoNotSub */ resizeThreshold = (int)(capacity * loadFactor);
keys = new int[capacity];
values = new Object[capacity];
// Cached to avoid allocation.
valueCollection = new ValueCollection<>();
keySet = new KeySet();
entrySet = new EntrySet<>();
}
/**
* Copy construct a new map from an existing one.
*
* @param mapToCopy for construction.
*/
public Int2ObjectHashMap(final Int2ObjectHashMap<V> mapToCopy)
{
this.loadFactor = mapToCopy.loadFactor;
this.resizeThreshold = mapToCopy.resizeThreshold;
this.size = mapToCopy.size;
keys = mapToCopy.keys.clone();
values = mapToCopy.values.clone();
// Cached to avoid allocation.
valueCollection = new ValueCollection<>();
keySet = new KeySet();
entrySet = new EntrySet<>();
}
/**
* Get the load factor beyond which the map will increase size.
*
* @return load factor for when the map should increase size.
*/
public float loadFactor()
{
return loadFactor;
}
/**
* Get the total capacity for the map to which the load factor with be a fraction of.
*
* @return the total capacity for the map.
*/
@DoNotSub public int capacity()
{
return values.length;
}
/**
* Get the actual threshold which when reached the map resize.
* This is a function of the current capacity and load factor.
*
* @return the threshold when the map will resize.
*/
@DoNotSub public int resizeThreshold()
{
return resizeThreshold;
}
/**
* {@inheritDoc}
*/
@DoNotSub public int size()
{
return size;
}
/**
* {@inheritDoc}
*/
public boolean isEmpty()
{
return 0 == size;
}
/**
* {@inheritDoc}
*/
public boolean containsKey(final Object key)
{
return containsKey(((Integer)key).intValue());
}
/**
* Overloaded version of {@link Map#containsKey(Object)} that takes a primitive int key.
*
* @param key for indexing the {@link Map}
* @return true if the key is found otherwise false.
*/
public boolean containsKey(final int key)
{
@DoNotSub final int mask = values.length - 1;
@DoNotSub int index = Hashing.hash(key, mask);
boolean found = false;
while (null != values[index])
{
if (key == keys[index])
{
found = true;
break;
}
index = ++index & mask;
}
return found;
}
/**
* {@inheritDoc}
*/
public boolean containsValue(final Object value)
{
boolean found = false;
if (null != value)
{
for (final Object v : values)
{
if (value.equals(v))
{
found = true;
break;
}
}
}
return found;
}
/**
* {@inheritDoc}
*/
public V get(final Object key)
{
return get(((Integer)key).intValue());
}
/**
* Overloaded version of {@link Map#get(Object)} that takes a primitive int key.
*
* @param key for indexing the {@link Map}
* @return the value if found otherwise null
*/
@SuppressWarnings("unchecked")
public V get(final int key)
{
@DoNotSub final int mask = values.length - 1;
@DoNotSub int index = Hashing.hash(key, mask);
Object value;
while (null != (value = values[index]))
{
if (key == keys[index])
{
break;
}
index = ++index & mask;
}
return (V)value;
}
/**
* Get a value for a given key, or if it does not exist then default the value via a
* {@link java.util.function.IntFunction} and put it in the map.
*
* Primitive specialized version of {@link java.util.Map#computeIfAbsent}.
*
* @param key to search on.
* @param mappingFunction to provide a value if the get returns null.
* @return the value if found otherwise the default.
*/
public V computeIfAbsent(final int key, final IntFunction<? extends V> mappingFunction)
{
V value = get(key);
if (value == null)
{
value = mappingFunction.apply(key);
if (value != null)
{
put(key, value);
}
}
return value;
}
/**
* {@inheritDoc}
*/
public V put(final Integer key, final V value)
{
return put(key.intValue(), value);
}
/**
* Overloaded version of {@link Map#put(Object, Object)} that takes a primitive int key.
*
* @param key for indexing the {@link Map}
* @param value to be inserted in the {@link Map}
* @return the previous value if found otherwise null
*/
@SuppressWarnings("unchecked")
public V put(final int key, final V value)
{
requireNonNull(value, "Value cannot be null");
V oldValue = null;
@DoNotSub final int mask = values.length - 1;
@DoNotSub int index = Hashing.hash(key, mask);
while (null != values[index])
{
if (key == keys[index])
{
oldValue = (V)values[index];
break;
}
index = ++index & mask;
}
if (null == oldValue)
{
++size;
keys[index] = key;
}
values[index] = value;
if (size > resizeThreshold)
{
increaseCapacity();
}
return oldValue;
}
/**
* {@inheritDoc}
*/
public V remove(final Object key)
{
return remove(((Integer)key).intValue());
}
/**
* Overloaded version of {@link Map#remove(Object)} that takes a primitive int key.
*
* @param key for indexing the {@link Map}
* @return the value if found otherwise null
*/
@SuppressWarnings("unchecked")
public V remove(final int key)
{
@DoNotSub final int mask = values.length - 1;
@DoNotSub int index = Hashing.hash(key, mask);
Object value;
while (null != (value = values[index]))
{
if (key == keys[index])
{
values[index] = null;
--size;
compactChain(index);
break;
}
index = ++index & mask;
}
return (V)value;
}
/**
* {@inheritDoc}
*/
public void clear()
{
size = 0;
Arrays.fill(values, null);
}
/**
* Compact the {@link Map} backing arrays by rehashing with a capacity just larger than current size
* and giving consideration to the load factor.
*/
public void compact()
{
@DoNotSub final int idealCapacity = (int)Math.round(size() * (1.0d / loadFactor));
rehash(BitUtil.findNextPositivePowerOfTwo(idealCapacity));
}
/**
* {@inheritDoc}
*/
public void putAll(final Map<? extends Integer, ? extends V> map)
{
for (final Entry<? extends Integer, ? extends V> entry : map.entrySet())
{
put(entry.getKey(), entry.getValue());
}
}
/**
* {@inheritDoc}
*/
public KeySet keySet()
{
return keySet;
}
/**
* {@inheritDoc}
*/
public Collection<V> values()
{
return valueCollection;
}
/**
* {@inheritDoc}
*/
public Set<Entry<Integer, V>> entrySet()
{
return entrySet;
}
/**
* {@inheritDoc}
*/
public String toString()
{
final StringBuilder sb = new StringBuilder();
sb.append('{');
for (@DoNotSub int i = 0, length = values.length; i < length; i++)
{
final Object value = values[i];
if (null != value)
{
sb.append(keys[i]);
sb.append('=');
sb.append(value);
sb.append(", ");
}
}
if (sb.length() > 1)
{
sb.setLength(sb.length() - 2);
}
sb.append('}');
return sb.toString();
}
/**
* {@inheritDoc}
*/
public boolean equals(final Object o)
{
if (this == o)
{
return true;
}
if (o == null || !(o instanceof Map))
{
return false;
}
final Map<?, ?> that = (Map<?, ?>)o;
if (size != that.size())
{
return false;
}
for (@DoNotSub int i = 0, length = values.length; i < length; i++)
{
final Object thisValue = values[i];
if (null != thisValue)
{
final Object thatValue = that.get(keys[i]);
if (null == thatValue || !thisValue.equals(thatValue))
{
return false;
}
}
}
return true;
}
/**
* {@inheritDoc}
*/
@DoNotSub public int hashCode()
{
@DoNotSub int result = 0;
for (@DoNotSub int i = 0, length = values.length; i < length; i++)
{
final Object value = values[i];
if (null != value)
{
result += (Integer.hashCode(keys[i]) ^ value.hashCode());
}
}
return result;
}
/**
* Primitive specialised version of {@link #replace(Object, Object)}
*
* @param key key with which the specified value is associated
* @param value value to be associated with the specified key
* @return the previous value associated with the specified key, or
* {@code null} if there was no mapping for the key.
*/
public V replace(final int key, final V value)
{
V curValue = get(key);
if (curValue != null)
{
curValue = put(key, value);
}
return curValue;
}
/**
* Primitive specialised version of {@link #replace(Object, Object, Object)}
*
* @param key key with which the specified value is associated
* @param oldValue value expected to be associated with the specified key
* @param newValue value to be associated with the specified key
* @return {@code true} if the value was replaced
*/
public boolean replace(final int key, final V oldValue, final V newValue)
{
final Object curValue = get(key);
if (curValue == null || !Objects.equals(curValue, oldValue))
{
return false;
}
put(key, newValue);
return true;
}
private void increaseCapacity()
{
@DoNotSub final int newCapacity = values.length << 1;
if (newCapacity < 0)
{
throw new IllegalStateException("Max capacity reached at size=" + size);
}
rehash(newCapacity);
}
private void rehash(@DoNotSub final int newCapacity)
{
@DoNotSub final int mask = newCapacity - 1;
/* @DoNotSub */ resizeThreshold = (int)(newCapacity * loadFactor);
final int[] tempKeys = new int[newCapacity];
final Object[] tempValues = new Object[newCapacity];
for (@DoNotSub int i = 0, size = values.length; i < size; i++)
{
final Object value = values[i];
if (null != value)
{
final int key = keys[i];
@DoNotSub int newHash = Hashing.hash(key, mask);
while (null != tempValues[newHash])
{
newHash = ++newHash & mask;
}
tempKeys[newHash] = key;
tempValues[newHash] = value;
}
}
keys = tempKeys;
values = tempValues;
}
@SuppressWarnings("FinalParameters")
private void compactChain(@DoNotSub int deleteIndex)
{
@DoNotSub final int mask = values.length - 1;
@DoNotSub int index = deleteIndex;
while (true)
{
index = ++index & mask;
if (null == values[index])
{
break;
}
@DoNotSub final int hash = Hashing.hash(keys[index], mask);
if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) ||
(hash <= deleteIndex && deleteIndex <= index))
{
keys[deleteIndex] = keys[index];
values[deleteIndex] = values[index];
values[index] = null;
deleteIndex = index;
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////
// Internal Sets and Collections
///////////////////////////////////////////////////////////////////////////////////////////////
public class KeySet extends AbstractSet<Integer>
{
private final KeyIterator iterator = new KeyIterator();
@DoNotSub public int size()
{
return Int2ObjectHashMap.this.size();
}
public boolean contains(final Object o)
{
return Int2ObjectHashMap.this.containsKey(o);
}
public boolean contains(final int key)
{
return Int2ObjectHashMap.this.containsKey(key);
}
public KeyIterator iterator()
{
iterator.reset();
return iterator;
}
public boolean remove(final Object o)
{
return null != Int2ObjectHashMap.this.remove(o);
}
public boolean remove(final int key)
{
return null != Int2ObjectHashMap.this.remove(key);
}
public void clear()
{
Int2ObjectHashMap.this.clear();
}
}
private class ValueCollection<V> extends AbstractCollection<V>
{
private final ValueIterator<V> iterator = new ValueIterator<V>();
@DoNotSub public int size()
{
return Int2ObjectHashMap.this.size();
}
public boolean contains(final Object o)
{
return Int2ObjectHashMap.this.containsValue(o);
}
public ValueIterator<V> iterator()
{
iterator.reset();
return iterator;
}
public void clear()
{
Int2ObjectHashMap.this.clear();
}
}
private class EntrySet<V> extends AbstractSet<Entry<Integer, V>>
{
private final EntryIterator<V> iterator = new EntryIterator<V>();
@DoNotSub public int size()
{
return Int2ObjectHashMap.this.size();
}
public Iterator<Entry<Integer, V>> iterator()
{
iterator.reset();
return iterator;
}
public void clear()
{
Int2ObjectHashMap.this.clear();
}
}
///////////////////////////////////////////////////////////////////////////////////////////////
// Iterators
///////////////////////////////////////////////////////////////////////////////////////////////
abstract class AbstractIterator<T> implements Iterator<T>
{
@DoNotSub private int posCounter;
@DoNotSub private int stopCounter;
@DoNotSub private int remaining;
private boolean isPositionValid = false;
protected int[] keys;
protected Object[] values;
protected AbstractIterator()
{
reset();
}
@DoNotSub protected int position()
{
return posCounter & (values.length - 1);
}
public boolean hasNext()
{
return remaining > 0;
}
protected void findNext()
{
@DoNotSub final int mask = values.length - 1;
isPositionValid = false;
for (@DoNotSub int i = posCounter - 1; i >= stopCounter; i--)
{
@DoNotSub final int index = i & mask;
if (null != values[index])
{
posCounter = i;
isPositionValid = true;
--remaining;
return;
}
}
throw new NoSuchElementException();
}
public abstract T next();
public void remove()
{
if (isPositionValid)
{
@DoNotSub final int position = position();
values[position] = null;
--size;
compactChain(position);
isPositionValid = false;
}
else
{
throw new IllegalStateException();
}
}
void reset()
{
remaining = Int2ObjectHashMap.this.size;
keys = Int2ObjectHashMap.this.keys;
values = Int2ObjectHashMap.this.values;
@DoNotSub final int capacity = values.length;
@DoNotSub int i = capacity;
if (null != values[capacity - 1])
{
for (i = 0; i < capacity; i++)
{
if (null == values[i])
{
break;
}
}
}
stopCounter = i;
posCounter = i + capacity;
isPositionValid = false;
}
}
public class ValueIterator<T> extends AbstractIterator<T>
{
@SuppressWarnings("unchecked")
public T next()
{
findNext();
return (T)values[position()];
}
}
public class KeyIterator extends AbstractIterator<Integer>
{
public Integer next()
{
return nextInt();
}
public int nextInt()
{
findNext();
return keys[position()];
}
}
@SuppressWarnings("unchecked")
public class EntryIterator<V>
extends AbstractIterator<Entry<Integer, V>>
implements Entry<Integer, V>
{
public Entry<Integer, V> next()
{
findNext();
return this;
}
public Integer getKey()
{
return keys[position()];
}
public V getValue()
{
return (V)values[position()];
}
public V setValue(final V value)
{
requireNonNull(value);
@DoNotSub final int pos = position();
final Object oldValue = values[pos];
values[pos] = value;
return (V)oldValue;
}
}
}