1/*
2 * Copyright (C) 2009 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.support.v4.util;
18
19/**
20 * SparseArray mapping longs to Objects, a version of the platform's
21 * {@link android.util.LongSparseArray} that can be used on older versions of the
22 * platform.  Unlike a normal array of Objects,
23 * there can be gaps in the indices.  It is intended to be more memory efficient
24 * than using a HashMap to map Longs to Objects, both because it avoids
25  * auto-boxing keys and its data structure doesn't rely on an extra entry object
26  * for each mapping.
27 *
28 * <p>Note that this container keeps its mappings in an array data structure,
29 * using a binary search to find keys.  The implementation is not intended to be appropriate for
30 * data structures
31 * that may contain large numbers of items.  It is generally slower than a traditional
32 * HashMap, since lookups require a binary search and adds and removes require inserting
33 * and deleting entries in the array.  For containers holding up to hundreds of items,
34 * the performance difference is not significant, less than 50%.</p>
35 *
36 * <p>To help with performance, the container includes an optimization when removing
37 * keys: instead of compacting its array immediately, it leaves the removed entry marked
38 * as deleted.  The entry can then be re-used for the same key, or compacted later in
39 * a single garbage collection step of all removed entries.  This garbage collection will
40 * need to be performed at any time the array needs to be grown or the the map size or
41 * entry values are retrieved.</p>
42 */
43public class LongSparseArray<E> implements Cloneable {
44    private static final Object DELETED = new Object();
45    private boolean mGarbage = false;
46
47    private long[] mKeys;
48    private Object[] mValues;
49    private int mSize;
50
51    /**
52     * Creates a new LongSparseArray containing no mappings.
53     */
54    public LongSparseArray() {
55        this(10);
56    }
57
58    /**
59     * Creates a new LongSparseArray containing no mappings that will not
60     * require any additional memory allocation to store the specified
61     * number of mappings.  If you supply an initial capacity of 0, the
62     * sparse array will be initialized with a light-weight representation
63     * not requiring any additional array allocations.
64     */
65    public LongSparseArray(int initialCapacity) {
66        if (initialCapacity == 0) {
67            mKeys = ContainerHelpers.EMPTY_LONGS;
68            mValues = ContainerHelpers.EMPTY_OBJECTS;
69        } else {
70            initialCapacity = ContainerHelpers.idealLongArraySize(initialCapacity);
71            mKeys = new long[initialCapacity];
72            mValues = new Object[initialCapacity];
73        }
74        mSize = 0;
75    }
76
77    @Override
78    @SuppressWarnings("unchecked")
79    public LongSparseArray<E> clone() {
80        LongSparseArray<E> clone = null;
81        try {
82            clone = (LongSparseArray<E>) super.clone();
83            clone.mKeys = mKeys.clone();
84            clone.mValues = mValues.clone();
85        } catch (CloneNotSupportedException cnse) {
86            /* ignore */
87        }
88        return clone;
89    }
90
91    /**
92     * Gets the Object mapped from the specified key, or <code>null</code>
93     * if no such mapping has been made.
94     */
95    public E get(long key) {
96        return get(key, null);
97    }
98
99    /**
100     * Gets the Object mapped from the specified key, or the specified Object
101     * if no such mapping has been made.
102     */
103    @SuppressWarnings("unchecked")
104    public E get(long key, E valueIfKeyNotFound) {
105        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
106
107        if (i < 0 || mValues[i] == DELETED) {
108            return valueIfKeyNotFound;
109        } else {
110            return (E) mValues[i];
111        }
112    }
113
114    /**
115     * Removes the mapping from the specified key, if there was any.
116     */
117    public void delete(long key) {
118        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
119
120        if (i >= 0) {
121            if (mValues[i] != DELETED) {
122                mValues[i] = DELETED;
123                mGarbage = true;
124            }
125        }
126    }
127
128    /**
129     * Alias for {@link #delete(long)}.
130     */
131    public void remove(long key) {
132        delete(key);
133    }
134
135    /**
136     * Removes the mapping at the specified index.
137     */
138    public void removeAt(int index) {
139        if (mValues[index] != DELETED) {
140            mValues[index] = DELETED;
141            mGarbage = true;
142        }
143    }
144
145    private void gc() {
146        // Log.e("SparseArray", "gc start with " + mSize);
147
148        int n = mSize;
149        int o = 0;
150        long[] keys = mKeys;
151        Object[] values = mValues;
152
153        for (int i = 0; i < n; i++) {
154            Object val = values[i];
155
156            if (val != DELETED) {
157                if (i != o) {
158                    keys[o] = keys[i];
159                    values[o] = val;
160                    values[i] = null;
161                }
162
163                o++;
164            }
165        }
166
167        mGarbage = false;
168        mSize = o;
169
170        // Log.e("SparseArray", "gc end with " + mSize);
171    }
172
173    /**
174     * Adds a mapping from the specified key to the specified value,
175     * replacing the previous mapping from the specified key if there
176     * was one.
177     */
178    public void put(long key, E value) {
179        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
180
181        if (i >= 0) {
182            mValues[i] = value;
183        } else {
184            i = ~i;
185
186            if (i < mSize && mValues[i] == DELETED) {
187                mKeys[i] = key;
188                mValues[i] = value;
189                return;
190            }
191
192            if (mGarbage && mSize >= mKeys.length) {
193                gc();
194
195                // Search again because indices may have changed.
196                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
197            }
198
199            if (mSize >= mKeys.length) {
200                int n = ContainerHelpers.idealLongArraySize(mSize + 1);
201
202                long[] nkeys = new long[n];
203                Object[] nvalues = new Object[n];
204
205                // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
206                System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
207                System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
208
209                mKeys = nkeys;
210                mValues = nvalues;
211            }
212
213            if (mSize - i != 0) {
214                // Log.e("SparseArray", "move " + (mSize - i));
215                System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
216                System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
217            }
218
219            mKeys[i] = key;
220            mValues[i] = value;
221            mSize++;
222        }
223    }
224
225    /**
226     * Returns the number of key-value mappings that this LongSparseArray
227     * currently stores.
228     */
229    public int size() {
230        if (mGarbage) {
231            gc();
232        }
233
234        return mSize;
235    }
236
237    /**
238     * Given an index in the range <code>0...size()-1</code>, returns
239     * the key from the <code>index</code>th key-value mapping that this
240     * LongSparseArray stores.
241     */
242    public long keyAt(int index) {
243        if (mGarbage) {
244            gc();
245        }
246
247        return mKeys[index];
248    }
249
250    /**
251     * Given an index in the range <code>0...size()-1</code>, returns
252     * the value from the <code>index</code>th key-value mapping that this
253     * LongSparseArray stores.
254     */
255    @SuppressWarnings("unchecked")
256    public E valueAt(int index) {
257        if (mGarbage) {
258            gc();
259        }
260
261        return (E) mValues[index];
262    }
263
264    /**
265     * Given an index in the range <code>0...size()-1</code>, sets a new
266     * value for the <code>index</code>th key-value mapping that this
267     * LongSparseArray stores.
268     */
269    public void setValueAt(int index, E value) {
270        if (mGarbage) {
271            gc();
272        }
273
274        mValues[index] = value;
275    }
276
277    /**
278     * Returns the index for which {@link #keyAt} would return the
279     * specified key, or a negative number if the specified
280     * key is not mapped.
281     */
282    public int indexOfKey(long key) {
283        if (mGarbage) {
284            gc();
285        }
286
287        return ContainerHelpers.binarySearch(mKeys, mSize, key);
288    }
289
290    /**
291     * Returns an index for which {@link #valueAt} would return the
292     * specified key, or a negative number if no keys map to the
293     * specified value.
294     * Beware that this is a linear search, unlike lookups by key,
295     * and that multiple keys can map to the same value and this will
296     * find only one of them.
297     */
298    public int indexOfValue(E value) {
299        if (mGarbage) {
300            gc();
301        }
302
303        for (int i = 0; i < mSize; i++)
304            if (mValues[i] == value)
305                return i;
306
307        return -1;
308    }
309
310    /**
311     * Removes all key-value mappings from this LongSparseArray.
312     */
313    public void clear() {
314        int n = mSize;
315        Object[] values = mValues;
316
317        for (int i = 0; i < n; i++) {
318            values[i] = null;
319        }
320
321        mSize = 0;
322        mGarbage = false;
323    }
324
325    /**
326     * Puts a key/value pair into the array, optimizing for the case where
327     * the key is greater than all existing keys in the array.
328     */
329    public void append(long key, E value) {
330        if (mSize != 0 && key <= mKeys[mSize - 1]) {
331            put(key, value);
332            return;
333        }
334
335        if (mGarbage && mSize >= mKeys.length) {
336            gc();
337        }
338
339        int pos = mSize;
340        if (pos >= mKeys.length) {
341            int n = ContainerHelpers.idealLongArraySize(pos + 1);
342
343            long[] nkeys = new long[n];
344            Object[] nvalues = new Object[n];
345
346            // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
347            System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
348            System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
349
350            mKeys = nkeys;
351            mValues = nvalues;
352        }
353
354        mKeys[pos] = key;
355        mValues[pos] = value;
356        mSize = pos + 1;
357    }
358
359    /**
360     * {@inheritDoc}
361     *
362     * <p>This implementation composes a string by iterating over its mappings. If
363     * this map contains itself as a value, the string "(this Map)"
364     * will appear in its place.
365     */
366    @Override
367    public String toString() {
368        if (size() <= 0) {
369            return "{}";
370        }
371
372        StringBuilder buffer = new StringBuilder(mSize * 28);
373        buffer.append('{');
374        for (int i=0; i<mSize; i++) {
375            if (i > 0) {
376                buffer.append(", ");
377            }
378            long key = keyAt(i);
379            buffer.append(key);
380            buffer.append('=');
381            Object value = valueAt(i);
382            if (value != this) {
383                buffer.append(value);
384            } else {
385                buffer.append("(this Map)");
386            }
387        }
388        buffer.append('}');
389        return buffer.toString();
390    }
391}
392