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

com.fluxtion.agrona.collections.BiInt2ObjectMap Maven / Gradle / Ivy

/*
 * Copyright 2014-2024 Real Logic Limited.
 *
 * 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
 *
 * https://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 com.fluxtion.agrona.collections;

import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;

import static java.util.Objects.requireNonNull;
import static com.fluxtion.agrona.BitUtil.findNextPositivePowerOfTwo;
import static com.fluxtion.agrona.collections.CollectionUtil.validateLoadFactor;
import static com.fluxtion.agrona.collections.Hashing.compoundKey;

/**
 * Map that takes two part int key and associates with an object.
 *
 * @param  type of the object stored in the map.
 */
public class BiInt2ObjectMap
{
    /**
     * Handler for a map entry
     *
     * @param  type of the value
     */
    public interface EntryConsumer
    {
        /**
         * A map entry
         *
         * @param keyPartA for the key
         * @param keyPartB for the key
         * @param value    for the entry
         */
        void accept(int keyPartA, int keyPartB, V value);
    }

    /**
     * Creates a new value based upon keys.
     *
     * @param  type of the value.
     */
    public interface EntryFunction
    {
        /**
         * A map entry.
         *
         * @param keyPartA for the key
         * @param keyPartB for the key
         * @return value for the entry
         */
        V apply(int keyPartA, int keyPartB);
    }

    /**
     * Creates a new value based upon keys.
     *
     * @param  type of the value.
     */
    public interface EntryRemap
    {
        /**
         * A map entry.
         *
         * @param keyPartA for the key
         * @param keyPartB for the key
         * @param oldValue to be remapped
         * @return value for the entry
         */
        V1 apply(int keyPartA, int keyPartB, V oldValue);
    }

    private static final int MIN_CAPACITY = 8;

    private final float loadFactor;
    private int resizeThreshold;
    private int size;

    private long[] keys;
    private Object[] values;

    /**
     * Construct an empty map.
     */
    public BiInt2ObjectMap()
    {
        this(MIN_CAPACITY, Hashing.DEFAULT_LOAD_FACTOR);
    }

