com.google.common.collect.HashBiMap Maven / Gradle / Ivy
/*
* Copyright (C) 2007 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.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.CollectPreconditions.checkNonnegative;
import static com.google.common.collect.Hashing.smearedHash;
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.J2ktIncompatible;
import com.google.common.base.Objects;
import com.google.common.collect.Maps.IteratorBasedAbstractMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.concurrent.LazyInit;
import com.google.j2objc.annotations.RetainedWith;
import com.google.j2objc.annotations.Weak;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import javax.annotation.CheckForNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* A {@link BiMap} backed by two hash tables. This implementation allows null keys and values. A
* {@code HashBiMap} and its inverse are both serializable.
*
* This implementation guarantees insertion-based iteration order of its keys.
*
*
See the Guava User Guide article on {@code BiMap} .
*
* @author Louis Wasserman
* @author Mike Bostock
* @since 2.0
*/
@GwtCompatible(emulated = true)
@ElementTypesAreNonnullByDefault
public final class HashBiMap
extends IteratorBasedAbstractMap implements BiMap, Serializable {
/** Returns a new, empty {@code HashBiMap} with the default initial capacity (16). */
public static HashBiMap create() {
return create(16);
}
/**
* Constructs a new, empty bimap with the specified expected size.
*
* @param expectedSize the expected number of entries
* @throws IllegalArgumentException if the specified expected size is negative
*/
public static HashBiMap create(
int expectedSize) {
return new HashBiMap<>(expectedSize);
}
/**
* Constructs a new bimap containing initial values from {@code map}. The bimap is created with an
* initial capacity sufficient to hold the mappings in the specified map.
*/
public static HashBiMap create(
Map extends K, ? extends V> map) {
HashBiMap bimap = create(map.size());
bimap.putAll(map);
return bimap;
}
static final class BiEntry
extends ImmutableEntry {
final int keyHash;
final int valueHash;
// All BiEntry instances are strongly reachable from owning HashBiMap through
// "HashBiMap.hashTableKToV" and "BiEntry.nextInKToVBucket" references.
// Under that assumption, the remaining references can be safely marked as @Weak.
// Using @Weak is necessary to avoid retain-cycles between BiEntry instances on iOS,
// which would cause memory leaks when non-empty HashBiMap with cyclic BiEntry
// instances is deallocated.
@CheckForNull BiEntry nextInKToVBucket;
@Weak @CheckForNull BiEntry nextInVToKBucket;
@Weak @CheckForNull BiEntry nextInKeyInsertionOrder;
@Weak @CheckForNull BiEntry prevInKeyInsertionOrder;
BiEntry(@ParametricNullness K key, int keyHash, @ParametricNullness V value, int valueHash) {
super(key, value);
this.keyHash = keyHash;
this.valueHash = valueHash;
}
}
private static final double LOAD_FACTOR = 1.0;
/*
* The following two arrays may *contain* nulls, but they are never *themselves* null: Even though
* they are not initialized inline in the constructor, they are initialized from init(), which the
* constructor calls (as does readObject()).
*/
@SuppressWarnings("nullness:initialization.field.uninitialized") // For J2KT (see above)
private transient BiEntry[] hashTableKToV;
@SuppressWarnings("nullness:initialization.field.uninitialized") // For J2KT (see above)
private transient BiEntry[] hashTableVToK;
@Weak @CheckForNull private transient BiEntry firstInKeyInsertionOrder;
@Weak @CheckForNull private transient BiEntry lastInKeyInsertionOrder;
private transient int size;
private transient int mask;
private transient int modCount;
private HashBiMap(int expectedSize) {
init(expectedSize);
}
private void init(int expectedSize) {
checkNonnegative(expectedSize, "expectedSize");
int tableSize = Hashing.closedTableSize(expectedSize, LOAD_FACTOR);
this.hashTableKToV = createTable(tableSize);
this.hashTableVToK = createTable(tableSize);
this.firstInKeyInsertionOrder = null;
this.lastInKeyInsertionOrder = null;
this.size = 0;
this.mask = tableSize - 1;
this.modCount = 0;
}
/**
* Finds and removes {@code entry} from the bucket linked lists in both the key-to-value direction
* and the value-to-key direction.
*/
private void delete(BiEntry entry) {
int keyBucket = entry.keyHash & mask;
BiEntry prevBucketEntry = null;
for (BiEntry bucketEntry = hashTableKToV[keyBucket];
true;
bucketEntry = bucketEntry.nextInKToVBucket) {
if (bucketEntry == entry) {
if (prevBucketEntry == null) {
hashTableKToV[keyBucket] = entry.nextInKToVBucket;
} else {
prevBucketEntry.nextInKToVBucket = entry.nextInKToVBucket;
}
break;
}
prevBucketEntry = bucketEntry;
}
int valueBucket = entry.valueHash & mask;
prevBucketEntry = null;
for (BiEntry bucketEntry = hashTableVToK[valueBucket];
true;
bucketEntry = bucketEntry.nextInVToKBucket) {
if (bucketEntry == entry) {
if (prevBucketEntry == null) {
hashTableVToK[valueBucket] = entry.nextInVToKBucket;
} else {
prevBucketEntry.nextInVToKBucket = entry.nextInVToKBucket;
}
break;
}
prevBucketEntry = bucketEntry;
}
if (entry.prevInKeyInsertionOrder == null) {
firstInKeyInsertionOrder = entry.nextInKeyInsertionOrder;
} else {
entry.prevInKeyInsertionOrder.nextInKeyInsertionOrder = entry.nextInKeyInsertionOrder;
}
if (entry.nextInKeyInsertionOrder == null) {
lastInKeyInsertionOrder = entry.prevInKeyInsertionOrder;
} else {
entry.nextInKeyInsertionOrder.prevInKeyInsertionOrder = entry.prevInKeyInsertionOrder;
}
size--;
modCount++;
}
private void insert(BiEntry entry, @CheckForNull BiEntry oldEntryForKey) {
int keyBucket = entry.keyHash & mask;
entry.nextInKToVBucket = hashTableKToV[keyBucket];
hashTableKToV[keyBucket] = entry;
int valueBucket = entry.valueHash & mask;
entry.nextInVToKBucket = hashTableVToK[valueBucket];
hashTableVToK[valueBucket] = entry;
if (oldEntryForKey == null) {
entry.prevInKeyInsertionOrder = lastInKeyInsertionOrder;
entry.nextInKeyInsertionOrder = null;
if (lastInKeyInsertionOrder == null) {
firstInKeyInsertionOrder = entry;
} else {
lastInKeyInsertionOrder.nextInKeyInsertionOrder = entry;
}
lastInKeyInsertionOrder = entry;
} else {
entry.prevInKeyInsertionOrder = oldEntryForKey.prevInKeyInsertionOrder;
if (entry.prevInKeyInsertionOrder == null) {
firstInKeyInsertionOrder = entry;
} else {
entry.prevInKeyInsertionOrder.nextInKeyInsertionOrder = entry;
}
entry.nextInKeyInsertionOrder = oldEntryForKey.nextInKeyInsertionOrder;
if (entry.nextInKeyInsertionOrder == null) {
lastInKeyInsertionOrder = entry;
} else {
entry.nextInKeyInsertionOrder.prevInKeyInsertionOrder = entry;
}
}
size++;
modCount++;
}
@CheckForNull
private BiEntry seekByKey(@CheckForNull Object key, int keyHash) {
for (BiEntry entry = hashTableKToV[keyHash & mask];
entry != null;
entry = entry.nextInKToVBucket) {
if (keyHash == entry.keyHash && Objects.equal(key, entry.key)) {
return entry;
}
}
return null;
}
@CheckForNull
private BiEntry seekByValue(@CheckForNull Object value, int valueHash) {
for (BiEntry entry = hashTableVToK[valueHash & mask];
entry != null;
entry = entry.nextInVToKBucket) {
if (valueHash == entry.valueHash && Objects.equal(value, entry.value)) {
return entry;
}
}
return null;
}
@Override
public boolean containsKey(@CheckForNull Object key) {
return seekByKey(key, smearedHash(key)) != null;
}
/**
* Returns {@code true} if this BiMap contains an entry whose value is equal to {@code value} (or,
* equivalently, if this inverse view contains a key that is equal to {@code value}).
*
* Due to the property that values in a BiMap are unique, this will tend to execute in
* faster-than-linear time.
*
* @param value the object to search for in the values of this BiMap
* @return true if a mapping exists from a key to the specified value
*/
@Override
public boolean containsValue(@CheckForNull Object value) {
return seekByValue(value, smearedHash(value)) != null;
}
@Override
@CheckForNull
public V get(@CheckForNull Object key) {
return Maps.valueOrNull(seekByKey(key, smearedHash(key)));
}
@CanIgnoreReturnValue
@Override
@CheckForNull
public V put(@ParametricNullness K key, @ParametricNullness V value) {
return put(key, value, false);
}
@CheckForNull
private V put(@ParametricNullness K key, @ParametricNullness V value, boolean force) {
int keyHash = smearedHash(key);
int valueHash = smearedHash(value);
BiEntry oldEntryForKey = seekByKey(key, keyHash);
if (oldEntryForKey != null
&& valueHash == oldEntryForKey.valueHash
&& Objects.equal(value, oldEntryForKey.value)) {
return value;
}
BiEntry oldEntryForValue = seekByValue(value, valueHash);
if (oldEntryForValue != null) {
if (force) {
delete(oldEntryForValue);
} else {
throw new IllegalArgumentException("value already present: " + value);
}
}
BiEntry newEntry = new BiEntry<>(key, keyHash, value, valueHash);
if (oldEntryForKey != null) {
delete(oldEntryForKey);
insert(newEntry, oldEntryForKey);
oldEntryForKey.prevInKeyInsertionOrder = null;
oldEntryForKey.nextInKeyInsertionOrder = null;
return oldEntryForKey.value;
} else {
insert(newEntry, null);
rehashIfNecessary();
return null;
}
}
@CanIgnoreReturnValue
@Override
@CheckForNull
public V forcePut(@ParametricNullness K key, @ParametricNullness V value) {
return put(key, value, true);
}
@CanIgnoreReturnValue
@CheckForNull
private K putInverse(@ParametricNullness V value, @ParametricNullness K key, boolean force) {
int valueHash = smearedHash(value);
int keyHash = smearedHash(key);
BiEntry oldEntryForValue = seekByValue(value, valueHash);
BiEntry oldEntryForKey = seekByKey(key, keyHash);
if (oldEntryForValue != null
&& keyHash == oldEntryForValue.keyHash
&& Objects.equal(key, oldEntryForValue.key)) {
return key;
} else if (oldEntryForKey != null && !force) {
throw new IllegalArgumentException("key already present: " + key);
}
/*
* The ordering here is important: if we deleted the key entry and then the value entry,
* the key entry's prev or next pointer might point to the dead value entry, and when we
* put the new entry in the key entry's position in iteration order, it might invalidate
* the linked list.
*/
if (oldEntryForValue != null) {
delete(oldEntryForValue);
}
if (oldEntryForKey != null) {
delete(oldEntryForKey);
}
BiEntry newEntry = new BiEntry<>(key, keyHash, value, valueHash);
insert(newEntry, oldEntryForKey);
if (oldEntryForKey != null) {
oldEntryForKey.prevInKeyInsertionOrder = null;
oldEntryForKey.nextInKeyInsertionOrder = null;
}
if (oldEntryForValue != null) {
oldEntryForValue.prevInKeyInsertionOrder = null;
oldEntryForValue.nextInKeyInsertionOrder = null;
}
rehashIfNecessary();
return Maps.keyOrNull(oldEntryForValue);
}
private void rehashIfNecessary() {
BiEntry[] oldKToV = hashTableKToV;
if (Hashing.needsResizing(size, oldKToV.length, LOAD_FACTOR)) {
int newTableSize = oldKToV.length * 2;
this.hashTableKToV = createTable(newTableSize);
this.hashTableVToK = createTable(newTableSize);
this.mask = newTableSize - 1;
this.size = 0;
for (BiEntry entry = firstInKeyInsertionOrder;
entry != null;
entry = entry.nextInKeyInsertionOrder) {
insert(entry, entry);
}
this.modCount++;
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
private BiEntry[] createTable(int length) {
return new BiEntry[length];
}
@CanIgnoreReturnValue
@Override
@CheckForNull
public V remove(@CheckForNull Object key) {
BiEntry entry = seekByKey(key, smearedHash(key));
if (entry == null) {
return null;
} else {
delete(entry);
entry.prevInKeyInsertionOrder = null;
entry.nextInKeyInsertionOrder = null;
return entry.value;
}
}
@Override
public void clear() {
size = 0;
Arrays.fill(hashTableKToV, null);
Arrays.fill(hashTableVToK, null);
firstInKeyInsertionOrder = null;
lastInKeyInsertionOrder = null;
modCount++;
}
@Override
public int size() {
return size;
}
private abstract class Itr implements Iterator {
@CheckForNull BiEntry next = firstInKeyInsertionOrder;
@CheckForNull BiEntry toRemove = null;
int expectedModCount = modCount;
int remaining = size();
@Override
public boolean hasNext() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
return next != null && remaining > 0;
}
@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
// requireNonNull is safe because of the hasNext check.
BiEntry entry = requireNonNull(next);
next = entry.nextInKeyInsertionOrder;
toRemove = entry;
remaining--;
return output(entry);
}
@Override
public void remove() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
if (toRemove == null) {
throw new IllegalStateException("no calls to next() since the last call to remove()");
}
delete(toRemove);
expectedModCount = modCount;
toRemove = null;
}
abstract T output(BiEntry entry);
}
@Override
public Set keySet() {
return new KeySet();
}
private final class KeySet extends Maps.KeySet {
KeySet() {
super(HashBiMap.this);
}
@Override
public Iterator iterator() {
return new Itr() {
@Override
@ParametricNullness
K output(BiEntry entry) {
return entry.key;
}
};
}
@Override
public boolean remove(@CheckForNull Object o) {
BiEntry entry = seekByKey(o, smearedHash(o));
if (entry == null) {
return false;
} else {
delete(entry);
entry.prevInKeyInsertionOrder = null;
entry.nextInKeyInsertionOrder = null;
return true;
}
}
}
@Override
public Set values() {
return inverse().keySet();
}
@Override
Iterator> entryIterator() {
return new Itr>() {
@Override
Entry output(BiEntry entry) {
return new MapEntry(entry);
}
class MapEntry extends AbstractMapEntry {
private BiEntry delegate;
MapEntry(BiEntry entry) {
this.delegate = entry;
}
@Override
@ParametricNullness
public K getKey() {
return delegate.key;
}
@Override
@ParametricNullness
public V getValue() {
return delegate.value;
}
@Override
@ParametricNullness
public V setValue(@ParametricNullness V value) {
V oldValue = delegate.value;
int valueHash = smearedHash(value);
if (valueHash == delegate.valueHash && Objects.equal(value, oldValue)) {
return value;
}
checkArgument(seekByValue(value, valueHash) == null, "value already present: %s", value);
delete(delegate);
BiEntry newEntry = new BiEntry<>(delegate.key, delegate.keyHash, value, valueHash);
insert(newEntry, delegate);
delegate.prevInKeyInsertionOrder = null;
delegate.nextInKeyInsertionOrder = null;
expectedModCount = modCount;
if (toRemove == delegate) {
toRemove = newEntry;
}
delegate = newEntry;
return oldValue;
}
}
};
}
@Override
public void forEach(BiConsumer super K, ? super V> action) {
checkNotNull(action);
for (BiEntry entry = firstInKeyInsertionOrder;
entry != null;
entry = entry.nextInKeyInsertionOrder) {
action.accept(entry.key, entry.value);
}
}
@Override
public void replaceAll(BiFunction super K, ? super V, ? extends V> function) {
checkNotNull(function);
BiEntry oldFirst = firstInKeyInsertionOrder;
clear();
for (BiEntry entry = oldFirst; entry != null; entry = entry.nextInKeyInsertionOrder) {
put(entry.key, function.apply(entry.key, entry.value));
}
}
@LazyInit @RetainedWith @CheckForNull private transient BiMap inverse;
@Override
public BiMap inverse() {
BiMap result = inverse;
return (result == null) ? inverse = new Inverse() : result;
}
private final class Inverse extends IteratorBasedAbstractMap
implements BiMap, Serializable {
BiMap forward() {
return HashBiMap.this;
}
@Override
public int size() {
return size;
}
@Override
public void clear() {
forward().clear();
}
@Override
public boolean containsKey(@CheckForNull Object value) {
return forward().containsValue(value);
}
@Override
@CheckForNull
public K get(@CheckForNull Object value) {
return Maps.keyOrNull(seekByValue(value, smearedHash(value)));
}
@CanIgnoreReturnValue
@Override
@CheckForNull
public K put(@ParametricNullness V value, @ParametricNullness K key) {
return putInverse(value, key, false);
}
@Override
@CheckForNull
public K forcePut(@ParametricNullness V value, @ParametricNullness K key) {
return putInverse(value, key, true);
}
@Override
@CheckForNull
public K remove(@CheckForNull Object value) {
BiEntry entry = seekByValue(value, smearedHash(value));
if (entry == null) {
return null;
} else {
delete(entry);
entry.prevInKeyInsertionOrder = null;
entry.nextInKeyInsertionOrder = null;
return entry.key;
}
}
@Override
public BiMap inverse() {
return forward();
}
@Override
public Set keySet() {
return new InverseKeySet();
}
private final class InverseKeySet extends Maps.KeySet {
InverseKeySet() {
super(Inverse.this);
}
@Override
public boolean remove(@CheckForNull Object o) {
BiEntry entry = seekByValue(o, smearedHash(o));
if (entry == null) {
return false;
} else {
delete(entry);
return true;
}
}
@Override
public Iterator iterator() {
return new Itr() {
@Override
@ParametricNullness
V output(BiEntry entry) {
return entry.value;
}
};
}
}
@Override
public Set values() {
return forward().keySet();
}
@Override
Iterator> entryIterator() {
return new Itr>() {
@Override
Entry output(BiEntry entry) {
return new InverseEntry(entry);
}
class InverseEntry extends AbstractMapEntry {
private BiEntry delegate;
InverseEntry(BiEntry entry) {
this.delegate = entry;
}
@Override
@ParametricNullness
public V getKey() {
return delegate.value;
}
@Override
@ParametricNullness
public K getValue() {
return delegate.key;
}
@Override
@ParametricNullness
public K setValue(@ParametricNullness K key) {
K oldKey = delegate.key;
int keyHash = smearedHash(key);
if (keyHash == delegate.keyHash && Objects.equal(key, oldKey)) {
return key;
}
checkArgument(seekByKey(key, keyHash) == null, "value already present: %s", key);
delete(delegate);
BiEntry newEntry =
new BiEntry<>(key, keyHash, delegate.value, delegate.valueHash);
delegate = newEntry;
insert(newEntry, null);
expectedModCount = modCount;
return oldKey;
}
}
};
}
@Override
public void forEach(BiConsumer super V, ? super K> action) {
checkNotNull(action);
HashBiMap.this.forEach((k, v) -> action.accept(v, k));
}
@Override
public void replaceAll(BiFunction super V, ? super K, ? extends K> function) {
checkNotNull(function);
BiEntry oldFirst = firstInKeyInsertionOrder;
clear();
for (BiEntry entry = oldFirst; entry != null; entry = entry.nextInKeyInsertionOrder) {
put(entry.value, function.apply(entry.value, entry.key));
}
}
Object writeReplace() {
return new InverseSerializedForm<>(HashBiMap.this);
}
@GwtIncompatible // serialization
@J2ktIncompatible
private void readObject(ObjectInputStream in) throws InvalidObjectException {
throw new InvalidObjectException("Use InverseSerializedForm");
}
}
private static final class InverseSerializedForm<
K extends Object, V extends Object>
implements Serializable {
private final HashBiMap bimap;
InverseSerializedForm(HashBiMap bimap) {
this.bimap = bimap;
}
Object readResolve() {
return bimap.inverse();
}
}
/**
* @serialData the number of entries, first key, first value, second key, second value, and so on.
*/
@GwtIncompatible // java.io.ObjectOutputStream
@J2ktIncompatible
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
Serialization.writeMap(this, stream);
}
@GwtIncompatible // java.io.ObjectInputStream
@J2ktIncompatible
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
int size = Serialization.readCount(stream);
init(16); // resist hostile attempts to allocate gratuitous heap
Serialization.populateMap(this, stream, size);
}
@GwtIncompatible // Not needed in emulated source
@J2ktIncompatible
private static final long serialVersionUID = 0;
}