blob: 051b90382b6641dc81e2609a7af55748da60995f [file] [log] [blame]
/*
* 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 com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.base.Objects;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.j2objc.annotations.RetainedWith;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.AbstractMap;
import java.util.AbstractSet;
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 org.checkerframework.checker.nullness.compatqual.MonotonicNonNullDecl;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
/**
* A {@link BiMap} backed by two hash tables. This implementation allows null keys and values. A
* {@code HashBiMap} and its inverse are both serializable.
*
* <p>This implementation guarantees insertion-based iteration order of its keys.
*
* <p>See the Guava User Guide article on <a href=
* "https://github.com/google/guava/wiki/NewCollectionTypesExplained#bimap"> {@code BiMap} </a>.
*
* @author Louis Wasserman
* @author Mike Bostock
* @since 2.0
*/
@GwtCompatible
public final class HashBiMap<K, V> extends AbstractMap<K, V> implements BiMap<K, V>, Serializable {
/** Returns a new, empty {@code HashBiMap} with the default initial capacity (16). */
public static <K, V> HashBiMap<K, V> 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 <K, V> HashBiMap<K, V> 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 <K, V> HashBiMap<K, V> create(Map<? extends K, ? extends V> map) {
HashBiMap<K, V> bimap = create(map.size());
bimap.putAll(map);
return bimap;
}
private static final int ABSENT = -1;
private static final int ENDPOINT = -2;
/** Maps an "entry" to the key of that entry. */
transient K[] keys;
/** Maps an "entry" to the value of that entry. */
transient V[] values;
transient int size;
transient int modCount;
/** Maps a bucket to the "entry" of its first element. */
private transient int[] hashTableKToV;
/** Maps a bucket to the "entry" of its first element. */
private transient int[] hashTableVToK;
/** Maps an "entry" to the "entry" that follows it in its bucket. */
private transient int[] nextInBucketKToV;
/** Maps an "entry" to the "entry" that follows it in its bucket. */
private transient int[] nextInBucketVToK;
/** The "entry" of the first element in insertion order. */
@NullableDecl private transient int firstInInsertionOrder;
/** The "entry" of the last element in insertion order. */
@NullableDecl private transient int lastInInsertionOrder;
/** Maps an "entry" to the "entry" that precedes it in insertion order. */
private transient int[] prevInInsertionOrder;
/** Maps an "entry" to the "entry" that follows it in insertion order. */
private transient int[] nextInInsertionOrder;
private HashBiMap(int expectedSize) {
init(expectedSize);
}
@SuppressWarnings("unchecked")
void init(int expectedSize) {
CollectPreconditions.checkNonnegative(expectedSize, "expectedSize");
int tableSize = Hashing.closedTableSize(expectedSize, 1.0);
size = 0;
keys = (K[]) new Object[expectedSize];
values = (V[]) new Object[expectedSize];
hashTableKToV = createFilledWithAbsent(tableSize);
hashTableVToK = createFilledWithAbsent(tableSize);
nextInBucketKToV = createFilledWithAbsent(expectedSize);
nextInBucketVToK = createFilledWithAbsent(expectedSize);
firstInInsertionOrder = ENDPOINT;
lastInInsertionOrder = ENDPOINT;
prevInInsertionOrder = createFilledWithAbsent(expectedSize);
nextInInsertionOrder = createFilledWithAbsent(expectedSize);
}
/** Returns an int array of the specified size, filled with ABSENT. */
private static int[] createFilledWithAbsent(int size) {
int[] array = new int[size];
Arrays.fill(array, ABSENT);
return array;
}
/** Equivalent to {@code Arrays.copyOf(array, newSize)}, save that the new elements are ABSENT. */
private static int[] expandAndFillWithAbsent(int[] array, int newSize) {
int oldSize = array.length;
int[] result = Arrays.copyOf(array, newSize);
Arrays.fill(result, oldSize, newSize, ABSENT);
return result;
}
@Override
public int size() {
return size;
}
/**
* Ensures that all of the internal structures in the HashBiMap are ready for this many elements.
*/
private void ensureCapacity(int minCapacity) {
if (nextInBucketKToV.length < minCapacity) {
int oldCapacity = nextInBucketKToV.length;
int newCapacity = ImmutableCollection.Builder.expandedCapacity(oldCapacity, minCapacity);
keys = Arrays.copyOf(keys, newCapacity);
values = Arrays.copyOf(values, newCapacity);
nextInBucketKToV = expandAndFillWithAbsent(nextInBucketKToV, newCapacity);
nextInBucketVToK = expandAndFillWithAbsent(nextInBucketVToK, newCapacity);
prevInInsertionOrder = expandAndFillWithAbsent(prevInInsertionOrder, newCapacity);
nextInInsertionOrder = expandAndFillWithAbsent(nextInInsertionOrder, newCapacity);
}
if (hashTableKToV.length < minCapacity) {
int newTableSize = Hashing.closedTableSize(minCapacity, 1.0);
hashTableKToV = createFilledWithAbsent(newTableSize);
hashTableVToK = createFilledWithAbsent(newTableSize);
for (int entryToRehash = 0; entryToRehash < size; entryToRehash++) {
int keyHash = Hashing.smearedHash(keys[entryToRehash]);
int keyBucket = bucket(keyHash);
nextInBucketKToV[entryToRehash] = hashTableKToV[keyBucket];
hashTableKToV[keyBucket] = entryToRehash;
int valueHash = Hashing.smearedHash(values[entryToRehash]);
int valueBucket = bucket(valueHash);
nextInBucketVToK[entryToRehash] = hashTableVToK[valueBucket];
hashTableVToK[valueBucket] = entryToRehash;
}
}
}
/**
* Returns the bucket (in either the K-to-V or V-to-K tables) where elements with the specified
* hash could be found, if present, or could be inserted.
*/
private int bucket(int hash) {
return hash & (hashTableKToV.length - 1);
}
/** Given a key, returns the index of the entry in the tables, or ABSENT if not found. */
int findEntryByKey(@NullableDecl Object key) {
return findEntryByKey(key, Hashing.smearedHash(key));
}
/**
* Given a key and its hash, returns the index of the entry in the tables, or ABSENT if not found.
*/
int findEntryByKey(@NullableDecl Object key, int keyHash) {
return findEntry(key, keyHash, hashTableKToV, nextInBucketKToV, keys);
}
/** Given a value, returns the index of the entry in the tables, or ABSENT if not found. */
int findEntryByValue(@NullableDecl Object value) {
return findEntryByValue(value, Hashing.smearedHash(value));
}
/**
* Given a value and its hash, returns the index of the entry in the tables, or ABSENT if not
* found.
*/
int findEntryByValue(@NullableDecl Object value, int valueHash) {
return findEntry(value, valueHash, hashTableVToK, nextInBucketVToK, values);
}
int findEntry(
@NullableDecl Object o, int oHash, int[] hashTable, int[] nextInBucket, Object[] array) {
for (int entry = hashTable[bucket(oHash)]; entry != ABSENT; entry = nextInBucket[entry]) {
if (Objects.equal(array[entry], o)) {
return entry;
}
}
return ABSENT;
}
@Override
public boolean containsKey(@NullableDecl Object key) {
return findEntryByKey(key) != ABSENT;
}
@Override
public boolean containsValue(@NullableDecl Object value) {
return findEntryByValue(value) != ABSENT;
}
@Override
@NullableDecl
public V get(@NullableDecl Object key) {
int entry = findEntryByKey(key);
return (entry == ABSENT) ? null : values[entry];
}
@NullableDecl
K getInverse(@NullableDecl Object value) {
int entry = findEntryByValue(value);
return (entry == ABSENT) ? null : keys[entry];
}
@Override
@CanIgnoreReturnValue
public V put(@NullableDecl K key, @NullableDecl V value) {
return put(key, value, false);
}
@NullableDecl
V put(@NullableDecl K key, @NullableDecl V value, boolean force) {
int keyHash = Hashing.smearedHash(key);
int entryForKey = findEntryByKey(key, keyHash);
if (entryForKey != ABSENT) {
V oldValue = values[entryForKey];
if (Objects.equal(oldValue, value)) {
return value;
} else {
replaceValueInEntry(entryForKey, value, force);
return oldValue;
}
}
int valueHash = Hashing.smearedHash(value);
int valueEntry = findEntryByValue(value, valueHash);
if (force) {
if (valueEntry != ABSENT) {
removeEntryValueHashKnown(valueEntry, valueHash);
}
} else {
checkArgument(valueEntry == ABSENT, "Value already present: %s", value);
}
ensureCapacity(size + 1);
keys[size] = key;
values[size] = value;
insertIntoTableKToV(size, keyHash);
insertIntoTableVToK(size, valueHash);
setSucceeds(lastInInsertionOrder, size);
setSucceeds(size, ENDPOINT);
size++;
modCount++;
return null;
}
@Override
@CanIgnoreReturnValue
@NullableDecl
public V forcePut(@NullableDecl K key, @NullableDecl V value) {
return put(key, value, true);
}
@NullableDecl
K putInverse(@NullableDecl V value, @NullableDecl K key, boolean force) {
int valueHash = Hashing.smearedHash(value);
int entryForValue = findEntryByValue(value, valueHash);
if (entryForValue != ABSENT) {
K oldKey = keys[entryForValue];
if (Objects.equal(oldKey, key)) {
return key;
} else {
replaceKeyInEntry(entryForValue, key, force);
return oldKey;
}
}
int predecessor = lastInInsertionOrder;
int keyHash = Hashing.smearedHash(key);
int keyEntry = findEntryByKey(key, keyHash);
if (force) {
if (keyEntry != ABSENT) {
predecessor = prevInInsertionOrder[keyEntry];
removeEntryKeyHashKnown(keyEntry, keyHash);
}
} else {
checkArgument(keyEntry == ABSENT, "Key already present: %s", key);
}
// insertion point for new entry is after predecessor
// note predecessor must still be a valid entry: either we deleted an entry that was *not*
// predecessor, or we didn't delete anything
ensureCapacity(size + 1);
keys[size] = key;
values[size] = value;
insertIntoTableKToV(size, keyHash);
insertIntoTableVToK(size, valueHash);
int successor =
(predecessor == ENDPOINT) ? firstInInsertionOrder : nextInInsertionOrder[predecessor];
setSucceeds(predecessor, size);
setSucceeds(size, successor);
size++;
modCount++;
return null;
}
/**
* Updates the pointers of the insertion order linked list so that {@code next} follows {@code
* prev}. {@code ENDPOINT} represents either the first or last entry in the entire map (as
* appropriate).
*/
private void setSucceeds(int prev, int next) {
if (prev == ENDPOINT) {
firstInInsertionOrder = next;
} else {
nextInInsertionOrder[prev] = next;
}
if (next == ENDPOINT) {
lastInInsertionOrder = prev;
} else {
prevInInsertionOrder[next] = prev;
}
}
/**
* Updates the K-to-V hash table to include the entry at the specified index, which is assumed to
* have not yet been added.
*/
private void insertIntoTableKToV(int entry, int keyHash) {
checkArgument(entry != ABSENT);
int keyBucket = bucket(keyHash);
nextInBucketKToV[entry] = hashTableKToV[keyBucket];
hashTableKToV[keyBucket] = entry;
}
/**
* Updates the V-to-K hash table to include the entry at the specified index, which is assumed to
* have not yet been added.
*/
private void insertIntoTableVToK(int entry, int valueHash) {
checkArgument(entry != ABSENT);
int valueBucket = bucket(valueHash);
nextInBucketVToK[entry] = hashTableVToK[valueBucket];
hashTableVToK[valueBucket] = entry;
}
/**
* Updates the K-to-V hash table to remove the entry at the specified index, which is assumed to
* be present. Does not update any other data structures.
*/
private void deleteFromTableKToV(int entry, int keyHash) {
checkArgument(entry != ABSENT);
int keyBucket = bucket(keyHash);
if (hashTableKToV[keyBucket] == entry) {
hashTableKToV[keyBucket] = nextInBucketKToV[entry];
nextInBucketKToV[entry] = ABSENT;
return;
}
int prevInBucket = hashTableKToV[keyBucket];
for (int entryInBucket = nextInBucketKToV[prevInBucket];
entryInBucket != ABSENT;
entryInBucket = nextInBucketKToV[entryInBucket]) {
if (entryInBucket == entry) {
nextInBucketKToV[prevInBucket] = nextInBucketKToV[entry];
nextInBucketKToV[entry] = ABSENT;
return;
}
prevInBucket = entryInBucket;
}
throw new AssertionError("Expected to find entry with key " + keys[entry]);
}
/**
* Updates the V-to-K hash table to remove the entry at the specified index, which is assumed to
* be present. Does not update any other data structures.
*/
private void deleteFromTableVToK(int entry, int valueHash) {
checkArgument(entry != ABSENT);
int valueBucket = bucket(valueHash);
if (hashTableVToK[valueBucket] == entry) {
hashTableVToK[valueBucket] = nextInBucketVToK[entry];
nextInBucketVToK[entry] = ABSENT;
return;
}
int prevInBucket = hashTableVToK[valueBucket];
for (int entryInBucket = nextInBucketVToK[prevInBucket];
entryInBucket != ABSENT;
entryInBucket = nextInBucketVToK[entryInBucket]) {
if (entryInBucket == entry) {
nextInBucketVToK[prevInBucket] = nextInBucketVToK[entry];
nextInBucketVToK[entry] = ABSENT;
return;
}
prevInBucket = entryInBucket;
}
throw new AssertionError("Expected to find entry with value " + values[entry]);
}
/**
* Updates the specified entry to point to the new value: removes the old value from the V-to-K
* mapping and puts the new one in. The entry does not move in the insertion order of the bimap.
*/
private void replaceValueInEntry(int entry, @NullableDecl V newValue, boolean force) {
checkArgument(entry != ABSENT);
int newValueHash = Hashing.smearedHash(newValue);
int newValueIndex = findEntryByValue(newValue, newValueHash);
if (newValueIndex != ABSENT) {
if (force) {
removeEntryValueHashKnown(newValueIndex, newValueHash);
if (entry == size) { // this entry got moved to newValueIndex
entry = newValueIndex;
}
} else {
throw new IllegalArgumentException("Value already present in map: " + newValue);
}
}
// we do *not* update insertion order, and it isn't a structural modification!
deleteFromTableVToK(entry, Hashing.smearedHash(values[entry]));
values[entry] = newValue;
insertIntoTableVToK(entry, newValueHash);
}
/**
* Updates the specified entry to point to the new value: removes the old value from the V-to-K
* mapping and puts the new one in. The entry is moved to the end of the insertion order, or to
* the position of the new key if it was previously present.
*/
private void replaceKeyInEntry(int entry, @NullableDecl K newKey, boolean force) {
checkArgument(entry != ABSENT);
int newKeyHash = Hashing.smearedHash(newKey);
int newKeyIndex = findEntryByKey(newKey, newKeyHash);
int newPredecessor = lastInInsertionOrder;
int newSuccessor = ENDPOINT;
if (newKeyIndex != ABSENT) {
if (force) {
newPredecessor = prevInInsertionOrder[newKeyIndex];
newSuccessor = nextInInsertionOrder[newKeyIndex];
removeEntryKeyHashKnown(newKeyIndex, newKeyHash);
if (entry == size) { // this entry got moved to newKeyIndex
entry = newKeyIndex;
}
} else {
throw new IllegalArgumentException("Key already present in map: " + newKey);
}
}
if (newPredecessor == entry) {
newPredecessor = prevInInsertionOrder[entry];
} else if (newPredecessor == size) {
newPredecessor = newKeyIndex;
}
if (newSuccessor == entry) {
newSuccessor = nextInInsertionOrder[entry];
} else if (newSuccessor == size) {
newSuccessor = newKeyIndex;
}
int oldPredecessor = prevInInsertionOrder[entry];
int oldSuccessor = nextInInsertionOrder[entry];
setSucceeds(oldPredecessor, oldSuccessor); // remove from insertion order linked list
deleteFromTableKToV(entry, Hashing.smearedHash(keys[entry]));
keys[entry] = newKey;
insertIntoTableKToV(entry, Hashing.smearedHash(newKey));
// insert into insertion order linked list, usually at the end
setSucceeds(newPredecessor, entry);
setSucceeds(entry, newSuccessor);
}
@Override
@CanIgnoreReturnValue
@NullableDecl
public V remove(@NullableDecl Object key) {
int keyHash = Hashing.smearedHash(key);
int entry = findEntryByKey(key, keyHash);
if (entry == ABSENT) {
return null;
} else {
@NullableDecl V value = values[entry];
removeEntryKeyHashKnown(entry, keyHash);
return value;
}
}
@NullableDecl
K removeInverse(@NullableDecl Object value) {
int valueHash = Hashing.smearedHash(value);
int entry = findEntryByValue(value, valueHash);
if (entry == ABSENT) {
return null;
} else {
@NullableDecl K key = keys[entry];
removeEntryValueHashKnown(entry, valueHash);
return key;
}
}
/** Removes the entry at the specified index with no additional data. */
void removeEntry(int entry) {
removeEntryKeyHashKnown(entry, Hashing.smearedHash(keys[entry]));
}
/** Removes the entry at the specified index, given the hash of its key and value. */
private void removeEntry(int entry, int keyHash, int valueHash) {
checkArgument(entry != ABSENT);
deleteFromTableKToV(entry, keyHash);
deleteFromTableVToK(entry, valueHash);
int oldPredecessor = prevInInsertionOrder[entry];
int oldSuccessor = nextInInsertionOrder[entry];
setSucceeds(oldPredecessor, oldSuccessor);
moveEntryToIndex(size - 1, entry);
keys[size - 1] = null;
values[size - 1] = null;
size--;
modCount++;
}
/** Removes the entry at the specified index, given the hash of its key. */
void removeEntryKeyHashKnown(int entry, int keyHash) {
removeEntry(entry, keyHash, Hashing.smearedHash(values[entry]));
}
/** Removes the entry at the specified index, given the hash of its value. */
void removeEntryValueHashKnown(int entry, int valueHash) {
removeEntry(entry, Hashing.smearedHash(keys[entry]), valueHash);
}
/**
* Moves the entry previously positioned at {@code src} to {@code dest}. Assumes the entry
* previously at {@code src} has already been removed from the data structures.
*/
private void moveEntryToIndex(int src, int dest) {
if (src == dest) {
return;
}
int predecessor = prevInInsertionOrder[src];
int successor = nextInInsertionOrder[src];
setSucceeds(predecessor, dest);
setSucceeds(dest, successor);
K key = keys[src];
V value = values[src];
keys[dest] = key;
values[dest] = value;
// update pointers in hashTableKToV
int keyHash = Hashing.smearedHash(key);
int keyBucket = bucket(keyHash);
if (hashTableKToV[keyBucket] == src) {
hashTableKToV[keyBucket] = dest;
} else {
int prevInBucket = hashTableKToV[keyBucket];
for (int entryInBucket = nextInBucketKToV[prevInBucket];
/* should never reach end */ ;
entryInBucket = nextInBucketKToV[entryInBucket]) {
if (entryInBucket == src) {
nextInBucketKToV[prevInBucket] = dest;
break;
}
prevInBucket = entryInBucket;
}
}
nextInBucketKToV[dest] = nextInBucketKToV[src];
nextInBucketKToV[src] = ABSENT;
// update pointers in hashTableVToK
int valueHash = Hashing.smearedHash(value);
int valueBucket = bucket(valueHash);
if (hashTableVToK[valueBucket] == src) {
hashTableVToK[valueBucket] = dest;
} else {
int prevInBucket = hashTableVToK[valueBucket];
for (int entryInBucket = nextInBucketVToK[prevInBucket];
/* should never reach end*/ ;
entryInBucket = nextInBucketVToK[entryInBucket]) {
if (entryInBucket == src) {
nextInBucketVToK[prevInBucket] = dest;
break;
}
prevInBucket = entryInBucket;
}
}
nextInBucketVToK[dest] = nextInBucketVToK[src];
nextInBucketVToK[src] = ABSENT;
}
@Override
public void clear() {
Arrays.fill(keys, 0, size, null);
Arrays.fill(values, 0, size, null);
Arrays.fill(hashTableKToV, ABSENT);
Arrays.fill(hashTableVToK, ABSENT);
Arrays.fill(nextInBucketKToV, 0, size, ABSENT);
Arrays.fill(nextInBucketVToK, 0, size, ABSENT);
Arrays.fill(prevInInsertionOrder, 0, size, ABSENT);
Arrays.fill(nextInInsertionOrder, 0, size, ABSENT);
size = 0;
firstInInsertionOrder = ENDPOINT;
lastInInsertionOrder = ENDPOINT;
modCount++;
}
/** Shared supertype of keySet, values, entrySet, and inverse.entrySet. */
abstract static class View<K, V, T> extends AbstractSet<T> {
final HashBiMap<K, V> biMap;
View(HashBiMap<K, V> biMap) {
this.biMap = biMap;
}
abstract T forEntry(int entry);
@Override
public Iterator<T> iterator() {
return new Iterator<T>() {
private int index = biMap.firstInInsertionOrder;
private int indexToRemove = ABSENT;
private int expectedModCount = biMap.modCount;
// Calls to setValue on inverse entries can move already-visited entries to the end.
// Make sure we don't visit those.
private int remaining = biMap.size;
private void checkForComodification() {
if (biMap.modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
@Override
public boolean hasNext() {
checkForComodification();
return index != ENDPOINT && remaining > 0;
}
@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
T result = forEntry(index);
indexToRemove = index;
index = biMap.nextInInsertionOrder[index];
remaining--;
return result;
}
@Override
public void remove() {
checkForComodification();
CollectPreconditions.checkRemove(indexToRemove != ABSENT);
biMap.removeEntry(indexToRemove);
if (index == biMap.size) {
index = indexToRemove;
}
indexToRemove = ABSENT;
expectedModCount = biMap.modCount;
}
};
}
@Override
public int size() {
return biMap.size;
}
@Override
public void clear() {
biMap.clear();
}
}
private transient Set<K> keySet;
@Override
public Set<K> keySet() {
Set<K> result = keySet;
return (result == null) ? keySet = new KeySet() : result;
}
final class KeySet extends View<K, V, K> {
KeySet() {
super(HashBiMap.this);
}
@Override
K forEntry(int entry) {
return keys[entry];
}
@Override
public boolean contains(@NullableDecl Object o) {
return HashBiMap.this.containsKey(o);
}
@Override
public boolean remove(@NullableDecl Object o) {
int oHash = Hashing.smearedHash(o);
int entry = findEntryByKey(o, oHash);
if (entry != ABSENT) {
removeEntryKeyHashKnown(entry, oHash);
return true;
} else {
return false;
}
}
}
private transient Set<V> valueSet;
@Override
public Set<V> values() {
Set<V> result = valueSet;
return (result == null) ? valueSet = new ValueSet() : result;
}
final class ValueSet extends View<K, V, V> {
ValueSet() {
super(HashBiMap.this);
}
@Override
V forEntry(int entry) {
return values[entry];
}
@Override
public boolean contains(@NullableDecl Object o) {
return HashBiMap.this.containsValue(o);
}
@Override
public boolean remove(@NullableDecl Object o) {
int oHash = Hashing.smearedHash(o);
int entry = findEntryByValue(o, oHash);
if (entry != ABSENT) {
removeEntryValueHashKnown(entry, oHash);
return true;
} else {
return false;
}
}
}
private transient Set<Entry<K, V>> entrySet;
@Override
public Set<Entry<K, V>> entrySet() {
Set<Entry<K, V>> result = entrySet;
return (result == null) ? entrySet = new EntrySet() : result;
}
final class EntrySet extends View<K, V, Entry<K, V>> {
EntrySet() {
super(HashBiMap.this);
}
@Override
public boolean contains(@NullableDecl Object o) {
if (o instanceof Entry) {
Entry<?, ?> e = (Entry<?, ?>) o;
@NullableDecl Object k = e.getKey();
@NullableDecl Object v = e.getValue();
int eIndex = findEntryByKey(k);
return eIndex != ABSENT && Objects.equal(v, values[eIndex]);
}
return false;
}
@Override
@CanIgnoreReturnValue
public boolean remove(@NullableDecl Object o) {
if (o instanceof Entry) {
Entry<?, ?> e = (Entry<?, ?>) o;
@NullableDecl Object k = e.getKey();
@NullableDecl Object v = e.getValue();
int kHash = Hashing.smearedHash(k);
int eIndex = findEntryByKey(k, kHash);
if (eIndex != ABSENT && Objects.equal(v, values[eIndex])) {
removeEntryKeyHashKnown(eIndex, kHash);
return true;
}
}
return false;
}
@Override
Entry<K, V> forEntry(int entry) {
return new EntryForKey(entry);
}
}
/**
* An {@code Entry} implementation that attempts to follow its key around the map -- that is, if
* the key is moved, deleted, or reinserted, it will account for that -- while not doing any extra
* work if the key has not moved.
*/
final class EntryForKey extends AbstractMapEntry<K, V> {
@NullableDecl final K key;
int index;
EntryForKey(int index) {
this.key = keys[index];
this.index = index;
}
void updateIndex() {
if (index == ABSENT || index > size || !Objects.equal(keys[index], key)) {
index = findEntryByKey(key);
}
}
@Override
public K getKey() {
return key;
}
@Override
@NullableDecl
public V getValue() {
updateIndex();
return (index == ABSENT) ? null : values[index];
}
@Override
public V setValue(V value) {
updateIndex();
if (index == ABSENT) {
return HashBiMap.this.put(key, value);
}
V oldValue = values[index];
if (Objects.equal(oldValue, value)) {
return value;
}
replaceValueInEntry(index, value, false);
return oldValue;
}
}
@MonotonicNonNullDecl @RetainedWith private transient BiMap<V, K> inverse;
@Override
public BiMap<V, K> inverse() {
BiMap<V, K> result = inverse;
return (result == null) ? inverse = new Inverse<K, V>(this) : result;
}
static class Inverse<K, V> extends AbstractMap<V, K> implements BiMap<V, K>, Serializable {
private final HashBiMap<K, V> forward;
Inverse(HashBiMap<K, V> forward) {
this.forward = forward;
}
@Override
public int size() {
return forward.size;
}
@Override
public boolean containsKey(@NullableDecl Object key) {
return forward.containsValue(key);
}
@Override
@NullableDecl
public K get(@NullableDecl Object key) {
return forward.getInverse(key);
}
@Override
public boolean containsValue(@NullableDecl Object value) {
return forward.containsKey(value);
}
@Override
@CanIgnoreReturnValue
@NullableDecl
public K put(@NullableDecl V value, @NullableDecl K key) {
return forward.putInverse(value, key, false);
}
@Override
@CanIgnoreReturnValue
@NullableDecl
public K forcePut(@NullableDecl V value, @NullableDecl K key) {
return forward.putInverse(value, key, true);
}
@Override
public BiMap<K, V> inverse() {
return forward;
}
@Override
@CanIgnoreReturnValue
@NullableDecl
public K remove(@NullableDecl Object value) {
return forward.removeInverse(value);
}
@Override
public void clear() {
forward.clear();
}
@Override
public Set<V> keySet() {
return forward.values();
}
@Override
public Set<K> values() {
return forward.keySet();
}
private transient Set<Entry<V, K>> inverseEntrySet;
@Override
public Set<Entry<V, K>> entrySet() {
Set<Entry<V, K>> result = inverseEntrySet;
return (result == null) ? inverseEntrySet = new InverseEntrySet<K, V>(forward) : result;
}
@GwtIncompatible("serialization")
private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
in.defaultReadObject();
this.forward.inverse = this;
}
}
static class InverseEntrySet<K, V> extends View<K, V, Entry<V, K>> {
InverseEntrySet(HashBiMap<K, V> biMap) {
super(biMap);
}
@Override
public boolean contains(@NullableDecl Object o) {
if (o instanceof Entry) {
Entry<?, ?> e = (Entry<?, ?>) o;
Object v = e.getKey();
Object k = e.getValue();
int eIndex = biMap.findEntryByValue(v);
return eIndex != ABSENT && Objects.equal(biMap.keys[eIndex], k);
}
return false;
}
@Override
public boolean remove(Object o) {
if (o instanceof Entry) {
Entry<?, ?> e = (Entry<?, ?>) o;
Object v = e.getKey();
Object k = e.getValue();
int vHash = Hashing.smearedHash(v);
int eIndex = biMap.findEntryByValue(v, vHash);
if (eIndex != ABSENT && Objects.equal(biMap.keys[eIndex], k)) {
biMap.removeEntryValueHashKnown(eIndex, vHash);
return true;
}
}
return false;
}
@Override
Entry<V, K> forEntry(int entry) {
return new EntryForValue<K, V>(biMap, entry);
}
}
/**
* An {@code Entry} implementation that attempts to follow its value around the map -- that is, if
* the value is moved, deleted, or reinserted, it will account for that -- while not doing any
* extra work if the value has not moved.
*/
static final class EntryForValue<K, V> extends AbstractMapEntry<V, K> {
final HashBiMap<K, V> biMap;
final V value;
int index;
EntryForValue(HashBiMap<K, V> biMap, int index) {
this.biMap = biMap;
this.value = biMap.values[index];
this.index = index;
}
private void updateIndex() {
if (index == ABSENT || index > biMap.size || !Objects.equal(value, biMap.values[index])) {
index = biMap.findEntryByValue(value);
}
}
@Override
public V getKey() {
return value;
}
@Override
public K getValue() {
updateIndex();
return (index == ABSENT) ? null : biMap.keys[index];
}
@Override
public K setValue(K key) {
updateIndex();
if (index == ABSENT) {
return biMap.putInverse(value, key, false);
}
K oldKey = biMap.keys[index];
if (Objects.equal(oldKey, key)) {
return key;
}
biMap.replaceKeyInEntry(index, key, false);
return oldKey;
}
}
/**
* @serialData the number of entries, first key, first value, second key, second value, and so on.
*/
@GwtIncompatible // java.io.ObjectOutputStream
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
Serialization.writeMap(this, stream);
}
@GwtIncompatible // java.io.ObjectInputStream
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);
}
}