blob: fc8082c7bbb28af989e23700331741b9e03f6df1 [file] [log] [blame]
/*
* Copyright (C) 2008 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.base.Preconditions.checkPositionIndex;
import static com.google.common.collect.CollectPreconditions.checkEntryNotNull;
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.VisibleForTesting;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Map.Entry;
import javax.annotation.CheckForNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* A hash-based implementation of {@link ImmutableMap}.
*
* @author Louis Wasserman
*/
@GwtCompatible(serializable = true, emulated = true)
@ElementTypesAreNonnullByDefault
final class RegularImmutableMap<K, V> extends ImmutableMap<K, V> {
private static final byte ABSENT = -1;
// Max size is halved due to indexing into double-sized alternatingKeysAndValues
private static final int BYTE_MAX_SIZE = 1 << (Byte.SIZE - 1); // 2^7 = 128
private static final int SHORT_MAX_SIZE = 1 << (Short.SIZE - 1); // 2^15 = 32_768
private static final int BYTE_MASK = (1 << Byte.SIZE) - 1; // 2^8 - 1 = 255
private static final int SHORT_MASK = (1 << Short.SIZE) - 1; // 2^16 - 1 = 65_535
@SuppressWarnings("unchecked")
static final ImmutableMap<Object, Object> EMPTY =
new RegularImmutableMap<>(null, new Object[0], 0);
/*
* This is an implementation of ImmutableMap optimized especially for Android, which does not like
* objects per entry. Instead we use an open-addressed hash table. This design is basically
* equivalent to RegularImmutableSet, save that instead of having a hash table containing the
* elements directly and null for empty positions, we store indices of the keys in the hash table,
* and ABSENT for empty positions. We then look up the keys in alternatingKeysAndValues.
*
* (The index actually stored is the index of the key in alternatingKeysAndValues, which is
* double the index of the entry in entrySet.asList.)
*
* The basic data structure is described in https://en.wikipedia.org/wiki/Open_addressing.
* The pointer to a key is stored in hashTable[Hashing.smear(key.hashCode()) % table.length],
* save that if that location is already full, we try the next index, and the next, until we
* find an empty table position. Since the table has a power-of-two size, we use
* & (table.length - 1) instead of % table.length, though.
*/
@CheckForNull private final transient Object hashTable;
@VisibleForTesting final transient @Nullable Object[] alternatingKeysAndValues;
private final transient int size;
/*
* We have some considerable complexity in these create methods because of
* Builder.buildKeepingLast(). The same Builder might be called with buildKeepingLast() and then
* buildOrThrow(), or vice versa. So in particular, if we modify alternatingKeysAndValues to
* eliminate duplicate keys (for buildKeepingLast()) then we have to ensure that a later call to
* buildOrThrow() will still throw as if the duplicates had not been eliminated. And the exception
* message must mention two values that were associated with the duplicate key in two different
* calls to Builder.put (though we don't really care *which* two values if there were more than
* two). These considerations lead us to have a field of type DuplicateKey in the Builder, which
* will remember the first duplicate key we encountered. All later calls to buildOrThrow() can
* mention that key with its values. Further duplicates might be added in the meantime but since
* builders only ever accumulate entries it will always be valid to throw from buildOrThrow() with
* the first duplicate.
*/
// This entry point is for callers other than ImmutableMap.Builder.
static <K, V> RegularImmutableMap<K, V> create(
int n, @Nullable Object[] alternatingKeysAndValues) {
return create(n, alternatingKeysAndValues, /* builder= */ null);
}
// This entry point is used by the other create method but also directly by
// ImmutableMap.Builder, so that it can remember any DuplicateKey encountered and produce an
// exception for a later buildOrThrow(). If builder is null that means that a duplicate
// key will lead to an immediate exception. If it is not null then a duplicate key will instead be
// stored in the builder, which may use it to throw an exception later.
static <K, V> RegularImmutableMap<K, V> create(
int n, @Nullable Object[] alternatingKeysAndValues, @Nullable Builder<K, V> builder) {
if (n == 0) {
@SuppressWarnings("unchecked")
RegularImmutableMap<K, V> empty = (RegularImmutableMap<K, V>) EMPTY;
return empty;
} else if (n == 1) {
// requireNonNull is safe because the first `2*n` elements have been filled in.
checkEntryNotNull(
requireNonNull(alternatingKeysAndValues[0]), requireNonNull(alternatingKeysAndValues[1]));
return new RegularImmutableMap<K, V>(null, alternatingKeysAndValues, 1);
}
checkPositionIndex(n, alternatingKeysAndValues.length >> 1);
int tableSize = ImmutableSet.chooseTableSize(n);
// If there are no duplicate keys, hashTablePlus is the final hashTable value. If there *are*
// duplicate keys, hashTablePlus consists of 3 elements: [0] the hashTable; [1] the number of
// entries in alternatingKeysAndValues that are still valid after rewriting to remove
// duplicates; [2] a Builder.DuplicateKey that records the first duplicate key we encountered
// for possible later use in exceptions, perhaps straight away.
Object hashTablePlus = createHashTable(alternatingKeysAndValues, n, tableSize, 0);
Object hashTable;
if (hashTablePlus instanceof Object[]) {
Object[] hashTableAndSizeAndDuplicate = (Object[]) hashTablePlus;
Builder.DuplicateKey duplicateKey = (Builder.DuplicateKey) hashTableAndSizeAndDuplicate[2];
if (builder == null) {
throw duplicateKey.exception();
}
builder.duplicateKey = duplicateKey;
hashTable = hashTableAndSizeAndDuplicate[0];
n = (Integer) hashTableAndSizeAndDuplicate[1];
alternatingKeysAndValues = Arrays.copyOf(alternatingKeysAndValues, n * 2);
} else {
hashTable = hashTablePlus;
}
return new RegularImmutableMap<K, V>(hashTable, alternatingKeysAndValues, n);
}
/**
* Returns a hash table for the specified keys and values, and ensures that neither keys nor
* values are null. This method may update {@code alternatingKeysAndValues} if there are duplicate
* keys. If so, the return value will indicate how many entries are still valid, and will also
* include a {@link Builder.DuplicateKey} in case duplicate keys are not allowed now or will not
* be allowed on a later {@link Builder#buildOrThrow()} call.
*
* @param keyOffset 1 if this is the reverse direction of a BiMap, 0 otherwise.
* @return an {@code Object} that is a {@code byte[]}, {@code short[]}, or {@code int[]}, the
* smallest possible to fit {@code tableSize}; or an {@code Object[]} where [0] is one of
* these; [1] indicates how many element pairs in {@code alternatingKeysAndValues} are valid;
* and [2] is a {@link Builder.DuplicateKey} for the first duplicate key encountered.
*/
@CheckForNull
private static Object createHashTable(
@Nullable Object[] alternatingKeysAndValues, int n, int tableSize, int keyOffset) {
if (n == 1) {
// for n=1 we don't create a hash table, but we need to do the checkEntryNotNull check!
// requireNonNull is safe because the first `2*n` elements have been filled in.
checkEntryNotNull(
requireNonNull(alternatingKeysAndValues[keyOffset]),
requireNonNull(alternatingKeysAndValues[keyOffset ^ 1]));
return null;
}
int mask = tableSize - 1;
Builder.DuplicateKey duplicateKey = null;
if (tableSize <= BYTE_MAX_SIZE) {
/*
* Use 8 bits per entry. The value is unsigned to allow use up to a size of 2^8.
*
* The absent indicator of -1 signed becomes 2^8 - 1 unsigned, which reduces the actual max
* size to 2^8 - 1. However, due to a load factor < 1 the limit is never approached.
*/
byte[] hashTable = new byte[tableSize];
Arrays.fill(hashTable, ABSENT);
int outI = 0;
entries:
for (int i = 0; i < n; i++) {
int keyIndex = 2 * i + keyOffset;
int outKeyIndex = 2 * outI + keyOffset;
// requireNonNull is safe because the first `2*n` elements have been filled in.
Object key = requireNonNull(alternatingKeysAndValues[keyIndex]);
Object value = requireNonNull(alternatingKeysAndValues[keyIndex ^ 1]);
checkEntryNotNull(key, value);
for (int h = Hashing.smear(key.hashCode()); ; h++) {
h &= mask;
int previousKeyIndex = hashTable[h] & BYTE_MASK; // unsigned read
if (previousKeyIndex == BYTE_MASK) { // -1 signed becomes 255 unsigned
hashTable[h] = (byte) outKeyIndex;
break;
} else if (key.equals(alternatingKeysAndValues[previousKeyIndex])) {
duplicateKey =
new Builder.DuplicateKey(
key, value, requireNonNull(alternatingKeysAndValues[previousKeyIndex ^ 1]));
alternatingKeysAndValues[previousKeyIndex ^ 1] = value;
continue entries;
}
}
if (outI < i) { // if outI == i don't bother writing the values back where they came from
alternatingKeysAndValues[outKeyIndex] = key;
alternatingKeysAndValues[outKeyIndex ^ 1] = value;
}
outI++;
}
return outI == n ? hashTable : new Object[] {hashTable, outI, duplicateKey};
} else if (tableSize <= SHORT_MAX_SIZE) {
/*
* Use 16 bits per entry. The value is unsigned to allow use up to a size of 2^16.
*
* The absent indicator of -1 signed becomes 2^16 - 1 unsigned, which reduces the actual max
* size to 2^16 - 1. However, due to a load factor < 1 the limit is never approached.
*/
short[] hashTable = new short[tableSize];
Arrays.fill(hashTable, ABSENT);
int outI = 0;
entries:
for (int i = 0; i < n; i++) {
int keyIndex = 2 * i + keyOffset;
int outKeyIndex = 2 * outI + keyOffset;
// requireNonNull is safe because the first `2*n` elements have been filled in.
Object key = requireNonNull(alternatingKeysAndValues[keyIndex]);
Object value = requireNonNull(alternatingKeysAndValues[keyIndex ^ 1]);
checkEntryNotNull(key, value);
for (int h = Hashing.smear(key.hashCode()); ; h++) {
h &= mask;
int previousKeyIndex = hashTable[h] & SHORT_MASK; // unsigned read
if (previousKeyIndex == SHORT_MASK) { // -1 signed becomes 65_535 unsigned
hashTable[h] = (short) outKeyIndex;
break;
} else if (key.equals(alternatingKeysAndValues[previousKeyIndex])) {
duplicateKey =
new Builder.DuplicateKey(
key, value, requireNonNull(alternatingKeysAndValues[previousKeyIndex ^ 1]));
alternatingKeysAndValues[previousKeyIndex ^ 1] = value;
continue entries;
}
}
if (outI < i) { // if outI == i don't bother writing the values back where they came from
alternatingKeysAndValues[outKeyIndex] = key;
alternatingKeysAndValues[outKeyIndex ^ 1] = value;
}
outI++;
}
return outI == n ? hashTable : new Object[] {hashTable, outI, duplicateKey};
} else {
/*
* Use 32 bits per entry.
*/
int[] hashTable = new int[tableSize];
Arrays.fill(hashTable, ABSENT);
int outI = 0;
entries:
for (int i = 0; i < n; i++) {
int keyIndex = 2 * i + keyOffset;
int outKeyIndex = 2 * outI + keyOffset;
// requireNonNull is safe because the first `2*n` elements have been filled in.
Object key = requireNonNull(alternatingKeysAndValues[keyIndex]);
Object value = requireNonNull(alternatingKeysAndValues[keyIndex ^ 1]);
checkEntryNotNull(key, value);
for (int h = Hashing.smear(key.hashCode()); ; h++) {
h &= mask;
int previousKeyIndex = hashTable[h];
if (previousKeyIndex == ABSENT) {
hashTable[h] = outKeyIndex;
break;
} else if (key.equals(alternatingKeysAndValues[previousKeyIndex])) {
duplicateKey =
new Builder.DuplicateKey(
key, value, requireNonNull(alternatingKeysAndValues[previousKeyIndex ^ 1]));
alternatingKeysAndValues[previousKeyIndex ^ 1] = value;
continue entries;
}
}
if (outI < i) { // if outI == i don't bother writing the values back where they came from
alternatingKeysAndValues[outKeyIndex] = key;
alternatingKeysAndValues[outKeyIndex ^ 1] = value;
}
outI++;
}
return outI == n ? hashTable : new Object[] {hashTable, outI, duplicateKey};
}
}
@CheckForNull
static Object createHashTableOrThrow(
@Nullable Object[] alternatingKeysAndValues, int n, int tableSize, int keyOffset) {
Object hashTablePlus = createHashTable(alternatingKeysAndValues, n, tableSize, keyOffset);
if (hashTablePlus instanceof Object[]) {
Object[] hashTableAndSizeAndDuplicate = (Object[]) hashTablePlus;
Builder.DuplicateKey duplicateKey = (Builder.DuplicateKey) hashTableAndSizeAndDuplicate[2];
throw duplicateKey.exception();
}
return hashTablePlus;
}
private RegularImmutableMap(
@CheckForNull Object hashTable, @Nullable Object[] alternatingKeysAndValues, int size) {
this.hashTable = hashTable;
this.alternatingKeysAndValues = alternatingKeysAndValues;
this.size = size;
}
@Override
public int size() {
return size;
}
@SuppressWarnings("unchecked")
@Override
@CheckForNull
public V get(@CheckForNull Object key) {
Object result = get(hashTable, alternatingKeysAndValues, size, 0, key);
/*
* We can't simply cast the result of `RegularImmutableMap.get` to V because of a bug in our
* nullness checker (resulting from https://github.com/jspecify/checker-framework/issues/8).
*/
if (result == null) {
return null;
} else {
return (V) result;
}
}
@CheckForNull
static Object get(
@CheckForNull Object hashTableObject,
@Nullable Object[] alternatingKeysAndValues,
int size,
int keyOffset,
@CheckForNull Object key) {
if (key == null) {
return null;
} else if (size == 1) {
// requireNonNull is safe because the first 2 elements have been filled in.
return requireNonNull(alternatingKeysAndValues[keyOffset]).equals(key)
? requireNonNull(alternatingKeysAndValues[keyOffset ^ 1])
: null;
} else if (hashTableObject == null) {
return null;
}
if (hashTableObject instanceof byte[]) {
byte[] hashTable = (byte[]) hashTableObject;
int mask = hashTable.length - 1;
for (int h = Hashing.smear(key.hashCode()); ; h++) {
h &= mask;
int keyIndex = hashTable[h] & BYTE_MASK; // unsigned read
if (keyIndex == BYTE_MASK) { // -1 signed becomes 255 unsigned
return null;
} else if (key.equals(alternatingKeysAndValues[keyIndex])) {
return alternatingKeysAndValues[keyIndex ^ 1];
}
}
} else if (hashTableObject instanceof short[]) {
short[] hashTable = (short[]) hashTableObject;
int mask = hashTable.length - 1;
for (int h = Hashing.smear(key.hashCode()); ; h++) {
h &= mask;
int keyIndex = hashTable[h] & SHORT_MASK; // unsigned read
if (keyIndex == SHORT_MASK) { // -1 signed becomes 65_535 unsigned
return null;
} else if (key.equals(alternatingKeysAndValues[keyIndex])) {
return alternatingKeysAndValues[keyIndex ^ 1];
}
}
} else {
int[] hashTable = (int[]) hashTableObject;
int mask = hashTable.length - 1;
for (int h = Hashing.smear(key.hashCode()); ; h++) {
h &= mask;
int keyIndex = hashTable[h];
if (keyIndex == ABSENT) {
return null;
} else if (key.equals(alternatingKeysAndValues[keyIndex])) {
return alternatingKeysAndValues[keyIndex ^ 1];
}
}
}
}
@Override
ImmutableSet<Entry<K, V>> createEntrySet() {
return new EntrySet<>(this, alternatingKeysAndValues, 0, size);
}
static class EntrySet<K, V> extends ImmutableSet<Entry<K, V>> {
private final transient ImmutableMap<K, V> map;
private final transient @Nullable Object[] alternatingKeysAndValues;
private final transient int keyOffset;
private final transient int size;
EntrySet(
ImmutableMap<K, V> map,
@Nullable Object[] alternatingKeysAndValues,
int keyOffset,
int size) {
this.map = map;
this.alternatingKeysAndValues = alternatingKeysAndValues;
this.keyOffset = keyOffset;
this.size = size;
}
@Override
public UnmodifiableIterator<Entry<K, V>> iterator() {
return asList().iterator();
}
@Override
int copyIntoArray(Object[] dst, int offset) {
return asList().copyIntoArray(dst, offset);
}
@Override
ImmutableList<Entry<K, V>> createAsList() {
return new ImmutableList<Entry<K, V>>() {
@Override
public Entry<K, V> get(int index) {
checkElementIndex(index, size);
/*
* requireNonNull is safe because the first `2*(size+keyOffset)` elements have been filled
* in.
*/
@SuppressWarnings("unchecked")
K key = (K) requireNonNull(alternatingKeysAndValues[2 * index + keyOffset]);
@SuppressWarnings("unchecked")
V value = (V) requireNonNull(alternatingKeysAndValues[2 * index + (keyOffset ^ 1)]);
return new AbstractMap.SimpleImmutableEntry<K, V>(key, value);
}
@Override
public int size() {
return size;
}
@Override
public boolean isPartialView() {
return true;
}
};
}
@Override
public boolean contains(@CheckForNull Object object) {
if (object instanceof Entry) {
Entry<?, ?> entry = (Entry<?, ?>) object;
Object k = entry.getKey();
Object v = entry.getValue();
return v != null && v.equals(map.get(k));
}
return false;
}
@Override
boolean isPartialView() {
return true;
}
@Override
public int size() {
return size;
}
}
@Override
ImmutableSet<K> createKeySet() {
@SuppressWarnings("unchecked")
ImmutableList<K> keyList =
(ImmutableList<K>) new KeysOrValuesAsList(alternatingKeysAndValues, 0, size);
return new KeySet<K>(this, keyList);
}
static final class KeysOrValuesAsList extends ImmutableList<Object> {
private final transient @Nullable Object[] alternatingKeysAndValues;
private final transient int offset;
private final transient int size;
KeysOrValuesAsList(@Nullable Object[] alternatingKeysAndValues, int offset, int size) {
this.alternatingKeysAndValues = alternatingKeysAndValues;
this.offset = offset;
this.size = size;
}
@Override
public Object get(int index) {
checkElementIndex(index, size);
// requireNonNull is safe because the first `2*(size+offset)` elements have been filled in.
return requireNonNull(alternatingKeysAndValues[2 * index + offset]);
}
@Override
boolean isPartialView() {
return true;
}
@Override
public int size() {
return size;
}
}
static final class KeySet<K> extends ImmutableSet<K> {
private final transient ImmutableMap<K, ?> map;
private final transient ImmutableList<K> list;
KeySet(ImmutableMap<K, ?> map, ImmutableList<K> list) {
this.map = map;
this.list = list;
}
@Override
public UnmodifiableIterator<K> iterator() {
return asList().iterator();
}
@Override
int copyIntoArray(Object[] dst, int offset) {
return asList().copyIntoArray(dst, offset);
}
@Override
public ImmutableList<K> asList() {
return list;
}
@Override
public boolean contains(@CheckForNull Object object) {
return map.get(object) != null;
}
@Override
boolean isPartialView() {
return true;
}
@Override
public int size() {
return map.size();
}
}
@SuppressWarnings("unchecked")
@Override
ImmutableCollection<V> createValues() {
return (ImmutableList<V>) new KeysOrValuesAsList(alternatingKeysAndValues, 1, size);
}
@Override
boolean isPartialView() {
return false;
}
// This class is never actually serialized directly, but we have to make the
// warning go away (and suppressing would suppress for all nested classes too)
private static final long serialVersionUID = 0;
}