001/*
002 * Copyright (C) 2007 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.google.common.collect;
018
019import static com.google.common.base.Preconditions.checkNotNull;
020import static com.google.common.base.Preconditions.checkState;
021import static com.google.common.collect.CollectPreconditions.checkNonnegative;
022import static com.google.common.collect.Hashing.closedTableSize;
023import static com.google.common.collect.Hashing.smearedHash;
024import static com.google.common.collect.Maps.capacity;
025import static java.util.Objects.requireNonNull;
026
027import com.google.common.annotations.GwtCompatible;
028import com.google.common.annotations.GwtIncompatible;
029import com.google.common.annotations.J2ktIncompatible;
030import com.google.common.annotations.VisibleForTesting;
031import com.google.errorprone.annotations.CanIgnoreReturnValue;
032import com.google.j2objc.annotations.WeakOuter;
033import java.io.IOException;
034import java.io.ObjectInputStream;
035import java.io.ObjectOutputStream;
036import java.util.AbstractMap.SimpleImmutableEntry;
037import java.util.Arrays;
038import java.util.Collection;
039import java.util.ConcurrentModificationException;
040import java.util.HashMap;
041import java.util.Iterator;
042import java.util.Map;
043import java.util.Map.Entry;
044import java.util.NoSuchElementException;
045import java.util.Objects;
046import java.util.Set;
047import java.util.Spliterator;
048import java.util.Spliterators;
049import java.util.function.Consumer;
050import org.jspecify.annotations.Nullable;
051
052/**
053 * Implementation of {@code Multimap} that does not allow duplicate key-value entries and that
054 * returns collections whose iterators follow the ordering in which the data was added to the
055 * multimap.
056 *
057 * <p>The collections returned by {@code keySet}, {@code keys}, and {@code asMap} iterate through
058 * the keys in the order they were first added to the multimap. Similarly, {@code get}, {@code
059 * removeAll}, and {@code replaceValues} return collections that iterate through the values in the
060 * order they were added. The collections generated by {@code entries} and {@code values} iterate
061 * across the key-value mappings in the order they were added to the multimap.
062 *
063 * <p>The iteration ordering of the collections generated by {@code keySet}, {@code keys}, and
064 * {@code asMap} has a few subtleties. As long as the set of keys remains unchanged, adding or
065 * removing mappings does not affect the key iteration order. However, if you remove all values
066 * associated with a key and then add the key back to the multimap, that key will come last in the
067 * key iteration order.
068 *
069 * <p>The multimap does not store duplicate key-value pairs. Adding a new key-value pair equal to an
070 * existing key-value pair has no effect.
071 *
072 * <p>Keys and values may be null. All optional multimap methods are supported, and all returned
073 * views are modifiable.
074 *
075 * <p>This class is not threadsafe when any concurrent operations update the multimap. Concurrent
076 * read operations will work correctly. To allow concurrent update operations, wrap your multimap
077 * with a call to {@link Multimaps#synchronizedSetMultimap}.
078 *
079 * <p><b>Warning:</b> Do not modify either a key <i>or a value</i> of a {@code LinkedHashMultimap}
080 * in a way that affects its {@link Object#equals} behavior. Undefined behavior and bugs will
081 * result.
082 *
083 * <p>See the Guava User Guide article on <a href=
084 * "https://github.com/google/guava/wiki/NewCollectionTypesExplained#multimap">{@code Multimap}</a>.
085 *
086 * @author Jared Levy
087 * @author Louis Wasserman
088 * @since 2.0
089 */
090@GwtCompatible
091public final class LinkedHashMultimap<K extends @Nullable Object, V extends @Nullable Object>
092    extends AbstractSetMultimap<K, V> {
093
094  /** Creates a new, empty {@code LinkedHashMultimap} with the default initial capacities. */
095  public static <K extends @Nullable Object, V extends @Nullable Object>
096      LinkedHashMultimap<K, V> create() {
097    return new LinkedHashMultimap<>(DEFAULT_KEY_CAPACITY, DEFAULT_VALUE_SET_CAPACITY);
098  }
099
100  /**
101   * Constructs an empty {@code LinkedHashMultimap} with enough capacity to hold the specified
102   * numbers of keys and values without rehashing.
103   *
104   * @param expectedKeys the expected number of distinct keys
105   * @param expectedValuesPerKey the expected average number of values per key
106   * @throws IllegalArgumentException if {@code expectedKeys} or {@code expectedValuesPerKey} is
107   *     negative
108   */
109  public static <K extends @Nullable Object, V extends @Nullable Object>
110      LinkedHashMultimap<K, V> create(int expectedKeys, int expectedValuesPerKey) {
111    return new LinkedHashMultimap<>(capacity(expectedKeys), capacity(expectedValuesPerKey));
112  }
113
114  /**
115   * Constructs a {@code LinkedHashMultimap} with the same mappings as the specified multimap. If a
116   * key-value mapping appears multiple times in the input multimap, it only appears once in the
117   * constructed multimap. The new multimap has the same {@link Multimap#entries()} iteration order
118   * as the input multimap, except for excluding duplicate mappings.
119   *
120   * @param multimap the multimap whose contents are copied to this multimap
121   */
122  public static <K extends @Nullable Object, V extends @Nullable Object>
123      LinkedHashMultimap<K, V> create(Multimap<? extends K, ? extends V> multimap) {
124    LinkedHashMultimap<K, V> result = create(multimap.keySet().size(), DEFAULT_VALUE_SET_CAPACITY);
125    result.putAll(multimap);
126    return result;
127  }
128
129  /**
130   * LinkedHashMultimap entries are in no less than three coexisting linked lists: a bucket in the
131   * hash table for a {@code Set<V>} associated with a key, the linked list of insertion-ordered
132   * entries in that {@code Set<V>}, and the linked list of entries in the LinkedHashMultimap as a
133   * whole.
134   */
135  @VisibleForTesting
136  static final class ValueEntry<K extends @Nullable Object, V extends @Nullable Object>
137      extends SimpleImmutableEntry<K, V> {
138    final int smearedValueHash;
139
140    @Nullable ValueEntry<K, V> nextInValueBucket;
141
142    private @Nullable ValueEntry<K, V> predecessorInValueSet;
143    private @Nullable ValueEntry<K, V> successorInValueSet;
144
145    private @Nullable ValueEntry<K, V> predecessorInMultimap;
146    private @Nullable ValueEntry<K, V> successorInMultimap;
147
148    ValueEntry(
149        @ParametricNullness K key,
150        @ParametricNullness V value,
151        int smearedValueHash,
152        @Nullable ValueEntry<K, V> nextInValueBucket) {
153      super(key, value);
154      this.smearedValueHash = smearedValueHash;
155      this.nextInValueBucket = nextInValueBucket;
156    }
157
158    boolean matchesValue(@Nullable Object v, int smearedVHash) {
159      return smearedValueHash == smearedVHash && Objects.equals(getValue(), v);
160    }
161  }
162
163  private static final int DEFAULT_KEY_CAPACITY = 16;
164  private static final int DEFAULT_VALUE_SET_CAPACITY = 2;
165  @VisibleForTesting static final double VALUE_SET_LOAD_FACTOR = 1.0;
166
167  /**
168   * Maximum allowed false positive probability of detecting a hash flooding attack given random
169   * input.
170   */
171  @VisibleForTesting(
172      )
173  static final double HASH_FLOODING_FPP = 0.001;
174
175  /**
176   * Maximum allowed length of a hash table bucket before falling back to a j.u.HashMap-based
177   * implementation. Experimentally determined.
178   */
179  private static final int MAX_HASH_BUCKET_LENGTH = 9;
180
181  @VisibleForTesting transient int valueSetCapacity;
182  private transient MultimapIterationChain<K, V> multimapIterationChain =
183      new MultimapIterationChain<>();
184
185  private LinkedHashMultimap(int keyCapacity, int valueSetCapacity) {
186    super(Platform.newLinkedHashMapWithExpectedSize(keyCapacity));
187    checkNonnegative(valueSetCapacity, "expectedValuesPerKey");
188
189    this.valueSetCapacity = valueSetCapacity;
190  }
191
192  /**
193   * {@inheritDoc}
194   *
195   * <p>Creates an empty {@code LinkedHashSet} for a collection of values for one key.
196   *
197   * @return a new {@code LinkedHashSet} containing a collection of values for one key
198   */
199  @Override
200  Set<V> createCollection() {
201    return Platform.newLinkedHashSetWithExpectedSize(valueSetCapacity);
202  }
203
204  /**
205   * {@inheritDoc}
206   *
207   * <p>Creates a decorated insertion-ordered set that also keeps track of the order in which
208   * key-value pairs are added to the multimap.
209   *
210   * @param key key to associate with values in the collection
211   * @return a new decorated set containing a collection of values for one key
212   */
213  @Override
214  Collection<V> createCollection(@ParametricNullness K key) {
215    return new ValueSet(key, valueSetCapacity);
216  }
217
218  /**
219   * {@inheritDoc}
220   *
221   * <p>If {@code values} is not empty and the multimap already contains a mapping for {@code key},
222   * the {@code keySet()} ordering is unchanged. However, the provided values always come last in
223   * the {@link #entries()} and {@link #values()} iteration orderings.
224   */
225  @CanIgnoreReturnValue
226  @Override
227  public Set<V> replaceValues(@ParametricNullness K key, Iterable<? extends V> values) {
228    return super.replaceValues(key, values);
229  }
230
231  /**
232   * Returns a set of all key-value pairs. Changes to the returned set will update the underlying
233   * multimap, and vice versa. The entries set does not support the {@code add} or {@code addAll}
234   * operations.
235   *
236   * <p>The iterator generated by the returned set traverses the entries in the order they were
237   * added to the multimap.
238   *
239   * <p>Each entry is an immutable snapshot of a key-value mapping in the multimap, taken at the
240   * time the entry is returned by a method call to the collection or its iterator.
241   */
242  @Override
243  public Set<Entry<K, V>> entries() {
244    return super.entries();
245  }
246
247  /**
248   * Returns a view collection of all <i>distinct</i> keys contained in this multimap. Note that the
249   * key set contains a key if and only if this multimap maps that key to at least one value.
250   *
251   * <p>The iterator generated by the returned set traverses the keys in the order they were first
252   * added to the multimap.
253   *
254   * <p>Changes to the returned set will update the underlying multimap, and vice versa. However,
255   * <i>adding</i> to the returned set is not possible.
256   */
257  @Override
258  public Set<K> keySet() {
259    return super.keySet();
260  }
261
262  /**
263   * Returns a collection of all values in the multimap. Changes to the returned collection will
264   * update the underlying multimap, and vice versa.
265   *
266   * <p>The iterator generated by the returned collection traverses the values in the order they
267   * were added to the multimap.
268   */
269  @Override
270  public Collection<V> values() {
271    return super.values();
272  }
273
274  @VisibleForTesting
275  @WeakOuter
276  final class ValueSet extends Sets.ImprovedAbstractSet<V> {
277    /*
278     * We currently use a fixed load factor of 1.0, a bit higher than normal to reduce memory
279     * consumption.
280     */
281
282    @ParametricNullness private final K key;
283
284    /*
285     * Either a `@Nullable ValueEntry<K, V>[]` or, if hash flooding has been detected, a `Map<V,
286     * ValueEntry<K, V>>`.
287     */
288    private Object hashTable;
289
290    private int size = 0;
291    private int modCount = 0;
292
293    private @Nullable ValueEntry<K, V> firstEntry;
294    private @Nullable ValueEntry<K, V> lastEntry;
295
296    ValueSet(@ParametricNullness K key, int expectedValues) {
297      this.key = key;
298      // Round expected values up to a power of 2 to get the table size.
299      int tableSize = closedTableSize(expectedValues, VALUE_SET_LOAD_FACTOR);
300
301      @SuppressWarnings({"rawtypes", "unchecked"})
302      @Nullable ValueEntry<K, V>[] hashTable = new @Nullable ValueEntry[tableSize];
303      this.hashTable = hashTable;
304    }
305
306    // The only place we put a Map in is convertToHashFloodingResistantImplementation.
307    @SuppressWarnings("unchecked")
308    @VisibleForTesting
309    @Nullable Map<V, ValueEntry<K, V>> delegateOrNull() {
310      if (hashTable instanceof Map) {
311        return (Map<V, ValueEntry<K, V>>) hashTable;
312      }
313      return null;
314    }
315
316    // The only places we put an array in are the constructor and rehashIfNecessary.
317    @SuppressWarnings("unchecked")
318    @VisibleForTesting
319    @Nullable ValueEntry<K, V>[] hashTableArray() {
320      return (@Nullable ValueEntry<K, V>[]) hashTable;
321    }
322
323    @VisibleForTesting
324    void convertToHashFloodingResistantImplementation() {
325      Map<V, ValueEntry<K, V>> newDelegate = new HashMap<>(hashTableArray().length, 1.0f);
326      for (ValueEntry<K, V> entry = firstEntry; entry != null; entry = entry.successorInValueSet) {
327        newDelegate.put(entry.getValue(), entry);
328        entry.nextInValueBucket = null;
329      }
330      this.hashTable = newDelegate;
331    }
332
333    private void succeedsInValueSet(
334        @Nullable ValueEntry<K, V> pred, @Nullable ValueEntry<K, V> succ) {
335      if (pred == null) {
336        firstEntry = succ;
337      } else {
338        pred.successorInValueSet = succ;
339      }
340      if (succ == null) {
341        lastEntry = pred;
342      } else {
343        succ.predecessorInValueSet = pred;
344      }
345    }
346
347    private void deleteEntryExceptFromHashTable(ValueEntry<K, V> entry) {
348      succeedsInValueSet(entry.predecessorInValueSet, entry.successorInValueSet);
349      multimapIterationChain.delete(entry);
350      // TODO(cpovirk): See the TODO in clear() about clearing links from removed entries.
351      size--;
352      modCount++;
353    }
354
355    private ValueEntry<K, V> addEntryExceptToHashTable(
356        @ParametricNullness V value,
357        int smearedHash,
358        @Nullable ValueEntry<K, V> nextInValueBucket) {
359      ValueEntry<K, V> newEntry = new ValueEntry<>(key, value, smearedHash, nextInValueBucket);
360      succeedsInValueSet(lastEntry, newEntry);
361      lastEntry = newEntry;
362      multimapIterationChain.append(newEntry);
363      size++;
364      modCount++;
365      return newEntry;
366    }
367
368    @Override
369    public Iterator<V> iterator() {
370      return new Iterator<V>() {
371        @Nullable ValueEntry<K, V> nextEntry = firstEntry;
372        @Nullable ValueEntry<K, V> toRemove;
373        int expectedModCount = modCount;
374
375        void checkForComodification() {
376          if (modCount != expectedModCount) {
377            throw new ConcurrentModificationException();
378          }
379        }
380
381        @Override
382        public boolean hasNext() {
383          checkForComodification();
384          return nextEntry != null;
385        }
386
387        @Override
388        @ParametricNullness
389        public V next() {
390          checkForComodification();
391          ValueEntry<K, V> entry = nextEntry;
392          if (entry == null) {
393            throw new NoSuchElementException();
394          }
395          V result = entry.getValue();
396          toRemove = entry;
397          nextEntry = entry.successorInValueSet;
398          return result;
399        }
400
401        @Override
402        public void remove() {
403          checkForComodification();
404          checkState(toRemove != null, "no calls to next() since the last call to remove()");
405          ValueSet.this.remove(toRemove.getValue());
406          expectedModCount = modCount;
407          toRemove = null;
408        }
409      };
410    }
411
412    @Override
413    public void forEach(Consumer<? super V> action) {
414      checkNotNull(action);
415      for (ValueEntry<K, V> entry = firstEntry; entry != null; entry = entry.successorInValueSet) {
416        action.accept(entry.getValue());
417      }
418    }
419
420    @Override
421    public int size() {
422      return size;
423    }
424
425    @Override
426    public boolean contains(@Nullable Object o) {
427      Map<V, ValueEntry<K, V>> delegate = delegateOrNull();
428      if (delegate != null) {
429        return delegate.containsKey(o);
430      }
431
432      @Nullable ValueEntry<K, V>[] hashTable = hashTableArray();
433      int smearedHash = smearedHash(o);
434      for (ValueEntry<K, V> entry = hashTable[smearedHash & (hashTable.length - 1)];
435          entry != null;
436          entry = entry.nextInValueBucket) {
437        if (entry.matchesValue(o, smearedHash)) {
438          return true;
439        }
440      }
441      return false;
442    }
443
444    @Override
445    public boolean add(@ParametricNullness V value) {
446      int smearedHash = smearedHash(value);
447
448      Map<V, ValueEntry<K, V>> delegate = delegateOrNull();
449      if (delegate != null) {
450        if (delegate.containsKey(value)) {
451          return false;
452        }
453        ValueEntry<K, V> newEntry =
454            addEntryExceptToHashTable(value, smearedHash, /* nextInValueBucket= */ null);
455        delegate.put(value, newEntry);
456        return true;
457      }
458
459      @Nullable ValueEntry<K, V>[] hashTable = hashTableArray();
460      int mask = hashTable.length - 1;
461      int bucket = smearedHash & mask;
462      ValueEntry<K, V> rowHead = hashTable[bucket];
463      int bucketLength = 0;
464      for (ValueEntry<K, V> entry = rowHead; entry != null; entry = entry.nextInValueBucket) {
465        if (entry.matchesValue(value, smearedHash)) {
466          return false;
467        }
468        bucketLength++;
469      }
470
471      hashTable[bucket] = addEntryExceptToHashTable(value, smearedHash, rowHead);
472      if (bucketLength >= MAX_HASH_BUCKET_LENGTH) {
473        convertToHashFloodingResistantImplementation();
474      } else {
475        rehashIfNecessary(hashTable);
476      }
477      return true;
478    }
479
480    private void rehashIfNecessary(@Nullable ValueEntry<K, V>[] oldHashTable) {
481      if (Hashing.needsResizing(size, oldHashTable.length, VALUE_SET_LOAD_FACTOR)) {
482        @SuppressWarnings("unchecked")
483        @Nullable ValueEntry<K, V>[] hashTable =
484            (@Nullable ValueEntry<K, V>[]) new ValueEntry<?, ?>[oldHashTable.length * 2];
485        this.hashTable = hashTable;
486        int mask = hashTable.length - 1;
487        for (ValueEntry<K, V> entry = firstEntry;
488            entry != null;
489            entry = entry.successorInValueSet) {
490          int bucket = entry.smearedValueHash & mask;
491          entry.nextInValueBucket = hashTable[bucket];
492          hashTable[bucket] = entry;
493        }
494      }
495    }
496
497    @CanIgnoreReturnValue
498    @Override
499    public boolean remove(@Nullable Object o) {
500      Map<V, ValueEntry<K, V>> delegate = delegateOrNull();
501      if (delegate != null) {
502        ValueEntry<K, V> entry = delegate.remove(o);
503        if (entry != null) {
504          deleteEntryExceptFromHashTable(entry);
505          return true;
506        }
507        return false;
508      }
509
510      @Nullable ValueEntry<K, V>[] hashTable = hashTableArray();
511      int smearedHash = smearedHash(o);
512      int bucket = smearedHash & (hashTable.length - 1);
513      ValueEntry<K, V> prev = null;
514      for (ValueEntry<K, V> entry = hashTable[bucket];
515          entry != null;
516          prev = entry, entry = entry.nextInValueBucket) {
517        if (entry.matchesValue(o, smearedHash)) {
518          if (prev == null) {
519            // first entry in the bucket
520            hashTable[bucket] = entry.nextInValueBucket;
521          } else {
522            prev.nextInValueBucket = entry.nextInValueBucket;
523          }
524          deleteEntryExceptFromHashTable(entry);
525          return true;
526        }
527      }
528      return false;
529    }
530
531    @Override
532    public void clear() {
533      Map<V, ValueEntry<K, V>> delegate = delegateOrNull();
534      if (delegate != null) {
535        delegate.clear();
536      } else {
537        Arrays.fill(hashTableArray(), null);
538      }
539      size = 0;
540      for (ValueEntry<K, V> entry = firstEntry; entry != null; entry = entry.successorInValueSet) {
541        multimapIterationChain.delete(entry);
542        /*
543         * TODO(cpovirk): Also clear *InValueSet (after reading next), *InMultimap, and
544         * nextInValueBucket (both here and in remove)? Because entryIterator() returns ValueEntry
545         * instances directly to callers of entries(), a caller that retains a removed Entry can
546         * otherwise keep the rest of the multimap's entries alive through those links (similar to
547         * what cl/846336997 fixed for HashBiMap). However, entryIterator() does not currently check
548         * for concurrent modification, so clearing successorInMultimap on removal could cause an
549         * in-progress entryIterator() to stop early if its nextEntry is removed externally.
550         */
551      }
552      firstEntry = null;
553      lastEntry = null;
554      modCount++;
555    }
556  }
557
558  @Override
559  Iterator<Entry<K, V>> entryIterator() {
560    return new Iterator<Entry<K, V>>() {
561      @Nullable ValueEntry<K, V> nextEntry = multimapIterationChain.firstEntry;
562      @Nullable ValueEntry<K, V> toRemove;
563
564      @Override
565      public boolean hasNext() {
566        return nextEntry != null;
567      }
568
569      @Override
570      public Entry<K, V> next() {
571        ValueEntry<K, V> entry = nextEntry;
572        if (entry == null) {
573          throw new NoSuchElementException();
574        }
575        toRemove = entry;
576        nextEntry = entry.successorInMultimap;
577        return entry;
578      }
579
580      @Override
581      public void remove() {
582        checkState(toRemove != null, "no calls to next() since the last call to remove()");
583        LinkedHashMultimap.this.remove(toRemove.getKey(), toRemove.getValue());
584        toRemove = null;
585      }
586    };
587  }
588
589  @Override
590  Spliterator<Entry<K, V>> entrySpliterator() {
591    return Spliterators.spliterator(
592        entries(), Spliterator.DISTINCT | Spliterator.ORDERED | Spliterator.NONNULL);
593  }
594
595  @Override
596  Iterator<V> valueIterator() {
597    return Maps.valueIterator(entryIterator());
598  }
599
600  @Override
601  Spliterator<V> valueSpliterator() {
602    return CollectSpliterators.map(entrySpliterator(), Spliterator.ORDERED, Entry::getValue);
603  }
604
605  /**
606   * @serialData the expected values per key, the number of distinct keys, the number of entries,
607   *     and the entries in order
608   */
609  @GwtIncompatible
610  @J2ktIncompatible
611    private void writeObject(ObjectOutputStream stream) throws IOException {
612    stream.defaultWriteObject();
613    stream.writeInt(keySet().size());
614    for (K key : keySet()) {
615      stream.writeObject(key);
616    }
617    stream.writeInt(size());
618    for (Entry<K, V> entry : entries()) {
619      stream.writeObject(entry.getKey());
620      stream.writeObject(entry.getValue());
621    }
622  }
623
624  @GwtIncompatible
625  @J2ktIncompatible
626    private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
627    stream.defaultReadObject();
628    multimapIterationChain = new MultimapIterationChain<>();
629    valueSetCapacity = DEFAULT_VALUE_SET_CAPACITY;
630    int distinctKeys = stream.readInt();
631    Map<K, Collection<V>> map = Platform.newLinkedHashMapWithExpectedSize(12);
632    for (int i = 0; i < distinctKeys; i++) {
633      @SuppressWarnings("unchecked")
634      K key = (K) stream.readObject();
635      map.put(key, createCollection(key));
636    }
637    int entries = stream.readInt();
638    for (int i = 0; i < entries; i++) {
639      @SuppressWarnings("unchecked")
640      K key = (K) stream.readObject();
641      @SuppressWarnings("unchecked")
642      V value = (V) stream.readObject();
643      /*
644       * requireNonNull is safe for a properly serialized multimap: We've already inserted a
645       * collection for each key that we expect.
646       */
647      requireNonNull(map.get(key)).add(value);
648    }
649    setMap(map);
650  }
651
652  private static final class MultimapIterationChain<
653      K extends @Nullable Object, V extends @Nullable Object> {
654    @Nullable ValueEntry<K, V> firstEntry;
655    @Nullable ValueEntry<K, V> lastEntry;
656
657    void succeeds(@Nullable ValueEntry<K, V> pred, @Nullable ValueEntry<K, V> succ) {
658      if (pred == null) {
659        firstEntry = succ;
660      } else {
661        pred.successorInMultimap = succ;
662      }
663      if (succ == null) {
664        lastEntry = pred;
665      } else {
666        succ.predecessorInMultimap = pred;
667      }
668    }
669
670    void delete(ValueEntry<K, V> entry) {
671      succeeds(entry.predecessorInMultimap, entry.successorInMultimap);
672    }
673
674    void append(ValueEntry<K, V> newEntry) {
675      succeeds(lastEntry, newEntry);
676      lastEntry = newEntry;
677    }
678  }
679
680  @GwtIncompatible @J2ktIncompatible private static final long serialVersionUID = 1;
681}