1 /*
2  * Copyright 2018 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 
17 package androidx.collection;
18 
19 import java.util.ConcurrentModificationException;
20 import java.util.Map;
21 
22 /**
23  * Base implementation of {@link ArrayMap} that doesn't include any standard Java
24  * container API interoperability.  These features are generally heavier-weight ways
25  * to interact with the container, so discouraged, but they can be useful to make it
26  * easier to use as a drop-in replacement for HashMap.  If you don't need them, this
27  * class can be preferrable since it doesn't bring in any of the implementation of those
28  * APIs, allowing that code to be stripped by ProGuard.
29  */
30 public class SimpleArrayMap<K, V> {
31     private static final boolean DEBUG = false;
32     private static final String TAG = "ArrayMap";
33 
34     /**
35      * Attempt to spot concurrent modifications to this data structure.
36      *
37      * It's best-effort, but any time we can throw something more diagnostic than an
38      * ArrayIndexOutOfBoundsException deep in the ArrayMap internals it's going to
39      * save a lot of development time.
40      *
41      * Good times to look for CME include after any allocArrays() call and at the end of
42      * functions that change mSize (put/remove/clear).
43      */
44     private static final boolean CONCURRENT_MODIFICATION_EXCEPTIONS = true;
45 
46     /**
47      * The minimum amount by which the capacity of a ArrayMap will increase.
48      * This is tuned to be relatively space-efficient.
49      */
50     private static final int BASE_SIZE = 4;
51 
52     /**
53      * Maximum number of entries to have in array caches.
54      */
55     private static final int CACHE_SIZE = 10;
56 
57     /**
58      * Caches of small array objects to avoid spamming garbage.  The cache
59      * Object[] variable is a pointer to a linked list of array objects.
60      * The first entry in the array is a pointer to the next array in the
61      * list; the second entry is a pointer to the int[] hash code array for it.
62      */
63     static Object[] mBaseCache;
64     static int mBaseCacheSize;
65     static Object[] mTwiceBaseCache;
66     static int mTwiceBaseCacheSize;
67 
68     int[] mHashes;
69     Object[] mArray;
70     int mSize;
71 
binarySearchHashes(int[] hashes, int N, int hash)72     private static int binarySearchHashes(int[] hashes, int N, int hash) {
73         try {
74             return ContainerHelpers.binarySearch(hashes, N, hash);
75         } catch (ArrayIndexOutOfBoundsException e) {
76             if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
77                 throw new ConcurrentModificationException();
78             } else {
79                 throw e; // the cache is poisoned at this point, there's not much we can do
80             }
81         }
82     }
83 
indexOf(Object key, int hash)84     int indexOf(Object key, int hash) {
85         final int N = mSize;
86 
87         // Important fast case: if nothing is in here, nothing to look for.
88         if (N == 0) {
89             return ~0;
90         }
91 
92         int index = binarySearchHashes(mHashes, N, hash);
93 
94         // If the hash code wasn't found, then we have no entry for this key.
95         if (index < 0) {
96             return index;
97         }
98 
99         // If the key at the returned index matches, that's what we want.
100         if (key.equals(mArray[index<<1])) {
101             return index;
102         }
103 
104         // Search for a matching key after the index.
105         int end;
106         for (end = index + 1; end < N && mHashes[end] == hash; end++) {
107             if (key.equals(mArray[end << 1])) return end;
108         }
109 
110         // Search for a matching key before the index.
111         for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
112             if (key.equals(mArray[i << 1])) return i;
113         }
114 
115         // Key not found -- return negative value indicating where a
116         // new entry for this key should go.  We use the end of the
117         // hash chain to reduce the number of array entries that will
118         // need to be copied when inserting.
119         return ~end;
120     }
121 
indexOfNull()122     int indexOfNull() {
123         final int N = mSize;
124 
125         // Important fast case: if nothing is in here, nothing to look for.
126         if (N == 0) {
127             return ~0;
128         }
129 
130         int index = binarySearchHashes(mHashes, N, 0);
131 
132         // If the hash code wasn't found, then we have no entry for this key.
133         if (index < 0) {
134             return index;
135         }
136 
137         // If the key at the returned index matches, that's what we want.
138         if (null == mArray[index<<1]) {
139             return index;
140         }
141 
142         // Search for a matching key after the index.
143         int end;
144         for (end = index + 1; end < N && mHashes[end] == 0; end++) {
145             if (null == mArray[end << 1]) return end;
146         }
147 
148         // Search for a matching key before the index.
149         for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) {
150             if (null == mArray[i << 1]) return i;
151         }
152 
153         // Key not found -- return negative value indicating where a
154         // new entry for this key should go.  We use the end of the
155         // hash chain to reduce the number of array entries that will
156         // need to be copied when inserting.
157         return ~end;
158     }
159 
160     @SuppressWarnings("ArrayToString")
allocArrays(final int size)161     private void allocArrays(final int size) {
162         if (size == (BASE_SIZE*2)) {
163             synchronized (ArrayMap.class) {
164                 if (mTwiceBaseCache != null) {
165                     final Object[] array = mTwiceBaseCache;
166                     mArray = array;
167                     mTwiceBaseCache = (Object[])array[0];
168                     mHashes = (int[])array[1];
169                     array[0] = array[1] = null;
170                     mTwiceBaseCacheSize--;
171                     if (DEBUG) System.out.println(TAG + " Retrieving 2x cache " + mHashes
172                             + " now have " + mTwiceBaseCacheSize + " entries");
173                     return;
174                 }
175             }
176         } else if (size == BASE_SIZE) {
177             synchronized (ArrayMap.class) {
178                 if (mBaseCache != null) {
179                     final Object[] array = mBaseCache;
180                     mArray = array;
181                     mBaseCache = (Object[])array[0];
182                     mHashes = (int[])array[1];
183                     array[0] = array[1] = null;
184                     mBaseCacheSize--;
185                     if (DEBUG) System.out.println(TAG + " Retrieving 1x cache " + mHashes
186                             + " now have " + mBaseCacheSize + " entries");
187                     return;
188                 }
189             }
190         }
191 
192         mHashes = new int[size];
193         mArray = new Object[size<<1];
194     }
195 
196     @SuppressWarnings("ArrayToString")
freeArrays(final int[] hashes, final Object[] array, final int size)197     private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
198         if (hashes.length == (BASE_SIZE*2)) {
199             synchronized (ArrayMap.class) {
200                 if (mTwiceBaseCacheSize < CACHE_SIZE) {
201                     array[0] = mTwiceBaseCache;
202                     array[1] = hashes;
203                     for (int i=(size<<1)-1; i>=2; i--) {
204                         array[i] = null;
205                     }
206                     mTwiceBaseCache = array;
207                     mTwiceBaseCacheSize++;
208                     if (DEBUG) System.out.println(TAG + " Storing 2x cache " + array
209                             + " now have " + mTwiceBaseCacheSize + " entries");
210                 }
211             }
212         } else if (hashes.length == BASE_SIZE) {
213             synchronized (ArrayMap.class) {
214                 if (mBaseCacheSize < CACHE_SIZE) {
215                     array[0] = mBaseCache;
216                     array[1] = hashes;
217                     for (int i=(size<<1)-1; i>=2; i--) {
218                         array[i] = null;
219                     }
220                     mBaseCache = array;
221                     mBaseCacheSize++;
222                     if (DEBUG) System.out.println(TAG + " Storing 1x cache " + array
223                             + " now have " + mBaseCacheSize + " entries");
224                 }
225             }
226         }
227     }
228 
229     /**
230      * Create a new empty ArrayMap.  The default capacity of an array map is 0, and
231      * will grow once items are added to it.
232      */
SimpleArrayMap()233     public SimpleArrayMap() {
234         mHashes = ContainerHelpers.EMPTY_INTS;
235         mArray = ContainerHelpers.EMPTY_OBJECTS;
236         mSize = 0;
237     }
238 
239     /**
240      * Create a new ArrayMap with a given initial capacity.
241      */
SimpleArrayMap(int capacity)242     public SimpleArrayMap(int capacity) {
243         if (capacity == 0) {
244             mHashes = ContainerHelpers.EMPTY_INTS;
245             mArray = ContainerHelpers.EMPTY_OBJECTS;
246         } else {
247             allocArrays(capacity);
248         }
249         mSize = 0;
250     }
251 
252     /**
253      * Create a new ArrayMap with the mappings from the given ArrayMap.
254      */
SimpleArrayMap(SimpleArrayMap<K, V> map)255     public SimpleArrayMap(SimpleArrayMap<K, V> map) {
256         this();
257         if (map != null) {
258             putAll(map);
259         }
260     }
261 
262     /**
263      * Make the array map empty.  All storage is released.
264      */
clear()265     public void clear() {
266         if (mSize > 0) {
267             final int[] ohashes = mHashes;
268             final Object[] oarray = mArray;
269             final int osize = mSize;
270             mHashes = ContainerHelpers.EMPTY_INTS;
271             mArray = ContainerHelpers.EMPTY_OBJECTS;
272             mSize = 0;
273             freeArrays(ohashes, oarray, osize);
274         }
275         if (CONCURRENT_MODIFICATION_EXCEPTIONS && mSize > 0) {
276             throw new ConcurrentModificationException();
277         }
278     }
279 
280     /**
281      * Ensure the array map can hold at least <var>minimumCapacity</var>
282      * items.
283      */
ensureCapacity(int minimumCapacity)284     public void ensureCapacity(int minimumCapacity) {
285         final int osize = mSize;
286         if (mHashes.length < minimumCapacity) {
287             final int[] ohashes = mHashes;
288             final Object[] oarray = mArray;
289             allocArrays(minimumCapacity);
290             if (mSize > 0) {
291                 System.arraycopy(ohashes, 0, mHashes, 0, osize);
292                 System.arraycopy(oarray, 0, mArray, 0, osize<<1);
293             }
294             freeArrays(ohashes, oarray, osize);
295         }
296         if (CONCURRENT_MODIFICATION_EXCEPTIONS && mSize != osize) {
297             throw new ConcurrentModificationException();
298         }
299     }
300 
301     /**
302      * Check whether a key exists in the array.
303      *
304      * @param key The key to search for.
305      * @return Returns true if the key exists, else false.
306      */
containsKey(Object key)307     public boolean containsKey(Object key) {
308         return indexOfKey(key) >= 0;
309     }
310 
311     /**
312      * Returns the index of a key in the set.
313      *
314      * @param key The key to search for.
315      * @return Returns the index of the key if it exists, else a negative integer.
316      */
indexOfKey(Object key)317     public int indexOfKey(Object key) {
318         return key == null ? indexOfNull() : indexOf(key, key.hashCode());
319     }
320 
indexOfValue(Object value)321     int indexOfValue(Object value) {
322         final int N = mSize*2;
323         final Object[] array = mArray;
324         if (value == null) {
325             for (int i=1; i<N; i+=2) {
326                 if (array[i] == null) {
327                     return i>>1;
328                 }
329             }
330         } else {
331             for (int i=1; i<N; i+=2) {
332                 if (value.equals(array[i])) {
333                     return i>>1;
334                 }
335             }
336         }
337         return -1;
338     }
339 
340     /**
341      * Check whether a value exists in the array.  This requires a linear search
342      * through the entire array.
343      *
344      * @param value The value to search for.
345      * @return Returns true if the value exists, else false.
346      */
containsValue(Object value)347     public boolean containsValue(Object value) {
348         return indexOfValue(value) >= 0;
349     }
350 
351     /**
352      * Retrieve a value from the array.
353      * @param key The key of the value to retrieve.
354      * @return Returns the value associated with the given key,
355      * or null if there is no such key.
356      */
get(Object key)357     public V get(Object key) {
358         final int index = indexOfKey(key);
359         return index >= 0 ? (V)mArray[(index<<1)+1] : null;
360     }
361 
362     /**
363      * Return the key at the given index in the array.
364      * @param index The desired index, must be between 0 and {@link #size()}-1.
365      * @return Returns the key stored at the given index.
366      */
keyAt(int index)367     public K keyAt(int index) {
368         return (K)mArray[index << 1];
369     }
370 
371     /**
372      * Return the value at the given index in the array.
373      * @param index The desired index, must be between 0 and {@link #size()}-1.
374      * @return Returns the value stored at the given index.
375      */
valueAt(int index)376     public V valueAt(int index) {
377         return (V)mArray[(index << 1) + 1];
378     }
379 
380     /**
381      * Set the value at a given index in the array.
382      * @param index The desired index, must be between 0 and {@link #size()}-1.
383      * @param value The new value to store at this index.
384      * @return Returns the previous value at the given index.
385      */
setValueAt(int index, V value)386     public V setValueAt(int index, V value) {
387         index = (index << 1) + 1;
388         V old = (V)mArray[index];
389         mArray[index] = value;
390         return old;
391     }
392 
393     /**
394      * Return true if the array map contains no items.
395      */
isEmpty()396     public boolean isEmpty() {
397         return mSize <= 0;
398     }
399 
400     /**
401      * Add a new value to the array map.
402      * @param key The key under which to store the value.  <b>Must not be null.</b>  If
403      * this key already exists in the array, its value will be replaced.
404      * @param value The value to store for the given key.
405      * @return Returns the old value that was stored for the given key, or null if there
406      * was no such key.
407      */
put(K key, V value)408     public V put(K key, V value) {
409         final int osize = mSize;
410         final int hash;
411         int index;
412         if (key == null) {
413             hash = 0;
414             index = indexOfNull();
415         } else {
416             hash = key.hashCode();
417             index = indexOf(key, hash);
418         }
419         if (index >= 0) {
420             index = (index<<1) + 1;
421             final V old = (V)mArray[index];
422             mArray[index] = value;
423             return old;
424         }
425 
426         index = ~index;
427         if (osize >= mHashes.length) {
428             final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
429                     : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
430 
431             if (DEBUG) System.out.println(TAG + " put: grow from " + mHashes.length + " to " + n);
432 
433             final int[] ohashes = mHashes;
434             final Object[] oarray = mArray;
435             allocArrays(n);
436 
437             if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
438                 throw new ConcurrentModificationException();
439             }
440 
441             if (mHashes.length > 0) {
442                 if (DEBUG) System.out.println(TAG + " put: copy 0-" + osize + " to 0");
443                 System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
444                 System.arraycopy(oarray, 0, mArray, 0, oarray.length);
445             }
446 
447             freeArrays(ohashes, oarray, osize);
448         }
449 
450         if (index < osize) {
451             if (DEBUG) System.out.println(TAG + " put: move " + index + "-" + (osize-index)
452                     + " to " + (index+1));
453             System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
454             System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
455         }
456 
457         if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
458             if (osize != mSize || index >= mHashes.length) {
459                 throw new ConcurrentModificationException();
460             }
461         }
462 
463         mHashes[index] = hash;
464         mArray[index<<1] = key;
465         mArray[(index<<1)+1] = value;
466         mSize++;
467         return null;
468     }
469 
470     /**
471      * Perform a {@link #put(Object, Object)} of all key/value pairs in <var>array</var>
472      * @param array The array whose contents are to be retrieved.
473      */
putAll(SimpleArrayMap<? extends K, ? extends V> array)474     public void putAll(SimpleArrayMap<? extends K, ? extends V> array) {
475         final int N = array.mSize;
476         ensureCapacity(mSize + N);
477         if (mSize == 0) {
478             if (N > 0) {
479                 System.arraycopy(array.mHashes, 0, mHashes, 0, N);
480                 System.arraycopy(array.mArray, 0, mArray, 0, N<<1);
481                 mSize = N;
482             }
483         } else {
484             for (int i=0; i<N; i++) {
485                 put(array.keyAt(i), array.valueAt(i));
486             }
487         }
488     }
489 
490     /**
491      * Remove an existing key from the array map.
492      * @param key The key of the mapping to remove.
493      * @return Returns the value that was stored under the key, or null if there
494      * was no such key.
495      */
remove(Object key)496     public V remove(Object key) {
497         final int index = indexOfKey(key);
498         if (index >= 0) {
499             return removeAt(index);
500         }
501 
502         return null;
503     }
504 
505     /**
506      * Remove the key/value mapping at the given index.
507      * @param index The desired index, must be between 0 and {@link #size()}-1.
508      * @return Returns the value that was stored at this index.
509      */
removeAt(int index)510     public V removeAt(int index) {
511         final Object old = mArray[(index << 1) + 1];
512         final int osize = mSize;
513         final int nsize;
514         if (osize <= 1) {
515             // Now empty.
516             if (DEBUG) System.out.println(TAG + " remove: shrink from " + mHashes.length + " to 0");
517             freeArrays(mHashes, mArray, osize);
518             mHashes = ContainerHelpers.EMPTY_INTS;
519             mArray = ContainerHelpers.EMPTY_OBJECTS;
520             nsize = 0;
521         } else {
522             nsize = osize - 1;
523             if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
524                 // Shrunk enough to reduce size of arrays.  We don't allow it to
525                 // shrink smaller than (BASE_SIZE*2) to avoid flapping between
526                 // that and BASE_SIZE.
527                 final int n = osize > (BASE_SIZE*2) ? (osize + (osize>>1)) : (BASE_SIZE*2);
528 
529                 if (DEBUG) System.out.println(TAG + " remove: shrink from " + mHashes.length + " to " + n);
530 
531                 final int[] ohashes = mHashes;
532                 final Object[] oarray = mArray;
533                 allocArrays(n);
534 
535                 if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
536                     throw new ConcurrentModificationException();
537                 }
538 
539                 if (index > 0) {
540                     if (DEBUG) System.out.println(TAG + " remove: copy from 0-" + index + " to 0");
541                     System.arraycopy(ohashes, 0, mHashes, 0, index);
542                     System.arraycopy(oarray, 0, mArray, 0, index << 1);
543                 }
544                 if (index < nsize) {
545                     if (DEBUG) System.out.println(TAG + " remove: copy from " + (index+1) + "-" + nsize
546                             + " to " + index);
547                     System.arraycopy(ohashes, index + 1, mHashes, index, nsize - index);
548                     System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,
549                             (nsize - index) << 1);
550                 }
551             } else {
552                 if (index < nsize) {
553                     if (DEBUG) System.out.println(TAG + " remove: move " + (index+1) + "-" + nsize
554                             + " to " + index);
555                     System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
556                     System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
557                             (nsize - index) << 1);
558                 }
559                 mArray[nsize << 1] = null;
560                 mArray[(nsize << 1) + 1] = null;
561             }
562         }
563         if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
564             throw new ConcurrentModificationException();
565         }
566         mSize = nsize;
567         return (V)old;
568     }
569 
570     /**
571      * Return the number of items in this array map.
572      */
size()573     public int size() {
574         return mSize;
575     }
576 
577     /**
578      * {@inheritDoc}
579      *
580      * <p>This implementation returns false if the object is not a Map or
581      * SimpleArrayMap, or if the maps have different sizes. Otherwise, for each
582      * key in this map, values of both maps are compared. If the values for any
583      * key are not equal, the method returns false, otherwise it returns true.
584      */
585     @Override
equals(Object object)586     public boolean equals(Object object) {
587         if (this == object) {
588             return true;
589         }
590         if (object instanceof SimpleArrayMap) {
591             SimpleArrayMap<?, ?> map = (SimpleArrayMap<?, ?>) object;
592             if (size() != map.size()) {
593                 return false;
594             }
595 
596             try {
597                 for (int i=0; i<mSize; i++) {
598                     K key = keyAt(i);
599                     V mine = valueAt(i);
600                     Object theirs = map.get(key);
601                     if (mine == null) {
602                         if (theirs != null || !map.containsKey(key)) {
603                             return false;
604                         }
605                     } else if (!mine.equals(theirs)) {
606                         return false;
607                     }
608                 }
609             } catch (NullPointerException ignored) {
610                 return false;
611             } catch (ClassCastException ignored) {
612                 return false;
613             }
614             return true;
615         } else if (object instanceof Map) {
616             Map<?, ?> map = (Map<?, ?>) object;
617             if (size() != map.size()) {
618                 return false;
619             }
620 
621             try {
622                 for (int i=0; i<mSize; i++) {
623                     K key = keyAt(i);
624                     V mine = valueAt(i);
625                     Object theirs = map.get(key);
626                     if (mine == null) {
627                         if (theirs != null || !map.containsKey(key)) {
628                             return false;
629                         }
630                     } else if (!mine.equals(theirs)) {
631                         return false;
632                     }
633                 }
634             } catch (NullPointerException ignored) {
635                 return false;
636             } catch (ClassCastException ignored) {
637                 return false;
638             }
639             return true;
640         }
641         return false;
642     }
643 
644     /**
645      * {@inheritDoc}
646      */
647     @Override
hashCode()648     public int hashCode() {
649         final int[] hashes = mHashes;
650         final Object[] array = mArray;
651         int result = 0;
652         for (int i = 0, v = 1, s = mSize; i < s; i++, v+=2) {
653             Object value = array[v];
654             result += hashes[i] ^ (value == null ? 0 : value.hashCode());
655         }
656         return result;
657     }
658 
659     /**
660      * {@inheritDoc}
661      *
662      * <p>This implementation composes a string by iterating over its mappings. If
663      * this map contains itself as a key or a value, the string "(this Map)"
664      * will appear in its place.
665      */
666     @Override
toString()667     public String toString() {
668         if (isEmpty()) {
669             return "{}";
670         }
671 
672         StringBuilder buffer = new StringBuilder(mSize * 28);
673         buffer.append('{');
674         for (int i=0; i<mSize; i++) {
675             if (i > 0) {
676                 buffer.append(", ");
677             }
678             Object key = keyAt(i);
679             if (key != this) {
680                 buffer.append(key);
681             } else {
682                 buffer.append("(this Map)");
683             }
684             buffer.append('=');
685             Object value = valueAt(i);
686             if (value != this) {
687                 buffer.append(value);
688             } else {
689                 buffer.append("(this Map)");
690             }
691         }
692         buffer.append('}');
693         return buffer.toString();
694     }
695 }
696