io.sirix.utils.IntToObjectMap Maven / Gradle / Ivy
package io.sirix.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;
/*
* 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.
*/
/**
* An Array-based hashtable which maps primitive int to Objects of generic type
* T.
* The hashtable is constracted with a given capacity, or 16 as a default. In
* case there's not enough room for new pairs, the hashtable grows.
* Capacity is adjusted to a power of 2, and there are 2 * capacity entries for
* the hash.
*
* The pre allocated arrays (for keys, values) are at length of capacity + 1,
* when index 0 is used as 'Ground' or 'NULL'.
*
* The arrays are allocated ahead of hash operations, and form an 'empty space'
* list, to which the key,value pair is allocated.
*
* @lucene.experimental
*/
public class IntToObjectMap implements Iterable {
/**
* Logger for this class.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(IntToObjectMap.class);
/**
* Implements an IntIterator which iterates over all the allocated indexes.
*/
private final class IndexIterator implements IntIterator {
/**
* The last used baseHashIndex. Needed for "jumping" from one hash entry
* to another.
*/
private int baseHashIndex = 0;
/**
* The next not-yet-visited index.
*/
private int index = 0;
/**
* Index of the last visited pair. Used in {@link #remove()}.
*/
private int lastIndex = 0;
/**
* Create the Iterator, make index
point to the "first"
* index which is not empty. If such does not exist (eg. the map is
* empty) it would be zero.
*/
public IndexIterator() {
for (baseHashIndex = 0; baseHashIndex < baseHash.length; ++baseHashIndex) {
index = baseHash[baseHashIndex];
if (index != 0) {
break;
}
}
}
public boolean hasNext() {
return (index != 0);
}
public int next() {
// Save the last index visited
lastIndex = index;
// next the index
index = next[index];
// if the next index points to the 'Ground' it means we're done with
// the current hash entry and we need to jump to the next one. This
// is done until all the hash entries had been visited.
while (index == 0 && ++baseHashIndex < baseHash.length) {
index = baseHash[baseHashIndex];
}
return lastIndex;
}
public void remove() {
IntToObjectMap.this.remove(keys[lastIndex]);
}
}
/**
* Implements an IntIterator, used for iteration over the map's keys.
*/
private final class KeyIterator implements IntIterator {
private IntIterator iterator = new IndexIterator();
KeyIterator() {
}
public boolean hasNext() {
return iterator.hasNext();
}
public int next() {
return keys[iterator.next()];
}
public void remove() {
iterator.remove();
}
}
/**
* Implements an Iterator of a generic type T used for iteration over the
* map's values.
*/
private final class ValueIterator implements Iterator {
private IntIterator iterator = new IndexIterator();
ValueIterator() {
}
public boolean hasNext() {
return iterator.hasNext();
}
@SuppressWarnings("unchecked")
public T next() {
if (!iterator.hasNext()) {
throw new NoSuchElementException();
}
return (T) values[iterator.next()];
}
@Override
public void remove() {
iterator.remove();
}
}
/**
* Default capacity - in case no capacity was specified in the constructor
*/
private static int defaultCapacity = 16;
/**
* Holds the base hash entries. if the capacity is 2^N, thn the base hash
* holds 2^(N+1). It can hold
*/
int[] baseHash;
/**
* The current capacity of the map. Always 2^N and never less than 16. We
* never use the zero index. It is needed to improve performance and is also
* used as "ground".
*/
private int capacity;
/**
* All objects are being allocated at map creation. Those objects are "free"
* or empty. Whenever a new pair comes along, a pair is being "allocated" or
* taken from the free-linked list. as this is just a free list.
*/
private int firstEmpty;
/**
* hashFactor is always (2^(N+1)) - 1. Used for faster hashing.
*/
private int hashFactor;
/**
* This array holds the unique keys
*/
int[] keys;
/**
* In case of collisions, we implement a double linked list of the colliding
* hash's with the following next[] and prev[]. Those are also used to store
* the "empty" list.
*/
int[] next;
private int prev;
/**
* Number of currently objects in the map.
*/
private int size;
/**
* This array holds the values
*/
Object[] values;
/**
* Constructs a map with default capacity.
*/
public IntToObjectMap() {
this(defaultCapacity);
}
/**
* Constructs a map with given capacity. Capacity is adjusted to a native
* power of 2, with minimum of 16.
*
* @param capacity
* minimum capacity for the map.
*/
public IntToObjectMap(int capacity) {
this.capacity = 16;
// Minimum capacity is 16..
while (this.capacity < capacity) {
// Multiply by 2 as long as we're still under the requested capacity
this.capacity <<= 1;
}
// As mentioned, we use the first index (0) as 'Ground', so we need the
// length of the arrays to be one more than the capacity
int arrayLength = this.capacity + 1;
this.values = new Object[arrayLength];
this.keys = new int[arrayLength];
this.next = new int[arrayLength];
// Hash entries are twice as big as the capacity.
int baseHashSize = this.capacity << 1;
this.baseHash = new int[baseHashSize];
// The has factor is 2^M - 1 which is used as an "AND" hashing operator.
// {@link #calcBaseHash()}
this.hashFactor = baseHashSize - 1;
this.size = 0;
clear();
}
/**
* Adds a pair to the map. Takes the first empty position from the
* empty-linked-list's head.
*
* New pairs are always inserted to baseHash, and are followed by the old
* colliding pair.
*
* @param key
* integer which maps the given Object
* @param e
* element which is being mapped using the given key
*/
private void prvtPut(int key, T e) {
// Hash entry to which the new pair would be inserted
int hashIndex = calcBaseHashIndex(key);
// 'Allocating' a pair from the "Empty" list.
int objectIndex = firstEmpty;
// Setting data
firstEmpty = next[firstEmpty];
values[objectIndex] = e;
keys[objectIndex] = key;
// Inserting the new pair as the first node in the specific hash entry
next[objectIndex] = baseHash[hashIndex];
baseHash[hashIndex] = objectIndex;
// Announcing a new pair was added!
++size;
}
/**
* Calculating the baseHash index using the internal hashFactor
.
*
* @param key
*/
protected int calcBaseHashIndex(int key) {
return key & hashFactor;
}
/**
* Empties the map. Generates the "Empty" space list for later allocation.
*/
public void clear() {
// Clears the hash entries
Arrays.fill(this.baseHash, 0);
// Set size to zero
size = 0;
// Mark all array entries as empty. This is done with
// firstEmpty
pointing to the first valid index (1 as 0 is
// used as 'Ground').
firstEmpty = 1;
// And setting all the next[i]
to point at
// i+1
.
for (int i = 1; i < this.capacity; ++i) {
next[i] = i + 1;
}
// Surly, the last one should point to the 'Ground'.
next[this.capacity] = 0;
}
/**
* Checks if a given key exists in the map.
*
* @param key
* that is checked against the map data.
* @return true if the key exists in the map. false otherwise.
*/
public boolean containsKey(int key) {
return find(key) != 0;
}
/**
* Checks if the given object exists in the map.
* This method iterates over the collection, trying to find an equal object.
*
* @param o
* object that is checked against the map data.
* @return true if the object exists in the map (in .equals() meaning).
* false otherwise.
*/
public boolean containsValue(Object o) {
for (T object : this) {
if (object.equals(o)) {
return true;
}
}
return false;
}
/**
* Find the actual index of a given key.
*
* @param key
* @return index of the key. zero if the key wasn't found.
*/
protected int find(int key) {
// Calculate the hash entry.
int baseHashIndex = calcBaseHashIndex(key);
// Start from the hash entry.
int localIndex = baseHash[baseHashIndex];
// while the index does not point to the 'Ground'
while (localIndex != 0) {
// returns the index found in case of a matching key.
if (keys[localIndex] == key) {
return localIndex;
}
// next the local index
localIndex = next[localIndex];
}
// If we got this far, it could only mean we did not find the key we
// were asked for. return 'Ground' index.
return 0;
}
/**
* Find the actual index of a given key with it's baseHashIndex.
* Some methods use the baseHashIndex. If those call find() there's
* no need to re-calculate that hash.
*
* @param key
* @param baseHashIndex
* @return the index of the given key, or 0 as 'Ground' if the key wasn't
* found.
*/
private int findForRemove(int key, int baseHashIndex) {
// Start from the hash entry.
this.prev = 0;
int index = baseHash[baseHashIndex];
// while the index does not point to the 'Ground'
while (index != 0) {
// returns the index found in case of a matching key.
if (keys[index] == key) {
return index;
}
// next the local index
prev = index;
index = next[index];
}
// If we got this far, it could only mean we did not find the key we
// were asked for. return 'Ground' index.
this.prev = 0;
return 0;
}
/**
* Returns the object mapped with the given key.
*
* @param key
* int who's mapped object we're interested in.
* @return an object mapped by the given key. null if the key wasn't found.
*/
@SuppressWarnings("unchecked")
public T get(int key) {
return (T) values[find(key)];
}
/**
* Grows the map. Allocates a new map of double the capacity, and
* fast-insert the old key-value pairs.
*/
@SuppressWarnings("unchecked")
protected void grow() {
IntToObjectMap that = new IntToObjectMap<>(this.capacity * 2);
// Iterates fast over the collection. Any valid pair is put into the new
// map without checking for duplicates or if there's enough space for
// it.
for (IndexIterator iterator = new IndexIterator(); iterator.hasNext(); ) {
int index = iterator.next();
that.prvtPut(this.keys[index], (T) this.values[index]);
}
// Copy that's data into this.
this.capacity = that.capacity;
this.size = that.size;
this.firstEmpty = that.firstEmpty;
this.values = that.values;
this.keys = that.keys;
this.next = that.next;
this.baseHash = that.baseHash;
this.hashFactor = that.hashFactor;
}
/**
*
* @return true if the map is empty. false otherwise.
*/
public boolean isEmpty() {
return size == 0;
}
/**
* Returns a new iterator for the mapped objects.
*/
public Iterator iterator() {
return new ValueIterator();
}
/** Returns an iterator on the map keys. */
public IntIterator keyIterator() {
return new KeyIterator();
}
/**
* Prints the baseHash array, used for debug purposes.
*/
@SuppressWarnings("unused")
private void printBaseHash() {
for (int i = 0; i < this.baseHash.length; i++) {
LOGGER.info("{}.\t{}", i, baseHash[i]);
}
}
/**
* Inserts the <key,value> pair into the map. If the key already exists,
* this method updates the mapped value to the given one, returning the old
* mapped value.
*
* @return the old mapped value, or null if the key didn't exist.
*/
@SuppressWarnings("unchecked")
public T put(int key, T e) {
// Does key exists?
int index = find(key);
// Yes!
if (index != 0) {
// Set new data and exit.
T old = (T) values[index];
values[index] = e;
return old;
}
// Is there enough room for a new pair?
if (size == capacity) {
// No? Then grow up!
grow();
}
// Now that everything is set, the pair can be just put inside with no
// worries.
prvtPut(key, e);
return null;
}
/**
* Removes a <key,value> pair from the map and returns the mapped value,
* or null if the none existed.
*
* @param key used to find the value to remove
* @return the removed value or null if none existed.
*/
@SuppressWarnings("unchecked")
public T remove(int key) {
int baseHashIndex = calcBaseHashIndex(key);
int index = findForRemove(key, baseHashIndex);
if (index != 0) {
// If it is the first in the collision list, we should promote its
// next colliding element.
if (prev == 0) {
baseHash[baseHashIndex] = next[index];
}
next[prev] = next[index];
next[index] = firstEmpty;
firstEmpty = index;
--size;
return (T) values[index];
}
return null;
}
/**
* @return number of pairs currently in the map
*/
public int size() {
return this.size;
}
/**
* Translates the mapped pairs' values into an array of Objects
*
* @return an object array of all the values currently in the map.
*/
public Object[] toArray() {
int j = -1;
Object[] array = new Object[size];
// Iterates over the values, adding them to the array.
for (T t : this) {
array[++j] = t;
}
return array;
}
/**
* Translates the mapped pairs' values into an array of T
*
* @param a
* the array into which the elements of the list are to be
* stored, if it is big enough; otherwise, use whatever space we
* have, setting the one after the true data as null.
*
* @return an array containing the elements of the list
*
*/
public T[] toArray(T[] a) {
int j = 0;
// Iterates over the values, adding them to the array.
for (Iterator iterator = iterator(); j < a.length && iterator.hasNext(); ++j) {
a[j] = iterator.next();
}
if (j < a.length) {
a[j] = null;
}
return a;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append('{');
IntIterator keyIterator = keyIterator();
while (keyIterator.hasNext()) {
int key = keyIterator.next();
sb.append(key);
sb.append('=');
sb.append(get(key));
if (keyIterator.hasNext()) {
sb.append(',');
sb.append(' ');
}
}
sb.append('}');
return sb.toString();
}
@Override
public int hashCode() {
return getClass().hashCode() ^ size();
}
@SuppressWarnings("unchecked")
@Override
public boolean equals(Object o) {
if (!(o instanceof IntToObjectMap)) {
return false;
}
IntToObjectMap that = (IntToObjectMap) o;
if (that.size() != this.size()) {
return false;
}
IntIterator it = keyIterator();
while (it.hasNext()) {
int key = it.next();
if (!that.containsKey(key)) {
return false;
}
T v1 = this.get(key);
T v2 = that.get(key);
if ((v1 == null && v2 != null) || (v1 != null && v2 == null) || (v1!= null && !v1.equals(v2))) {
return false;
}
}
return true;
}
/**
* 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.
*/
/**
* Iterator interface for primitive int iteration. *
*
* @lucene.experimental
*/
private interface IntIterator {
boolean hasNext();
int next();
void remove();
}
}