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