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