All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.argot.util.IntObjectHashMap Maven / Gradle / Ivy

package com.argot.util;

/*
 * Borrowed from the Netty project and adapted. io.netty.util.collection.IntObjectHashMap.
 *
 * Copyright 2014 The Netty Project
 *
 * The Netty Project 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.
 */

import java.util.Arrays;

/**
 * A hash map implementation of that uses open addressing for keys. To minimize the memory footprint, this class uses
 * open addressing rather than chaining. Collisions are resolved using linear probing. Deletions implement compaction,
 * so cost of remove can approach O(N) for full maps, which makes a small loadFactor recommended.
 *
 * @param  The value type stored in the map.
 */
public class IntObjectHashMap {

    /** Default initial capacity. Used if not specified in the constructor */
    private static final int DEFAULT_CAPACITY = 11;

    /** Default load factor. Used if not specified in the constructor */
    private static final float DEFAULT_LOAD_FACTOR = 0.5f;

    /**
     * Placeholder for null values, so we can use the actual null to mean available. (Better than using a placeholder
     * for available: less references for GC processing.)
     */
    private static final Object NULL_VALUE = new Object();

    /** The maximum number of elements allowed without allocating more space. */
    private int maxSize;

    /** The load factor for the map. Used to calculate {@link #maxSize}. */
    private final float loadFactor;

    private int[] keys;
    private V[] values;

    private int size;

