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 * Returns true if the key exists in the array. This is equivalent to 108 * {@link #indexOfKey(int)} >= 0. 109 * 110 * @param key Potential key in the mapping 111 * @return true if the key is defined in the mapping 112 */ contains(int key)113 public boolean contains(int key) { 114 return indexOfKey(key) >= 0; 115 } 116 117 /** 118 * Gets the Object mapped from the specified key, or <code>null</code> 119 * if no such mapping has been made. 120 */ get(int key)121 public E get(int key) { 122 return get(key, null); 123 } 124 125 /** 126 * Gets the Object mapped from the specified key, or the specified Object 127 * if no such mapping has been made. 128 */ 129 @SuppressWarnings("unchecked") get(int key, E valueIfKeyNotFound)130 public E get(int key, E valueIfKeyNotFound) { 131 int i = ContainerHelpers.binarySearch(mKeys, mSize, key); 132 133 if (i < 0 || mValues[i] == DELETED) { 134 return valueIfKeyNotFound; 135 } else { 136 return (E) mValues[i]; 137 } 138 } 139 140 /** 141 * Removes the mapping from the specified key, if there was any. 142 */ delete(int key)143 public void delete(int key) { 144 int i = ContainerHelpers.binarySearch(mKeys, mSize, key); 145 146 if (i >= 0) { 147 if (mValues[i] != DELETED) { 148 mValues[i] = DELETED; 149 mGarbage = true; 150 } 151 } 152 } 153 154 /** 155 * @hide 156 * Removes the mapping from the specified key, if there was any, returning the old value. 157 */ removeReturnOld(int key)158 public E removeReturnOld(int key) { 159 int i = ContainerHelpers.binarySearch(mKeys, mSize, key); 160 161 if (i >= 0) { 162 if (mValues[i] != DELETED) { 163 final E old = (E) mValues[i]; 164 mValues[i] = DELETED; 165 mGarbage = true; 166 return old; 167 } 168 } 169 return null; 170 } 171 172 /** 173 * Alias for {@link #delete(int)}. 174 */ remove(int key)175 public void remove(int key) { 176 delete(key); 177 } 178 179 /** 180 * Removes the mapping at the specified index. 181 * 182 * <p>For indices outside of the range <code>0...size()-1</code>, 183 * the behavior is undefined for apps targeting {@link android.os.Build.VERSION_CODES#P} and 184 * earlier, and an {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting 185 * {@link android.os.Build.VERSION_CODES#Q} and later.</p> 186 */ removeAt(int index)187 public void removeAt(int index) { 188 if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) { 189 // The array might be slightly bigger than mSize, in which case, indexing won't fail. 190 // Check if exception should be thrown outside of the critical path. 191 throw new ArrayIndexOutOfBoundsException(index); 192 } 193 if (mValues[index] != DELETED) { 194 mValues[index] = DELETED; 195 mGarbage = true; 196 } 197 } 198 199 /** 200 * Remove a range of mappings as a batch. 201 * 202 * @param index Index to begin at 203 * @param size Number of mappings to remove 204 * 205 * <p>For indices outside of the range <code>0...size()-1</code>, 206 * the behavior is undefined.</p> 207 */ removeAtRange(int index, int size)208 public void removeAtRange(int index, int size) { 209 final int end = Math.min(mSize, index + size); 210 for (int i = index; i < end; i++) { 211 removeAt(i); 212 } 213 } 214 gc()215 private void gc() { 216 // Log.e("SparseArray", "gc start with " + mSize); 217 218 int n = mSize; 219 int o = 0; 220 int[] keys = mKeys; 221 Object[] values = mValues; 222 223 for (int i = 0; i < n; i++) { 224 Object val = values[i]; 225 226 if (val != DELETED) { 227 if (i != o) { 228 keys[o] = keys[i]; 229 values[o] = val; 230 values[i] = null; 231 } 232 233 o++; 234 } 235 } 236 237 mGarbage = false; 238 mSize = o; 239 240 // Log.e("SparseArray", "gc end with " + mSize); 241 } 242 243 /** 244 * Adds a mapping from the specified key to the specified value, 245 * replacing the previous mapping from the specified key if there 246 * was one. 247 */ put(int key, E value)248 public void put(int key, E value) { 249 int i = ContainerHelpers.binarySearch(mKeys, mSize, key); 250 251 if (i >= 0) { 252 mValues[i] = value; 253 } else { 254 i = ~i; 255 256 if (i < mSize && mValues[i] == DELETED) { 257 mKeys[i] = key; 258 mValues[i] = value; 259 return; 260 } 261 262 if (mGarbage && mSize >= mKeys.length) { 263 gc(); 264 265 // Search again because indices may have changed. 266 i = ~ContainerHelpers.binarySearch(mKeys, mSize, key); 267 } 268 269 mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); 270 mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); 271 mSize++; 272 } 273 } 274 275 /** 276 * Returns the number of key-value mappings that this SparseArray 277 * currently stores. 278 */ size()279 public int size() { 280 if (mGarbage) { 281 gc(); 282 } 283 284 return mSize; 285 } 286 287 /** 288 * Given an index in the range <code>0...size()-1</code>, returns 289 * the key from the <code>index</code>th key-value mapping that this 290 * SparseArray stores. 291 * 292 * <p>The keys corresponding to indices in ascending order are guaranteed to 293 * be in ascending order, e.g., <code>keyAt(0)</code> will return the 294 * smallest key and <code>keyAt(size()-1)</code> will return the largest 295 * key.</p> 296 * 297 * <p>For indices outside of the range <code>0...size()-1</code>, 298 * the behavior is undefined for apps targeting {@link android.os.Build.VERSION_CODES#P} and 299 * earlier, and an {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting 300 * {@link android.os.Build.VERSION_CODES#Q} and later.</p> 301 */ keyAt(int index)302 public int keyAt(int index) { 303 if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) { 304 // The array might be slightly bigger than mSize, in which case, indexing won't fail. 305 // Check if exception should be thrown outside of the critical path. 306 throw new ArrayIndexOutOfBoundsException(index); 307 } 308 if (mGarbage) { 309 gc(); 310 } 311 312 return mKeys[index]; 313 } 314 315 /** 316 * Given an index in the range <code>0...size()-1</code>, returns 317 * the value from the <code>index</code>th key-value mapping that this 318 * SparseArray stores. 319 * 320 * <p>The values corresponding to indices in ascending order are guaranteed 321 * to be associated with keys in ascending order, e.g., 322 * <code>valueAt(0)</code> will return the value associated with the 323 * smallest key and <code>valueAt(size()-1)</code> will return the value 324 * associated with the largest key.</p> 325 * 326 * <p>For indices outside of the range <code>0...size()-1</code>, 327 * the behavior is undefined for apps targeting {@link android.os.Build.VERSION_CODES#P} and 328 * earlier, and an {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting 329 * {@link android.os.Build.VERSION_CODES#Q} and later.</p> 330 */ 331 @SuppressWarnings("unchecked") valueAt(int index)332 public E valueAt(int index) { 333 if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) { 334 // The array might be slightly bigger than mSize, in which case, indexing won't fail. 335 // Check if exception should be thrown outside of the critical path. 336 throw new ArrayIndexOutOfBoundsException(index); 337 } 338 if (mGarbage) { 339 gc(); 340 } 341 342 return (E) mValues[index]; 343 } 344 345 /** 346 * Given an index in the range <code>0...size()-1</code>, sets a new 347 * value for the <code>index</code>th key-value mapping that this 348 * SparseArray stores. 349 * 350 * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for 351 * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an 352 * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting 353 * {@link android.os.Build.VERSION_CODES#Q} and later.</p> 354 */ setValueAt(int index, E value)355 public void setValueAt(int index, E value) { 356 if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) { 357 // The array might be slightly bigger than mSize, in which case, indexing won't fail. 358 // Check if exception should be thrown outside of the critical path. 359 throw new ArrayIndexOutOfBoundsException(index); 360 } 361 if (mGarbage) { 362 gc(); 363 } 364 365 mValues[index] = value; 366 } 367 368 /** 369 * Returns the index for which {@link #keyAt} would return the 370 * specified key, or a negative number if the specified 371 * key is not mapped. 372 */ indexOfKey(int key)373 public int indexOfKey(int key) { 374 if (mGarbage) { 375 gc(); 376 } 377 378 return ContainerHelpers.binarySearch(mKeys, mSize, key); 379 } 380 381 /** 382 * Returns an index for which {@link #valueAt} would return the 383 * specified value, or a negative number if no keys map to the 384 * specified value. 385 * <p>Beware that this is a linear search, unlike lookups by key, 386 * and that multiple keys can map to the same value and this will 387 * find only one of them. 388 * <p>Note also that unlike most collections' {@code indexOf} methods, 389 * this method compares values using {@code ==} rather than {@code equals}. 390 */ indexOfValue(E value)391 public int indexOfValue(E value) { 392 if (mGarbage) { 393 gc(); 394 } 395 396 for (int i = 0; i < mSize; i++) { 397 if (mValues[i] == value) { 398 return i; 399 } 400 } 401 402 return -1; 403 } 404 405 /** 406 * Returns an index for which {@link #valueAt} would return the 407 * specified value, or a negative number if no keys map to the 408 * specified value. 409 * <p>Beware that this is a linear search, unlike lookups by key, 410 * and that multiple keys can map to the same value and this will 411 * find only one of them. 412 * <p>Note also that this method uses {@code equals} unlike {@code indexOfValue}. 413 * @hide 414 */ indexOfValueByValue(E value)415 public int indexOfValueByValue(E value) { 416 if (mGarbage) { 417 gc(); 418 } 419 420 for (int i = 0; i < mSize; i++) { 421 if (value == null) { 422 if (mValues[i] == null) { 423 return i; 424 } 425 } else { 426 if (value.equals(mValues[i])) { 427 return i; 428 } 429 } 430 } 431 return -1; 432 } 433 434 /** 435 * Removes all key-value mappings from this SparseArray. 436 */ clear()437 public void clear() { 438 int n = mSize; 439 Object[] values = mValues; 440 441 for (int i = 0; i < n; i++) { 442 values[i] = null; 443 } 444 445 mSize = 0; 446 mGarbage = false; 447 } 448 449 /** 450 * Puts a key/value pair into the array, optimizing for the case where 451 * the key is greater than all existing keys in the array. 452 */ append(int key, E value)453 public void append(int key, E value) { 454 if (mSize != 0 && key <= mKeys[mSize - 1]) { 455 put(key, value); 456 return; 457 } 458 459 if (mGarbage && mSize >= mKeys.length) { 460 gc(); 461 } 462 463 mKeys = GrowingArrayUtils.append(mKeys, mSize, key); 464 mValues = GrowingArrayUtils.append(mValues, mSize, value); 465 mSize++; 466 } 467 468 /** 469 * {@inheritDoc} 470 * 471 * <p>This implementation composes a string by iterating over its mappings. If 472 * this map contains itself as a value, the string "(this Map)" 473 * will appear in its place. 474 */ 475 @Override toString()476 public String toString() { 477 if (size() <= 0) { 478 return "{}"; 479 } 480 481 StringBuilder buffer = new StringBuilder(mSize * 28); 482 buffer.append('{'); 483 for (int i=0; i<mSize; i++) { 484 if (i > 0) { 485 buffer.append(", "); 486 } 487 int key = keyAt(i); 488 buffer.append(key); 489 buffer.append('='); 490 Object value = valueAt(i); 491 if (value != this) { 492 buffer.append(value); 493 } else { 494 buffer.append("(this Map)"); 495 } 496 } 497 buffer.append('}'); 498 return buffer.toString(); 499 } 500 } 501