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 
17 package android.util;
18 
19 import android.annotation.NonNull;
20 import android.os.Parcel;
21 
22 import com.android.internal.util.ArrayUtils;
23 import com.android.internal.util.GrowingArrayUtils;
24 import com.android.internal.util.Preconditions;
25 import com.android.internal.util.function.LongObjPredicate;
26 
27 import libcore.util.EmptyArray;
28 
29 import java.util.Arrays;
30 import java.util.Objects;
31 
32 /**
33  * SparseArray mapping longs to Objects.  Unlike a normal array of Objects,
34  * there can be gaps in the indices.  It is intended to be more memory efficient
35  * than using a HashMap to map Longs to Objects, both because it avoids
36  * auto-boxing keys and its data structure doesn't rely on an extra entry object
37  * for each mapping.
38  *
39  * <p>Note that this container keeps its mappings in an array data structure,
40  * using a binary search to find keys.  The implementation is not intended to be appropriate for
41  * data structures
42  * that may contain large numbers of items.  It is generally slower than a traditional
43  * HashMap, since lookups require a binary search and adds and removes require inserting
44  * and deleting entries in the array.  For containers holding up to hundreds of items,
45  * the performance difference is not significant, less than 50%.</p>
46  *
47  * <p>To help with performance, the container includes an optimization when removing
48  * keys: instead of compacting its array immediately, it leaves the removed entry marked
49  * as deleted.  The entry can then be re-used for the same key, or compacted later in
50  * a single garbage collection step of all removed entries.  This garbage collection will
51  * need to be performed at any time the array needs to be grown or the the map size or
52  * entry values are retrieved.</p>
53  *
54  * <p>It is possible to iterate over the items in this container using
55  * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using
56  * <code>keyAt(int)</code> with ascending values of the index will return the
57  * keys in ascending order, or the values corresponding to the keys in ascending
58  * order in the case of <code>valueAt(int)</code>.</p>
59  */
60 public class LongSparseArray<E> implements Cloneable {
61     private static final Object DELETED = new Object();
62     private boolean mGarbage = false;
63 
64     private long[] mKeys;
65     private Object[] mValues;
66     private int mSize;
67 
68     /**
69      * Creates a new LongSparseArray containing no mappings.
70      */
LongSparseArray()71     public LongSparseArray() {
72         this(10);
73     }
74 
75     /**
76      * Creates a new LongSparseArray containing no mappings that will not
77      * require any additional memory allocation to store the specified
78      * number of mappings.  If you supply an initial capacity of 0, the
79      * sparse array will be initialized with a light-weight representation
80      * not requiring any additional array allocations.
81      */
LongSparseArray(int initialCapacity)82     public LongSparseArray(int initialCapacity) {
83         if (initialCapacity == 0) {
84             mKeys = EmptyArray.LONG;
85             mValues = EmptyArray.OBJECT;
86         } else {
87             mKeys = ArrayUtils.newUnpaddedLongArray(initialCapacity);
88             mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
89         }
90         mSize = 0;
91     }
92 
93     @Override
94     @SuppressWarnings("unchecked")
clone()95     public LongSparseArray<E> clone() {
96         LongSparseArray<E> clone = null;
97         try {
98             clone = (LongSparseArray<E>) super.clone();
99             clone.mKeys = mKeys.clone();
100             clone.mValues = mValues.clone();
101         } catch (CloneNotSupportedException cnse) {
102             /* ignore */
103         }
104         return clone;
105     }
106 
107     /**
108      * Gets the Object mapped from the specified key, or <code>null</code>
109      * if no such mapping has been made.
110      */
get(long key)111     public E get(long key) {
112         return get(key, null);
113     }
114 
115     /**
116      * Gets the Object mapped from the specified key, or the specified Object
117      * if no such mapping has been made.
118      */
119     @SuppressWarnings("unchecked")
get(long key, E valueIfKeyNotFound)120     public E get(long key, E valueIfKeyNotFound) {
121         int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
122 
123         if (i < 0 || mValues[i] == DELETED) {
124             return valueIfKeyNotFound;
125         } else {
126             return (E) mValues[i];
127         }
128     }
129 
130     /**
131      * Removes the mapping from the specified key, if there was any.
132      */
delete(long key)133     public void delete(long key) {
134         int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
135 
136         if (i >= 0) {
137             if (mValues[i] != DELETED) {
138                 mValues[i] = DELETED;
139                 mGarbage = true;
140             }
141         }
142     }
143 
144     /**
145      * Alias for {@link #delete(long)}.
146      */
remove(long key)147     public void remove(long key) {
148         delete(key);
149     }
150 
151     /** @hide */
152     @SuppressWarnings("unchecked")
removeIf(@onNull LongObjPredicate<? super E> filter)153     public void removeIf(@NonNull LongObjPredicate<? super E> filter) {
154         Objects.requireNonNull(filter);
155         for (int i = 0; i < mSize; ++i) {
156             if (mValues[i] != DELETED && filter.test(mKeys[i], (E) mValues[i])) {
157                 mValues[i] = DELETED;
158                 mGarbage = true;
159             }
160         }
161     }
162 
163     /**
164      * Removes the mapping at the specified index.
165      *
166      * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
167      * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
168      * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
169      * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
170      */
removeAt(int index)171     public void removeAt(int index) {
172         if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
173             // The array might be slightly bigger than mSize, in which case, indexing won't fail.
174             // Check if exception should be thrown outside of the critical path.
175             throw new ArrayIndexOutOfBoundsException(index);
176         }
177         if (mValues[index] != DELETED) {
178             mValues[index] = DELETED;
179             mGarbage = true;
180         }
181     }
182 
gc()183     private void gc() {
184         // Log.e("SparseArray", "gc start with " + mSize);
185 
186         int n = mSize;
187         int o = 0;
188         long[] keys = mKeys;
189         Object[] values = mValues;
190 
191         for (int i = 0; i < n; i++) {
192             Object val = values[i];
193 
194             if (val != DELETED) {
195                 if (i != o) {
196                     keys[o] = keys[i];
197                     values[o] = val;
198                     values[i] = null;
199                 }
200 
201                 o++;
202             }
203         }
204 
205         mGarbage = false;
206         mSize = o;
207 
208         // Log.e("SparseArray", "gc end with " + mSize);
209     }
210 
211     /**
212      * Adds a mapping from the specified key to the specified value,
213      * replacing the previous mapping from the specified key if there
214      * was one.
215      */
put(long key, E value)216     public void put(long key, E value) {
217         int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
218 
219         if (i >= 0) {
220             mValues[i] = value;
221         } else {
222             i = ~i;
223 
224             if (i < mSize && mValues[i] == DELETED) {
225                 mKeys[i] = key;
226                 mValues[i] = value;
227                 return;
228             }
229 
230             if (mGarbage && mSize >= mKeys.length) {
231                 gc();
232 
233                 // Search again because indices may have changed.
234                 i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
235             }
236 
237             mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
238             mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
239             mSize++;
240         }
241     }
242 
243     /**
244      * Returns the number of key-value mappings that this LongSparseArray
245      * currently stores.
246      */
size()247     public int size() {
248         if (mGarbage) {
249             gc();
250         }
251 
252         return mSize;
253     }
254 
255     /**
256      * Given an index in the range <code>0...size()-1</code>, returns
257      * the key from the <code>index</code>th key-value mapping that this
258      * LongSparseArray stores.
259      *
260      * <p>The keys corresponding to indices in ascending order are guaranteed to
261      * be in ascending order, e.g., <code>keyAt(0)</code> will return the
262      * smallest key and <code>keyAt(size()-1)</code> will return the largest
263      * key.</p>
264      *
265      * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
266      * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
267      * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
268      * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
269      */
keyAt(int index)270     public long keyAt(int index) {
271         if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
272             // The array might be slightly bigger than mSize, in which case, indexing won't fail.
273             // Check if exception should be thrown outside of the critical path.
274             throw new ArrayIndexOutOfBoundsException(index);
275         }
276         if (mGarbage) {
277             gc();
278         }
279 
280         return mKeys[index];
281     }
282 
283     /**
284      * Given an index in the range <code>0...size()-1</code>, returns
285      * the value from the <code>index</code>th key-value mapping that this
286      * LongSparseArray stores.
287      *
288      * <p>The values corresponding to indices in ascending order are guaranteed
289      * to be associated with keys in ascending order, e.g.,
290      * <code>valueAt(0)</code> will return the value associated with the
291      * smallest key and <code>valueAt(size()-1)</code> will return the value
292      * associated with the largest key.</p>
293      *
294      * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
295      * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
296      * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
297      * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
298      */
299     @SuppressWarnings("unchecked")
valueAt(int index)300     public E valueAt(int index) {
301         if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
302             // The array might be slightly bigger than mSize, in which case, indexing won't fail.
303             // Check if exception should be thrown outside of the critical path.
304             throw new ArrayIndexOutOfBoundsException(index);
305         }
306         if (mGarbage) {
307             gc();
308         }
309 
310         return (E) mValues[index];
311     }
312 
313     /**
314      * Given an index in the range <code>0...size()-1</code>, sets a new
315      * value for the <code>index</code>th key-value mapping that this
316      * LongSparseArray stores.
317      *
318      * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
319      * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
320      * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
321      * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
322      */
setValueAt(int index, E value)323     public void setValueAt(int index, E value) {
324         if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
325             // The array might be slightly bigger than mSize, in which case, indexing won't fail.
326             // Check if exception should be thrown outside of the critical path.
327             throw new ArrayIndexOutOfBoundsException(index);
328         }
329         if (mGarbage) {
330             gc();
331         }
332 
333         mValues[index] = value;
334     }
335 
336     /**
337      * Returns the index for which {@link #keyAt} would return the
338      * specified key, or a negative number if the specified
339      * key is not mapped.
340      */
indexOfKey(long key)341     public int indexOfKey(long key) {
342         if (mGarbage) {
343             gc();
344         }
345 
346         return ContainerHelpers.binarySearch(mKeys, mSize, key);
347     }
348 
349     /**
350      * Returns an index for which {@link #valueAt} would return the
351      * specified key, or a negative number if no keys map to the
352      * specified value.
353      * Beware that this is a linear search, unlike lookups by key,
354      * and that multiple keys can map to the same value and this will
355      * find only one of them.
356      */
indexOfValue(E value)357     public int indexOfValue(E value) {
358         if (mGarbage) {
359             gc();
360         }
361 
362         for (int i = 0; i < mSize; i++) {
363             if (mValues[i] == value) {
364                 return i;
365             }
366         }
367         return -1;
368     }
369 
370     /**
371      * Returns an index for which {@link #valueAt} would return the
372      * specified key, or a negative number if no keys map to the
373      * specified value.
374      * <p>Beware that this is a linear search, unlike lookups by key,
375      * and that multiple keys can map to the same value and this will
376      * find only one of them.
377      * <p>Note also that this method uses {@code equals} unlike {@code indexOfValue}.
378      * @hide
379      */
indexOfValueByValue(E value)380     public int indexOfValueByValue(E value) {
381         if (mGarbage) {
382             gc();
383         }
384 
385         for (int i = 0; i < mSize; i++) {
386             if (value == null) {
387                 if (mValues[i] == null) {
388                     return i;
389                 }
390             } else {
391                 if (value.equals(mValues[i])) {
392                     return i;
393                 }
394             }
395         }
396         return -1;
397     }
398 
399     /**
400      * Removes all key-value mappings from this LongSparseArray.
401      */
clear()402     public void clear() {
403         int n = mSize;
404         Object[] values = mValues;
405 
406         for (int i = 0; i < n; i++) {
407             values[i] = null;
408         }
409 
410         mSize = 0;
411         mGarbage = false;
412     }
413 
414     /**
415      * Puts a key/value pair into the array, optimizing for the case where
416      * the key is greater than all existing keys in the array.
417      */
append(long key, E value)418     public void append(long key, E value) {
419         if (mSize != 0 && key <= mKeys[mSize - 1]) {
420             put(key, value);
421             return;
422         }
423 
424         if (mGarbage && mSize >= mKeys.length) {
425             gc();
426         }
427 
428         mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
429         mValues = GrowingArrayUtils.append(mValues, mSize, value);
430         mSize++;
431     }
432 
433     /**
434      * {@inheritDoc}
435      *
436      * <p>This implementation composes a string by iterating over its mappings. If
437      * this map contains itself as a value, the string "(this Map)"
438      * will appear in its place.
439      */
440     @Override
toString()441     public String toString() {
442         if (size() <= 0) {
443             return "{}";
444         }
445 
446         StringBuilder buffer = new StringBuilder(mSize * 28);
447         buffer.append('{');
448         for (int i=0; i<mSize; i++) {
449             if (i > 0) {
450                 buffer.append(", ");
451             }
452             long key = keyAt(i);
453             buffer.append(key);
454             buffer.append('=');
455             Object value = valueAt(i);
456             if (value != this) {
457                 buffer.append(value);
458             } else {
459                 buffer.append("(this Map)");
460             }
461         }
462         buffer.append('}');
463         return buffer.toString();
464     }
465 
466     /**
467      * @hide
468      */
469     public static class StringParcelling implements
470             com.android.internal.util.Parcelling<LongSparseArray<String>> {
471         @Override
parcel(LongSparseArray<String> array, Parcel dest, int parcelFlags)472         public void parcel(LongSparseArray<String> array, Parcel dest, int parcelFlags) {
473             if (array == null) {
474                 dest.writeInt(-1);
475                 return;
476             }
477 
478             int size = array.mSize;
479 
480             dest.writeInt(size);
481             dest.writeLongArray(array.mKeys);
482 
483             dest.writeStringArray(Arrays.copyOfRange(array.mValues, 0, size, String[].class));
484         }
485 
486         @Override
unparcel(Parcel source)487         public LongSparseArray<String> unparcel(Parcel source) {
488             int size = source.readInt();
489             if (size == -1) {
490                 return null;
491             }
492 
493             LongSparseArray<String> array = new LongSparseArray<>(0);
494             array.mSize = size;
495             array.mKeys = source.createLongArray();
496             array.mValues = source.createStringArray();
497 
498             // Make sure array is sane
499             Preconditions.checkArgument(array.mKeys.length >= size);
500             Preconditions.checkArgument(array.mValues.length >= size);
501 
502             if (size > 0) {
503                 long last = array.mKeys[0];
504                 for (int i = 1; i < size; i++) {
505                     Preconditions.checkArgument(last < array.mKeys[i]);
506                 }
507             }
508 
509             return array;
510         }
511     }
512 }
513