    /**
     * Construct a map that sets it initial capacity and load factor.
     *
     * @param initialCapacity for the underlying hash map
     * @param loadFactor      for the underlying hash map
     */
    public BiInt2ObjectMap(final int initialCapacity, final float loadFactor)
    {
        validateLoadFactor(loadFactor);

        this.loadFactor = loadFactor;
        final int capacity = findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, initialCapacity));
        resizeThreshold = (int)(capacity * loadFactor);

        keys = new long[capacity];
        values = new Object[capacity];
    }

    /**
     * Get the total capacity for the map to which the load factor with be a fraction of.
     *
     * @return the total capacity for the map.
     */
    public int capacity()
    {
        return values.length;
    }

    /**
     * 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 actual threshold which when reached the map will resize.
     * This is a function of the current capacity and load factor.
     *
     * @return the threshold when the map will resize.
     */
    public int resizeThreshold()
    {
        return resizeThreshold;
    }

    /**
     * Clear out the map of all entries.
     *
     * @see Map#clear()
     */
    public void clear()
    {
        if (size > 0)
        {
            Arrays.fill(values, null);
            size = 0;
        }
    }

    /**
     * Compact the backing arrays by rehashing with a capacity just larger than current size
     * and giving consideration to the load factor.
     */
    public void compact()
    {
        final int idealCapacity = (int)Math.round(size() * (1.0 / loadFactor));
        rehash(findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, idealCapacity)));
    }

    /**
     * Put a value into the map.
     *
     * @param keyPartA for the key
     * @param keyPartB for the key
     * @param value    to put into the map
     * @return the previous value if found otherwise null
     * @see Map#put(Object, Object)
     */
    @SuppressWarnings("unchecked")
    public V put(final int keyPartA, final int keyPartB, final V value)
    {
        final V val = (V)mapNullValue(value);
        final long key = compoundKey(keyPartA, keyPartB);
        requireNonNull(val, "value cannot be null");

        final long[] keys = this.keys;
        final Object[] values = this.values;
        final int mask = values.length - 1;
        int index = Hashing.hash(key, mask);

        Object oldValue;
        while (null != (oldValue = values[index]))
        {
            if (key == keys[index])
            {
                break;
            }

            index = ++index & mask;
        }

        if (null == oldValue)
        {
            ++size;
            keys[index] = key;
        }

        values[index] = val;

        if (size > resizeThreshold)
        {
            increaseCapacity();
        }

        return unmapNullValue(oldValue);
    }

    /**
     * Interceptor for masking null values.
     *
     * @param value value to mask.
     * @return masked value.
     */
    protected Object mapNullValue(final Object value)
    {
        return value;
    }

    /**
     * Interceptor for unmasking null values.
     *
     * @param value value to unmask.
     * @return unmasked value.
     */
    @SuppressWarnings("unchecked")
    protected V unmapNullValue(final Object value)
    {
        return (V)value;
    }

    @SuppressWarnings("unchecked")
    private V getMapping(final int keyPartA, final int keyPartB)
    {
        final long key = compoundKey(keyPartA, keyPartB);
        final long[] keys = this.keys;
        final Object[] values = this.values;
        final int mask = values.length - 1;
        int index = Hashing.hash(key, mask);

        Object value;
        while (null != (value = values[index]))
        {
            if (key == keys[index])
            {
                break;
            }

            index = ++index & mask;
        }

        return (V)value;
    }

    /**
     * Retrieve a value from the map.
     *
     * @param keyPartA for the key
     * @param keyPartB for the key
     * @return value matching the key if found or null if not found.
     * @see Map#get(Object)
     */
    public V get(final int keyPartA, final int keyPartB)
    {
        return unmapNullValue(getMapping(keyPartA, keyPartB));
    }

    /**
     * Retrieve a value from the map or defaultValue if this map contains not mapping for the key.
     *
     * @param keyPartA for the key
     * @param keyPartB for the key
     * @param defaultValue the default mapping of the key
     * @return value matching the key if found or defaultValue if not found.
     * @see java.util.Map#getOrDefault(Object, Object)
     */
    public V getOrDefault(final int keyPartA, final int keyPartB, final V defaultValue)
    {
        final V val = getMapping(keyPartA, keyPartB);
        return unmapNullValue(null != val ? val : defaultValue);
    }

    /**
     * Returns true if this map contains a mapping for the specified key.
     *
     * @param keyPartA for the key
     * @param keyPartB for the key
     * @return true if this map contains a mapping for the specified key
     * @see java.util.Map#containsKey(Object)
     */
    public boolean containsKey(final int keyPartA, final int keyPartB)
    {
        final long key = compoundKey(keyPartA, keyPartB);
        final long[] keys = this.keys;
        final Object[] values = this.values;
        final int mask = values.length - 1;
        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;
    }

    /**
     * Remove a value from the map and return the value.
     *
     * @param keyPartA for the key
     * @param keyPartB for the key
     * @return the previous value if found otherwise null
     * @see Map#remove(Object)
     */
    @SuppressWarnings("unchecked")
    public V remove(final int keyPartA, final int keyPartB)
    {
        final long key = compoundKey(keyPartA, keyPartB);
        final long[] keys = this.keys;
        final Object[] values = this.values;
        final int mask = values.length - 1;
        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;
    }

    /**
     * If the specified key is not already associated with a value (or is mapped
     * to {@code null}), attempts to compute its value using the given mapping
     * function and enters it into this map unless {@code null}.
     *
     * @param keyPartA        for the key
     * @param keyPartB        for the key
     * @param mappingFunction creates values based upon keys if the key pair is missing
     * @return the newly created or stored value.
     * @see Map#computeIfAbsent(Object, Function)
     */
    public V computeIfAbsent(final int keyPartA, final int keyPartB, final EntryFunction mappingFunction)
    {
        final long key = compoundKey(keyPartA, keyPartB);
        requireNonNull(mappingFunction);
        final long[] keys = this.keys;
        final Object[] values = this.values;
        final int mask = values.length - 1;
        int index = Hashing.hash(key, mask);

        Object mappedValue;
        while (null != (mappedValue = values[index]))
        {
            if (key == keys[index])
            {
                break;
            }

            index = ++index & mask;
        }

        V value = unmapNullValue(mappedValue);

        if (null == value && (value = mappingFunction.apply(keyPartA, keyPartB)) != null)
        {
            values[index] = value;
            if (null == mappedValue)
            {
                keys[index] = key;
                if (++size > resizeThreshold)
                {
                    increaseCapacity();
                }
            }
        }

        return value;
    }

    /**
     * If the value for the specified key is present and non-null, attempts to compute a new mapping given the key and
     * its current mapped value.
     * 

* If the function returns null, the mapping is removed. If the function itself throws an (unchecked) exception, * the exception is rethrown, and the current mapping is left unchanged. * * @param keyPartA for the key * @param keyPartB for the key * @param remappingFunction the function to compute a value * @return the new value associated with the specified key, or null if none * @see Map#computeIfPresent(Object, BiFunction) */ public V computeIfPresent( final int keyPartA, final int keyPartB, final EntryRemap remappingFunction) { final long key = compoundKey(keyPartA, keyPartB); requireNonNull(remappingFunction); final long[] keys = this.keys; final Object[] values = this.values; final int mask = values.length - 1; int index = Hashing.hash(key, mask); Object mappedValue; while (null != (mappedValue = values[index])) { if (key == keys[index]) { break; } index = ++index & mask; } V value = unmapNullValue(mappedValue); if (null != value) { value = remappingFunction.apply(keyPartA, keyPartB, value); values[index] = value; if (null == value) { --size; compactChain(index); } } return value; } /** * Attempts to compute a mapping for the specified key and its current mapped value (or null if there is no current * mapping). * * @param keyPartA for the key * @param keyPartB for the key * @param remappingFunction the function to compute a value * @return the new value associated with the specified key, or null if none * @see Map#compute(Object, BiFunction) */ public V compute(final int keyPartA, final int keyPartB, final EntryRemap remappingFunction) { final long key = compoundKey(keyPartA, keyPartB); requireNonNull(remappingFunction); final long[] keys = this.keys; final Object[] values = this.values; final int mask = values.length - 1; int index = Hashing.hash(key, mask); Object mappedvalue; while (null != (mappedvalue = values[index])) { if (key == keys[index]) { break; } index = ++index & mask; } final V newValue = remappingFunction.apply(keyPartA, keyPartB, unmapNullValue(mappedvalue)); if (null != newValue) { values[index] = newValue; if (null == mappedvalue) { keys[index] = key; if (++size > resizeThreshold) { increaseCapacity(); } } } else if (null != mappedvalue) { values[index] = null; size--; compactChain(index); } return newValue; } /** * If the specified key is not already associated with a value or is associated with null, associates it with the * given non-null value. Otherwise, replaces the associated value with the results of the given remapping function, * or removes if the result is null. * * @param keyPartA for the key * @param keyPartB for the key * @param value the non-null value to be merged with the existing value associated with the key or, if * no existing value or a null value is associated with the key, to be associated with the * key * @param remappingFunction the function to recompute a value if present * @return the new value associated with the specified key, or null if no value is associated with the key * @see Map#merge(Object, Object, BiFunction) */ public V merge( final int keyPartA, final int keyPartB, final V value, final BiFunction remappingFunction) { final long key = compoundKey(keyPartA, keyPartB); requireNonNull(value); requireNonNull(remappingFunction); final long[] keys = this.keys; final Object[] values = this.values; final int mask = values.length - 1; int index = Hashing.hash(key, mask); Object mappedvalue; while (null != (mappedvalue = values[index])) { if (key == keys[index]) { break; } index = ++index & mask; } final V oldValue = unmapNullValue(mappedvalue); final V newValue = null == oldValue ? value : remappingFunction.apply(oldValue, value); if (null != newValue) { values[index] = newValue; if (null == mappedvalue) { keys[index] = key; if (++size > resizeThreshold) { increaseCapacity(); } } } else if (null != mappedvalue) { values[index] = null; size--; compactChain(index); } return newValue; } /** * Iterate over the contents of the map * * @param consumer to apply to each value in the map */ @SuppressWarnings("unchecked") public void forEach(final Consumer consumer) { int remaining = this.size; final Object[] values = this.values; for (int i = 0, length = values.length; remaining > 0 && i < length; i++) { final Object value = values[i]; if (null != value) { consumer.accept((V)value); --remaining; } } } /** * Iterate over the contents of the map * * @param consumer to apply to each value in the map */ public void forEach(final EntryConsumer consumer) { int remaining = this.size; final long[] keys = this.keys; final Object[] values = this.values; for (int i = 0, length = values.length; remaining > 0 && i < length; i++) { final Object value = values[i]; if (null != value) { final long compoundKey = keys[i]; final int keyPartA = (int)(compoundKey >>> 32); final int keyPartB = (int)(compoundKey & 0xFFFF_FFFFL); consumer.accept(keyPartA, keyPartB, unmapNullValue(value)); --remaining; } } } /** * Replaces the entry for the specified key only if currently mapped to the specified value. * * @param keyPartA for the key * @param keyPartB for the key * @param value value to be associated with the specified key * @return the previous value associated with the specified key, or null if there was no mapping for the key. * (A null return can also indicate that the map previously associated null with the key, if the implementation * supports null values.) * @see Map#replace(Object, Object) */ @SuppressWarnings("unchecked") public V replace(final int keyPartA, final int keyPartB, final V value) { final long key = compoundKey(keyPartA, keyPartB); final V val = (V)mapNullValue(value); requireNonNull(val, "value cannot be null"); final long[] keys = this.keys; final Object[] values = this.values; final int mask = values.length - 1; int index = Hashing.hash(key, mask); Object oldValue; while (null != (oldValue = values[index])) { if (key == keys[index]) { values[index] = val; break; } index = ++index & mask; } return unmapNullValue(oldValue); } /** * Replaces the entry for the specified key only if currently mapped to the specified value. * * @param keyPartA for the key * @param keyPartB for the key * @param oldValue value expected to be associated with the specified key * @param newValue to be associated with the specified key * @return true if the value was replaced */ @SuppressWarnings("unchecked") public boolean replace(final int keyPartA, final int keyPartB, final V oldValue, final V newValue) { final long key = compoundKey(keyPartA, keyPartB); final V val = (V)mapNullValue(newValue); requireNonNull(val, "value cannot be null"); final long[] keys = this.keys; final Object[] values = this.values; final int mask = values.length - 1; int index = Hashing.hash(key, mask); Object mappedValue; while (null != (mappedValue = values[index])) { if (key == keys[index]) { if (Objects.equals(unmapNullValue(mappedValue), oldValue)) { values[index] = val; return true; } break; } index = ++index & mask; } return false; } /** * If the specified key is not already associated with a value (or is mapped to null) associates it with the given * value and returns null, else returns the current value. * * @param keyPartA for the key * @param keyPartB for the key * @param value to put into the map * @return the previous value if found otherwise null */ @SuppressWarnings("unchecked") public V putIfAbsent(final int keyPartA, final int keyPartB, final V value) { final long key = compoundKey(keyPartA, keyPartB); final V val = (V)mapNullValue(value); requireNonNull(val, "value cannot be null"); final long[] keys = this.keys; final Object[] values = this.values; final int mask = values.length - 1; int index = Hashing.hash(key, mask); Object mappedValue; while (null != (mappedValue = values[index])) { if (key == keys[index]) { break; } index = ++index & mask; } final V oldValue = unmapNullValue(mappedValue); if (null == oldValue) { if (null == mappedValue) { ++size; keys[index] = key; } values[index] = val; if (size > resizeThreshold) { increaseCapacity(); } } return oldValue; } /** * Removes the entry for the specified key only if it is currently mapped to the specified value. * * @param keyPartA for the key * @param keyPartB for the key * @param value value expected to be associated with the specified key * @return true if the value was removed * @see Map#remove(Object, Object) */ public boolean remove(final int keyPartA, final int keyPartB, final V value) { final long key = compoundKey(keyPartA, keyPartB); final Object val = mapNullValue(value); if (null != val) { final long[] keys = this.keys; final Object[] values = this.values; final int mask = values.length - 1; int index = Hashing.hash(key, mask); Object mappedValue; while (null != (mappedValue = values[index])) { if (key == keys[index]) { if (Objects.equals(unmapNullValue(mappedValue), value)) { values[index] = null; --size; compactChain(index); return true; } break; } index = ++index & mask; } } return false; } /** * Return the number of unique entries in the map. * * @return number of unique entries in the map. */ public int size() { return size; } /** * Is map empty or not. * * @return boolean indicating empty map or not */ public boolean isEmpty() { return 0 == size; } /** * {@inheritDoc} */ public String toString() { final StringBuilder sb = new StringBuilder(); sb.append('{'); final long[] keys = this.keys; final Object[] values = this.values; for (int i = 0, size = values.length; i < size; i++) { final Object value = values[i]; if (null != value) { final long compoundKey = keys[i]; final int keyPartA = (int)(compoundKey >>> 32); final int keyPartB = (int)(compoundKey & 0xFFFF_FFFFL); sb.append(keyPartA).append('_').append(keyPartB).append('=').append(value).append(", "); } } if (sb.length() > 1) { sb.setLength(sb.length() - 2); } sb.append('}'); return sb.toString(); } private void rehash(final int newCapacity) { final int mask = newCapacity - 1; resizeThreshold = (int)(newCapacity * loadFactor); final long[] tempKeys = new long[newCapacity]; final Object[] tempValues = new Object[newCapacity]; final long[] keys = this.keys; final Object[] values = this.values; for (int i = 0, size = values.length; i < size; i++) { final Object value = values[i]; if (null != value) { final long key = keys[i]; int newHash = Hashing.hash(key, mask); while (null != tempValues[newHash]) { newHash = ++newHash & mask; } tempKeys[newHash] = key; tempValues[newHash] = value; } } this.keys = tempKeys; this.values = tempValues; } @SuppressWarnings("FinalParameters") private void compactChain(int deleteIndex) { final int mask = values.length - 1; int index = deleteIndex; final long[] keys = this.keys; final Object[] values = this.values; while (true) { index = ++index & mask; final Object value = values[index]; if (null == value) { break; } final long key = keys[index]; final int hash = Hashing.hash(key, mask); if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) || (hash <= deleteIndex && deleteIndex <= index)) { keys[deleteIndex] = key; values[deleteIndex] = value; values[index] = null; deleteIndex = index; } } } private void increaseCapacity() { final int newCapacity = values.length << 1; if (newCapacity < 0) { throw new IllegalStateException("max capacity reached at size=" + size); } rehash(newCapacity); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy