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