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

org.htmlunit.util.OrderedFastHashMap Maven / Gradle / Ivy

Go to download

XLT (Xceptance LoadTest) is an extensive load and performance test tool developed and maintained by Xceptance.

The newest version!
/*
 * Copyright (c) 2002-2024 Gargoyle Software 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
 * 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 org.htmlunit.util;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;

/**
 * Simple and efficient linked map or better ordered map implementation to
 * replace the default linked list which is heavy.
 *
 * This map does not support null and it is not thread-safe. It implements the
 * map interface but only for compatibility reason in the sense of replacing a
 * regular map. Iterator and streaming methods are either not implemented or
 * less efficient.
 *
 * It goes the extra mile to avoid the overhead of wrapper objects.
 *
 * Because you typically know what you do, we run minimal index checks only and
 * rely on the default exceptions by Java. Why should we do things twice?
 *
 * Important Note: This is meant for small maps because to save on memory
 * allocation and churn, we are not keeping a wrapper for a reference from the
 * map to the list, only from the list to the map. Hence when you remove a key,
 * we have to iterate the entire list. Mostly, half of it most likely, but still
 * expensive enough. When you have something small like 10 to 20 entries, this
 * won't matter that much especially when a remove might be a rare event.
 *
 * This is based on FashHashMap from XLT which is based on a version from:
 * https://github.com/mikvor/hashmapTest/blob/master/src/main/java/map/objobj/ObjObjMap.java
 * No concrete license specified at the source. The project is public domain.
 *
 * @param  the type of the key
 * @param  the type of the value
 *
 * @author René Schwietzke
 */
public class OrderedFastHashMap implements Map, Serializable {
    // our placeholders in the map
    private static final Object FREE_KEY_ = null;
    private static final Object REMOVED_KEY_ = new Object();

    // Fill factor, must be between (0 and 1)
    private static final double FILLFACTOR_ = 0.7d;

    // The map with the key value pairs */
    private Object[] mapData_;

    // We will resize a map once it reaches this size
    private int mapThreshold_;

    // Current map size
    private int mapSize_;

    // the list to impose order, the list refers to the key and value
    // position in the map, hence needs an update every time the
    // map sees an update (in regards to positions).
    private int[] orderedList_;

    // the size of the orderedList, in case we proactivly sized
    // it larger
    private int orderedListSize_;

    /**
     * Default constructor which create an ordered map with default size.
     */
    public OrderedFastHashMap() {
        this(8);
    }

    /**
     * Custom constructor to get a map with a custom size and fill factor. We are
     * not spending time on range checks, rather use a default if things are wrong.
     *
     * @param size the size to use, must 0 or positive, negative values default to 0
     */
    public OrderedFastHashMap(final int size) {
        if (size > 0) {
            final int capacity = arraySize(size, FILLFACTOR_);

            this.mapData_ = new Object[capacity << 1];
            this.mapThreshold_ = (int) (capacity * FILLFACTOR_);

            this.orderedList_ = new int[capacity];
        }
        else {
            this.mapData_ = new Object[0];
            this.mapThreshold_ = 0;

            this.orderedList_ = new int[0];
        }
    }

    /**
     * Get a value for a key, any key type is permitted due to
     * the nature of the Map interface.
     *
     * @param key the key
     * @return the value or null, if the key does not exist
     */
    @Override
    public V get(final Object key) {
        final int length = this.mapData_.length;

        // nothing in it
        if (length == 0) {
            return null;
        }

        int ptr = (key.hashCode() & ((length >> 1) - 1)) << 1;
        Object k = mapData_[ptr];

        if (k == FREE_KEY_) {
            return null; // end of chain already
        }

        // we checked FREE
        if (k.hashCode() == key.hashCode() && k.equals(key)) {
            return (V) this.mapData_[ptr + 1];
        }

        // we have not found it, search longer
        final int originalPtr = ptr;
        while (true) {
            ptr = (ptr + 2) & (length - 1); // that's next index

            // if we searched the entire array, we can stop
            if (originalPtr == ptr) {
                return null;
            }

            k = this.mapData_[ptr];

            if (k == FREE_KEY_) {
                return null;
            }

            if (k != REMOVED_KEY_) {
                if (k.hashCode() == key.hashCode() && k.equals(key)) {
                    return (V) this.mapData_[ptr + 1];
                }
            }
        }
    }

