com.google.common.collect.AbstractBiMap 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 static com.google.common.collect.NullnessCasts.uncheckedCastNullableTToT;
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 com.google.j2objc.annotations.WeakOuter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import javax.annotation.CheckForNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* A general-purpose bimap implementation using any two backing {@code Map} instances.
*
* Note that this class contains {@code equals()} calls that keep it from supporting {@code
* IdentityHashMap} backing maps.
*
* @author Kevin Bourrillion
* @author Mike Bostock
*/
@GwtCompatible(emulated = true)
@ElementTypesAreNonnullByDefault
abstract class AbstractBiMap
extends ForwardingMap implements BiMap, Serializable {
@SuppressWarnings("nullness:initialization.field.uninitialized") // For J2KT (lateinit)
private transient Map delegate;
@SuppressWarnings("nullness:initialization.field.uninitialized") // For J2KT (lateinit)
@RetainedWith
transient AbstractBiMap inverse;
/** Package-private constructor for creating a map-backed bimap. */
AbstractBiMap(Map forward, Map backward) {
setDelegates(forward, backward);
}
/** Private constructor for inverse bimap. */
private AbstractBiMap(Map backward, AbstractBiMap forward) {
delegate = backward;
inverse = forward;
}
@Override
protected Map delegate() {
return delegate;
}
/** Returns its input, or throws an exception if this is not a valid key. */
@CanIgnoreReturnValue
@ParametricNullness
K checkKey(@ParametricNullness K key) {
return key;
}
/** Returns its input, or throws an exception if this is not a valid value. */
@CanIgnoreReturnValue
@ParametricNullness
V checkValue(@ParametricNullness V value) {
return value;
}
/**
* Specifies the delegate maps going in each direction. Called by the constructor and by
* subclasses during deserialization.
*/
void setDelegates(Map forward, Map backward) {
checkState(delegate == null);
checkState(inverse == null);
checkArgument(forward.isEmpty());
checkArgument(backward.isEmpty());
checkArgument(forward != backward);
delegate = forward;
inverse = makeInverse(backward);
}
AbstractBiMap makeInverse(Map backward) {
return new Inverse<>(backward, this);
}
void setInverse(AbstractBiMap inverse) {
this.inverse = inverse;
}
// Query Operations (optimizations)
@Override
public boolean containsValue(@CheckForNull Object value) {
return inverse.containsKey(value);
}
// Modification Operations
@CanIgnoreReturnValue
@Override
@CheckForNull
public V put(@ParametricNullness K key, @ParametricNullness V value) {
return putInBothMaps(key, value, false);
}
@CanIgnoreReturnValue
@Override
@CheckForNull
public V forcePut(@ParametricNullness K key, @ParametricNullness V value) {
return putInBothMaps(key, value, true);
}
@CheckForNull
private V putInBothMaps(@ParametricNullness K key, @ParametricNullness V value, boolean force) {
checkKey(key);
checkValue(value);
boolean containedKey = containsKey(key);
if (containedKey && Objects.equal(value, get(key))) {
return value;
}
if (force) {
inverse().remove(value);
} else {
checkArgument(!containsValue(value), "value already present: %s", value);
}
V oldValue = delegate.put(key, value);
updateInverseMap(key, containedKey, oldValue, value);
return oldValue;
}
private void updateInverseMap(
@ParametricNullness K key,
boolean containedKey,
@CheckForNull V oldValue,
@ParametricNullness V newValue) {
if (containedKey) {
// The cast is safe because of the containedKey check.
removeFromInverseMap(uncheckedCastNullableTToT(oldValue));
}
inverse.delegate.put(newValue, key);
}
@CanIgnoreReturnValue
@Override
@CheckForNull
public V remove(@CheckForNull Object key) {
return containsKey(key) ? removeFromBothMaps(key) : null;
}
@CanIgnoreReturnValue
@ParametricNullness
private V removeFromBothMaps(@CheckForNull Object key) {
// The cast is safe because the callers of this method first check that the key is present.
V oldValue = uncheckedCastNullableTToT(delegate.remove(key));
removeFromInverseMap(oldValue);
return oldValue;
}
private void removeFromInverseMap(@ParametricNullness V oldValue) {
inverse.delegate.remove(oldValue);
}
// Bulk Operations
@Override
public void putAll(Map extends K, ? extends V> map) {
for (Entry extends K, ? extends V> entry : map.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
@Override
public void replaceAll(BiFunction super K, ? super V, ? extends V> function) {
this.delegate.replaceAll(function);
inverse.delegate.clear();
Entry broken = null;
Iterator> itr = this.delegate.entrySet().iterator();
while (itr.hasNext()) {
Entry entry = itr.next();
K k = entry.getKey();
V v = entry.getValue();
K conflict = inverse.delegate.putIfAbsent(v, k);
if (conflict != null) {
broken = entry;
// We're definitely going to throw, but we'll try to keep the BiMap in an internally
// consistent state by removing the bad entry.
itr.remove();
}
}
if (broken != null) {
throw new IllegalArgumentException("value already present: " + broken.getValue());
}
}
@Override
public void clear() {
delegate.clear();
inverse.delegate.clear();
}
// Views
@Override
public BiMap inverse() {
return inverse;
}
@LazyInit @CheckForNull private transient Set keySet;
@Override
public Set keySet() {
Set result = keySet;
return (result == null) ? keySet = new KeySet() : result;
}
@WeakOuter
private class KeySet extends ForwardingSet {
@Override
protected Set delegate() {
return delegate.keySet();
}
@Override
public void clear() {
AbstractBiMap.this.clear();
}
@Override
public boolean remove(@CheckForNull Object key) {
if (!contains(key)) {
return false;
}
removeFromBothMaps(key);
return true;
}
@Override
public boolean removeAll(Collection> keysToRemove) {
return standardRemoveAll(keysToRemove);
}
@Override
public boolean retainAll(Collection> keysToRetain) {
return standardRetainAll(keysToRetain);
}
@Override
public Iterator iterator() {
return Maps.keyIterator(entrySet().iterator());
}
}
@LazyInit @CheckForNull private transient Set valueSet;
@Override
public Set values() {
/*
* We can almost reuse the inverse's keySet, except we have to fix the
* iteration order so that it is consistent with the forward map.
*/
Set result = valueSet;
return (result == null) ? valueSet = new ValueSet() : result;
}
@WeakOuter
private class ValueSet extends ForwardingSet {
final Set valuesDelegate = inverse.keySet();
@Override
protected Set delegate() {
return valuesDelegate;
}
@Override
public Iterator iterator() {
return Maps.valueIterator(entrySet().iterator());
}
@Override
public @Nullable Object[] toArray() {
return standardToArray();
}
@Override
@SuppressWarnings("nullness") // bug in our checker's handling of toArray signatures
public T[] toArray(T[] array) {
return standardToArray(array);
}
@Override
public String toString() {
return standardToString();
}
}
@LazyInit @CheckForNull private transient Set> entrySet;
@Override
public Set> entrySet() {
Set> result = entrySet;
return (result == null) ? entrySet = new EntrySet() : result;
}
class BiMapEntry extends ForwardingMapEntry {
private final Entry delegate;
BiMapEntry(Entry delegate) {
this.delegate = delegate;
}
@Override
protected Entry delegate() {
return delegate;
}
@Override
public V setValue(V value) {
checkValue(value);
// Preconditions keep the map and inverse consistent.
checkState(entrySet().contains(this), "entry no longer in map");
// similar to putInBothMaps, but set via entry
if (Objects.equal(value, getValue())) {
return value;
}
checkArgument(!containsValue(value), "value already present: %s", value);
V oldValue = delegate.setValue(value);
checkState(Objects.equal(value, get(getKey())), "entry no longer in map");
updateInverseMap(getKey(), true, oldValue, value);
return oldValue;
}
}
Iterator> entrySetIterator() {
final Iterator> iterator = delegate.entrySet().iterator();
return new Iterator>() {
@CheckForNull Entry entry;
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public Entry next() {
entry = iterator.next();
return new BiMapEntry(entry);
}
@Override
public void remove() {
if (entry == null) {
throw new IllegalStateException("no calls to next() since the last call to remove()");
}
V value = entry.getValue();
iterator.remove();
removeFromInverseMap(value);
entry = null;
}
};
}
@WeakOuter
private class EntrySet extends ForwardingSet> {
final Set> esDelegate = delegate.entrySet();
@Override
protected Set> delegate() {
return esDelegate;
}
@Override
public void clear() {
AbstractBiMap.this.clear();
}
@Override
public boolean remove(@CheckForNull Object object) {
/*
* `o instanceof Entry` is guaranteed by `contains`, but we check it here to satisfy our
* nullness checker.
*/
if (!esDelegate.contains(object) || !(object instanceof Entry)) {
return false;
}
Entry, ?> entry = (Entry, ?>) object;
inverse.delegate.remove(entry.getValue());
/*
* Remove the mapping in inverse before removing from esDelegate because
* if entry is part of esDelegate, entry might be invalidated after the
* mapping is removed from esDelegate.
*/
esDelegate.remove(entry);
return true;
}
@Override
public Iterator> iterator() {
return entrySetIterator();
}
// See java.util.Collections.CheckedEntrySet for details on attacks.
@Override
public @Nullable Object[] toArray() {
return standardToArray();
}
@Override
@SuppressWarnings("nullness") // bug in our checker's handling of toArray signatures
public T[] toArray(T[] array) {
return standardToArray(array);
}
@Override
public boolean contains(@CheckForNull Object o) {
return Maps.containsEntryImpl(delegate(), o);
}
@Override
public boolean containsAll(Collection> c) {
return standardContainsAll(c);
}
@Override
public boolean removeAll(Collection> c) {
return standardRemoveAll(c);
}
@Override
public boolean retainAll(Collection> c) {
return standardRetainAll(c);
}
}
/** The inverse of any other {@code AbstractBiMap} subclass. */
static class Inverse
extends AbstractBiMap {
Inverse(Map backward, AbstractBiMap forward) {
super(backward, forward);
}
/*
* Serialization stores the forward bimap, the inverse of this inverse.
* Deserialization calls inverse() on the forward bimap and returns that
* inverse.
*
* If a bimap and its inverse are serialized together, the deserialized
* instances have inverse() methods that return the other.
*/
@Override
@ParametricNullness
K checkKey(@ParametricNullness K key) {
return inverse.checkValue(key);
}
@Override
@ParametricNullness
V checkValue(@ParametricNullness V value) {
return inverse.checkKey(value);
}
/**
* @serialData the forward bimap
*/
@GwtIncompatible // java.io.ObjectOutputStream
@J2ktIncompatible
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
stream.writeObject(inverse());
}
@GwtIncompatible // java.io.ObjectInputStream
@J2ktIncompatible
@SuppressWarnings("unchecked") // reading data stored by writeObject
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
setInverse((AbstractBiMap) stream.readObject());
}
@GwtIncompatible // Not needed in the emulated source.
@J2ktIncompatible
Object readResolve() {
return inverse().inverse();
}
@GwtIncompatible // Not needed in emulated source.
@J2ktIncompatible
private static final long serialVersionUID = 0;
}
@GwtIncompatible // Not needed in emulated source.
@J2ktIncompatible
private static final long serialVersionUID = 0;
}