/* * 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.lang.reflect.Array; import java.util.*; import static org.agrona.collections.CollectionUtil.validateLoadFactor; /** * Open-addressing with linear-probing expandable hash set. Allocation free in steady state use when expanded. * * By storing elements as int primitives this significantly reduces memory consumption compared with Java's builtin * <code>HashSet<Integer></code>. It implements <code>Set<Integer></code> for convenience, but calling * functionality via those methods can add boxing overhead to your usage. * * Not Threadsafe. * * This HashSet caches its iterator object, so nested iteration is not supported. * * @see IntIterator * @see Set */ public final class IntHashSet extends AbstractSet<Integer> { /** * The initial capacity used when none is specified in the constructor. */ @DoNotSub public static final int DEFAULT_INITIAL_CAPACITY = 8; static final int MISSING_VALUE = -1; private final float loadFactor; @DoNotSub private int resizeThreshold; // NB: excludes missing value @DoNotSub private int sizeOfArrayValues; private int[] values; private final IntHashSetIterator iterator = new IntHashSetIterator(); private boolean containsMissingValue; public IntHashSet() { this(DEFAULT_INITIAL_CAPACITY); } public IntHashSet( @DoNotSub final int proposedCapacity) { this(proposedCapacity, Hashing.DEFAULT_LOAD_FACTOR); } public IntHashSet( @DoNotSub final int initialCapacity, final float loadFactor) { validateLoadFactor(loadFactor); this.loadFactor = loadFactor; sizeOfArrayValues = 0; @DoNotSub final int capacity = BitUtil.findNextPositivePowerOfTwo(initialCapacity); resizeThreshold = (int)(capacity * loadFactor); // @DoNotSub values = new int[capacity]; Arrays.fill(values, MISSING_VALUE); } /** * {@inheritDoc} */ public boolean add(final Integer value) { return add(value.intValue()); } /** * Primitive specialised overload of {this#add(Integer)} * * @param value the value to add * @return true if the collection has changed, false otherwise * @throws IllegalArgumentException if value is missingValue */ public boolean add(final int value) { if (value == MISSING_VALUE) { final boolean previousContainsMissingValue = this.containsMissingValue; containsMissingValue = true; return !previousContainsMissingValue; } final int[] values = this.values; @DoNotSub final int mask = values.length - 1; @DoNotSub int index = Hashing.hash(value, mask); while (values[index] != MISSING_VALUE) { if (values[index] == value) { return false; } index = next(index, mask); } values[index] = value; sizeOfArrayValues++; if (sizeOfArrayValues > resizeThreshold) { increaseCapacity(); } return true; } private void increaseCapacity() { @DoNotSub final int newCapacity = values.length * 2; if (newCapacity < 0) { throw new IllegalStateException("Max capacity reached at size=" + size()); } rehash(newCapacity); } private void rehash(@DoNotSub final int newCapacity) { @DoNotSub final int capacity = newCapacity; @DoNotSub final int mask = newCapacity - 1; resizeThreshold = (int)(newCapacity * loadFactor); // @DoNotSub final int[] tempValues = new int[capacity]; Arrays.fill(tempValues, MISSING_VALUE); for (final int value : values) { if (value != MISSING_VALUE) { @DoNotSub int newHash = Hashing.hash(value, mask); while (tempValues[newHash] != MISSING_VALUE) { newHash = ++newHash & mask; } tempValues[newHash] = value; } } values = tempValues; } /** * {@inheritDoc} */ public boolean remove(final Object value) { return value instanceof Integer && remove(((Integer)value).intValue()); } /** * An int specialised version of {this#remove(Object)}. * * @param value the value to remove * @return true if the value was present, false otherwise */ public boolean remove(final int value) { if (value == MISSING_VALUE) { final boolean previousContainsMissingValue = this.containsMissingValue; containsMissingValue = false; return previousContainsMissingValue; } final int[] values = this.values; @DoNotSub final int mask = values.length - 1; @DoNotSub int index = Hashing.hash(value, mask); while (values[index] != MISSING_VALUE) { if (values[index] == value) { values[index] = MISSING_VALUE; compactChain(index); sizeOfArrayValues--; return true; } index = next(index, mask); } return false; } @DoNotSub private static int next(final int index, final int mask) { return (index + 1) & mask; } @SuppressWarnings("FinalParameters") @DoNotSub void compactChain(int deleteIndex) { final int[] values = this.values; @DoNotSub final int mask = values.length - 1; @DoNotSub int index = deleteIndex; while (true) { index = next(index, mask); if (values[index] == MISSING_VALUE) { return; } @DoNotSub final int hash = Hashing.hash(values[index], mask); if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) || (hash <= deleteIndex && deleteIndex <= index)) { values[deleteIndex] = values[index]; values[index] = MISSING_VALUE; deleteIndex = index; } } } /** * Compact the 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.0 / loadFactor)); rehash(BitUtil.findNextPositivePowerOfTwo(idealCapacity)); } /** * {@inheritDoc} */ public boolean contains(final Object value) { return value instanceof Integer && contains(((Integer)value).intValue()); } /** * {@inheritDoc} */ public boolean contains(final int value) { if (value == MISSING_VALUE) { return containsMissingValue; } final int[] values = this.values; @DoNotSub final int mask = values.length - 1; @DoNotSub int index = Hashing.hash(value, mask); while (values[index] != MISSING_VALUE) { if (values[index] == value) { return true; } index = next(index, mask); } return false; } /** * {@inheritDoc} */ @DoNotSub public int size() { return sizeOfArrayValues + (containsMissingValue ? 1 : 0); } /** * {@inheritDoc} */ public boolean isEmpty() { return size() == 0; } /** * Get the load factor beyond which the set will increase size. * * @return load factor for when the set should increase size. */ public float loadFactor() { return loadFactor; } /** * Get the total capacity for the set to which the load factor with be a fraction of. * * @return the total capacity for the set. */ public int capacity() { return values.length; } /** * {@inheritDoc} */ public void clear() { Arrays.fill(values, MISSING_VALUE); sizeOfArrayValues = 0; containsMissingValue = false; } /** * {@inheritDoc} */ public boolean addAll(final Collection<? extends Integer> coll) { boolean added = false; for (final Integer value : coll) { added |= add(value); } return added; } /** * Alias for {@link #addAll(Collection)} for the specialized case when adding another IntHashSet, * avoids boxing and allocations * * @param coll containing the values to be added. * @return <tt>true</tt> if this set changed as a result of the call */ public boolean addAll(final IntHashSet coll) { boolean acc = false; for (final int value : coll.values) { if (value != MISSING_VALUE) { acc |= add(value); } } if (coll.containsMissingValue) { acc |= add(MISSING_VALUE); } return acc; } /** * IntHashSet specialised variant of {this#containsAll(Collection)}. * * @param other int hash set to compare against. * @return true if every element in other is in this. */ public boolean containsAll(final IntHashSet other) { for (final int value : other.values) { if (value != MISSING_VALUE && !contains(value)) { return false; } } return !other.containsMissingValue || this.containsMissingValue; } /** * Fast Path set difference for comparison with another IntHashSet. * * NB: garbage free in the identical case, allocates otherwise. * * @param other the other set to subtract * @return null if identical, otherwise the set of differences */ public IntHashSet difference(final IntHashSet other) { IntHashSet difference = null; for (final int value : values) { if (value != MISSING_VALUE && !other.contains(value)) { if (difference == null) { difference = new IntHashSet(); } difference.add(value); } } if (other.containsMissingValue && !this.containsMissingValue) { if (difference == null) { difference = new IntHashSet(); } difference.add(MISSING_VALUE); } return difference; } /** * {@inheritDoc} */ public boolean removeAll(final Collection<?> coll) { boolean removed = false; for (final Object value : coll) { removed |= remove(value); } return removed; } /** * Alias for {@link #removeAll(Collection)} for the specialized case when removing another IntHashSet, * avoids boxing and allocations * * @param coll containing the values to be removed. * @return <tt>true</tt> if this set changed as a result of the call */ public boolean removeAll(final IntHashSet coll) { boolean acc = false; for (final int value : coll.values) { if (value != MISSING_VALUE) { acc |= remove(value); } } if (coll.containsMissingValue) { acc |= remove(MISSING_VALUE); } return acc; } /** * {@inheritDoc} */ public IntIterator iterator() { iterator.reset(values, containsMissingValue, size()); return iterator; } public void copy(final IntHashSet that) { if (this.values.length != that.values.length) { throw new IllegalArgumentException("Cannot copy object: masks not equal"); } System.arraycopy(that.values, 0, this.values, 0, this.values.length); this.sizeOfArrayValues = that.sizeOfArrayValues; this.containsMissingValue = that.containsMissingValue; } /** * {@inheritDoc} */ public String toString() { final StringBuilder sb = new StringBuilder(); sb.append('{'); for (final int value : values) { if (value != MISSING_VALUE) { sb.append(value); sb.append(", "); } } if (containsMissingValue) { sb.append(MISSING_VALUE); sb.append(", "); } if (sb.length() > 1) { sb.setLength(sb.length() - 2); } sb.append('}'); return sb.toString(); } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") public <T> T[] toArray(final T[] into) { final Class<?> componentType = into.getClass().getComponentType(); if (!componentType.isAssignableFrom(Integer.class)) { throw new ArrayStoreException("Cannot store Integers in array of type " + componentType); } @DoNotSub final int size = size(); final T[] arrayCopy = into.length >= size ? into : (T[])Array.newInstance(componentType, size); copyValues(arrayCopy); return arrayCopy; } /** * {@inheritDoc} */ public Object[] toArray() { final Object[] arrayCopy = new Object[size()]; copyValues(arrayCopy); return arrayCopy; } private void copyValues(final Object[] arrayCopy) { @DoNotSub int i = 0; final int[] values = this.values; for (final int value : values) { if (MISSING_VALUE != value) { arrayCopy[i++] = value; } } if (containsMissingValue) { arrayCopy[sizeOfArrayValues] = MISSING_VALUE; } } /** * {@inheritDoc} */ public boolean equals(final Object other) { if (other == this) { return true; } if (other instanceof IntHashSet) { final IntHashSet otherSet = (IntHashSet)other; return otherSet.containsMissingValue == containsMissingValue && otherSet.sizeOfArrayValues == sizeOfArrayValues && containsAll(otherSet); } return false; } /** * {@inheritDoc} */ @DoNotSub public int hashCode() { @DoNotSub int hashCode = 0; for (final int value : values) { if (value != MISSING_VALUE) { hashCode += Integer.hashCode(value); } } if (containsMissingValue) { hashCode += Integer.hashCode(MISSING_VALUE); } return hashCode; } public final class IntHashSetIterator extends IntIterator { public void remove() { if (isPositionValid) { if (remaining() == 1 && containsMissingValue) { containsMissingValue = false; } else { @DoNotSub final int position = position(); values[position] = MISSING_VALUE; --sizeOfArrayValues; compactChain(position); } isPositionValid = false; } else { throw new IllegalStateException(); } } } }