    /**
     * Adds a key and value to the internal position structure
     *
     * @param key the key
     * @param value the value to store
     * @param listPosition defines where to add the new key/value pair
     *
     * @return the old value or null if they key was not known before
     */
    private V put(final K key, final V value, final Position listPosition) {
        if (mapSize_ >= mapThreshold_) {
            rehash(this.mapData_.length == 0 ? 4 : this.mapData_.length << 1);
        }

        int ptr = getStartIndex(key) << 1;
        Object k = mapData_[ptr];

        if (k == FREE_KEY_) {
            // end of chain already
            mapData_[ptr] = key;
            mapData_[ptr + 1] = value;

            // ok, remember position, it is a new entry
            orderedListAdd(listPosition, ptr);

            mapSize_++;

            return null;
        }
        else if (k.equals(key)) {
            // we check FREE and REMOVED prior to this call
            final Object ret = mapData_[ptr + 1];
            mapData_[ptr + 1] = value;

            /// existing entry, no need to update the position

            return (V) ret;
        }

        int firstRemoved = -1;
        if (k == REMOVED_KEY_) {
            firstRemoved = ptr; // we may find a key later
        }

        while (true) {
            ptr = (ptr + 2) & (this.mapData_.length - 1); // that's next index calculation
            k = mapData_[ptr];

            if (k == FREE_KEY_) {
                if (firstRemoved != -1) {
                    ptr = firstRemoved;
                }
                mapData_[ptr] = key;
                mapData_[ptr + 1] = value;

                // ok, remember position, it is a new entry
                orderedListAdd(listPosition, ptr);

                mapSize_++;

                return null;
            }
            else if (k.equals(key)) {
                final Object ret = mapData_[ptr + 1];
                mapData_[ptr + 1] = value;

                // same key, different value, this does not change the order

                return (V) ret;
            }
            else if (k == REMOVED_KEY_) {
                if (firstRemoved == -1) {
                    firstRemoved = ptr;
                }
            }
        }
    }

    /**
     * Remove a key from the map. Returns the stored value or
     * null of the key is not known.
     *
     * @param key the key to remove
     * @return the stored value or null if the key does not exist
     */
    @Override
    public V remove(final Object key) {
        final int length = this.mapData_.length;
        // it is empty
        if (length == 0) {
            return null;
        }

        int ptr = getStartIndex(key) << 1;
        Object k = this.mapData_[ptr];

        if (k == FREE_KEY_) {
            return null; // end of chain already
        }
        else if (k.equals(key)) {
            // we check FREE and REMOVED prior to this call
            this.mapSize_--;

            if (this.mapData_[(ptr + 2) & (length - 1)] == FREE_KEY_) {
                this.mapData_[ptr] = FREE_KEY_;
            }
            else {
                this.mapData_[ptr] = REMOVED_KEY_;
            }

            final V ret = (V) this.mapData_[ptr + 1];
            this.mapData_[ptr + 1] = null;

            // take this out of the list
            orderedListRemove(ptr);

            return ret;
        }

        while (true) {
            ptr = (ptr + 2) & (length - 1); // that's next index calculation
            k = this.mapData_[ptr];

            if (k == FREE_KEY_) {
                return null;
            }
            else if (k.equals(key)) {
                this.mapSize_--;
                if (this.mapData_[(ptr + 2) & (length - 1)] == FREE_KEY_) {
                    this.mapData_[ptr] = FREE_KEY_;
                }
                else {
                    this.mapData_[ptr] = REMOVED_KEY_;
                }

                final V ret = (V) this.mapData_[ptr + 1];
                this.mapData_[ptr + 1] = null;

                // take this out of the list
                orderedListRemove(ptr);

                return ret;
            }
        }
    }

    /**
     * Returns the size of the map, effectively the number of entries.
     *
     * @return the size of the map
     */
    @Override
    public int size() {
        return mapSize_;
    }

