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}