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