    public IntObjectHashMap() {
        this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    public IntObjectHashMap(final int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public IntObjectHashMap(final int initialCapacity, final float loadFactor) {
        if (initialCapacity < 1) {
            throw new IllegalArgumentException("initialCapacity must be >= 1");
        }
        if (loadFactor <= 0.0f || loadFactor > 1.0f) {
            // Cannot exceed 1 because we can never store more than capacity elements;
            // using a bigger loadFactor would trigger rehashing before the desired load is reached.
            throw new IllegalArgumentException("loadFactor must be > 0 and <= 1");
        }

        this.loadFactor = loadFactor;

        // Adjust the initial capacity if necessary.
        final int capacity = adjustCapacity(initialCapacity);

        // Allocate the arrays.
        keys = new int[capacity];
        @SuppressWarnings("unchecked")
        final V[] temp = (V[]) new Object[capacity];
        values = temp;

        // Initialize the maximum size value.
        maxSize = calcMaxSize(capacity);
    }

    private static  T toExternal(final T value) {
        return value == NULL_VALUE ? null : value;
    }

    @SuppressWarnings("unchecked")
    private static  T toInternal(final T value) {
        return value == null ? (T) NULL_VALUE : value;
    }

    public V get(final int key) {
        final int index = indexOf(key);
        return index == -1 ? null : toExternal(values[index]);
    }

    public V put(final int key, final V value) {
        final int startIndex = hashIndex(key);
        int index = startIndex;

        for (;;) {
            if (values[index] == null) {
                // Found empty slot, use it.
                keys[index] = key;
                values[index] = toInternal(value);
                growSize();
                return null;
            }
            if (keys[index] == key) {
                // Found existing entry with this key, just replace the value.
                final V previousValue = values[index];
                values[index] = toInternal(value);
                return toExternal(previousValue);
            }

            // Conflict, keep probing ...
            if ((index = probeNext(index)) == startIndex) {
                // Can only happen if the map was full at MAX_ARRAY_SIZE and couldn't grow.
                throw new IllegalStateException("Unable to insert");
            }
        }
    }

    private int probeNext(final int index) {
        return index == values.length - 1 ? 0 : index + 1;
    }

    public V remove(final int key) {
        final int index = indexOf(key);
        if (index == -1) {
            return null;
        }

        final V prev = values[index];
        removeAt(index);
        return toExternal(prev);
    }

    public int size() {
        return size;
    }

    public int length() {
        return keys.length;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public void clear() {
        Arrays.fill(keys, 0);
        Arrays.fill(values, null);
        size = 0;
    }

    public boolean containsKey(final int key) {
        return indexOf(key) >= 0;
    }

    public boolean containsValue(final V value) {
        final V v1 = toInternal(value);
        for (final V v2 : values) {
            // The map supports null values; this will be matched as NULL_VALUE.equals(NULL_VALUE).
            if (v2 != null && v2.equals(v1)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public int hashCode() {
        // Hashcode is based on all non-zero, valid keys. We have to scan the whole keys
        // array, which may have different lengths for two maps of same size(), so the
        // capacity cannot be used as input for hashing but the size can.
        int hash = size;
        for (final int key : keys) {
            // 0 can be a valid key or unused slot, but won't impact the hashcode in either case.
            // This way we can use a cheap loop without conditionals, or hard-to-unroll operations,
            // or the devastatingly bad memory locality of visiting value objects.
            // Also, it's important to use a hash function that does not depend on the ordering
            // of terms, only their values; since the map is an unordered collection and
            // entries can end up in different positions in different maps that have the same
            // elements, but with different history of puts/removes, due to conflicts.
            hash ^= key;
        }
        return hash;
    }

    /**
     * Locates the index for the given key. This method probes using double hashing.
     *
     * @param key the key for an entry in the map.
     * @return the index where the key was found, or {@code -1} if no entry is found for that key.
     */
    private int indexOf(final int key) {
        final int startIndex = hashIndex(key);
        int index = startIndex;

        for (;;) {
            if (values[index] == null) {
                // It's available, so no chance that this value exists anywhere in the map.
                return -1;
            }
            if (key == keys[index]) {
                return index;
            }

            // Conflict, keep probing ...
            if ((index = probeNext(index)) == startIndex) {
                return -1;
            }
        }
    }

    /**
     * Returns the hashed index for the given key.
     */
    private int hashIndex(final int key) {
        // Allowing for negative keys by adding the length after the first mod operation.
        return (key % keys.length + keys.length) % keys.length;
    }

    /**
     * Grows the map size after an insertion. If necessary, performs a rehash of the map.
     */
    private void growSize() {
        size++;

        if (size > maxSize) {
            // Need to grow the arrays. We take care to detect integer overflow,
            // also limit array size to ArrayList.MAX_ARRAY_SIZE.
            rehash(adjustCapacity((int) Math.min(keys.length * 2.0, Integer.MAX_VALUE - 8)));
        } else if (size == keys.length) {
            // Open addressing requires that we have at least 1 slot available. Need to refresh
            // the arrays to clear any removed elements.
            rehash(keys.length);
        }
    }

    /**
     * Adjusts the given capacity value to ensure that it's odd. Even capacities can break probing.
     */
    private static int adjustCapacity(final int capacity) {
        return capacity | 1;
    }

    /**
     * Removes entry at the given index position. Also performs opportunistic, incremental rehashing if necessary to not
     * break conflict chains.
     *
     * @param index the index position of the element to remove.
     */
    private void removeAt(final int index) {
        --size;
        // Clearing the key is not strictly necessary (for GC like in a regular collection),
        // but recommended for security. The memory location is still fresh in the cache anyway.
        keys[index] = 0;
        values[index] = null;

        // In the interval from index to the next available entry, the arrays may have entries
        // that are displaced from their base position due to prior conflicts. Iterate these
        // entries and move them back if possible, optimizing future lookups.
        // Knuth Section 6.4 Algorithm R, also used by the JDK's IdentityHashMap.

        int nextFree = index;
        for (int i = probeNext(index); values[i] != null; i = probeNext(i)) {
            final int bucket = hashIndex(keys[i]);
            if (i < bucket && (bucket <= nextFree || nextFree <= i) || bucket <= nextFree && nextFree <= i) {
                // Move the displaced entry "back" to the first available position.
                keys[nextFree] = keys[i];
                values[nextFree] = values[i];
                // Put the first entry after the displaced entry
                keys[i] = 0;
                values[i] = null;
                nextFree = i;
            }
        }
    }

    /**
     * Calculates the maximum size allowed before rehashing.
     */
    private int calcMaxSize(final int capacity) {
        // Clip the upper bound so that there will always be at least one available slot.
        final int upperBound = capacity - 1;
        return Math.min(upperBound, (int) (capacity * loadFactor));
    }

    /**
     * Rehashes the map for the given capacity.
     *
     * @param newCapacity the new capacity for the map.
     */
    private void rehash(final int newCapacity) {
        final int[] oldKeys = keys;
        final V[] oldVals = values;

        keys = new int[newCapacity];
        @SuppressWarnings("unchecked")
        final V[] temp = (V[]) new Object[newCapacity];
        values = temp;

        maxSize = calcMaxSize(newCapacity);

        // Insert to the new arrays.
        for (int i = 0; i < oldVals.length; ++i) {
            final V oldVal = oldVals[i];
            if (oldVal != null) {
                // Inlined put(), but much simpler: we don't need to worry about
                // duplicated keys, growing/rehashing, or failing to insert.
                final int oldKey = oldKeys[i];
                int index = hashIndex(oldKey);

                for (;;) {
                    if (values[index] == null) {
                        keys[index] = oldKey;
                        values[index] = toInternal(oldVal);
                        break;
                    }

                    // Conflict, keep probing. Can wrap around, but never reaches startIndex again.
                    index = probeNext(index);
                }
            }
        }
    }

    @Override
    public String toString() {
        if (size == 0) {
            return "{}";
        }
        final StringBuilder sb = new StringBuilder(4 * size);
        for (int i = 0; i < values.length; ++i) {
            final V value = values[i];
            if (value != null) {
                sb.append(sb.length() == 0 ? "{" : ", ");
                sb.append(keyToString(keys[i])).append('=').append(value == this ? "(this Map)" : value);
            }
        }
        return sb.append('}').toString();
    }

    /**
     * Helper method called by {@link #toString()} in order to convert a single map key into a string.
     */
    protected String keyToString(final int key) {
        return Integer.toString(key);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy