001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.collections4.map;
018
019import java.io.IOException;
020import java.io.ObjectInputStream;
021import java.io.ObjectOutputStream;
022import java.io.Serializable;
023import java.util.Map;
024import java.util.Objects;
025
026import org.apache.commons.collections4.MapIterator;
027import org.apache.commons.collections4.keyvalue.MultiKey;
028import org.apache.commons.collections4.map.AbstractHashedMap.HashEntry;
029
030/**
031 * A {@code Map} implementation that uses multiple keys to map the value.
032 * <p>
033 * This class is the most efficient way to uses multiple keys to map to a value.
034 * The best way to use this class is via the additional map-style methods.
035 * These provide {@code get}, {@code containsKey}, {@code put} and
036 * {@code remove} for individual keys which operate without extra object creation.
037 * </p>
038 * <p>
039 * The additional methods are the main interface of this map.
040 * As such, you will not normally hold this map in a variable of type {@code Map}.
041 * </p>
042 * <p>
043 * The normal map methods take in and return a {@link MultiKey}.
044 * If you try to use {@code put()} with any other object type a
045 * {@code ClassCastException} is thrown. If you try to use {@code null} as
046 * the key in {@code put()} a {@code NullPointerException} is thrown.
047 * </p>
048 * <p>
049 * This map is implemented as a decorator of a {@code AbstractHashedMap} which
050 * enables extra behavior to be added easily.
051 * </p>
052 * <ul>
053 * <li>{@code MultiKeyMap.decorate(new LinkedMap())} creates an ordered map.
054 * <li>{@code MultiKeyMap.decorate(new LRUMap())} creates an least recently used map.
055 * <li>{@code MultiKeyMap.decorate(new ReferenceMap())} creates a garbage collector sensitive map.
056 * </ul>
057 * <p>
058 * Note that {@code IdentityMap} and {@code ReferenceIdentityMap} are unsuitable
059 * for use as the key comparison would work on the whole MultiKey, not the elements within.
060 * </p>
061 * <p>
062 * As an example, consider a least recently used cache that uses a String airline code
063 * and a Locale to lookup the airline's name:
064 * </p>
065 * <pre>
066 * private MultiKeyMap cache = MultiKeyMap.multiKeyMap(new LRUMap(50));
067 *
068 * public String getAirlineName(String code, String locale) {
069 *   String name = (String) cache.get(code, locale);
070 *   if (name == null) {
071 *     name = getAirlineNameFromDB(code, locale);
072 *     cache.put(code, locale, name);
073 *   }
074 *   return name;
075 * }
076 * </pre>
077 * <p>
078 * <strong>Note that MultiKeyMap is not synchronized and is not thread-safe.</strong>
079 * If you wish to use this map from multiple threads concurrently, you must use
080 * appropriate synchronization. This class may throw exceptions when accessed
081 * by concurrent threads without synchronization.
082 * </p>
083 *
084 * @param <K> the type of the keys in this map
085 * @param <V> the type of the values in this map
086 * @since 3.1
087 */
088public class MultiKeyMap<K, V> extends AbstractMapDecorator<MultiKey<? extends K>, V>
089        implements Serializable, Cloneable {
090
091    /** Serialisation version */
092    private static final long serialVersionUID = -1788199231038721040L;
093
094    /**
095     * Decorates the specified map to add the MultiKeyMap API and fast query.
096     * The map must not be null and must be empty.
097     *
098     * @param <K>  the key type
099     * @param <V>  the value type
100     * @param map  the map to decorate, not null
101     * @return a new multi key map
102     * @throws NullPointerException if map is null
103     * @throws IllegalArgumentException if the map is not empty
104     * @since 4.0
105     */
106    public static <K, V> MultiKeyMap<K, V> multiKeyMap(final AbstractHashedMap<MultiKey<? extends K>, V> map) {
107        Objects.requireNonNull(map, "map");
108        if (map.isEmpty()) {
109            return new MultiKeyMap<>(map);
110        }
111        throw new IllegalArgumentException("Map must be empty");
112    }
113
114    /**
115     * Constructs a new MultiKeyMap that decorates a {@code HashedMap}.
116     */
117    public MultiKeyMap() {
118        this(new HashedMap<>());
119    }
120
121    /**
122     * Constructor that decorates the specified map and is called from
123     * {@link #multiKeyMap(AbstractHashedMap)}.
124     * The map must not be null and should be empty or only contain valid keys.
125     * This constructor performs no validation.
126     *
127     * @param map  the map to decorate
128     */
129    protected MultiKeyMap(final AbstractHashedMap<MultiKey<? extends K>, V> map) {
130        super(map);
131        this.map = map;
132    }
133
134    /**
135     * Check to ensure that input keys are valid MultiKey objects.
136     *
137     * @param key  the key to check
138     */
139    protected void checkKey(final MultiKey<?> key) {
140        Objects.requireNonNull(key, "key");
141    }
142
143    /**
144     * Clones the map without cloning the keys or values.
145     *
146     * @return a shallow clone
147     */
148    @SuppressWarnings("unchecked")
149    @Override
150    public MultiKeyMap<K, V> clone() {
151        try {
152            return (MultiKeyMap<K, V>) super.clone();
153        } catch (final CloneNotSupportedException e) {
154            throw new UnsupportedOperationException(e);
155        }
156    }
157
158    /**
159     * Checks whether the map contains the specified multi-key.
160     *
161     * @param key1  the first key
162     * @param key2  the second key
163     * @return true if the map contains the key
164     */
165    public boolean containsKey(final Object key1, final Object key2) {
166        final int hashCode = hash(key1, key2);
167        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry = decoratedHashEntry(hashCode);
168        while (entry != null) {
169            if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2)) {
170                return true;
171            }
172            entry = entry.next;
173        }
174        return false;
175    }
176
177    /**
178     * Checks whether the map contains the specified multi-key.
179     *
180     * @param key1  the first key
181     * @param key2  the second key
182     * @param key3  the third key
183     * @return true if the map contains the key
184     */
185    public boolean containsKey(final Object key1, final Object key2, final Object key3) {
186        final int hashCode = hash(key1, key2, key3);
187        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry = decoratedHashEntry(hashCode);
188        while (entry != null) {
189            if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3)) {
190                return true;
191            }
192            entry = entry.next;
193        }
194        return false;
195    }
196
197    /**
198     * Checks whether the map contains the specified multi-key.
199     *
200     * @param key1  the first key
201     * @param key2  the second key
202     * @param key3  the third key
203     * @param key4  the fourth key
204     * @return true if the map contains the key
205     */
206    public boolean containsKey(final Object key1, final Object key2, final Object key3, final Object key4) {
207        final int hashCode = hash(key1, key2, key3, key4);
208        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry = decoratedHashEntry(hashCode);
209        while (entry != null) {
210            if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3, key4)) {
211                return true;
212            }
213            entry = entry.next;
214        }
215        return false;
216    }
217
218    /**
219     * Checks whether the map contains the specified multi-key.
220     *
221     * @param key1  the first key
222     * @param key2  the second key
223     * @param key3  the third key
224     * @param key4  the fourth key
225     * @param key5  the fifth key
226     * @return true if the map contains the key
227     */
228    public boolean containsKey(final Object key1, final Object key2, final Object key3, final Object key4, final Object key5) {
229        final int hashCode = hash(key1, key2, key3, key4, key5);
230        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry = decoratedHashEntry(hashCode);
231        while (entry != null) {
232            if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3, key4, key5)) {
233                return true;
234            }
235            entry = entry.next;
236        }
237        return false;
238    }
239
240    /**
241     * {@inheritDoc}
242     */
243    @Override
244    protected AbstractHashedMap<MultiKey<? extends K>, V> decorated() {
245        return (AbstractHashedMap<MultiKey<? extends K>, V>) super.decorated();
246    }
247
248    HashEntry<MultiKey<? extends K>, V> decoratedHashEntry(final int hashCode) {
249        return decorated().data[decoratedHashIndex(hashCode)];
250    }
251
252    int decoratedHashIndex(final int hashCode) {
253        return decorated().hashIndex(hashCode, decorated().data.length);
254    }
255
256    /**
257     * Gets the value mapped to the specified multi-key.
258     *
259     * @param key1  the first key
260     * @param key2  the second key
261     * @return the mapped value, null if no match
262     */
263    public V get(final Object key1, final Object key2) {
264        final int hashCode = hash(key1, key2);
265        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry = decoratedHashEntry(hashCode);
266        while (entry != null) {
267            if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2)) {
268                return entry.getValue();
269            }
270            entry = entry.next;
271        }
272        return null;
273    }
274
275    /**
276     * Gets the value mapped to the specified multi-key.
277     *
278     * @param key1  the first key
279     * @param key2  the second key
280     * @param key3  the third key
281     * @return the mapped value, null if no match
282     */
283    public V get(final Object key1, final Object key2, final Object key3) {
284        final int hashCode = hash(key1, key2, key3);
285        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry = decoratedHashEntry(hashCode);
286        while (entry != null) {
287            if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3)) {
288                return entry.getValue();
289            }
290            entry = entry.next;
291        }
292        return null;
293    }
294
295    /**
296     * Gets the value mapped to the specified multi-key.
297     *
298     * @param key1  the first key
299     * @param key2  the second key
300     * @param key3  the third key
301     * @param key4  the fourth key
302     * @return the mapped value, null if no match
303     */
304    public V get(final Object key1, final Object key2, final Object key3, final Object key4) {
305        final int hashCode = hash(key1, key2, key3, key4);
306        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry = decoratedHashEntry(hashCode);
307        while (entry != null) {
308            if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3, key4)) {
309                return entry.getValue();
310            }
311            entry = entry.next;
312        }
313        return null;
314    }
315
316    /**
317     * Gets the value mapped to the specified multi-key.
318     *
319     * @param key1  the first key
320     * @param key2  the second key
321     * @param key3  the third key
322     * @param key4  the fourth key
323     * @param key5  the fifth key
324     * @return the mapped value, null if no match
325     */
326    public V get(final Object key1, final Object key2, final Object key3, final Object key4, final Object key5) {
327        final int hashCode = hash(key1, key2, key3, key4, key5);
328        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry = decoratedHashEntry(hashCode);
329        while (entry != null) {
330            if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3, key4, key5)) {
331                return entry.getValue();
332            }
333            entry = entry.next;
334        }
335        return null;
336    }
337
338    /**
339     * Gets the hash code for the specified multi-key.
340     *
341     * @param key1  the first key
342     * @param key2  the second key
343     * @return the hash code
344     */
345    protected int hash(final Object key1, final Object key2) {
346        int h = 0;
347        if (key1 != null) {
348            h ^= key1.hashCode();
349        }
350        if (key2 != null) {
351            h ^= key2.hashCode();
352        }
353        h += ~(h << 9);
354        h ^=  h >>> 14;
355        h +=  h << 4;
356        h ^=  h >>> 10;
357        return h;
358    }
359
360    /**
361     * Gets the hash code for the specified multi-key.
362     *
363     * @param key1  the first key
364     * @param key2  the second key
365     * @param key3  the third key
366     * @return the hash code
367     */
368    protected int hash(final Object key1, final Object key2, final Object key3) {
369        int h = 0;
370        if (key1 != null) {
371            h ^= key1.hashCode();
372        }
373        if (key2 != null) {
374            h ^= key2.hashCode();
375        }
376        if (key3 != null) {
377            h ^= key3.hashCode();
378        }
379        h += ~(h << 9);
380        h ^=  h >>> 14;
381        h +=  h << 4;
382        h ^=  h >>> 10;
383        return h;
384    }
385
386    /**
387     * Gets the hash code for the specified multi-key.
388     *
389     * @param key1  the first key
390     * @param key2  the second key
391     * @param key3  the third key
392     * @param key4  the fourth key
393     * @return the hash code
394     */
395    protected int hash(final Object key1, final Object key2, final Object key3, final Object key4) {
396        int h = 0;
397        if (key1 != null) {
398            h ^= key1.hashCode();
399        }
400        if (key2 != null) {
401            h ^= key2.hashCode();
402        }
403        if (key3 != null) {
404            h ^= key3.hashCode();
405        }
406        if (key4 != null) {
407            h ^= key4.hashCode();
408        }
409        h += ~(h << 9);
410        h ^=  h >>> 14;
411        h +=  h << 4;
412        h ^=  h >>> 10;
413        return h;
414    }
415
416    /**
417     * Gets the hash code for the specified multi-key.
418     *
419     * @param key1  the first key
420     * @param key2  the second key
421     * @param key3  the third key
422     * @param key4  the fourth key
423     * @param key5  the fifth key
424     * @return the hash code
425     */
426    protected int hash(final Object key1, final Object key2, final Object key3, final Object key4, final Object key5) {
427        int h = 0;
428        if (key1 != null) {
429            h ^= key1.hashCode();
430        }
431        if (key2 != null) {
432            h ^= key2.hashCode();
433        }
434        if (key3 != null) {
435            h ^= key3.hashCode();
436        }
437        if (key4 != null) {
438            h ^= key4.hashCode();
439        }
440        if (key5 != null) {
441            h ^= key5.hashCode();
442        }
443        h += ~(h << 9);
444        h ^=  h >>> 14;
445        h +=  h << 4;
446        h ^=  h >>> 10;
447        return h;
448    }
449
450    /**
451     * Is the key equal to the combined key.
452     *
453     * @param entry  the entry to compare to
454     * @param key1  the first key
455     * @param key2  the second key
456     * @return true if the key matches
457     */
458    protected boolean isEqualKey(final AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry,
459            final Object key1, final Object key2) {
460        final MultiKey<? extends K> multi = entry.getKey();
461        return
462            multi.size() == 2 &&
463            Objects.equals(key1, multi.getKey(0)) &&
464            Objects.equals(key2, multi.getKey(1));
465    }
466
467    /**
468     * Is the key equal to the combined key.
469     *
470     * @param entry  the entry to compare to
471     * @param key1  the first key
472     * @param key2  the second key
473     * @param key3  the third key
474     * @return true if the key matches
475     */
476    protected boolean isEqualKey(final AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry,
477                                 final Object key1, final Object key2, final Object key3) {
478        final MultiKey<? extends K> multi = entry.getKey();
479        return
480            multi.size() == 3 &&
481            Objects.equals(key1, multi.getKey(0)) &&
482            Objects.equals(key2, multi.getKey(1)) &&
483            Objects.equals(key3, multi.getKey(2));
484    }
485
486    /**
487     * Is the key equal to the combined key.
488     *
489     * @param entry  the entry to compare to
490     * @param key1  the first key
491     * @param key2  the second key
492     * @param key3  the third key
493     * @param key4  the fourth key
494     * @return true if the key matches
495     */
496    protected boolean isEqualKey(final AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry,
497                                 final Object key1, final Object key2, final Object key3, final Object key4) {
498        final MultiKey<? extends K> multi = entry.getKey();
499        return
500            multi.size() == 4 &&
501            Objects.equals(key1, multi.getKey(0)) &&
502            Objects.equals(key2, multi.getKey(1)) &&
503            Objects.equals(key3, multi.getKey(2)) &&
504            Objects.equals(key4, multi.getKey(3));
505    }
506
507    /**
508     * Is the key equal to the combined key.
509     *
510     * @param entry  the entry to compare to
511     * @param key1  the first key
512     * @param key2  the second key
513     * @param key3  the third key
514     * @param key4  the fourth key
515     * @param key5  the fifth key
516     * @return true if the key matches
517     */
518    protected boolean isEqualKey(final AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry,
519            final Object key1, final Object key2, final Object key3, final Object key4, final Object key5) {
520        final MultiKey<? extends K> multi = entry.getKey();
521        return
522            multi.size() == 5 &&
523            Objects.equals(key1, multi.getKey(0)) &&
524            Objects.equals(key2, multi.getKey(1)) &&
525            Objects.equals(key3, multi.getKey(2)) &&
526            Objects.equals(key4, multi.getKey(3)) &&
527            Objects.equals(key5, multi.getKey(4));
528    }
529
530    @Override
531    public MapIterator<MultiKey<? extends K>, V> mapIterator() {
532        return decorated().mapIterator();
533    }
534
535    /**
536     * Associates the specified value with the specified keys in this map.
537     *
538     * @param key1  the first key
539     * @param key2  the second key
540     * @param key3  the third key
541     * @param key4  the fourth key
542     * @param key5  the fifth key
543     * @param value  the value to store
544     * @return the value previously mapped to this combined key, null if none
545     */
546    public V put(final K key1, final K key2, final K key3, final K key4, final K key5, final V value) {
547        final int hashCode = hash(key1, key2, key3, key4, key5);
548        final int index = decoratedHashIndex(hashCode);
549        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry = decorated().data[index];
550        while (entry != null) {
551            if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3, key4, key5)) {
552                final V oldValue = entry.getValue();
553                decorated().updateEntry(entry, value);
554                return oldValue;
555            }
556            entry = entry.next;
557        }
558        decorated().addMapping(index, hashCode, new MultiKey<>(key1, key2, key3, key4, key5), value);
559        return null;
560    }
561
562    /**
563     * Associates the specified value with the specified keys in this map.
564     *
565     * @param key1  the first key
566     * @param key2  the second key
567     * @param key3  the third key
568     * @param key4  the fourth key
569     * @param value  the value to store
570     * @return the value previously mapped to this combined key, null if none
571     */
572    public V put(final K key1, final K key2, final K key3, final K key4, final V value) {
573        final int hashCode = hash(key1, key2, key3, key4);
574        final int index = decoratedHashIndex(hashCode);
575        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry = decorated().data[index];
576        while (entry != null) {
577            if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3, key4)) {
578                final V oldValue = entry.getValue();
579                decorated().updateEntry(entry, value);
580                return oldValue;
581            }
582            entry = entry.next;
583        }
584        decorated().addMapping(index, hashCode, new MultiKey<>(key1, key2, key3, key4), value);
585        return null;
586    }
587
588    /**
589     * Associates the specified value with the specified keys in this map.
590     *
591     * @param key1  the first key
592     * @param key2  the second key
593     * @param key3  the third key
594     * @param value  the value to store
595     * @return the value previously mapped to this combined key, null if none
596     */
597    public V put(final K key1, final K key2, final K key3, final V value) {
598        final int hashCode = hash(key1, key2, key3);
599        final int index = decoratedHashIndex(hashCode);
600        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry = decorated().data[index];
601        while (entry != null) {
602            if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3)) {
603                final V oldValue = entry.getValue();
604                decorated().updateEntry(entry, value);
605                return oldValue;
606            }
607            entry = entry.next;
608        }
609        decorated().addMapping(index, hashCode, new MultiKey<>(key1, key2, key3), value);
610        return null;
611    }
612
613    /**
614     * Associates the specified value with the specified keys in this map.
615     *
616     * @param key1  the first key
617     * @param key2  the second key
618     * @param value  the value to store
619     * @return the value previously mapped to this combined key, null if none
620     */
621    public V put(final K key1, final K key2, final V value) {
622        final int hashCode = hash(key1, key2);
623        final int index = decoratedHashIndex(hashCode);
624        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry = decorated().data[index];
625        while (entry != null) {
626            if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2)) {
627                final V oldValue = entry.getValue();
628                decorated().updateEntry(entry, value);
629                return oldValue;
630            }
631            entry = entry.next;
632        }
633        decorated().addMapping(index, hashCode, new MultiKey<>(key1, key2), value);
634        return null;
635    }
636
637    /**
638     * Puts the key and value into the map, where the key must be a non-null
639     * MultiKey object.
640     *
641     * @param key  the non-null MultiKey object
642     * @param value  the value to store
643     * @return the previous value for the key
644     * @throws NullPointerException if the key is null
645     * @throws ClassCastException if the key is not a MultiKey
646     */
647    @Override
648    public V put(final MultiKey<? extends K> key, final V value) {
649        checkKey(key);
650        return super.put(key, value);
651    }
652
653    /**
654     * Copies all of the keys and values from the specified map to this map.
655     * Each key must be non-null and a MultiKey object.
656     *
657     * @param mapToCopy  to this map
658     * @throws NullPointerException if the mapToCopy or any key within is null
659     * @throws ClassCastException if any key in mapToCopy is not a MultiKey
660     */
661    @Override
662    public void putAll(final Map<? extends MultiKey<? extends K>, ? extends V> mapToCopy) {
663        for (final MultiKey<? extends K> key : mapToCopy.keySet()) {
664            checkKey(key);
665        }
666        super.putAll(mapToCopy);
667    }
668
669    /**
670     * Deserializes the map in using a custom routine.
671     *
672     * @param in  the input stream
673     * @throws IOException if an error occurs while reading from the stream
674     * @throws ClassNotFoundException if an object read from the stream cannot be loaded
675     */
676    @SuppressWarnings("unchecked")
677    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
678        in.defaultReadObject();
679        map = (Map<MultiKey<? extends K>, V>) in.readObject();
680    }
681
682    /**
683     * Removes all mappings where the first key is that specified.
684     * <p>
685     * This method removes all the mappings where the {@code MultiKey}
686     * has one or more keys, and the first matches that specified.
687     * </p>
688     *
689     * @param key1  the first key
690     * @return true if any elements were removed
691     */
692    public boolean removeAll(final Object key1) {
693        boolean modified = false;
694        final MapIterator<MultiKey<? extends K>, V> it = mapIterator();
695        while (it.hasNext()) {
696            final MultiKey<? extends K> multi = it.next();
697            if (multi.size() >= 1 &&
698                Objects.equals(key1, multi.getKey(0))) {
699                it.remove();
700                modified = true;
701            }
702        }
703        return modified;
704    }
705
706    /**
707     * Removes all mappings where the first two keys are those specified.
708     * <p>
709     * This method removes all the mappings where the {@code MultiKey}
710     * has two or more keys, and the first two match those specified.
711     * </p>
712     *
713     * @param key1  the first key
714     * @param key2  the second key
715     * @return true if any elements were removed
716     */
717    public boolean removeAll(final Object key1, final Object key2) {
718        boolean modified = false;
719        final MapIterator<MultiKey<? extends K>, V> it = mapIterator();
720        while (it.hasNext()) {
721            final MultiKey<? extends K> multi = it.next();
722            if (multi.size() >= 2 &&
723                Objects.equals(key1, multi.getKey(0)) &&
724                Objects.equals(key2, multi.getKey(1))) {
725                it.remove();
726                modified = true;
727            }
728        }
729        return modified;
730    }
731
732    /**
733     * Removes all mappings where the first three keys are those specified.
734     * <p>
735     * This method removes all the mappings where the {@code MultiKey}
736     * has three or more keys, and the first three match those specified.
737     * </p>
738     *
739     * @param key1  the first key
740     * @param key2  the second key
741     * @param key3  the third key
742     * @return true if any elements were removed
743     */
744    public boolean removeAll(final Object key1, final Object key2, final Object key3) {
745        boolean modified = false;
746        final MapIterator<MultiKey<? extends K>, V> it = mapIterator();
747        while (it.hasNext()) {
748            final MultiKey<? extends K> multi = it.next();
749            if (multi.size() >= 3 &&
750                Objects.equals(key1, multi.getKey(0)) &&
751                Objects.equals(key2, multi.getKey(1)) &&
752                Objects.equals(key3, multi.getKey(2))) {
753                it.remove();
754                modified = true;
755            }
756        }
757        return modified;
758    }
759
760    /**
761     * Removes all mappings where the first four keys are those specified.
762     * <p>
763     * This method removes all the mappings where the {@code MultiKey}
764     * has four or more keys, and the first four match those specified.
765     * </p>
766     *
767     * @param key1  the first key
768     * @param key2  the second key
769     * @param key3  the third key
770     * @param key4  the fourth key
771     * @return true if any elements were removed
772     */
773    public boolean removeAll(final Object key1, final Object key2, final Object key3, final Object key4) {
774        boolean modified = false;
775        final MapIterator<MultiKey<? extends K>, V> it = mapIterator();
776        while (it.hasNext()) {
777            final MultiKey<? extends K> multi = it.next();
778            if (multi.size() >= 4 &&
779                Objects.equals(key1, multi.getKey(0)) &&
780                Objects.equals(key2, multi.getKey(1)) &&
781                Objects.equals(key3, multi.getKey(2)) &&
782                Objects.equals(key4, multi.getKey(3))) {
783                it.remove();
784                modified = true;
785            }
786        }
787        return modified;
788    }
789
790    /**
791     * Removes the specified multi-key from this map.
792     *
793     * @param key1  the first key
794     * @param key2  the second key
795     * @return the value mapped to the removed key, null if key not in map
796     * @since 4.0 (previous name: remove(Object, Object))
797     */
798    public V removeMultiKey(final Object key1, final Object key2) {
799        final int hashCode = hash(key1, key2);
800        final int index = decoratedHashIndex(hashCode);
801        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry = decorated().data[index];
802        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> previous = null;
803        while (entry != null) {
804            if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2)) {
805                final V oldValue = entry.getValue();
806                decorated().removeMapping(entry, index, previous);
807                return oldValue;
808            }
809            previous = entry;
810            entry = entry.next;
811        }
812        return null;
813    }
814
815    /**
816     * Removes the specified multi-key from this map.
817     *
818     * @param key1  the first key
819     * @param key2  the second key
820     * @param key3  the third key
821     * @return the value mapped to the removed key, null if key not in map
822     * @since 4.0 (previous name: remove(Object, Object, Object))
823     */
824    public V removeMultiKey(final Object key1, final Object key2, final Object key3) {
825        final int hashCode = hash(key1, key2, key3);
826        final int index = decoratedHashIndex(hashCode);
827        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry = decorated().data[index];
828        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> previous = null;
829        while (entry != null) {
830            if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3)) {
831                final V oldValue = entry.getValue();
832                decorated().removeMapping(entry, index, previous);
833                return oldValue;
834            }
835            previous = entry;
836            entry = entry.next;
837        }
838        return null;
839    }
840
841    /**
842     * Removes the specified multi-key from this map.
843     *
844     * @param key1  the first key
845     * @param key2  the second key
846     * @param key3  the third key
847     * @param key4  the fourth key
848     * @return the value mapped to the removed key, null if key not in map
849     * @since 4.0 (previous name: remove(Object, Object, Object, Object))
850     */
851    public V removeMultiKey(final Object key1, final Object key2, final Object key3, final Object key4) {
852        final int hashCode = hash(key1, key2, key3, key4);
853        final int index = decoratedHashIndex(hashCode);
854        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry = decorated().data[index];
855        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> previous = null;
856        while (entry != null) {
857            if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3, key4)) {
858                final V oldValue = entry.getValue();
859                decorated().removeMapping(entry, index, previous);
860                return oldValue;
861            }
862            previous = entry;
863            entry = entry.next;
864        }
865        return null;
866    }
867
868    /**
869     * Removes the specified multi-key from this map.
870     *
871     * @param key1  the first key
872     * @param key2  the second key
873     * @param key3  the third key
874     * @param key4  the fourth key
875     * @param key5  the fifth key
876     * @return the value mapped to the removed key, null if key not in map
877     * @since 4.0 (previous name: remove(Object, Object, Object, Object, Object))
878     */
879    public V removeMultiKey(final Object key1, final Object key2, final Object key3,
880                            final Object key4, final Object key5) {
881        final int hashCode = hash(key1, key2, key3, key4, key5);
882        final int index = decoratedHashIndex(hashCode);
883        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> entry = decorated().data[index];
884        AbstractHashedMap.HashEntry<MultiKey<? extends K>, V> previous = null;
885        while (entry != null) {
886            if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3, key4, key5)) {
887                final V oldValue = entry.getValue();
888                decorated().removeMapping(entry, index, previous);
889                return oldValue;
890            }
891            previous = entry;
892            entry = entry.next;
893        }
894        return null;
895    }
896
897    /**
898     * Serializes this object to an ObjectOutputStream.
899     *
900     * @param out the target ObjectOutputStream.
901     * @throws IOException thrown when an I/O errors occur writing to the target stream.
902     */
903    private void writeObject(final ObjectOutputStream out) throws IOException {
904        out.defaultWriteObject();
905        out.writeObject(map);
906    }
907
908}