1/*
2 * Copyright (c) 2017, 2017, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23package org.graalvm.util.impl;
24
25import java.util.Iterator;
26import java.util.Objects;
27import java.util.function.BiFunction;
28
29import org.graalvm.util.Equivalence;
30import org.graalvm.util.EconomicMap;
31import org.graalvm.util.EconomicSet;
32import org.graalvm.util.UnmodifiableEconomicMap;
33import org.graalvm.util.UnmodifiableEconomicSet;
34import org.graalvm.util.MapCursor;
35
36/**
37 * Implementation of a map with a memory-efficient structure that always preserves insertion order
38 * when iterating over keys. Particularly efficient when number of entries is 0 or smaller equal
39 * {@link #INITIAL_CAPACITY} or smaller 256.
40 *
41 * The key/value pairs are kept in an expanding flat object array with keys at even indices and
42 * values at odd indices. If the map has smaller or equal to {@link #HASH_THRESHOLD} entries, there
43 * is no additional hash data structure and comparisons are done via linear checking of the
44 * key/value pairs. For the case where the equality check is particularly cheap (e.g., just an
45 * object identity comparison), this limit below which the map is without an actual hash table is
46 * higher and configured at {@link #HASH_THRESHOLD_IDENTITY_COMPARE}.
47 *
48 * When the hash table needs to be constructed, the field {@link #hashArray} becomes a new hash
49 * array where an entry of 0 means no hit and otherwise denotes the entry number in the
50 * {@link #entries} array. The hash array is interpreted as an actual byte array if the indices fit
51 * within 8 bit, or as an array of short values if the indices fit within 16 bit, or as an array of
52 * integer values in other cases.
53 *
54 * Hash collisions are handled by chaining a linked list of {@link CollisionLink} objects that take
55 * the place of the values in the {@link #entries} array.
56 *
57 * Removing entries will put {@code null} into the {@link #entries} array. If the occupation of the
58 * map falls below a specific threshold, the map will be compressed via the
59 * {@link #maybeCompress(int)} method.
60 */
61public final class EconomicMapImpl<K, V> implements EconomicMap<K, V>, EconomicSet<K> {
62
63    /**
64     * Initial number of key/value pair entries that is allocated in the first entries array.
65     */
66    private static final int INITIAL_CAPACITY = 4;
67
68    /**
69     * Maximum number of entries that are moved linearly forward if a key is removed.
70     */
71    private static final int COMPRESS_IMMEDIATE_CAPACITY = 8;
72
73    /**
74     * Minimum number of key/value pair entries added when the entries array is increased in size.
75     */
76    private static final int MIN_CAPACITY_INCREASE = 8;
77
78    /**
79     * Number of entries above which a hash table is created.
80     */
81    private static final int HASH_THRESHOLD = 4;
82
83    /**
84     * Number of entries above which a hash table is created when equality can be checked with
85     * object identity.
86     */
87    private static final int HASH_THRESHOLD_IDENTITY_COMPARE = 8;
88
89    /**
90     * Maximum number of entries allowed in the map.
91     */
92    private static final int MAX_ELEMENT_COUNT = Integer.MAX_VALUE >> 1;
93
94    /**
95     * Number of entries above which more than 1 byte is necessary for the hash index.
96     */
97    private static final int LARGE_HASH_THRESHOLD = ((1 << Byte.SIZE) << 1);
98
99    /**
100     * Number of entries above which more than 2 bytes are are necessary for the hash index.
101     */
102    private static final int VERY_LARGE_HASH_THRESHOLD = (LARGE_HASH_THRESHOLD << Byte.SIZE);
103
104    /**
105     * Total number of entries (actual entries plus deleted entries).
106     */
107    private int totalEntries;
108
109    /**
110     * Number of deleted entries.
111     */
112    private int deletedEntries;
113
114    /**
115     * Entries array with even indices storing keys and odd indices storing values.
116     */
117    private Object[] entries;
118
119    /**
120     * Hash array that is interpreted either as byte or short or int array depending on number of
121     * map entries.
122     */
123    private byte[] hashArray;
124
125    /**
126     * The strategy used for comparing keys or {@code null} for denoting special strategy
127     * {@link Equivalence#IDENTITY}.
128     */
129    private final Equivalence strategy;
130
131    /**
132     * Intercept method for debugging purposes.
133     */
134    private static <K, V> EconomicMapImpl<K, V> intercept(EconomicMapImpl<K, V> map) {
135        return map;
136    }
137
138    public static <K, V> EconomicMapImpl<K, V> create(Equivalence strategy) {
139        return intercept(new EconomicMapImpl<>(strategy));
140    }
141
142    public static <K, V> EconomicMapImpl<K, V> create(Equivalence strategy, int initialCapacity) {
143        return intercept(new EconomicMapImpl<>(strategy, initialCapacity));
144    }
145
146    public static <K, V> EconomicMapImpl<K, V> create(Equivalence strategy, UnmodifiableEconomicMap<K, V> other) {
147        return intercept(new EconomicMapImpl<>(strategy, other));
148    }
149
150    public static <K, V> EconomicMapImpl<K, V> create(Equivalence strategy, UnmodifiableEconomicSet<K> other) {
151        return intercept(new EconomicMapImpl<>(strategy, other));
152    }
153
154    private EconomicMapImpl(Equivalence strategy) {
155        if (strategy == Equivalence.IDENTITY) {
156            this.strategy = null;
157        } else {
158            this.strategy = strategy;
159        }
160    }
161
162    private EconomicMapImpl(Equivalence strategy, int initialCapacity) {
163        this(strategy);
164        init(initialCapacity);
165    }
166
167    private EconomicMapImpl(Equivalence strategy, UnmodifiableEconomicMap<K, V> other) {
168        this(strategy);
169        if (!initFrom(other)) {
170            init(other.size());
171            putAll(other);
172        }
173    }
174
175    private EconomicMapImpl(Equivalence strategy, UnmodifiableEconomicSet<K> other) {
176        this(strategy);
177        if (!initFrom(other)) {
178            init(other.size());
179            addAll(other);
180        }
181    }
182
183    @SuppressWarnings("unchecked")
184    private boolean initFrom(Object o) {
185        if (o instanceof EconomicMapImpl) {
186            EconomicMapImpl<K, V> otherMap = (EconomicMapImpl<K, V>) o;
187            // We are only allowed to directly copy if the strategies of the two maps are the same.
188            if (strategy == otherMap.strategy) {
189                totalEntries = otherMap.totalEntries;
190                deletedEntries = otherMap.deletedEntries;
191                if (otherMap.entries != null) {
192                    entries = otherMap.entries.clone();
193                }
194                if (otherMap.hashArray != null) {
195                    hashArray = otherMap.hashArray.clone();
196                }
197                return true;
198            }
199        }
200        return false;
201    }
202
203    private void init(int size) {
204        if (size > INITIAL_CAPACITY) {
205            entries = new Object[size << 1];
206        }
207    }
208
209    /**
210     * Links the collisions. Needs to be immutable class for allowing efficient shallow copy from
211     * other map on construction.
212     */
213    private static final class CollisionLink {
214
215        CollisionLink(Object value, int next) {
216            this.value = value;
217            this.next = next;
218        }
219
220        final Object value;
221
222        /**
223         * Index plus one of the next entry in the collision link chain.
224         */
225        final int next;
226    }
227
228    @SuppressWarnings("unchecked")
229    @Override
230    public V get(K key) {
231        Objects.requireNonNull(key);
232
233        int index = find(key);
234        if (index != -1) {
235            return (V) getValue(index);
236        }
237        return null;
238    }
239
240    private int find(K key) {
241        if (hasHashArray()) {
242            return findHash(key);
243        } else {
244            return findLinear(key);
245        }
246    }
247
248    private int findLinear(K key) {
249        for (int i = 0; i < totalEntries; i++) {
250            Object entryKey = entries[i << 1];
251            if (entryKey != null && compareKeys(key, entryKey)) {
252                return i;
253            }
254        }
255        return -1;
256    }
257
258    private boolean compareKeys(Object key, Object entryKey) {
259        if (key == entryKey) {
260            return true;
261        }
262        if (strategy != null && strategy != Equivalence.IDENTITY_WITH_SYSTEM_HASHCODE) {
263            if (strategy == Equivalence.DEFAULT) {
264                return key.equals(entryKey);
265            } else {
266                return strategy.equals(key, entryKey);
267            }
268        }
269        return false;
270    }
271
272    private int findHash(K key) {
273        int index = getHashArray(getHashIndex(key)) - 1;
274        if (index != -1) {
275            Object entryKey = getKey(index);
276            if (compareKeys(key, entryKey)) {
277                return index;
278            } else {
279                Object entryValue = getRawValue(index);
280                if (entryValue instanceof CollisionLink) {
281                    return findWithCollision(key, (CollisionLink) entryValue);
282                }
283            }
284        }
285
286        return -1;
287    }
288
289    private int findWithCollision(K key, CollisionLink initialEntryValue) {
290        int index;
291        Object entryKey;
292        CollisionLink entryValue = initialEntryValue;
293        while (true) {
294            CollisionLink collisionLink = entryValue;
295            index = collisionLink.next;
296            entryKey = getKey(index);
297            if (compareKeys(key, entryKey)) {
298                return index;
299            } else {
300                Object value = getRawValue(index);
301                if (value instanceof CollisionLink) {
302                    entryValue = (CollisionLink) getRawValue(index);
303                } else {
304                    return -1;
305                }
306            }
307        }
308    }
309
310    private int getHashArray(int index) {
311        if (entries.length < LARGE_HASH_THRESHOLD) {
312            return (hashArray[index] & 0xFF);
313        } else if (entries.length < VERY_LARGE_HASH_THRESHOLD) {
314            int adjustedIndex = index << 1;
315            return (hashArray[adjustedIndex] & 0xFF) | ((hashArray[adjustedIndex + 1] & 0xFF) << 8);
316        } else {
317            int adjustedIndex = index << 2;
318            return (hashArray[adjustedIndex] & 0xFF) | ((hashArray[adjustedIndex + 1] & 0xFF) << 8) | ((hashArray[adjustedIndex + 2] & 0xFF) << 16) | ((hashArray[adjustedIndex + 3] & 0xFF) << 24);
319        }
320    }
321
322    private void setHashArray(int index, int value) {
323        if (entries.length < LARGE_HASH_THRESHOLD) {
324            hashArray[index] = (byte) value;
325        } else if (entries.length < VERY_LARGE_HASH_THRESHOLD) {
326            int adjustedIndex = index << 1;
327            hashArray[adjustedIndex] = (byte) value;
328            hashArray[adjustedIndex + 1] = (byte) (value >> 8);
329        } else {
330            int adjustedIndex = index << 2;
331            hashArray[adjustedIndex] = (byte) value;
332            hashArray[adjustedIndex + 1] = (byte) (value >> 8);
333            hashArray[adjustedIndex + 2] = (byte) (value >> 16);
334            hashArray[adjustedIndex + 3] = (byte) (value >> 24);
335        }
336    }
337
338    private int findAndRemoveHash(Object key) {
339        int hashIndex = getHashIndex(key);
340        int index = getHashArray(hashIndex) - 1;
341        if (index != -1) {
342            Object entryKey = getKey(index);
343            if (compareKeys(key, entryKey)) {
344                Object value = getRawValue(index);
345                int nextIndex = -1;
346                if (value instanceof CollisionLink) {
347                    CollisionLink collisionLink = (CollisionLink) value;
348                    nextIndex = collisionLink.next;
349                }
350                setHashArray(hashIndex, nextIndex + 1);
351                return index;
352            } else {
353                Object entryValue = getRawValue(index);
354                if (entryValue instanceof CollisionLink) {
355                    return findAndRemoveWithCollision(key, (CollisionLink) entryValue, index);
356                }
357            }
358        }
359
360        return -1;
361    }
362
363    private int findAndRemoveWithCollision(Object key, CollisionLink initialEntryValue, int initialIndexValue) {
364        int index;
365        Object entryKey;
366        CollisionLink entryValue = initialEntryValue;
367        int lastIndex = initialIndexValue;
368        while (true) {
369            CollisionLink collisionLink = entryValue;
370            index = collisionLink.next;
371            entryKey = getKey(index);
372            if (compareKeys(key, entryKey)) {
373                Object value = getRawValue(index);
374                if (value instanceof CollisionLink) {
375                    CollisionLink thisCollisionLink = (CollisionLink) value;
376                    setRawValue(lastIndex, new CollisionLink(collisionLink.value, thisCollisionLink.next));
377                } else {
378                    setRawValue(lastIndex, collisionLink.value);
379                }
380                return index;
381            } else {
382                Object value = getRawValue(index);
383                if (value instanceof CollisionLink) {
384                    entryValue = (CollisionLink) getRawValue(index);
385                    lastIndex = index;
386                } else {
387                    return -1;
388                }
389            }
390        }
391    }
392
393    private int getHashIndex(Object key) {
394        int hash;
395        if (strategy != null && strategy != Equivalence.DEFAULT) {
396            if (strategy == Equivalence.IDENTITY_WITH_SYSTEM_HASHCODE) {
397                hash = System.identityHashCode(key);
398            } else {
399                hash = strategy.hashCode(key);
400            }
401        } else {
402            hash = key.hashCode();
403        }
404        hash = hash ^ (hash >>> 16);
405        return hash & (getHashTableSize() - 1);
406    }
407
408    @SuppressWarnings("unchecked")
409    @Override
410    public V put(K key, V value) {
411        if (key == null) {
412            throw new UnsupportedOperationException("null not supported as key!");
413        }
414        int index = find(key);
415        if (index != -1) {
416            Object oldValue = getValue(index);
417            setValue(index, value);
418            return (V) oldValue;
419        }
420
421        int nextEntryIndex = totalEntries;
422        if (entries == null) {
423            entries = new Object[INITIAL_CAPACITY << 1];
424        } else if (entries.length == nextEntryIndex << 1) {
425            grow();
426
427            assert entries.length > totalEntries << 1;
428            // Can change if grow is actually compressing.
429            nextEntryIndex = totalEntries;
430        }
431
432        setKey(nextEntryIndex, key);
433        setValue(nextEntryIndex, value);
434        totalEntries++;
435
436        if (hasHashArray()) {
437            // Rehash on collision if hash table is more than three quarters full.
438            boolean rehashOnCollision = (getHashTableSize() < (size() + (size() >> 1)));
439            putHashEntry(key, nextEntryIndex, rehashOnCollision);
440        } else if (totalEntries > getHashThreshold()) {
441            createHash();
442        }
443
444        return null;
445    }
446
447    /**
448     * Number of entries above which a hash table should be constructed.
449     */
450    private int getHashThreshold() {
451        if (strategy == null || strategy == Equivalence.IDENTITY_WITH_SYSTEM_HASHCODE) {
452            return HASH_THRESHOLD_IDENTITY_COMPARE;
453        } else {
454            return HASH_THRESHOLD;
455        }
456    }
457
458    private void grow() {
459        int entriesLength = entries.length;
460        int newSize = (entriesLength >> 1) + Math.max(MIN_CAPACITY_INCREASE, entriesLength >> 2);
461        if (newSize > MAX_ELEMENT_COUNT) {
462            throw new UnsupportedOperationException("map grown too large!");
463        }
464        Object[] newEntries = new Object[newSize << 1];
465        System.arraycopy(entries, 0, newEntries, 0, entriesLength);
466        entries = newEntries;
467        if ((entriesLength < LARGE_HASH_THRESHOLD && newEntries.length >= LARGE_HASH_THRESHOLD) ||
468                        (entriesLength < VERY_LARGE_HASH_THRESHOLD && newEntries.length > VERY_LARGE_HASH_THRESHOLD)) {
469            // Rehash in order to change number of bits reserved for hash indices.
470            createHash();
471        }
472    }
473
474    /**
475     * Compresses the graph if there is a large number of deleted entries and returns the translated
476     * new next index.
477     */
478    private int maybeCompress(int nextIndex) {
479        if (entries.length != INITIAL_CAPACITY << 1 && deletedEntries >= (totalEntries >> 1) + (totalEntries >> 2)) {
480            return compressLarge(nextIndex);
481        }
482        return nextIndex;
483    }
484
485    /**
486     * Compresses the graph and returns the translated new next index.
487     */
488    private int compressLarge(int nextIndex) {
489        int size = INITIAL_CAPACITY;
490        int remaining = totalEntries - deletedEntries;
491
492        while (size <= remaining) {
493            size += Math.max(MIN_CAPACITY_INCREASE, size >> 1);
494        }
495
496        Object[] newEntries = new Object[size << 1];
497        int z = 0;
498        int newNextIndex = remaining;
499        for (int i = 0; i < totalEntries; ++i) {
500            Object key = getKey(i);
501            if (i == nextIndex) {
502                newNextIndex = z;
503            }
504            if (key != null) {
505                newEntries[z << 1] = key;
506                newEntries[(z << 1) + 1] = getValue(i);
507                z++;
508            }
509        }
510
511        this.entries = newEntries;
512        totalEntries = z;
513        deletedEntries = 0;
514        if (z <= getHashThreshold()) {
515            this.hashArray = null;
516        } else {
517            createHash();
518        }
519        return newNextIndex;
520    }
521
522    private int getHashTableSize() {
523        if (entries.length < LARGE_HASH_THRESHOLD) {
524            return hashArray.length;
525        } else if (entries.length < VERY_LARGE_HASH_THRESHOLD) {
526            return hashArray.length >> 1;
527        } else {
528            return hashArray.length >> 2;
529        }
530    }
531
532    private void createHash() {
533        int entryCount = size();
534
535        // Calculate smallest 2^n that is greater number of entries.
536        int size = getHashThreshold();
537        while (size <= entryCount) {
538            size <<= 1;
539        }
540
541        // Give extra size to avoid collisions.
542        size <<= 1;
543
544        if (this.entries.length >= VERY_LARGE_HASH_THRESHOLD) {
545            // Every entry has 4 bytes.
546            size <<= 2;
547        } else if (this.entries.length >= LARGE_HASH_THRESHOLD) {
548            // Every entry has 2 bytes.
549            size <<= 1;
550        } else {
551            // Entries are very small => give extra size to further reduce collisions.
552            size <<= 1;
553        }
554
555        hashArray = new byte[size];
556        for (int i = 0; i < totalEntries; i++) {
557            Object entryKey = getKey(i);
558            if (entryKey != null) {
559                putHashEntry(entryKey, i, false);
560            }
561        }
562    }
563
564    private void putHashEntry(Object key, int entryIndex, boolean rehashOnCollision) {
565        int hashIndex = getHashIndex(key);
566        int oldIndex = getHashArray(hashIndex) - 1;
567        if (oldIndex != -1 && rehashOnCollision) {
568            this.createHash();
569            return;
570        }
571        setHashArray(hashIndex, entryIndex + 1);
572        Object value = getRawValue(entryIndex);
573        if (oldIndex != -1) {
574            assert entryIndex != oldIndex : "this cannot happend and would create an endless collision link cycle";
575            if (value instanceof CollisionLink) {
576                CollisionLink collisionLink = (CollisionLink) value;
577                setRawValue(entryIndex, new CollisionLink(collisionLink.value, oldIndex));
578            } else {
579                setRawValue(entryIndex, new CollisionLink(getRawValue(entryIndex), oldIndex));
580            }
581        } else {
582            if (value instanceof CollisionLink) {
583                CollisionLink collisionLink = (CollisionLink) value;
584                setRawValue(entryIndex, collisionLink.value);
585            }
586        }
587    }
588
589    @Override
590    public int size() {
591        return totalEntries - deletedEntries;
592    }
593
594    @Override
595    public boolean containsKey(K key) {
596        return find(key) != -1;
597    }
598
599    @Override
600    public void clear() {
601        entries = null;
602        hashArray = null;
603        totalEntries = deletedEntries = 0;
604    }
605
606    private boolean hasHashArray() {
607        return hashArray != null;
608    }
609
610    @SuppressWarnings("unchecked")
611    @Override
612    public V removeKey(K key) {
613        if (key == null) {
614            throw new UnsupportedOperationException("null not supported as key!");
615        }
616        int index;
617        if (hasHashArray()) {
618            index = this.findAndRemoveHash(key);
619        } else {
620            index = this.findLinear(key);
621        }
622
623        if (index != -1) {
624            Object value = getValue(index);
625            remove(index);
626            return (V) value;
627        }
628        return null;
629    }
630
631    /**
632     * Removes the element at the specific index and returns the index of the next element. This can
633     * be a different value if graph compression was triggered.
634     */
635    private int remove(int indexToRemove) {
636        int index = indexToRemove;
637        int entriesAfterIndex = totalEntries - index - 1;
638        int result = index + 1;
639
640        // Without hash array, compress immediately.
641        if (entriesAfterIndex <= COMPRESS_IMMEDIATE_CAPACITY && !hasHashArray()) {
642            while (index < totalEntries - 1) {
643                setKey(index, getKey(index + 1));
644                setRawValue(index, getRawValue(index + 1));
645                index++;
646            }
647            result--;
648        }
649
650        setKey(index, null);
651        setRawValue(index, null);
652        if (index == totalEntries - 1) {
653            // Make sure last element is always non-null.
654            totalEntries--;
655            while (index > 0 && getKey(index - 1) == null) {
656                totalEntries--;
657                deletedEntries--;
658                index--;
659            }
660        } else {
661            deletedEntries++;
662            result = maybeCompress(result);
663        }
664
665        return result;
666    }
667
668    private abstract class SparseMapIterator<E> implements Iterator<E> {
669
670        protected int current;
671
672        @Override
673        public boolean hasNext() {
674            return current < totalEntries;
675        }
676
677        @Override
678        public void remove() {
679            if (hasHashArray()) {
680                EconomicMapImpl.this.findAndRemoveHash(getKey(current - 1));
681            }
682            current = EconomicMapImpl.this.remove(current - 1);
683        }
684    }
685
686    @Override
687    public Iterable<V> getValues() {
688        return new Iterable<V>() {
689            @Override
690            public Iterator<V> iterator() {
691                return new SparseMapIterator<V>() {
692                    @SuppressWarnings("unchecked")
693                    @Override
694                    public V next() {
695                        Object result;
696                        while (true) {
697                            result = getValue(current);
698                            if (result == null && getKey(current) == null) {
699                                // values can be null, double-check if key is also null
700                                current++;
701                            } else {
702                                current++;
703                                break;
704                            }
705                        }
706                        return (V) result;
707                    }
708                };
709            }
710        };
711    }
712
713    @Override
714    public Iterable<K> getKeys() {
715        return this;
716    }
717
718    @Override
719    public boolean isEmpty() {
720        return this.size() == 0;
721    }
722
723    @Override
724    public MapCursor<K, V> getEntries() {
725        return new MapCursor<K, V>() {
726            int current = -1;
727
728            @Override
729            public boolean advance() {
730                current++;
731                if (current >= totalEntries) {
732                    return false;
733                } else {
734                    while (EconomicMapImpl.this.getKey(current) == null) {
735                        // Skip over null entries
736                        current++;
737                    }
738                    return true;
739                }
740            }
741
742            @SuppressWarnings("unchecked")
743            @Override
744            public K getKey() {
745                return (K) EconomicMapImpl.this.getKey(current);
746            }
747
748            @SuppressWarnings("unchecked")
749            @Override
750            public V getValue() {
751                return (V) EconomicMapImpl.this.getValue(current);
752            }
753
754            @Override
755            public void remove() {
756                if (hasHashArray()) {
757                    EconomicMapImpl.this.findAndRemoveHash(EconomicMapImpl.this.getKey(current));
758                }
759                current = EconomicMapImpl.this.remove(current) - 1;
760            }
761        };
762    }
763
764    @SuppressWarnings("unchecked")
765    @Override
766    public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
767        for (int i = 0; i < totalEntries; i++) {
768            Object entryKey = getKey(i);
769            if (entryKey != null) {
770                Object newValue = function.apply((K) entryKey, (V) getValue(i));
771                setValue(i, newValue);
772            }
773        }
774    }
775
776    private Object getKey(int index) {
777        return entries[index << 1];
778    }
779
780    private void setKey(int index, Object newValue) {
781        entries[index << 1] = newValue;
782    }
783
784    private void setValue(int index, Object newValue) {
785        Object oldValue = getRawValue(index);
786        if (oldValue instanceof CollisionLink) {
787            CollisionLink collisionLink = (CollisionLink) oldValue;
788            setRawValue(index, new CollisionLink(newValue, collisionLink.next));
789        } else {
790            setRawValue(index, newValue);
791        }
792    }
793
794    private void setRawValue(int index, Object newValue) {
795        entries[(index << 1) + 1] = newValue;
796    }
797
798    private Object getRawValue(int index) {
799        return entries[(index << 1) + 1];
800    }
801
802    private Object getValue(int index) {
803        Object object = getRawValue(index);
804        if (object instanceof CollisionLink) {
805            return ((CollisionLink) object).value;
806        }
807        return object;
808    }
809
810    @Override
811    public String toString() {
812        StringBuilder builder = new StringBuilder();
813        builder.append("map(size=").append(size()).append(", {");
814        MapCursor<K, V> cursor = getEntries();
815        while (cursor.advance()) {
816            builder.append("(").append(cursor.getKey()).append(",").append(cursor.getValue()).append("),");
817        }
818        builder.append("})");
819        return builder.toString();
820    }
821
822    @Override
823    public Iterator<K> iterator() {
824        return new SparseMapIterator<K>() {
825            @SuppressWarnings("unchecked")
826            @Override
827            public K next() {
828                Object result;
829                while ((result = getKey(current++)) == null) {
830                    // skip null entries
831                }
832                return (K) result;
833            }
834        };
835    }
836
837    @Override
838    public boolean contains(K element) {
839        return containsKey(element);
840    }
841
842    @SuppressWarnings("unchecked")
843    @Override
844    public boolean add(K element) {
845        return put(element, (V) element) == null;
846    }
847
848    @Override
849    public void remove(K element) {
850        removeKey(element);
851    }
852}
853