ArraySet.java revision 9f837a99d48c5bb8ad7fbc133943e5bf622ce065
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 *
46 * @hide
47 */
48public 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