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.checkState;
import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.base.Objects;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.AbstractMap;
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.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.
*
* See the Guava User Guide article on {@code BiMap}
* .
*
* @author Louis Wasserman
* @author Mike Bostock
* @since 2.0 (imported from Google Collections Library)
*/
@GwtCompatible(emulated = true)
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 extends K, ? extends V> map) {
HashBiMap bimap = create(map.size());
bimap.putAll(map);
return bimap;
}
private static final class BiEntry {
final K key;
final int keyHash;
final V value;
final int valueHash;
@Nullable
BiEntry nextInKToVBucket;
@Nullable
BiEntry nextInVToKBucket;
BiEntry(K key, int keyHash, V value, int valueHash) {
this.key = key;
this.keyHash = keyHash;
this.value = value;
this.valueHash = valueHash;
}
}
private static final double LOAD_FACTOR = 1.0;
private transient BiEntry[] hashTableKToV;
private transient BiEntry[] hashTableVToK;
private transient int size;
private transient int mask;
private transient int modCount;
private HashBiMap(int expectedSize) {
init(expectedSize);
}
private void init(int expectedSize) {
checkArgument(expectedSize >= 0, "expectedSize must be >= 0 but was %s", expectedSize);
int tableSize = Hashing.closedTableSize(expectedSize, LOAD_FACTOR);
this.hashTableKToV = createTable(tableSize);
this.hashTableVToK = createTable(tableSize);
this.mask = tableSize - 1;
this.modCount = 0;
this.size = 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];;
bucketEntry = bucketEntry.nextInVToKBucket) {
if (bucketEntry == entry) {
if (prevBucketEntry == null) {
hashTableVToK[valueBucket] = entry.nextInVToKBucket;
} else {
prevBucketEntry.nextInVToKBucket = entry.nextInVToKBucket;
}
break;
}
prevBucketEntry = bucketEntry;
}
size--;
modCount++;
}
private void insert(BiEntry entry) {
int keyBucket = entry.keyHash & mask;
entry.nextInKToVBucket = hashTableKToV[keyBucket];
hashTableKToV[keyBucket] = entry;
int valueBucket = entry.valueHash & mask;
entry.nextInVToKBucket = hashTableVToK[valueBucket];
hashTableVToK[valueBucket] = entry;
size++;
modCount++;
}
private static int hash(@Nullable Object o) {
return Hashing.smear((o == null) ? 0 : o.hashCode());
}
private BiEntry seekByKey(@Nullable 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;
}
private BiEntry seekByValue(@Nullable 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(@Nullable Object key) {
return seekByKey(key, hash(key)) != null;
}
@Override
public boolean containsValue(@Nullable Object value) {
return seekByValue(value, hash(value)) != null;
}
@Nullable
@Override
public V get(@Nullable Object key) {
BiEntry entry = seekByKey(key, hash(key));
return (entry == null) ? null : entry.value;
}
@Override
public V put(@Nullable K key, @Nullable V value) {
return put(key, value, false);
}
@Override
public V forcePut(@Nullable K key, @Nullable V value) {
return put(key, value, true);
}
private V put(@Nullable K key, @Nullable V value, boolean force) {
int keyHash = hash(key);
int valueHash = hash(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);
}
}
if (oldEntryForKey != null) {
delete(oldEntryForKey);
}
BiEntry newEntry = new BiEntry(key, keyHash, value, valueHash);
insert(newEntry);
rehashIfNecessary();
return (oldEntryForKey == null) ? null : oldEntryForKey.value;
}
@Nullable
private K putInverse(@Nullable V value, @Nullable K key, boolean force) {
int valueHash = hash(value);
int keyHash = hash(key);
BiEntry oldEntryForValue = seekByValue(value, valueHash);
if (oldEntryForValue != null && keyHash == oldEntryForValue.keyHash
&& Objects.equal(key, oldEntryForValue.key)) {
return key;
}
BiEntry oldEntryForKey = seekByKey(key, keyHash);
if (oldEntryForKey != null) {
if (force) {
delete(oldEntryForKey);
} else {
throw new IllegalArgumentException("value already present: " + key);
}
}
if (oldEntryForValue != null) {
delete(oldEntryForValue);
}
BiEntry newEntry = new BiEntry(key, keyHash, value, valueHash);
insert(newEntry);
rehashIfNecessary();
return (oldEntryForValue == null) ? null : oldEntryForValue.key;
}
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 (int bucket = 0; bucket < oldKToV.length; bucket++) {
BiEntry entry = oldKToV[bucket];
while (entry != null) {
BiEntry nextEntry = entry.nextInKToVBucket;
insert(entry);
entry = nextEntry;
}
}
this.modCount++;
}
}
@SuppressWarnings("unchecked")
private BiEntry[] createTable(int length) {
return new BiEntry[length];
}
@Override
public V remove(@Nullable Object key) {
BiEntry entry = seekByKey(key, hash(key));
if (entry == null) {
return null;
} else {
delete(entry);
return entry.value;
}
}
@Override
public void clear() {
size = 0;
Arrays.fill(hashTableKToV, null);
Arrays.fill(hashTableVToK, null);
modCount++;
}
@Override
public int size() {
return size;
}
abstract class Itr implements Iterator {
int nextBucket = 0;
BiEntry next = null;
BiEntry toRemove = null;
int expectedModCount = modCount;
private void checkForConcurrentModification() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
@Override
public boolean hasNext() {
checkForConcurrentModification();
if (next != null) {
return true;
}
while (nextBucket < hashTableKToV.length) {
if (hashTableKToV[nextBucket] != null) {
next = hashTableKToV[nextBucket++];
return true;
}
nextBucket++;
}
return false;
}
@Override
public T next() {
checkForConcurrentModification();
if (!hasNext()) {
throw new NoSuchElementException();
}
BiEntry entry = next;
next = entry.nextInKToVBucket;
toRemove = entry;
return output(entry);
}
@Override
public void remove() {
checkForConcurrentModification();
checkState(toRemove != null, "Only one remove() call allowed per call to next");
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 {
@Override
Map map() {
return HashBiMap.this;
}
@Override
public Iterator iterator() {
return new Itr() {
@Override
K output(BiEntry entry) {
return entry.key;
}
};
}
@Override
public boolean remove(@Nullable Object o) {
BiEntry entry = seekByKey(o, hash(o));
if (entry == null) {
return false;
} else {
delete(entry);
return true;
}
}
}
@Override
public Set values() {
return inverse().keySet();
}
@Override
public Set> entrySet() {
return new EntrySet();
}
private final class EntrySet extends Maps.EntrySet {
@Override
Map map() {
return HashBiMap.this;
}
@Override
public Iterator> iterator() {
return new Itr>() {
@Override
Entry output(BiEntry entry) {
return new MapEntry(entry);
}
class MapEntry extends AbstractMapEntry {
BiEntry delegate;
MapEntry(BiEntry entry) {
this.delegate = entry;
}
@Override public K getKey() {
return delegate.key;
}
@Override public V getValue() {
return delegate.value;
}
@Override public V setValue(V value) {
V oldValue = delegate.value;
int valueHash = hash(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);
expectedModCount = modCount;
if (toRemove == delegate) {
toRemove = newEntry;
}
delegate = newEntry;
return oldValue;
}
}
};
}
}
private transient BiMap inverse;
@Override
public BiMap inverse() {
return (inverse == null) ? inverse = new Inverse() : inverse;
}
private final class Inverse extends AbstractMap implements BiMap, Serializable {
BiMap forward() {
return HashBiMap.this;
}
@Override
public int size() {
return size;
}
@Override
public void clear() {
forward().clear();
}
@Override
public boolean containsKey(@Nullable Object value) {
return forward().containsValue(value);
}
@Override
public K get(@Nullable Object value) {
BiEntry entry = seekByValue(value, hash(value));
return (entry == null) ? null : entry.key;
}
@Override
public K put(@Nullable V value, @Nullable K key) {
return putInverse(value, key, false);
}
@Override
public K forcePut(@Nullable V value, @Nullable K key) {
return putInverse(value, key, true);
}
@Override
public K remove(@Nullable Object value) {
BiEntry entry = seekByValue(value, hash(value));
if (entry == null) {
return null;
} else {
delete(entry);
return entry.key;
}
}
@Override
public BiMap inverse() {
return forward();
}
@Override
public Set keySet() {
return new InverseKeySet();
}
private final class InverseKeySet extends Maps.KeySet {
@Override
Map map() {
return Inverse.this;
}
@Override
public boolean remove(@Nullable Object o) {
BiEntry entry = seekByValue(o, hash(o));
if (entry == null) {
return false;
} else {
delete(entry);
return true;
}
}
@Override
public Iterator iterator() {
return new Itr() {
@Override V output(BiEntry entry) {
return entry.value;
}
};
}
}
@Override
public Set values() {
return forward().keySet();
}
@Override
public Set> entrySet() {
return new Maps.EntrySet() {
@Override
Map map() {
return Inverse.this;
}
@Override
public Iterator> iterator() {
return new Itr>() {
@Override
Entry output(BiEntry entry) {
return new InverseEntry(entry);
}
class InverseEntry extends AbstractMapEntry {
BiEntry delegate;
InverseEntry(BiEntry entry) {
this.delegate = entry;
}
@Override
public V getKey() {
return delegate.value;
}
@Override
public K getValue() {
return delegate.key;
}
@Override
public K setValue(K key) {
K oldKey = delegate.key;
int keyHash = hash(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);
insert(newEntry);
expectedModCount = modCount;
// This is safe because entries can only get bumped up to earlier in the iteration,
// so they can't get revisited.
return oldKey;
}
}
};
}
};
}
Object writeReplace() {
return new InverseSerializedForm(HashBiMap.this);
}
}
private static final class InverseSerializedForm 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")
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(size);
Serialization.populateMap(this, stream, size);
}
@GwtIncompatible("Not needed in emulated source")
private static final long serialVersionUID = 0;
}