    /**
     * Rehash the map.
     *
     * @param newCapacity the new size of the map
     */
    private void rehash(final int newCapacity) {
        this.mapThreshold_ = (int) (newCapacity / 2 * FILLFACTOR_);

        final Object[] oldData = this.mapData_;

        this.mapData_ = new Object[newCapacity];

        // we just have to grow it and not touch it at all after that,
        // just use it as source for the new map via the old
        final int[] oldOrderedList = this.orderedList_;
        final int oldOrderedListSize = this.orderedListSize_;
        this.orderedList_ = new int[newCapacity];

        this.mapSize_ = 0;
        this.orderedListSize_ = 0;

        // we use our ordered list as source and the old
        // array as reference
        // we basically rebuild the map and the ordering
        // from scratch
        for (int i = 0; i < oldOrderedListSize; i++) {
            final int pos = oldOrderedList[i];

            // get us the old data
            final K key = (K) oldData[pos];
            final V value = (V) oldData[pos + 1];

            // write the old to the new map without updating
            // the positioning
            put(key, value, Position.LAST);
        }
    }

    /**
     * Returns a list of all keys in order of addition.
     * This is an expensive operation, because we get a static
     * list back that is not backed by the implementation. Changes
     * to the returned list are not reflected in the map.
     *
     * @return a list of keys as inserted into the map
     */
    public List keys() {
        final List result = new ArrayList<>(this.orderedListSize_);

        for (int i = 0; i < this.orderedListSize_; i++) {
            final int pos = this.orderedList_[i];
            final Object o = this.mapData_[pos];
            result.add((K) o);
        }

        return result;
    }

    /**
     * Returns a list of all values ordered by when the key was
     * added. This is an expensive operation, because we get a static
     * list back that is not backed by the implementation. Changes
     * to the returned list are not reflected in the map.
     *
     * @return a list of values
     */
    @Override
    public List values() {
        final List result = new ArrayList<>(this.orderedListSize_);

        for (int i = 0; i < this.orderedListSize_; i++) {
            final int pos = this.orderedList_[i];
            final Object o = this.mapData_[pos + 1];
            result.add((V) o);
        }

        return result;
    }

    /**
     * Clears the map, reuses the data structure by clearing it out. It won't shrink
     * the underlying arrays!
     */
    @Override
    public void clear() {
        this.mapSize_ = 0;
        this.orderedListSize_ = 0;
        Arrays.fill(this.mapData_, FREE_KEY_);
        // Arrays.fill(this.orderedList, 0);
    }

    /**
     * Get us the start index from where we search or insert into the map
     *
     * @param key the key to calculate the position for
     * @return the start position
     */
    private int getStartIndex(final Object key) {
        // key is not null here
        return key.hashCode() & ((this.mapData_.length >> 1) - 1);
    }

