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