ArrayMap.java revision f4130cf35fa128e36f96e55955d4f5db86197e4a
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.util;
18
19import java.util.Collection;
20import java.util.Map;
21import java.util.Set;
22
23/**
24 * ArrayMap is a generic key->value mapping data structure that is
25 * designed to be more memory efficient than a traditional {@link java.util.HashMap}.
26 * It keeps its mappings in an array data structure -- an integer array of hash
27 * codes for each item, and an Object array of the key/value pairs.  This allows it to
28 * avoid having to create an extra object for every entry put in to the map, and it
29 * also tries to control the growth of the size of these arrays more aggressively
30 * (since growing them only requires copying the entries in the array, not rebuilding
31 * a hash map).
32 *
33 * <p>Note that this implementation is not intended to be appropriate for data structures
34 * that may contain large numbers of items.  It is generally slower than a traditional
35 * HashMap, since lookups require a binary search and adds and removes require inserting
36 * and deleting entries in the array.  For containers holding up to hundreds of items,
37 * the performance difference is not significant, less than 50%.  For larger numbers of items
38 * this data structure should be avoided.</p>
39 *
40 * <p><b>Note:</b> unlike {@link java.util.HashMap}, this container does not support
41 * null keys.</p>
42 *
43 * <p>Because this container is intended to better balance memory use, unlike most other
44 * standard Java containers it will shrink its array as items are removed from it.  Currently
45 * you have no control over this shrinking -- if you set a capacity and then remove an
46 * item, it may reduce the capacity to better match the current size.  In the future an
47 * explicitly call to set the capacity should turn off this aggressive shrinking behavior.</p>
48 *
49 * @hide
50 */
51public final class ArrayMap<K, V> implements Map<K, V> {
52    private static final boolean DEBUG = false;
53    private static final String TAG = "ArrayMap";
54
55    /**
56     * The minimum amount by which the capacity of a ArrayMap will increase.
57     * This is tuned to be relatively space-efficient.
58     */
59    private static final int BASE_SIZE = 4;
60
61    /**
62     * Maximum number of entries to have in array caches.
63     */
64    private static final int CACHE_SIZE = 10;
65
66    /**
67     * Caches of small array objects to avoid spamming garbage.  The cache
68     * Object[] variable is a pointer to a linked list of array objects.
69     * The first entry in the array is a pointer to the next array in the
70     * list; the second entry is a pointer to the int[] hash code array for it.
71     */
72    static Object[] mBaseCache;
73    static int mBaseCacheSize;
74    static Object[] mTwiceBaseCache;
75    static int mTwiceBaseCacheSize;
76
77    int[] mHashes;
78    Object[] mArray;
79    int mSize;
80    MapCollections<K, V> mCollections;
81
82    private int indexOf(Object key, int hash) {
83        final int N = mSize;
84
85        // Important fast case: if nothing is in here, nothing to look for.
86        if (N == 0) {
87            return ~0;
88        }
89
90        int index = SparseArray.binarySearch(mHashes, N, hash);
91
92        // If the hash code wasn't found, then we have no entry for this key.
93        if (index < 0) {
94            return index;
95        }
96
97        // If the key at the returned index matches, that's what we want.
98        if (mArray[index<<1].equals(key)) {
99            return index;
100        }
101
102        // Search for a matching key after the index.
103        int end;
104        for (end = index + 1; end < N && mHashes[end] == hash; end++) {
105            if (mArray[end << 1].equals(key)) return end;
106        }
107
108        // Search for a matching key before the index.
109        for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
110            if (mArray[i << 1].equals(key)) return i;
111        }
112
113        // Key not found -- return negative value indicating where a
114        // new entry for this key should go.  We use the end of the
115        // hash chain to reduce the number of array entries that will
116        // need to be copied when inserting.
117        return ~end;
118    }
119
120    private void allocArrays(final int size) {
121        if (size == (BASE_SIZE*2)) {
122            synchronized (ArrayMap.class) {
123                if (mTwiceBaseCache != null) {
124                    final Object[] array = mTwiceBaseCache;
125                    mArray = array;
126                    mTwiceBaseCache = (Object[])array[0];
127                    mHashes = (int[])array[1];
128                    array[0] = array[1] = null;
129                    mTwiceBaseCacheSize--;
130                    if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes
131                            + " now have " + mTwiceBaseCacheSize + " entries");
132                    return;
133                }
134            }
135        } else if (size == BASE_SIZE) {
136            synchronized (ArrayMap.class) {
137                if (mBaseCache != null) {
138                    final Object[] array = mBaseCache;
139                    mArray = array;
140                    mBaseCache = (Object[])array[0];
141                    mHashes = (int[])array[1];
142                    array[0] = array[1] = null;
143                    mBaseCacheSize--;
144                    if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes
145                            + " now have " + mBaseCacheSize + " entries");
146                    return;
147                }
148            }
149        }
150
151        mHashes = new int[size];
152        mArray = new Object[size<<1];
153    }
154
155    private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
156        if (hashes.length == (BASE_SIZE*2)) {
157            synchronized (ArrayMap.class) {
158                if (mTwiceBaseCacheSize < CACHE_SIZE) {
159                    array[0] = mTwiceBaseCache;
160                    array[1] = hashes;
161                    for (int i=(size<<1)-1; i>=2; i--) {
162                        array[i] = null;
163                    }
164                    mTwiceBaseCache = array;
165                    mTwiceBaseCacheSize++;
166                    if (DEBUG) Log.d(TAG, "Storing 2x cache " + array
167                            + " now have " + mTwiceBaseCacheSize + " entries");
168                }
169            }
170        } else if (hashes.length == BASE_SIZE) {
171            synchronized (ArrayMap.class) {
172                if (mBaseCacheSize < CACHE_SIZE) {
173                    array[0] = mBaseCache;
174                    array[1] = hashes;
175                    for (int i=(size<<1)-1; i>=2; i--) {
176                        array[i] = null;
177                    }
178                    mBaseCache = array;
179                    mBaseCacheSize++;
180                    if (DEBUG) Log.d(TAG, "Storing 1x cache " + array
181                            + " now have " + mBaseCacheSize + " entries");
182                }
183            }
184        }
185    }
186
187    /**
188     * Create a new empty ArrayMap.  The default capacity of an array map is 0, and
189     * will grow once items are added to it.
190     */
191    public ArrayMap() {
192        mHashes = SparseArray.EMPTY_INTS;
193        mArray = SparseArray.EMPTY_OBJECTS;
194        mSize = 0;
195    }
196
197    /**
198     * Create a new ArrayMap with a given initial capacity.
199     */
200    public ArrayMap(int capacity) {
201        if (capacity == 0) {
202            mHashes = SparseArray.EMPTY_INTS;
203            mArray = SparseArray.EMPTY_OBJECTS;
204        } else {
205            allocArrays(capacity);
206        }
207        mSize = 0;
208    }
209    /**
210     * Create a new ArrayMap with the mappings from the given ArrayMap.
211     */
212    public ArrayMap(ArrayMap map) {
213        this();
214        if (map != null) {
215            putAll(map);
216        }
217    }
218
219
220    /**
221     * Make the array map empty.  All storage is released.
222     */
223    @Override
224    public void clear() {
225        if (mSize != 0) {
226            freeArrays(mHashes, mArray, mSize);
227            mHashes = SparseArray.EMPTY_INTS;
228            mArray = SparseArray.EMPTY_OBJECTS;
229            mSize = 0;
230        }
231    }
232
233    /**
234     * Ensure the array map can hold at least <var>minimumCapacity</var>
235     * items.
236     */
237    public void ensureCapacity(int minimumCapacity) {
238        if (mHashes.length < minimumCapacity) {
239            int[] ohashes = mHashes;
240            Object[] oarray = mArray;
241            allocArrays(minimumCapacity);
242            if (mSize > 0) {
243                System.arraycopy(ohashes, 0, mHashes, 0, mSize);
244                System.arraycopy(oarray, 0, mArray, 0, mSize<<1);
245            }
246            freeArrays(ohashes, oarray, mSize);
247        }
248    }
249
250    /**
251     * Check whether a key exists in the array.
252     *
253     * @param key The key to search for.
254     * @return Returns true if the key exists, else false.
255     */
256    @Override
257    public boolean containsKey(Object key) {
258        return indexOf(key, key.hashCode()) >= 0;
259    }
260
261    private int indexOfValue(Object value) {
262        final int N = mSize*2;
263        final Object[] array = mArray;
264        if (value == null) {
265            for (int i=1; i<N; i+=2) {
266                if (array[i] == null) {
267                    return i>>1;
268                }
269            }
270        } else {
271            for (int i=1; i<N; i+=2) {
272                if (value.equals(array[i])) {
273                    return i>>1;
274                }
275            }
276        }
277        return -1;
278    }
279
280    /**
281     * Check whether a value exists in the array.  This requires a linear search
282     * through the entire array.
283     *
284     * @param value The value to search for.
285     * @return Returns true if the value exists, else false.
286     */
287    @Override
288    public boolean containsValue(Object value) {
289        return indexOfValue(value) >= 0;
290    }
291
292    /**
293     * Retrieve a value from the array.
294     * @param key The key of the value to retrieve.
295     * @return Returns the value associated with the given key,
296     * or null if there is no such key.
297     */
298    @Override
299    public V get(Object key) {
300        final int index = indexOf(key, key.hashCode());
301        return index >= 0 ? (V)mArray[(index<<1)+1] : null;
302    }
303
304    /**
305     * Return the key at the given index in the array.
306     * @param index The desired index, must be between 0 and {@link #size()}-1.
307     * @return Returns the key stored at the given index.
308     */
309    public K keyAt(int index) {
310        return (K)mArray[index << 1];
311    }
312
313    /**
314     * Return the value at the given index in the array.
315     * @param index The desired index, must be between 0 and {@link #size()}-1.
316     * @return Returns the value stored at the given index.
317     */
318    public V valueAt(int index) {
319        return (V)mArray[(index << 1) + 1];
320    }
321
322    /**
323     * Set the value at a given index in the array.
324     * @param index The desired index, must be between 0 and {@link #size()}-1.
325     * @param value The new value to store at this index.
326     * @return Returns the previous value at the given index.
327     */
328    public V setValueAt(int index, V value) {
329        index = (index << 1) + 1;
330        V old = (V)mArray[index];
331        mArray[index] = value;
332        return old;
333    }
334
335    /**
336     * Return true if the array map contains no items.
337     */
338    @Override
339    public boolean isEmpty() {
340        return mSize <= 0;
341    }
342
343    /**
344     * Add a new value to the array map.
345     * @param key The key under which to store the value.  <b>Must not be null.</b>  If
346     * this key already exists in the array, its value will be replaced.
347     * @param value The value to store for the given key.
348     * @return Returns the old value that was stored for the given key, or null if there
349     * was no such key.
350     */
351    @Override
352    public V put(K key, V value) {
353        final int hash = key.hashCode();
354        int index = indexOf(key, hash);
355        if (index >= 0) {
356            index = (index<<1) + 1;
357            final V old = (V)mArray[index];
358            mArray[index] = value;
359            return old;
360        }
361
362        index = ~index;
363        if (mSize >= mHashes.length) {
364            final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1))
365                    : (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
366
367            if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);
368
369            final int[] ohashes = mHashes;
370            final Object[] oarray = mArray;
371            allocArrays(n);
372
373            if (mHashes.length > 0) {
374                if (DEBUG) Log.d(TAG, "put: copy 0-" + mSize + " to 0");
375                System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
376                System.arraycopy(oarray, 0, mArray, 0, oarray.length);
377            }
378
379            freeArrays(ohashes, oarray, mSize);
380        }
381
382        if (index < mSize) {
383            if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (mSize-index)
384                    + " to " + (index+1));
385            System.arraycopy(mHashes, index, mHashes, index + 1, mSize - index);
386            System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
387        }
388
389        mHashes[index] = hash;
390        mArray[index<<1] = key;
391        mArray[(index<<1)+1] = value;
392        mSize++;
393        return null;
394    }
395
396    /**
397     * Perform a {@link #put(Object, Object)} of all key/value pairs in <var>array</var>
398     * @param array The array whose contents are to be retrieved.
399     */
400    public void putAll(ArrayMap<? extends K, ? extends V> array) {
401        final int N = array.mSize;
402        ensureCapacity(mSize + N);
403        if (mSize == 0) {
404            if (N > 0) {
405                System.arraycopy(array.mHashes, 0, mHashes, 0, N);
406                System.arraycopy(array.mArray, 0, mArray, 0, N<<1);
407                mSize = N;
408            }
409        } else {
410            for (int i=0; i<N; i++) {
411                put(array.keyAt(i), array.valueAt(i));
412            }
413        }
414    }
415
416    /**
417     * Remove an existing key from the array map.
418     * @param key The key of the mapping to remove.
419     * @return Returns the value that was stored under the key, or null if there
420     * was no such key.
421     */
422    @Override
423    public V remove(Object key) {
424        int index = indexOf(key, key.hashCode());
425        if (index >= 0) {
426            return removeAt(index);
427        }
428
429        return null;
430    }
431
432    /**
433     * Remove the key/value mapping at the given index.
434     * @param index The desired index, must be between 0 and {@link #size()}-1.
435     * @return Returns the value that was stored at this index.
436     */
437    public V removeAt(int index) {
438        final V old = (V)mArray[(index << 1) + 1];
439        if (mSize <= 1) {
440            // Now empty.
441            if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0");
442            freeArrays(mHashes, mArray, mSize);
443            mHashes = SparseArray.EMPTY_INTS;
444            mArray = SparseArray.EMPTY_OBJECTS;
445            mSize = 0;
446        } else {
447            if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
448                // Shrunk enough to reduce size of arrays.  We don't allow it to
449                // shrink smaller than (BASE_SIZE*2) to avoid flapping between
450                // that and BASE_SIZE.
451                final int n = mSize > (BASE_SIZE*2) ? (mSize + (mSize>>1)) : (BASE_SIZE*2);
452
453                if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n);
454
455                final int[] ohashes = mHashes;
456                final Object[] oarray = mArray;
457                allocArrays(n);
458
459                mSize--;
460                if (index > 0) {
461                    if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0");
462                    System.arraycopy(ohashes, 0, mHashes, 0, index);
463                    System.arraycopy(oarray, 0, mArray, 0, index << 1);
464                }
465                if (index < mSize) {
466                    if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + mSize
467                            + " to " + index);
468                    System.arraycopy(ohashes, index + 1, mHashes, index, mSize - index);
469                    System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,
470                            (mSize - index) << 1);
471                }
472            } else {
473                mSize--;
474                if (index < mSize) {
475                    if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + mSize
476                            + " to " + index);
477                    System.arraycopy(mHashes, index + 1, mHashes, index, mSize - index);
478                    System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
479                            (mSize - index) << 1);
480                }
481                mArray[mSize << 1] = null;
482                mArray[(mSize << 1) + 1] = null;
483            }
484        }
485        return old;
486    }
487
488    /**
489     * Return the number of items in this array map.
490     */
491    @Override
492    public int size() {
493        return mSize;
494    }
495
496    // ------------------------------------------------------------------------
497    // Interop with traditional Java containers.  Not as efficient as using
498    // specialized collection APIs.
499    // ------------------------------------------------------------------------
500
501    private MapCollections<K, V> getCollection() {
502        if (mCollections == null) {
503            mCollections = new MapCollections<K, V>() {
504                @Override
505                protected int colGetSize() {
506                    return mSize;
507                }
508
509                @Override
510                protected Object colGetEntry(int index, int offset) {
511                    return mArray[(index<<1) + offset];
512                }
513
514                @Override
515                protected int colIndexOfKey(Object key) {
516                    return indexOf(key, key.hashCode());
517                }
518
519                @Override
520                protected int colIndexOfValue(Object value) {
521                    return indexOfValue(value);
522                }
523
524                @Override
525                protected Map<K, V> colGetMap() {
526                    return ArrayMap.this;
527                }
528
529                @Override
530                protected void colPut(K key, V value) {
531                    put(key, value);
532                }
533
534                @Override
535                protected V colSetValue(int index, V value) {
536                    return setValueAt(index, value);
537                }
538
539                @Override
540                protected void colRemoveAt(int index) {
541                    removeAt(index);
542                }
543
544                @Override
545                protected void colClear() {
546                    clear();
547                }
548            };
549        }
550        return mCollections;
551    }
552
553    /**
554     * Determine if the array map contains all of the keys in the given collection.
555     * @param collection The collection whose contents are to be checked against.
556     * @return Returns true if this array map contains a key for every entry
557     * in <var>collection</var>, else returns false.
558     */
559    public boolean containsAll(Collection<?> collection) {
560        return MapCollections.containsAllHelper(this, collection);
561    }
562
563    /**
564     * Perform a {@link #put(Object, Object)} of all key/value pairs in <var>map</var>
565     * @param map The map whose contents are to be retrieved.
566     */
567    @Override
568    public void putAll(Map<? extends K, ? extends V> map) {
569        ensureCapacity(mSize + map.size());
570        for (Map.Entry<? extends K, ? extends V> entry : map.entrySet()) {
571            put(entry.getKey(), entry.getValue());
572        }
573    }
574
575    /**
576     * Remove all keys in the array map that exist in the given collection.
577     * @param collection The collection whose contents are to be used to remove keys.
578     * @return Returns true if any keys were removed from the array map, else false.
579     */
580    public boolean removeAll(Collection<?> collection) {
581        return MapCollections.removeAllHelper(this, collection);
582    }
583
584    /**
585     * Remove all keys in the array map that do <b>not</b> exist in the given collection.
586     * @param collection The collection whose contents are to be used to determine which
587     * keys to keep.
588     * @return Returns true if any keys were removed from the array map, else false.
589     */
590    public boolean retainAll(Collection<?> collection) {
591        return MapCollections.retainAllHelper(this, collection);
592    }
593
594    /**
595     * Return a {@link java.util.Set} for iterating over and interacting with all mappings
596     * in the array map.
597     *
598     * <p><b>Note:</b> this is a very inefficient way to access the array contents, it
599     * requires generating a number of temporary objects.</p>
600     *
601     * <p><b>Note:</b></p> the semantics of this
602     * Set are subtly different than that of a {@link java.util.HashMap}: most important,
603     * the {@link java.util.Map.Entry Map.Entry} object returned by its iterator is a single
604     * object that exists for the entire iterator, so you can <b>not</b> hold on to it
605     * after calling {@link java.util.Iterator#next() Iterator.next}.</p>
606     */
607    @Override
608    public Set<Map.Entry<K, V>> entrySet() {
609        return getCollection().getEntrySet();
610    }
611
612    /**
613     * Return a {@link java.util.Set} for iterating over and interacting with all keys
614     * in the array map.
615     *
616     * <p><b>Note:</b> this is a fairly inefficient way to access the array contents, it
617     * requires generating a number of temporary objects.</p>
618     */
619    @Override
620    public Set<K> keySet() {
621        return getCollection().getKeySet();
622    }
623
624    /**
625     * Return a {@link java.util.Collection} for iterating over and interacting with all values
626     * in the array map.
627     *
628     * <p><b>Note:</b> this is a fairly inefficient way to access the array contents, it
629     * requires generating a number of temporary objects.</p>
630     */
631    @Override
632    public Collection<V> values() {
633        return getCollection().getValues();
634    }
635
636    /**
637     * {@inheritDoc}
638     *
639     * <p>This implementation returns false if the object is not a map, or
640     * if the maps have different sizes. Otherwise, for each key in this map,
641     * values of both maps are compared. If the values for any key are not
642     * equal, the method returns false, otherwise it returns true.
643     */
644    @Override
645    public boolean equals(Object object) {
646        if (this == object) {
647            return true;
648        }
649        if (object instanceof Map) {
650            Map<?, ?> map = (Map<?, ?>) object;
651            if (size() != map.size()) {
652                return false;
653            }
654
655            try {
656                for (int i=0; i<mSize; i++) {
657                    K key = keyAt(i);
658                    V mine = valueAt(i);
659                    Object theirs = map.get(key);
660                    if (mine == null) {
661                        if (theirs != null || !map.containsKey(key)) {
662                            return false;
663                        }
664                    } else if (!mine.equals(theirs)) {
665                        return false;
666                    }
667                }
668            } catch (NullPointerException ignored) {
669                return false;
670            } catch (ClassCastException ignored) {
671                return false;
672            }
673            return true;
674        }
675        return false;
676    }
677
678    /**
679     * {@inheritDoc}
680     *
681     * <p>This implementation sums a hashcode using all keys and values.
682     */
683    @Override
684    public int hashCode() {
685        int result = 0;
686        for (int i=0; i<mSize; i++) {
687            K key = keyAt(i);
688            V value = valueAt(i);
689            result += (key == null ? 0 : key.hashCode())
690                    ^ (value == null ? 0 : value.hashCode());
691        }
692        return result;
693    }
694
695}
696