blob: 8f6003f2ce1d5a35ab03ae53065973b8d6ffac03 [file] [log] [blame]
/*
* Copyright (C) 2017 The Guava Authors
*
* 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 com.google.common.collect;
import static com.google.common.base.Preconditions.checkElementIndex;
import static com.google.common.collect.CollectPreconditions.checkPositive;
import static com.google.common.collect.Hashing.smearedHash;
import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Multiset.Entry;
import com.google.common.collect.Multisets.AbstractEntry;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Arrays;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
/**
* ObjectCountHashMap is an implementation of {@code AbstractObjectCountMap} that uses arrays to
* store key objects and count values. Comparing to using a traditional {@code HashMap}
* implementation which stores keys and count values as map entries, {@code ObjectCountHashMap}
* minimizes object allocation and reduces memory footprint.
*
* <p>In the absence of element deletions, this will iterate over elements in insertion order.
*/
@GwtCompatible(serializable = true, emulated = true)
class ObjectCountHashMap<K> {
/** Creates an empty {@code ObjectCountHashMap} instance. */
public static <K> ObjectCountHashMap<K> create() {
return new ObjectCountHashMap<K>();
}
/**
* Creates a {@code ObjectCountHashMap} instance, with a high enough "initial capacity" that it
* <i>should</i> hold {@code expectedSize} elements without growth.
*
* @param expectedSize the number of elements you expect to add to the returned set
* @return a new, empty {@code ObjectCountHashMap} with enough capacity to hold {@code
* expectedSize} elements without resizing
* @throws IllegalArgumentException if {@code expectedSize} is negative
*/
public static <K> ObjectCountHashMap<K> createWithExpectedSize(int expectedSize) {
return new ObjectCountHashMap<K>(expectedSize);
}
private static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 1.0f;
/** Bitmask that selects the low 32 bits. */
private static final long NEXT_MASK = (1L << 32) - 1;
/** Bitmask that selects the high 32 bits. */
private static final long HASH_MASK = ~NEXT_MASK;
static final int DEFAULT_SIZE = 3;
// used to indicate blank table entries
static final int UNSET = -1;
/** The keys of the entries in the map. */
transient Object[] keys;
/** The values of the entries in the map. */
transient int[] values;
transient int size;
transient int modCount;
/**
* The hashtable. Its values are indexes to the keys, values, and entries arrays.
*
* <p>Currently, the UNSET value means "null pointer", and any non negative value x is the actual
* index.
*
* <p>Its size must be a power of two.
*/
private transient int[] table;
/**
* Contains the logical entries, in the range of [0, size()). The high 32 bits of each long is the
* smeared hash of the element, whereas the low 32 bits is the "next" pointer (pointing to the
* next entry in the bucket chain). The pointers in [size(), entries.length) are all "null"
* (UNSET).
*/
@VisibleForTesting transient long[] entries;
/** The load factor. */
private transient float loadFactor;
/** When we have this many elements, resize the hashtable. */
private transient int threshold;
/** Constructs a new empty instance of {@code ObjectCountHashMap}. */
ObjectCountHashMap() {
init(DEFAULT_SIZE, DEFAULT_LOAD_FACTOR);
}
ObjectCountHashMap(ObjectCountHashMap<? extends K> map) {
init(map.size(), DEFAULT_LOAD_FACTOR);
for (int i = map.firstIndex(); i != -1; i = map.nextIndex(i)) {
put(map.getKey(i), map.getValue(i));
}
}
/**
* Constructs a new instance of {@code ObjectCountHashMap} with the specified capacity.
*
* @param capacity the initial capacity of this {@code ObjectCountHashMap}.
*/
ObjectCountHashMap(int capacity) {
this(capacity, DEFAULT_LOAD_FACTOR);
}
ObjectCountHashMap(int expectedSize, float loadFactor) {
init(expectedSize, loadFactor);
}
void init(int expectedSize, float loadFactor) {
Preconditions.checkArgument(expectedSize >= 0, "Initial capacity must be non-negative");
Preconditions.checkArgument(loadFactor > 0, "Illegal load factor");
int buckets = Hashing.closedTableSize(expectedSize, loadFactor);
this.table = newTable(buckets);
this.loadFactor = loadFactor;
this.keys = new Object[expectedSize];
this.values = new int[expectedSize];
this.entries = newEntries(expectedSize);
this.threshold = Math.max(1, (int) (buckets * loadFactor));
}
private static int[] newTable(int size) {
int[] array = new int[size];
Arrays.fill(array, UNSET);
return array;
}
private static long[] newEntries(int size) {
long[] array = new long[size];
Arrays.fill(array, UNSET);
return array;
}
private int hashTableMask() {
return table.length - 1;
}
int firstIndex() {
return (size == 0) ? -1 : 0;
}
int nextIndex(int index) {
return (index + 1 < size) ? index + 1 : -1;
}
int nextIndexAfterRemove(int oldNextIndex, @SuppressWarnings("unused") int removedIndex) {
return oldNextIndex - 1;
}
int size() {
return size;
}
@SuppressWarnings("unchecked")
K getKey(int index) {
checkElementIndex(index, size);
return (K) keys[index];
}
int getValue(int index) {
checkElementIndex(index, size);
return values[index];
}
void setValue(int index, int newValue) {
checkElementIndex(index, size);
values[index] = newValue;
}
Entry<K> getEntry(int index) {
checkElementIndex(index, size);
return new MapEntry(index);
}
class MapEntry extends AbstractEntry<K> {
@NullableDecl final K key;
int lastKnownIndex;
@SuppressWarnings("unchecked") // keys only contains Ks
MapEntry(int index) {
this.key = (K) keys[index];
this.lastKnownIndex = index;
}
@Override
public K getElement() {
return key;
}
void updateLastKnownIndex() {
if (lastKnownIndex == -1
|| lastKnownIndex >= size()
|| !Objects.equal(key, keys[lastKnownIndex])) {
lastKnownIndex = indexOf(key);
}
}
@SuppressWarnings("unchecked") // values only contains Vs
@Override
public int getCount() {
updateLastKnownIndex();
return (lastKnownIndex == -1) ? 0 : values[lastKnownIndex];
}
@SuppressWarnings("unchecked") // values only contains Vs
@CanIgnoreReturnValue
public int setCount(int count) {
updateLastKnownIndex();
if (lastKnownIndex == -1) {
put(key, count);
return 0;
} else {
int old = values[lastKnownIndex];
values[lastKnownIndex] = count;
return old;
}
}
}
private static int getHash(long entry) {
return (int) (entry >>> 32);
}
/** Returns the index, or UNSET if the pointer is "null" */
private static int getNext(long entry) {
return (int) entry;
}
/** Returns a new entry value by changing the "next" index of an existing entry */
private static long swapNext(long entry, int newNext) {
return (HASH_MASK & entry) | (NEXT_MASK & newNext);
}
void ensureCapacity(int minCapacity) {
if (minCapacity > entries.length) {
resizeEntries(minCapacity);
}
if (minCapacity >= threshold) {
int newTableSize = Math.max(2, Integer.highestOneBit(minCapacity - 1) << 1);
resizeTable(newTableSize);
}
}
@CanIgnoreReturnValue
public int put(@NullableDecl K key, int value) {
checkPositive(value, "count");
long[] entries = this.entries;
Object[] keys = this.keys;
int[] values = this.values;
int hash = smearedHash(key);
int tableIndex = hash & hashTableMask();
int newEntryIndex = this.size; // current size, and pointer to the entry to be appended
int next = table[tableIndex];
if (next == UNSET) {
table[tableIndex] = newEntryIndex;
} else {
int last;
long entry;
do {
last = next;
entry = entries[next];
if (getHash(entry) == hash && Objects.equal(key, keys[next])) {
int oldValue = values[next];
values[next] = value;
return oldValue;
}
next = getNext(entry);
} while (next != UNSET);
entries[last] = swapNext(entry, newEntryIndex);
}
if (newEntryIndex == Integer.MAX_VALUE) {
throw new IllegalStateException("Cannot contain more than Integer.MAX_VALUE elements!");
}
int newSize = newEntryIndex + 1;
resizeMeMaybe(newSize);
insertEntry(newEntryIndex, key, value, hash);
this.size = newSize;
if (newEntryIndex >= threshold) {
resizeTable(2 * table.length);
}
modCount++;
return 0;
}
/**
* Creates a fresh entry with the specified object at the specified position in the entry array.
*/
void insertEntry(int entryIndex, @NullableDecl K key, int value, int hash) {
this.entries[entryIndex] = ((long) hash << 32) | (NEXT_MASK & UNSET);
this.keys[entryIndex] = key;
this.values[entryIndex] = value;
}
/** Returns currentSize + 1, after resizing the entries storage if necessary. */
private void resizeMeMaybe(int newSize) {
int entriesSize = entries.length;
if (newSize > entriesSize) {
int newCapacity = entriesSize + Math.max(1, entriesSize >>> 1);
if (newCapacity < 0) {
newCapacity = Integer.MAX_VALUE;
}
if (newCapacity != entriesSize) {
resizeEntries(newCapacity);
}
}
}
/**
* Resizes the internal entries array to the specified capacity, which may be greater or less than
* the current capacity.
*/
void resizeEntries(int newCapacity) {
this.keys = Arrays.copyOf(keys, newCapacity);
this.values = Arrays.copyOf(values, newCapacity);
long[] entries = this.entries;
int oldCapacity = entries.length;
entries = Arrays.copyOf(entries, newCapacity);
if (newCapacity > oldCapacity) {
Arrays.fill(entries, oldCapacity, newCapacity, UNSET);
}
this.entries = entries;
}
private void resizeTable(int newCapacity) { // newCapacity always a power of two
int[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
int newThreshold = 1 + (int) (newCapacity * loadFactor);
int[] newTable = newTable(newCapacity);
long[] entries = this.entries;
int mask = newTable.length - 1;
for (int i = 0; i < size; i++) {
long oldEntry = entries[i];
int hash = getHash(oldEntry);
int tableIndex = hash & mask;
int next = newTable[tableIndex];
newTable[tableIndex] = i;
entries[i] = ((long) hash << 32) | (NEXT_MASK & next);
}
this.threshold = newThreshold;
this.table = newTable;
}
int indexOf(@NullableDecl Object key) {
int hash = smearedHash(key);
int next = table[hash & hashTableMask()];
while (next != UNSET) {
long entry = entries[next];
if (getHash(entry) == hash && Objects.equal(key, keys[next])) {
return next;
}
next = getNext(entry);
}
return -1;
}
public boolean containsKey(@NullableDecl Object key) {
return indexOf(key) != -1;
}
public int get(@NullableDecl Object key) {
int index = indexOf(key);
return (index == -1) ? 0 : values[index];
}
@CanIgnoreReturnValue
public int remove(@NullableDecl Object key) {
return remove(key, smearedHash(key));
}
private int remove(@NullableDecl Object key, int hash) {
int tableIndex = hash & hashTableMask();
int next = table[tableIndex];
if (next == UNSET) { // empty bucket
return 0;
}
int last = UNSET;
do {
if (getHash(entries[next]) == hash) {
if (Objects.equal(key, keys[next])) {
int oldValue = values[next];
if (last == UNSET) {
// we need to update the root link from table[]
table[tableIndex] = getNext(entries[next]);
} else {
// we need to update the link from the chain
entries[last] = swapNext(entries[last], getNext(entries[next]));
}
moveLastEntry(next);
size--;
modCount++;
return oldValue;
}
}
last = next;
next = getNext(entries[next]);
} while (next != UNSET);
return 0;
}
@CanIgnoreReturnValue
int removeEntry(int entryIndex) {
return remove(keys[entryIndex], getHash(entries[entryIndex]));
}
/**
* Moves the last entry in the entry array into {@code dstIndex}, and nulls out its old position.
*/
void moveLastEntry(int dstIndex) {
int srcIndex = size() - 1;
if (dstIndex < srcIndex) {
// move last entry to deleted spot
keys[dstIndex] = keys[srcIndex];
values[dstIndex] = values[srcIndex];
keys[srcIndex] = null;
values[srcIndex] = 0;
// move the last entry to the removed spot, just like we moved the element
long lastEntry = entries[srcIndex];
entries[dstIndex] = lastEntry;
entries[srcIndex] = UNSET;
// also need to update whoever's "next" pointer was pointing to the last entry place
// reusing "tableIndex" and "next"; these variables were no longer needed
int tableIndex = getHash(lastEntry) & hashTableMask();
int lastNext = table[tableIndex];
if (lastNext == srcIndex) {
// we need to update the root pointer
table[tableIndex] = dstIndex;
} else {
// we need to update a pointer in an entry
int previous;
long entry;
do {
previous = lastNext;
lastNext = getNext(entry = entries[lastNext]);
} while (lastNext != srcIndex);
// here, entries[previous] points to the old entry location; update it
entries[previous] = swapNext(entry, dstIndex);
}
} else {
keys[dstIndex] = null;
values[dstIndex] = 0;
entries[dstIndex] = UNSET;
}
}
public void clear() {
modCount++;
Arrays.fill(keys, 0, size, null);
Arrays.fill(values, 0, size, 0);
Arrays.fill(table, UNSET);
Arrays.fill(entries, UNSET);
this.size = 0;
}
}