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