    /**
     * Return the least power of two greater than or equal to the specified value.
     *
     * 

* Note that this function will return 1 when the argument is 0. * * @param x a long integer smaller than or equal to 262. * @return the least power of two greater than or equal to the specified value. */ private static long nextPowerOfTwo(final long x) { if (x == 0) { return 1; } long r = x - 1; r |= r >> 1; r |= r >> 2; r |= r >> 4; r |= r >> 8; r |= r >> 16; return (r | r >> 32) + 1; } /** * Returns the least power of two smaller than or equal to 230 and * larger than or equal to Math.ceil( expected / f ). * * @param expected the expected number of elements in a hash table. * @param f the load factor. * @return the minimum possible size for a backing array. * @throws IllegalArgumentException if the necessary size is larger than * 230. */ private static int arraySize(final int expected, final double f) { final long s = Math.max(2, nextPowerOfTwo((long) Math.ceil(expected / f))); if (s > (1 << 30)) { throw new IllegalArgumentException( "Too large (" + expected + " expected elements with load factor " + f + ")"); } return (int) s; } /** * Returns an entry consisting of key and value at a given position. * This position relates to the ordered key list that maintain the * addition order for this map. * * @param index the position to fetch * @return an entry of key and value * @throws IndexOutOfBoundsException when the ask for the position is invalid */ public Entry getEntry(final int index) { if (index < 0 || index >= this.orderedListSize_) { throw new IndexOutOfBoundsException(String.format("Index: %s, Size: %s", index, this.orderedListSize_)); } final int pos = this.orderedList_[index]; return new Entry(this.mapData_[pos], this.mapData_[pos + 1]); } /** * Returns the key at a certain position of the ordered list that * keeps the addition order of this map. * * @param index the position to fetch * @return the key at this position * @throws IndexOutOfBoundsException when the ask for the position is invalid */ public K getKey(final int index) { if (index < 0 || index >= this.orderedListSize_) { throw new IndexOutOfBoundsException(String.format("Index: %s, Size: %s", index, this.orderedListSize_)); } final int pos = this.orderedList_[index]; return (K) this.mapData_[pos]; } /** * Returns the value at a certain position of the ordered list that * keeps the addition order of this map. * * @param index the position to fetch * @return the value at this position * @throws IndexOutOfBoundsException when the ask for the position is invalid */ public V getValue(final int index) { if (index < 0 || index >= this.orderedListSize_) { throw new IndexOutOfBoundsException(String.format("Index: %s, Size: %s", index, this.orderedListSize_)); } final int pos = this.orderedList_[index]; return (V) this.mapData_[pos + 1]; } /** * Removes a key and value from this map based on the position * in the backing list, rather by key as usual. * * @param index the position to remove the data from * @return the value stored * @throws IndexOutOfBoundsException when the ask for the position is invalid */ public V remove(final int index) { if (index < 0 || index >= this.orderedListSize_) { throw new IndexOutOfBoundsException(String.format("Index: %s, Size: %s", index, this.orderedListSize_)); } final int pos = this.orderedList_[index]; final K key = (K) this.mapData_[pos]; return remove(key); } @Override public V put(final K key, final V value) { return this.put(key, value, Position.LAST); } public V addFirst(final K key, final V value) { return this.put(key, value, Position.FIRST); } public V add(final K key, final V value) { return this.put(key, value, Position.LAST); } public V addLast(final K key, final V value) { return this.put(key, value, Position.LAST); } public V getFirst() { return getValue(0); } public V getLast() { return getValue(this.orderedListSize_ - 1); } public V removeFirst() { if (this.orderedListSize_ > 0) { final int pos = this.orderedList_[0]; final K key = (K) this.mapData_[pos]; return remove(key); } return null; } public V removeLast() { if (this.orderedListSize_ > 0) { final int pos = this.orderedList_[this.orderedListSize_ - 1]; final K key = (K) this.mapData_[pos]; return remove(key); } return null; } /** * Checks of a key is in the map. * * @param key the key to check * @return true of the key is in the map, false otherwise */ @Override public boolean containsKey(final Object key) { return get(key) != null; } @Override public boolean containsValue(final Object value) { // that is expensive, we have to iterate everything for (int i = 0; i < this.orderedListSize_; i++) { final int pos = this.orderedList_[i] + 1; final Object v = this.mapData_[pos]; // do we match? if (v == value || v.equals(value)) { return true; } } return false; } @Override public boolean isEmpty() { return this.mapSize_ == 0; } @Override public Set> entrySet() { final Set> set = new OrderedEntrySet<>(this); return set; } @Override public Set keySet() { final Set set = new OrderedKeySet<>(this); return set; } /** * Just reverses the ordering of the map as created so far. */ public void reverse() { // In-place reversal final int to = this.orderedListSize_ / 2; for (int i = 0; i < to; i++) { // Swapping the elements final int j = this.orderedList_[i]; this.orderedList_[i] = this.orderedList_[this.orderedListSize_ - i - 1]; this.orderedList_[this.orderedListSize_ - i - 1] = j; } } /** * We have to overwrite the export due to the use of static object as marker * * @param aInputStream the inputstream to read from * @throws IOException when the reading from the source fails * @throws ClassNotFoundException in case we cannot restore a class */ private void readObject(final ObjectInputStream aInputStream) throws ClassNotFoundException, IOException { // perform the default de-serialization first aInputStream.defaultReadObject(); // we have to restore order, keep relevant data final Object[] srcData = Arrays.copyOf(this.mapData_, this.mapData_.length); final int[] srcIndex = Arrays.copyOf(this.orderedList_, this.orderedList_.length); final int orderedListSize = this.orderedListSize_; // now, empty the original map clear(); // sort things in, so we get a nice clean new map, this will // also cleanup what was previously a removed entry, we have not // kept that information anyway for (int i = 0; i < orderedListSize; i++) { final int pos = srcIndex[i]; final K key = (K) srcData[pos]; final V value = (V) srcData[pos + 1]; put(key, value); } } /** * We have to overwrite the export due to the use of static object as marker * * @param aOutputStream the stream to write to * @throws IOException in case we have issue writing our data to the stream */ private void writeObject(final ObjectOutputStream aOutputStream) throws IOException { // we will remove all placeholder object references, // when putting it back together, we rebuild the map from scratch for (int i = 0; i < this.mapData_.length; i++) { final Object entry = this.mapData_[i]; if (entry == FREE_KEY_ || entry == REMOVED_KEY_) { this.mapData_[i] = null; } } // perform the default serialization for all non-transient, non-static fields aOutputStream.defaultWriteObject(); } /** * This set does not support any modifications through its interface. All such * methods will throw {@link UnsupportedOperationException}. */ static class OrderedEntrySet implements Set> { private final OrderedFastHashMap backingMap_; OrderedEntrySet(final OrderedFastHashMap backingMap) { this.backingMap_ = backingMap; } @Override public int size() { return this.backingMap_.size(); } @Override public boolean isEmpty() { return this.backingMap_.isEmpty(); } @Override public boolean contains(final Object o) { if (o instanceof Map.Entry) { final Map.Entry ose = (Map.Entry) o; final Object k = ose.getKey(); final Object v = ose.getValue(); final Object value = this.backingMap_.get(k); if (value != null) { return v.equals(value); } } return false; } @Override public Object[] toArray() { final Object[] array = new Object[this.backingMap_.orderedListSize_]; return toArray(array); } @Override @SuppressWarnings("unchecked") public T[] toArray(final T[] a) { final T[] array; if (a.length >= this.backingMap_.orderedListSize_) { array = a; } else { array = (T[]) java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), this.backingMap_.orderedListSize_); } for (int i = 0; i < this.backingMap_.orderedListSize_; i++) { array[i] = (T) this.backingMap_.getEntry(i); } return array; } @Override public Iterator> iterator() { return new OrderedEntryIterator(); } @Override public boolean add(final Map.Entry e) { throw new UnsupportedOperationException(); } @Override public boolean remove(final Object o) { throw new UnsupportedOperationException(); } @Override public boolean containsAll(final Collection c) { throw new UnsupportedOperationException(); } @Override public boolean addAll(final Collection> c) { throw new UnsupportedOperationException(); } @Override public boolean retainAll(final Collection c) { throw new UnsupportedOperationException(); } @Override public boolean removeAll(final Collection c) { throw new UnsupportedOperationException(); } @Override public void clear() { throw new UnsupportedOperationException(); } class OrderedEntryIterator implements Iterator> { private int pos_ = 0; @Override public boolean hasNext() { return pos_ < backingMap_.orderedListSize_; } @Override public Map.Entry next() { if (pos_ < backingMap_.orderedListSize_) { return backingMap_.getEntry(pos_++); } throw new NoSuchElementException(); } } } static class OrderedKeySet implements Set { private final OrderedFastHashMap backingMap_; OrderedKeySet(final OrderedFastHashMap backingMap) { this.backingMap_ = backingMap; } @Override public int size() { return this.backingMap_.size(); } @Override public boolean isEmpty() { return this.backingMap_.isEmpty(); } @Override public boolean contains(final Object o) { return this.backingMap_.containsKey(o); } @Override public Object[] toArray() { final Object[] array = new Object[this.backingMap_.orderedListSize_]; return toArray(array); } @Override @SuppressWarnings("unchecked") public T[] toArray(final T[] a) { final T[] array; if (a.length >= this.backingMap_.orderedListSize_) { array = a; } else { array = (T[]) java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), this.backingMap_.orderedListSize_); } for (int i = 0; i < this.backingMap_.orderedListSize_; i++) { array[i] = (T) this.backingMap_.getKey(i); } return array; } @Override public Iterator iterator() { return new OrderedKeyIterator(); } class OrderedKeyIterator implements Iterator { private int pos_ = 0; @Override public boolean hasNext() { return this.pos_ < backingMap_.orderedListSize_; } @Override public K next() { if (this.pos_ < backingMap_.orderedListSize_) { return backingMap_.getKey(this.pos_++); } throw new NoSuchElementException(); } } @Override public boolean add(final K e) { throw new UnsupportedOperationException(); } @Override public boolean remove(final Object o) { throw new UnsupportedOperationException(); } @Override public boolean containsAll(final Collection c) { throw new UnsupportedOperationException(); } @Override public boolean addAll(final Collection c) { throw new UnsupportedOperationException(); } @Override public boolean retainAll(final Collection c) { throw new UnsupportedOperationException(); } @Override public boolean removeAll(final Collection c) { throw new UnsupportedOperationException(); } @Override public void clear() { throw new UnsupportedOperationException(); } } @Override public void putAll(final Map src) { if (src == this) { throw new IllegalArgumentException("Cannot add myself"); } for (final Map.Entry entry : src.entrySet()) { put(entry.getKey(), entry.getValue(), Position.LAST); } } private void orderedListAdd(final Position listPosition, final int position) { // the list should still have room, otherwise the map was // grown already and the ordering list with it if (listPosition == Position.FIRST) { System.arraycopy(this.orderedList_, 0, this.orderedList_, 1, this.orderedList_.length - 1); this.orderedList_[0] = position; this.orderedListSize_++; } else if (listPosition == Position.LAST) { this.orderedList_[this.orderedListSize_] = position; this.orderedListSize_++; } else { // if none, we are rebuilding the map and don't have to do a thing } } private void orderedListRemove(final int position) { // find the positional information int i = 0; for ( ; i < this.orderedListSize_; i++) { if (this.orderedList_[i] == position) { this.orderedList_[i] = -1; if (i < this.orderedListSize_) { // not the last element, compact System.arraycopy(this.orderedList_, i + 1, this.orderedList_, i, this.orderedListSize_ - i); } this.orderedListSize_--; return; } } if (i == this.orderedListSize_) { throw new IllegalArgumentException(String.format("Position %s was not in order list", position)); } } @Override public String toString() { final int maxLen = 10; return String.format( "mapData=%s, mapFillFactor=%s, mapThreshold=%s, mapSize=%s,%norderedList=%s, orderedListSize=%s", mapData_ != null ? Arrays.asList(mapData_).subList(0, Math.min(mapData_.length, maxLen)) : null, FILLFACTOR_, mapThreshold_, mapSize_, orderedList_ != null ? Arrays.toString(Arrays.copyOf(orderedList_, Math.min(orderedList_.length, maxLen))) : null, orderedListSize_); } /** * Helper for identifying if we need to position our new entry differently. */ private enum Position { NONE, FIRST, LAST } /** * Well, we need that to satisfy the map implementation concept. * * @param the key * @param the value */ static class Entry implements Map.Entry { private final K key_; private final V value_; Entry(final K key, final V value) { this.key_ = key; this.value_ = value; } @Override public K getKey() { return key_; } @Override public V getValue() { return value_; } @Override public V setValue(final V value) { throw new UnsupportedOperationException("This map does not support write-through via an entry"); } @Override public int hashCode() { return Objects.hashCode(key_) ^ Objects.hashCode(value_); } @Override public String toString() { return key_ + "=" + value_; } @Override public boolean equals(final Object o) { if (o == this) { return true; } if (o instanceof Map.Entry) { final Map.Entry e = (Map.Entry) o; if (Objects.equals(key_, e.getKey()) && Objects.equals(value_, e.getValue())) { return true; } } return false; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy