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