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