All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.common.collect.HashBiMap Maven / Gradle / Ivy

Go to download

Guava is a suite of core and expanded libraries that include utility classes, google's collections, io classes, and much much more.

There is a newer version: 33.3.0-jre
Show newest version
/*
 * 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.collect.NullnessCasts.uncheckedCastNullableTToT;
import static com.google.common.collect.NullnessCasts.unsafeNull;

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.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.concurrent.LazyInit;
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 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 @ElementTypesAreNonnullByDefault public final class HashBiMap extends AbstractMap 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 map) { HashBiMap 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 @Nullable K[] keys; /** Maps an "entry" to the value of that entry. */ transient @Nullable 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. */ private transient int firstInInsertionOrder; /** The "entry" of the last element in insertion order. */ 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(@CheckForNull 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(@CheckForNull 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(@CheckForNull 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(@CheckForNull Object value, int valueHash) { return findEntry(value, valueHash, hashTableVToK, nextInBucketVToK, values); } int findEntry( @CheckForNull Object o, int oHash, int[] hashTable, int[] nextInBucket, @Nullable 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(@CheckForNull Object key) { return findEntryByKey(key) != ABSENT; } /** * 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 findEntryByValue(value) != ABSENT; } @Override @CheckForNull public V get(@CheckForNull Object key) { int entry = findEntryByKey(key); return (entry == ABSENT) ? null : values[entry]; } @CheckForNull K getInverse(@CheckForNull Object value) { int entry = findEntryByValue(value); return (entry == ABSENT) ? null : keys[entry]; } @Override @CanIgnoreReturnValue @CheckForNull public V put(@ParametricNullness K key, @ParametricNullness V value) { return put(key, value, false); } @CheckForNull V put(@ParametricNullness K key, @ParametricNullness 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 @CheckForNull public V forcePut(@ParametricNullness K key, @ParametricNullness V value) { return put(key, value, true); } @CanIgnoreReturnValue @CheckForNull K putInverse(@ParametricNullness V value, @ParametricNullness 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, @ParametricNullness 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, @ParametricNullness 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 @CheckForNull public V remove(@CheckForNull Object key) { int keyHash = Hashing.smearedHash(key); int entry = findEntryByKey(key, keyHash); if (entry == ABSENT) { return null; } else { V value = values[entry]; removeEntryKeyHashKnown(entry, keyHash); return value; } } @CheckForNull K removeInverse(@CheckForNull Object value) { int valueHash = Hashing.smearedHash(value); int entry = findEntryByValue(value, valueHash); if (entry == ABSENT) { return null; } else { 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 extends @Nullable Object, V extends @Nullable Object, T extends @Nullable Object> extends AbstractSet { final HashBiMap biMap; View(HashBiMap biMap) { this.biMap = biMap; } @ParametricNullness abstract T forEntry(int entry); @Override public Iterator iterator() { return new Iterator() { 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 @ParametricNullness 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(); } } @LazyInit private transient Set keySet; @Override public Set keySet() { Set result = keySet; return (result == null) ? keySet = new KeySet() : result; } final class KeySet extends View { KeySet() { super(HashBiMap.this); } @Override @ParametricNullness K forEntry(int entry) { // The cast is safe because we call forEntry only for indexes that contain entries. return uncheckedCastNullableTToT(keys[entry]); } @Override public boolean contains(@CheckForNull Object o) { return HashBiMap.this.containsKey(o); } @Override public boolean remove(@CheckForNull Object o) { int oHash = Hashing.smearedHash(o); int entry = findEntryByKey(o, oHash); if (entry != ABSENT) { removeEntryKeyHashKnown(entry, oHash); return true; } else { return false; } } } @LazyInit private transient Set valueSet; @Override public Set values() { Set result = valueSet; return (result == null) ? valueSet = new ValueSet() : result; } final class ValueSet extends View { ValueSet() { super(HashBiMap.this); } @Override @ParametricNullness V forEntry(int entry) { // The cast is safe because we call forEntry only for indexes that contain entries. return uncheckedCastNullableTToT(values[entry]); } @Override public boolean contains(@CheckForNull Object o) { return HashBiMap.this.containsValue(o); } @Override public boolean remove(@CheckForNull Object o) { int oHash = Hashing.smearedHash(o); int entry = findEntryByValue(o, oHash); if (entry != ABSENT) { removeEntryValueHashKnown(entry, oHash); return true; } else { return false; } } } @LazyInit private transient Set> entrySet; @Override public Set> entrySet() { Set> result = entrySet; return (result == null) ? entrySet = new EntrySet() : result; } final class EntrySet extends View> { EntrySet() { super(HashBiMap.this); } @Override public boolean contains(@CheckForNull Object o) { if (o instanceof Entry) { Entry e = (Entry) o; Object k = e.getKey(); Object v = e.getValue(); int eIndex = findEntryByKey(k); return eIndex != ABSENT && Objects.equal(v, values[eIndex]); } return false; } @Override @CanIgnoreReturnValue public boolean remove(@CheckForNull Object o) { if (o instanceof Entry) { Entry e = (Entry) o; Object k = e.getKey(); 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 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. One quirk: The {@link #getValue()} method can return {@code * null} even for a map which supposedly does not contain null elements, if the key is not present * when {@code getValue()} is called. */ final class EntryForKey extends AbstractMapEntry { @ParametricNullness final K key; int index; EntryForKey(int index) { // The cast is safe because we call forEntry only for indexes that contain entries. this.key = uncheckedCastNullableTToT(keys[index]); this.index = index; } void updateIndex() { if (index == ABSENT || index > size || !Objects.equal(keys[index], key)) { index = findEntryByKey(key); } } @Override @ParametricNullness public K getKey() { return key; } @Override @ParametricNullness public V getValue() { updateIndex(); /* * If the entry has been removed from the map, we return null, even though that might not be a * valid value. That's the best we can do, short of holding a reference to the most recently * seen value. And while we *could* do that, we aren't required to: Map.Entry explicitly says * that behavior is undefined when the backing map is modified through another API. (It even * permits us to throw IllegalStateException. Maybe we should have done that, but we probably * shouldn't change now for fear of breaking people.) * * If the entry is still in the map, then updateIndex ensured that `index` points to the right * element. Because that element is present, uncheckedCastNullableTToT is safe. */ return (index == ABSENT) ? unsafeNull() : uncheckedCastNullableTToT(values[index]); } @Override @ParametricNullness public V setValue(@ParametricNullness V value) { updateIndex(); if (index == ABSENT) { HashBiMap.this.put(key, value); return unsafeNull(); // See the discussion in getValue(). } /* * The cast is safe because updateIndex found the entry for this key. (If it hadn't, then we * would have returned above.) Thus, we know that it and its corresponding value are in * position `index`. */ V oldValue = uncheckedCastNullableTToT(values[index]); if (Objects.equal(oldValue, value)) { return value; } replaceValueInEntry(index, value, false); return oldValue; } } @LazyInit @RetainedWith @CheckForNull private transient BiMap inverse; @Override public BiMap inverse() { BiMap result = inverse; return (result == null) ? inverse = new Inverse(this) : result; } static class Inverse extends AbstractMap implements BiMap, Serializable { private final HashBiMap forward; Inverse(HashBiMap forward) { this.forward = forward; } @Override public int size() { return forward.size; } @Override public boolean containsKey(@CheckForNull Object key) { return forward.containsValue(key); } @Override @CheckForNull public K get(@CheckForNull Object key) { return forward.getInverse(key); } @Override public boolean containsValue(@CheckForNull Object value) { return forward.containsKey(value); } @Override @CanIgnoreReturnValue @CheckForNull public K put(@ParametricNullness V value, @ParametricNullness K key) { return forward.putInverse(value, key, false); } @Override @CanIgnoreReturnValue @CheckForNull public K forcePut(@ParametricNullness V value, @ParametricNullness K key) { return forward.putInverse(value, key, true); } @Override public BiMap inverse() { return forward; } @Override @CanIgnoreReturnValue @CheckForNull public K remove(@CheckForNull Object value) { return forward.removeInverse(value); } @Override public void clear() { forward.clear(); } @Override public Set keySet() { return forward.values(); } @Override public Set values() { return forward.keySet(); } private transient Set> inverseEntrySet; @Override public Set> entrySet() { Set> result = inverseEntrySet; return (result == null) ? inverseEntrySet = new InverseEntrySet(forward) : result; } @GwtIncompatible("serialization") private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException { in.defaultReadObject(); this.forward.inverse = this; } } static class InverseEntrySet extends View> { InverseEntrySet(HashBiMap biMap) { super(biMap); } @Override public boolean contains(@CheckForNull 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(@CheckForNull 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 forEntry(int entry) { return new EntryForValue(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 extends AbstractMapEntry { final HashBiMap biMap; @ParametricNullness final V value; int index; EntryForValue(HashBiMap biMap, int index) { this.biMap = biMap; // The cast is safe because we call forEntry only for indexes that contain entries. this.value = uncheckedCastNullableTToT(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 @ParametricNullness public V getKey() { return value; } @Override @ParametricNullness public K getValue() { updateIndex(); // For discussion of unsafeNull() and uncheckedCastNullableTToT(), see EntryForKey.getValue(). return (index == ABSENT) ? unsafeNull() : uncheckedCastNullableTToT(biMap.keys[index]); } @Override @ParametricNullness public K setValue(@ParametricNullness K key) { updateIndex(); if (index == ABSENT) { biMap.putInverse(value, key, false); return unsafeNull(); // see EntryForKey.setValue() } K oldKey = uncheckedCastNullableTToT(biMap.keys[index]); // see EntryForKey.setValue() 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 @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); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy