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