Home | History | Annotate | Download | only in util
      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 
     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 
     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 
    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")
    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")
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    319     public int indexOfKey(Object key) {
    320         return key == null ? indexOfNull() : indexOf(key, key.hashCode());
    321     }
    322 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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
    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
    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