1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.util;
18
19import libcore.util.EmptyArray;
20
21import java.lang.reflect.Array;
22import java.util.Collection;
23import java.util.Iterator;
24import java.util.Map;
25import 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 */
46public final class ArraySet<E> implements Collection<E>, Set<E> {
47    private static final boolean DEBUG = false;
48    private static final String TAG = "ArraySet";
49
50    /**
51     * The minimum amount by which the capacity of a ArraySet will increase.
52     * This is tuned to be relatively space-efficient.
53     */
54    private static final int BASE_SIZE = 4;
55
56    /**
57     * Maximum number of entries to have in array caches.
58     */
59    private static final int CACHE_SIZE = 10;
60
61    /**
62     * Caches of small array objects to avoid spamming garbage.  The cache
63     * Object[] variable is a pointer to a linked list of array objects.
64     * The first entry in the array is a pointer to the next array in the
65     * list; the second entry is a pointer to the int[] hash code array for it.
66     */
67    static Object[] mBaseCache;
68    static int mBaseCacheSize;
69    static Object[] mTwiceBaseCache;
70    static int mTwiceBaseCacheSize;
71
72    final boolean mIdentityHashCode;
73    int[] mHashes;
74    Object[] mArray;
75    int mSize;
76    MapCollections<E, E> mCollections;
77
78    private int indexOf(Object key, int hash) {
79        final int N = mSize;
80
81        // Important fast case: if nothing is in here, nothing to look for.
82        if (N == 0) {
83            return ~0;
84        }
85
86        int index = ContainerHelpers.binarySearch(mHashes, N, hash);
87
88        // If the hash code wasn't found, then we have no entry for this key.
89        if (index < 0) {
90            return index;
91        }
92
93        // If the key at the returned index matches, that's what we want.
94        if (key.equals(mArray[index])) {
95            return index;
96        }
97
98        // Search for a matching key after the index.
99        int end;
100        for (end = index + 1; end < N && mHashes[end] == hash; end++) {
101            if (key.equals(mArray[end])) return end;
102        }
103
104        // Search for a matching key before the index.
105        for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
106            if (key.equals(mArray[i])) return i;
107        }
108
109        // Key not found -- return negative value indicating where a
110        // new entry for this key should go.  We use the end of the
111        // hash chain to reduce the number of array entries that will
112        // need to be copied when inserting.
113        return ~end;
114    }
115
116    private int indexOfNull() {
117        final int N = mSize;
118
119        // Important fast case: if nothing is in here, nothing to look for.
120        if (N == 0) {
121            return ~0;
122        }
123
124        int index = ContainerHelpers.binarySearch(mHashes, N, 0);
125
126        // If the hash code wasn't found, then we have no entry for this key.
127        if (index < 0) {
128            return index;
129        }
130
131        // If the key at the returned index matches, that's what we want.
132        if (null == mArray[index]) {
133            return index;
134        }
135
136        // Search for a matching key after the index.
137        int end;
138        for (end = index + 1; end < N && mHashes[end] == 0; end++) {
139            if (null == mArray[end]) return end;
140        }
141
142        // Search for a matching key before the index.
143        for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) {
144            if (null == mArray[i]) return i;
145        }
146
147        // Key not found -- return negative value indicating where a
148        // new entry for this key should go.  We use the end of the
149        // hash chain to reduce the number of array entries that will
150        // need to be copied when inserting.
151        return ~end;
152    }
153
154    private void allocArrays(final int size) {
155        if (size == (BASE_SIZE*2)) {
156            synchronized (ArraySet.class) {
157                if (mTwiceBaseCache != null) {
158                    final Object[] array = mTwiceBaseCache;
159                    try {
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                    } catch (ClassCastException e) {
169                    }
170                    // Whoops!  Someone trampled the array (probably due to not protecting
171                    // their access with a lock).  Our cache is corrupt; report and give up.
172                    Slog.wtf(TAG, "Found corrupt ArraySet cache: [0]=" + array[0]
173                            + " [1]=" + array[1]);
174                    mTwiceBaseCache = null;
175                    mTwiceBaseCacheSize = 0;
176                }
177            }
178        } else if (size == BASE_SIZE) {
179            synchronized (ArraySet.class) {
180                if (mBaseCache != null) {
181                    final Object[] array = mBaseCache;
182                    try {
183                        mArray = array;
184                        mBaseCache = (Object[]) array[0];
185                        mHashes = (int[]) array[1];
186                        array[0] = array[1] = null;
187                        mBaseCacheSize--;
188                        if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes
189                                + " now have " + mBaseCacheSize + " entries");
190                        return;
191                    } catch (ClassCastException e) {
192                    }
193                    // Whoops!  Someone trampled the array (probably due to not protecting
194                    // their access with a lock).  Our cache is corrupt; report and give up.
195                    Slog.wtf(TAG, "Found corrupt ArraySet cache: [0]=" + array[0]
196                            + " [1]=" + array[1]);
197                    mBaseCache = null;
198                    mBaseCacheSize = 0;
199                }
200            }
201        }
202
203        mHashes = new int[size];
204        mArray = new Object[size];
205    }
206
207    private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
208        if (hashes.length == (BASE_SIZE*2)) {
209            synchronized (ArraySet.class) {
210                if (mTwiceBaseCacheSize < CACHE_SIZE) {
211                    array[0] = mTwiceBaseCache;
212                    array[1] = hashes;
213                    for (int i=size-1; i>=2; i--) {
214                        array[i] = null;
215                    }
216                    mTwiceBaseCache = array;
217                    mTwiceBaseCacheSize++;
218                    if (DEBUG) Log.d(TAG, "Storing 2x cache " + array
219                            + " now have " + mTwiceBaseCacheSize + " entries");
220                }
221            }
222        } else if (hashes.length == BASE_SIZE) {
223            synchronized (ArraySet.class) {
224                if (mBaseCacheSize < CACHE_SIZE) {
225                    array[0] = mBaseCache;
226                    array[1] = hashes;
227                    for (int i=size-1; i>=2; i--) {
228                        array[i] = null;
229                    }
230                    mBaseCache = array;
231                    mBaseCacheSize++;
232                    if (DEBUG) Log.d(TAG, "Storing 1x cache " + array
233                            + " now have " + mBaseCacheSize + " entries");
234                }
235            }
236        }
237    }
238
239    /**
240     * Create a new empty ArraySet.  The default capacity of an array map is 0, and
241     * will grow once items are added to it.
242     */
243    public ArraySet() {
244        this(0, false);
245    }
246
247    /**
248     * Create a new ArraySet with a given initial capacity.
249     */
250    public ArraySet(int capacity) {
251        this(capacity, false);
252    }
253
254    /** {@hide} */
255    public ArraySet(int capacity, boolean identityHashCode) {
256        mIdentityHashCode = identityHashCode;
257        if (capacity == 0) {
258            mHashes = EmptyArray.INT;
259            mArray = EmptyArray.OBJECT;
260        } else {
261            allocArrays(capacity);
262        }
263        mSize = 0;
264    }
265
266    /**
267     * Create a new ArraySet with the mappings from the given ArraySet.
268     */
269    public ArraySet(ArraySet<E> set) {
270        this();
271        if (set != null) {
272            addAll(set);
273        }
274    }
275
276    /** {@hide} */
277    public ArraySet(Collection<E> set) {
278        this();
279        if (set != null) {
280            addAll(set);
281        }
282    }
283
284    /**
285     * Make the array map empty.  All storage is released.
286     */
287    @Override
288    public void clear() {
289        if (mSize != 0) {
290            freeArrays(mHashes, mArray, mSize);
291            mHashes = EmptyArray.INT;
292            mArray = EmptyArray.OBJECT;
293            mSize = 0;
294        }
295    }
296
297    /**
298     * Ensure the array map can hold at least <var>minimumCapacity</var>
299     * items.
300     */
301    public void ensureCapacity(int minimumCapacity) {
302        if (mHashes.length < minimumCapacity) {
303            final int[] ohashes = mHashes;
304            final Object[] oarray = mArray;
305            allocArrays(minimumCapacity);
306            if (mSize > 0) {
307                System.arraycopy(ohashes, 0, mHashes, 0, mSize);
308                System.arraycopy(oarray, 0, mArray, 0, mSize);
309            }
310            freeArrays(ohashes, oarray, mSize);
311        }
312    }
313
314    /**
315     * Check whether a value exists in the set.
316     *
317     * @param key The value to search for.
318     * @return Returns true if the value exists, else false.
319     */
320    @Override
321    public boolean contains(Object key) {
322        return indexOf(key) >= 0;
323    }
324
325    /**
326     * Returns the index of a value in the set.
327     *
328     * @param key The value to search for.
329     * @return Returns the index of the value if it exists, else a negative integer.
330     */
331    public int indexOf(Object key) {
332        return key == null ? indexOfNull()
333                : indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
334    }
335
336    /**
337     * Return the value at the given index in the array.
338     * @param index The desired index, must be between 0 and {@link #size()}-1.
339     * @return Returns the value stored at the given index.
340     */
341    public E valueAt(int index) {
342        return (E)mArray[index];
343    }
344
345    /**
346     * Return true if the array map contains no items.
347     */
348    @Override
349    public boolean isEmpty() {
350        return mSize <= 0;
351    }
352
353    /**
354     * Adds the specified object to this set. The set is not modified if it
355     * already contains the object.
356     *
357     * @param value the object to add.
358     * @return {@code true} if this set is modified, {@code false} otherwise.
359     * @throws ClassCastException
360     *             when the class of the object is inappropriate for this set.
361     */
362    @Override
363    public boolean add(E value) {
364        final int hash;
365        int index;
366        if (value == null) {
367            hash = 0;
368            index = indexOfNull();
369        } else {
370            hash = mIdentityHashCode ? System.identityHashCode(value) : value.hashCode();
371            index = indexOf(value, hash);
372        }
373        if (index >= 0) {
374            return false;
375        }
376
377        index = ~index;
378        if (mSize >= mHashes.length) {
379            final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1))
380                    : (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
381
382            if (DEBUG) Log.d(TAG, "add: grow from " + mHashes.length + " to " + n);
383
384            final int[] ohashes = mHashes;
385            final Object[] oarray = mArray;
386            allocArrays(n);
387
388            if (mHashes.length > 0) {
389                if (DEBUG) Log.d(TAG, "add: copy 0-" + mSize + " to 0");
390                System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
391                System.arraycopy(oarray, 0, mArray, 0, oarray.length);
392            }
393
394            freeArrays(ohashes, oarray, mSize);
395        }
396
397        if (index < mSize) {
398            if (DEBUG) Log.d(TAG, "add: move " + index + "-" + (mSize-index)
399                    + " to " + (index+1));
400            System.arraycopy(mHashes, index, mHashes, index + 1, mSize - index);
401            System.arraycopy(mArray, index, mArray, index + 1, mSize - index);
402        }
403
404        mHashes[index] = hash;
405        mArray[index] = value;
406        mSize++;
407        return true;
408    }
409
410    /**
411     * Special fast path for appending items to the end of the array without validation.
412     * The array must already be large enough to contain the item.
413     * @hide
414     */
415    public void append(E value) {
416        final int index = mSize;
417        final int hash = value == null ? 0
418                : (mIdentityHashCode ? System.identityHashCode(value) : value.hashCode());
419        if (index >= mHashes.length) {
420            throw new IllegalStateException("Array is full");
421        }
422        if (index > 0 && mHashes[index - 1] > hash) {
423            // Cannot optimize since it would break the sorted order - fallback to add()
424            if (DEBUG) {
425                RuntimeException e = new RuntimeException("here");
426                e.fillInStackTrace();
427                Log.w(TAG, "New hash " + hash
428                        + " is before end of array hash " + mHashes[index - 1]
429                        + " at index " + index, e);
430            }
431            add(value);
432            return;
433        }
434        mSize = index + 1;
435        mHashes[index] = hash;
436        mArray[index] = value;
437    }
438
439    /**
440     * Perform a {@link #add(Object)} of all values in <var>array</var>
441     * @param array The array whose contents are to be retrieved.
442     */
443    public void addAll(ArraySet<? extends E> array) {
444        final int N = array.mSize;
445        ensureCapacity(mSize + N);
446        if (mSize == 0) {
447            if (N > 0) {
448                System.arraycopy(array.mHashes, 0, mHashes, 0, N);
449                System.arraycopy(array.mArray, 0, mArray, 0, N);
450                mSize = N;
451            }
452        } else {
453            for (int i=0; i<N; i++) {
454                add(array.valueAt(i));
455            }
456        }
457    }
458
459    /**
460     * Removes the specified object from this set.
461     *
462     * @param object the object to remove.
463     * @return {@code true} if this set was modified, {@code false} otherwise.
464     */
465    @Override
466    public boolean remove(Object object) {
467        final int index = indexOf(object);
468        if (index >= 0) {
469            removeAt(index);
470            return true;
471        }
472        return false;
473    }
474
475    /**
476     * Remove the key/value mapping at the given index.
477     * @param index The desired index, must be between 0 and {@link #size()}-1.
478     * @return Returns the value that was stored at this index.
479     */
480    public E removeAt(int index) {
481        final Object old = mArray[index];
482        if (mSize <= 1) {
483            // Now empty.
484            if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0");
485            freeArrays(mHashes, mArray, mSize);
486            mHashes = EmptyArray.INT;
487            mArray = EmptyArray.OBJECT;
488            mSize = 0;
489        } else {
490            if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
491                // Shrunk enough to reduce size of arrays.  We don't allow it to
492                // shrink smaller than (BASE_SIZE*2) to avoid flapping between
493                // that and BASE_SIZE.
494                final int n = mSize > (BASE_SIZE*2) ? (mSize + (mSize>>1)) : (BASE_SIZE*2);
495
496                if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n);
497
498                final int[] ohashes = mHashes;
499                final Object[] oarray = mArray;
500                allocArrays(n);
501
502                mSize--;
503                if (index > 0) {
504                    if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0");
505                    System.arraycopy(ohashes, 0, mHashes, 0, index);
506                    System.arraycopy(oarray, 0, mArray, 0, index);
507                }
508                if (index < mSize) {
509                    if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + mSize
510                            + " to " + index);
511                    System.arraycopy(ohashes, index + 1, mHashes, index, mSize - index);
512                    System.arraycopy(oarray, index + 1, mArray, index, mSize - index);
513                }
514            } else {
515                mSize--;
516                if (index < mSize) {
517                    if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + mSize
518                            + " to " + index);
519                    System.arraycopy(mHashes, index + 1, mHashes, index, mSize - index);
520                    System.arraycopy(mArray, index + 1, mArray, index, mSize - index);
521                }
522                mArray[mSize] = null;
523            }
524        }
525        return (E)old;
526    }
527
528    /**
529     * Perform a {@link #remove(Object)} of all values in <var>array</var>
530     * @param array The array whose contents are to be removed.
531     */
532    public boolean removeAll(ArraySet<? extends E> array) {
533        // TODO: If array is sufficiently large, a marking approach might be beneficial. In a first
534        //       pass, use the property that the sets are sorted by hash to make this linear passes
535        //       (except for hash collisions, which means worst case still n*m), then do one
536        //       collection pass into a new array. This avoids binary searches and excessive memcpy.
537        final int N = array.mSize;
538
539        // Note: ArraySet does not make thread-safety guarantees. So instead of OR-ing together all
540        //       the single results, compare size before and after.
541        final int originalSize = mSize;
542        for (int i = 0; i < N; i++) {
543            remove(array.valueAt(i));
544        }
545        return originalSize != mSize;
546    }
547
548    /**
549     * Return the number of items in this array map.
550     */
551    @Override
552    public int size() {
553        return mSize;
554    }
555
556    @Override
557    public Object[] toArray() {
558        Object[] result = new Object[mSize];
559        System.arraycopy(mArray, 0, result, 0, mSize);
560        return result;
561    }
562
563    @Override
564    public <T> T[] toArray(T[] array) {
565        if (array.length < mSize) {
566            @SuppressWarnings("unchecked") T[] newArray
567                = (T[]) Array.newInstance(array.getClass().getComponentType(), mSize);
568            array = newArray;
569        }
570        System.arraycopy(mArray, 0, array, 0, mSize);
571        if (array.length > mSize) {
572            array[mSize] = null;
573        }
574        return array;
575    }
576
577    /**
578     * {@inheritDoc}
579     *
580     * <p>This implementation returns false if the object is not a set, or
581     * if the sets have different sizes.  Otherwise, for each value in this
582     * set, it checks to make sure the value also exists in the other set.
583     * If any value doesn't exist, the method returns false; otherwise, it
584     * returns true.
585     */
586    @Override
587    public boolean equals(Object object) {
588        if (this == object) {
589            return true;
590        }
591        if (object instanceof Set) {
592            Set<?> set = (Set<?>) object;
593            if (size() != set.size()) {
594                return false;
595            }
596
597            try {
598                for (int i=0; i<mSize; i++) {
599                    E mine = valueAt(i);
600                    if (!set.contains(mine)) {
601                        return false;
602                    }
603                }
604            } catch (NullPointerException ignored) {
605                return false;
606            } catch (ClassCastException ignored) {
607                return false;
608            }
609            return true;
610        }
611        return false;
612    }
613
614    /**
615     * {@inheritDoc}
616     */
617    @Override
618    public int hashCode() {
619        final int[] hashes = mHashes;
620        int result = 0;
621        for (int i = 0, s = mSize; i < s; i++) {
622            result += hashes[i];
623        }
624        return result;
625    }
626
627    /**
628     * {@inheritDoc}
629     *
630     * <p>This implementation composes a string by iterating over its values. If
631     * this set contains itself as a value, the string "(this Set)"
632     * will appear in its place.
633     */
634    @Override
635    public String toString() {
636        if (isEmpty()) {
637            return "{}";
638        }
639
640        StringBuilder buffer = new StringBuilder(mSize * 14);
641        buffer.append('{');
642        for (int i=0; i<mSize; i++) {
643            if (i > 0) {
644                buffer.append(", ");
645            }
646            Object value = valueAt(i);
647            if (value != this) {
648                buffer.append(value);
649            } else {
650                buffer.append("(this Set)");
651            }
652        }
653        buffer.append('}');
654        return buffer.toString();
655    }
656
657    // ------------------------------------------------------------------------
658    // Interop with traditional Java containers.  Not as efficient as using
659    // specialized collection APIs.
660    // ------------------------------------------------------------------------
661
662    private MapCollections<E, E> getCollection() {
663        if (mCollections == null) {
664            mCollections = new MapCollections<E, E>() {
665                @Override
666                protected int colGetSize() {
667                    return mSize;
668                }
669
670                @Override
671                protected Object colGetEntry(int index, int offset) {
672                    return mArray[index];
673                }
674
675                @Override
676                protected int colIndexOfKey(Object key) {
677                    return indexOf(key);
678                }
679
680                @Override
681                protected int colIndexOfValue(Object value) {
682                    return indexOf(value);
683                }
684
685                @Override
686                protected Map<E, E> colGetMap() {
687                    throw new UnsupportedOperationException("not a map");
688                }
689
690                @Override
691                protected void colPut(E key, E value) {
692                    add(key);
693                }
694
695                @Override
696                protected E colSetValue(int index, E value) {
697                    throw new UnsupportedOperationException("not a map");
698                }
699
700                @Override
701                protected void colRemoveAt(int index) {
702                    removeAt(index);
703                }
704
705                @Override
706                protected void colClear() {
707                    clear();
708                }
709            };
710        }
711        return mCollections;
712    }
713
714    /**
715     * Return an {@link java.util.Iterator} over all values in the set.
716     *
717     * <p><b>Note:</b> this is a fairly inefficient way to access the array contents, it
718     * requires generating a number of temporary objects and allocates additional state
719     * information associated with the container that will remain for the life of the container.</p>
720     */
721    @Override
722    public Iterator<E> iterator() {
723        return getCollection().getKeySet().iterator();
724    }
725
726    /**
727     * Determine if the array set contains all of the values in the given collection.
728     * @param collection The collection whose contents are to be checked against.
729     * @return Returns true if this array set contains a value for every entry
730     * in <var>collection</var>, else returns false.
731     */
732    @Override
733    public boolean containsAll(Collection<?> collection) {
734        Iterator<?> it = collection.iterator();
735        while (it.hasNext()) {
736            if (!contains(it.next())) {
737                return false;
738            }
739        }
740        return true;
741    }
742
743    /**
744     * Perform an {@link #add(Object)} of all values in <var>collection</var>
745     * @param collection The collection whose contents are to be retrieved.
746     */
747    @Override
748    public boolean addAll(Collection<? extends E> collection) {
749        ensureCapacity(mSize + collection.size());
750        boolean added = false;
751        for (E value : collection) {
752            added |= add(value);
753        }
754        return added;
755    }
756
757    /**
758     * Remove all values in the array set that exist in the given collection.
759     * @param collection The collection whose contents are to be used to remove values.
760     * @return Returns true if any values were removed from the array set, else false.
761     */
762    @Override
763    public boolean removeAll(Collection<?> collection) {
764        boolean removed = false;
765        for (Object value : collection) {
766            removed |= remove(value);
767        }
768        return removed;
769    }
770
771    /**
772     * Remove all values in the array set that do <b>not</b> exist in the given collection.
773     * @param collection The collection whose contents are to be used to determine which
774     * values to keep.
775     * @return Returns true if any values were removed from the array set, else false.
776     */
777    @Override
778    public boolean retainAll(Collection<?> collection) {
779        boolean removed = false;
780        for (int i=mSize-1; i>=0; i--) {
781            if (!collection.contains(mArray[i])) {
782                removeAt(i);
783                removed = true;
784            }
785        }
786        return removed;
787    }
788}
789