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 import java.util.ConcurrentModificationException; 20 import java.util.Map; 21 22 /** 23 * Base implementation of {@link ArrayMap} that doesn't include any standard Java 24 * container API interoperability. These features are generally heavier-weight ways 25 * to interact with the container, so discouraged, but they can be useful to make it 26 * easier to use as a drop-in replacement for HashMap. If you don't need them, this 27 * class can be preferrable since it doesn't bring in any of the implementation of those 28 * APIs, allowing that code to be stripped by ProGuard. 29 */ 30 public class SimpleArrayMap<K, V> { 31 private static final boolean DEBUG = false; 32 private static final String TAG = "ArrayMap"; 33 34 /** 35 * Attempt to spot concurrent modifications to this data structure. 36 * 37 * It's best-effort, but any time we can throw something more diagnostic than an 38 * ArrayIndexOutOfBoundsException deep in the ArrayMap internals it's going to 39 * save a lot of development time. 40 * 41 * Good times to look for CME include after any allocArrays() call and at the end of 42 * functions that change mSize (put/remove/clear). 43 */ 44 private static final boolean CONCURRENT_MODIFICATION_EXCEPTIONS = true; 45 46 /** 47 * The minimum amount by which the capacity of a ArrayMap will increase. 48 * This is tuned to be relatively space-efficient. 49 */ 50 private static final int BASE_SIZE = 4; 51 52 /** 53 * Maximum number of entries to have in array caches. 54 */ 55 private static final int CACHE_SIZE = 10; 56 57 /** 58 * Caches of small array objects to avoid spamming garbage. The cache 59 * Object[] variable is a pointer to a linked list of array objects. 60 * The first entry in the array is a pointer to the next array in the 61 * list; the second entry is a pointer to the int[] hash code array for it. 62 */ 63 static Object[] mBaseCache; 64 static int mBaseCacheSize; 65 static Object[] mTwiceBaseCache; 66 static int mTwiceBaseCacheSize; 67 68 int[] mHashes; 69 Object[] mArray; 70 int mSize; 71 binarySearchHashes(int[] hashes, int N, int hash)72 private static int binarySearchHashes(int[] hashes, int N, int hash) { 73 try { 74 return ContainerHelpers.binarySearch(hashes, N, hash); 75 } catch (ArrayIndexOutOfBoundsException e) { 76 if (CONCURRENT_MODIFICATION_EXCEPTIONS) { 77 throw new ConcurrentModificationException(); 78 } else { 79 throw e; // the cache is poisoned at this point, there's not much we can do 80 } 81 } 82 } 83 indexOf(Object key, int hash)84 int indexOf(Object key, int hash) { 85 final int N = mSize; 86 87 // Important fast case: if nothing is in here, nothing to look for. 88 if (N == 0) { 89 return ~0; 90 } 91 92 int index = binarySearchHashes(mHashes, N, hash); 93 94 // If the hash code wasn't found, then we have no entry for this key. 95 if (index < 0) { 96 return index; 97 } 98 99 // If the key at the returned index matches, that's what we want. 100 if (key.equals(mArray[index<<1])) { 101 return index; 102 } 103 104 // Search for a matching key after the index. 105 int end; 106 for (end = index + 1; end < N && mHashes[end] == hash; end++) { 107 if (key.equals(mArray[end << 1])) return end; 108 } 109 110 // Search for a matching key before the index. 111 for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) { 112 if (key.equals(mArray[i << 1])) return i; 113 } 114 115 // Key not found -- return negative value indicating where a 116 // new entry for this key should go. We use the end of the 117 // hash chain to reduce the number of array entries that will 118 // need to be copied when inserting. 119 return ~end; 120 } 121 indexOfNull()122 int indexOfNull() { 123 final int N = mSize; 124 125 // Important fast case: if nothing is in here, nothing to look for. 126 if (N == 0) { 127 return ~0; 128 } 129 130 int index = binarySearchHashes(mHashes, N, 0); 131 132 // If the hash code wasn't found, then we have no entry for this key. 133 if (index < 0) { 134 return index; 135 } 136 137 // If the key at the returned index matches, that's what we want. 138 if (null == mArray[index<<1]) { 139 return index; 140 } 141 142 // Search for a matching key after the index. 143 int end; 144 for (end = index + 1; end < N && mHashes[end] == 0; end++) { 145 if (null == mArray[end << 1]) return end; 146 } 147 148 // Search for a matching key before the index. 149 for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) { 150 if (null == mArray[i << 1]) return i; 151 } 152 153 // Key not found -- return negative value indicating where a 154 // new entry for this key should go. We use the end of the 155 // hash chain to reduce the number of array entries that will 156 // need to be copied when inserting. 157 return ~end; 158 } 159 160 @SuppressWarnings("ArrayToString") allocArrays(final int size)161 private void allocArrays(final int size) { 162 if (size == (BASE_SIZE*2)) { 163 synchronized (ArrayMap.class) { 164 if (mTwiceBaseCache != null) { 165 final Object[] array = mTwiceBaseCache; 166 mArray = array; 167 mTwiceBaseCache = (Object[])array[0]; 168 mHashes = (int[])array[1]; 169 array[0] = array[1] = null; 170 mTwiceBaseCacheSize--; 171 if (DEBUG) System.out.println(TAG + " Retrieving 2x cache " + mHashes 172 + " now have " + mTwiceBaseCacheSize + " entries"); 173 return; 174 } 175 } 176 } else if (size == BASE_SIZE) { 177 synchronized (ArrayMap.class) { 178 if (mBaseCache != null) { 179 final Object[] array = mBaseCache; 180 mArray = array; 181 mBaseCache = (Object[])array[0]; 182 mHashes = (int[])array[1]; 183 array[0] = array[1] = null; 184 mBaseCacheSize--; 185 if (DEBUG) System.out.println(TAG + " Retrieving 1x cache " + mHashes 186 + " now have " + mBaseCacheSize + " entries"); 187 return; 188 } 189 } 190 } 191 192 mHashes = new int[size]; 193 mArray = new Object[size<<1]; 194 } 195 196 @SuppressWarnings("ArrayToString") freeArrays(final int[] hashes, final Object[] array, final int size)197 private static void freeArrays(final int[] hashes, final Object[] array, final int size) { 198 if (hashes.length == (BASE_SIZE*2)) { 199 synchronized (ArrayMap.class) { 200 if (mTwiceBaseCacheSize < CACHE_SIZE) { 201 array[0] = mTwiceBaseCache; 202 array[1] = hashes; 203 for (int i=(size<<1)-1; i>=2; i--) { 204 array[i] = null; 205 } 206 mTwiceBaseCache = array; 207 mTwiceBaseCacheSize++; 208 if (DEBUG) System.out.println(TAG + " Storing 2x cache " + array 209 + " now have " + mTwiceBaseCacheSize + " entries"); 210 } 211 } 212 } else if (hashes.length == BASE_SIZE) { 213 synchronized (ArrayMap.class) { 214 if (mBaseCacheSize < CACHE_SIZE) { 215 array[0] = mBaseCache; 216 array[1] = hashes; 217 for (int i=(size<<1)-1; i>=2; i--) { 218 array[i] = null; 219 } 220 mBaseCache = array; 221 mBaseCacheSize++; 222 if (DEBUG) System.out.println(TAG + " Storing 1x cache " + array 223 + " now have " + mBaseCacheSize + " entries"); 224 } 225 } 226 } 227 } 228 229 /** 230 * Create a new empty ArrayMap. The default capacity of an array map is 0, and 231 * will grow once items are added to it. 232 */ SimpleArrayMap()233 public SimpleArrayMap() { 234 mHashes = ContainerHelpers.EMPTY_INTS; 235 mArray = ContainerHelpers.EMPTY_OBJECTS; 236 mSize = 0; 237 } 238 239 /** 240 * Create a new ArrayMap with a given initial capacity. 241 */ SimpleArrayMap(int capacity)242 public SimpleArrayMap(int capacity) { 243 if (capacity == 0) { 244 mHashes = ContainerHelpers.EMPTY_INTS; 245 mArray = ContainerHelpers.EMPTY_OBJECTS; 246 } else { 247 allocArrays(capacity); 248 } 249 mSize = 0; 250 } 251 252 /** 253 * Create a new ArrayMap with the mappings from the given ArrayMap. 254 */ SimpleArrayMap(SimpleArrayMap<K, V> map)255 public SimpleArrayMap(SimpleArrayMap<K, V> map) { 256 this(); 257 if (map != null) { 258 putAll(map); 259 } 260 } 261 262 /** 263 * Make the array map empty. All storage is released. 264 */ clear()265 public void clear() { 266 if (mSize > 0) { 267 final int[] ohashes = mHashes; 268 final Object[] oarray = mArray; 269 final int osize = mSize; 270 mHashes = ContainerHelpers.EMPTY_INTS; 271 mArray = ContainerHelpers.EMPTY_OBJECTS; 272 mSize = 0; 273 freeArrays(ohashes, oarray, osize); 274 } 275 if (CONCURRENT_MODIFICATION_EXCEPTIONS && mSize > 0) { 276 throw new ConcurrentModificationException(); 277 } 278 } 279 280 /** 281 * Ensure the array map can hold at least <var>minimumCapacity</var> 282 * items. 283 */ ensureCapacity(int minimumCapacity)284 public void ensureCapacity(int minimumCapacity) { 285 final int osize = mSize; 286 if (mHashes.length < minimumCapacity) { 287 final int[] ohashes = mHashes; 288 final Object[] oarray = mArray; 289 allocArrays(minimumCapacity); 290 if (mSize > 0) { 291 System.arraycopy(ohashes, 0, mHashes, 0, osize); 292 System.arraycopy(oarray, 0, mArray, 0, osize<<1); 293 } 294 freeArrays(ohashes, oarray, osize); 295 } 296 if (CONCURRENT_MODIFICATION_EXCEPTIONS && mSize != osize) { 297 throw new ConcurrentModificationException(); 298 } 299 } 300 301 /** 302 * Check whether a key exists in the array. 303 * 304 * @param key The key to search for. 305 * @return Returns true if the key exists, else false. 306 */ containsKey(Object key)307 public boolean containsKey(Object key) { 308 return indexOfKey(key) >= 0; 309 } 310 311 /** 312 * Returns the index of a key in the set. 313 * 314 * @param key The key to search for. 315 * @return Returns the index of the key if it exists, else a negative integer. 316 */ indexOfKey(Object key)317 public int indexOfKey(Object key) { 318 return key == null ? indexOfNull() : indexOf(key, key.hashCode()); 319 } 320 indexOfValue(Object value)321 int indexOfValue(Object value) { 322 final int N = mSize*2; 323 final Object[] array = mArray; 324 if (value == null) { 325 for (int i=1; i<N; i+=2) { 326 if (array[i] == null) { 327 return i>>1; 328 } 329 } 330 } else { 331 for (int i=1; i<N; i+=2) { 332 if (value.equals(array[i])) { 333 return i>>1; 334 } 335 } 336 } 337 return -1; 338 } 339 340 /** 341 * Check whether a value exists in the array. This requires a linear search 342 * through the entire array. 343 * 344 * @param value The value to search for. 345 * @return Returns true if the value exists, else false. 346 */ containsValue(Object value)347 public boolean containsValue(Object value) { 348 return indexOfValue(value) >= 0; 349 } 350 351 /** 352 * Retrieve a value from the array. 353 * @param key The key of the value to retrieve. 354 * @return Returns the value associated with the given key, 355 * or null if there is no such key. 356 */ get(Object key)357 public V get(Object key) { 358 final int index = indexOfKey(key); 359 return index >= 0 ? (V)mArray[(index<<1)+1] : null; 360 } 361 362 /** 363 * Return the key at the given index in the array. 364 * @param index The desired index, must be between 0 and {@link #size()}-1. 365 * @return Returns the key stored at the given index. 366 */ keyAt(int index)367 public K keyAt(int index) { 368 return (K)mArray[index << 1]; 369 } 370 371 /** 372 * Return the value at the given index in the array. 373 * @param index The desired index, must be between 0 and {@link #size()}-1. 374 * @return Returns the value stored at the given index. 375 */ valueAt(int index)376 public V valueAt(int index) { 377 return (V)mArray[(index << 1) + 1]; 378 } 379 380 /** 381 * Set the value at a given index in the array. 382 * @param index The desired index, must be between 0 and {@link #size()}-1. 383 * @param value The new value to store at this index. 384 * @return Returns the previous value at the given index. 385 */ setValueAt(int index, V value)386 public V setValueAt(int index, V value) { 387 index = (index << 1) + 1; 388 V old = (V)mArray[index]; 389 mArray[index] = value; 390 return old; 391 } 392 393 /** 394 * Return true if the array map contains no items. 395 */ isEmpty()396 public boolean isEmpty() { 397 return mSize <= 0; 398 } 399 400 /** 401 * Add a new value to the array map. 402 * @param key The key under which to store the value. <b>Must not be null.</b> If 403 * this key already exists in the array, its value will be replaced. 404 * @param value The value to store for the given key. 405 * @return Returns the old value that was stored for the given key, or null if there 406 * was no such key. 407 */ put(K key, V value)408 public V put(K key, V value) { 409 final int osize = mSize; 410 final int hash; 411 int index; 412 if (key == null) { 413 hash = 0; 414 index = indexOfNull(); 415 } else { 416 hash = key.hashCode(); 417 index = indexOf(key, hash); 418 } 419 if (index >= 0) { 420 index = (index<<1) + 1; 421 final V old = (V)mArray[index]; 422 mArray[index] = value; 423 return old; 424 } 425 426 index = ~index; 427 if (osize >= mHashes.length) { 428 final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1)) 429 : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE); 430 431 if (DEBUG) System.out.println(TAG + " put: grow from " + mHashes.length + " to " + n); 432 433 final int[] ohashes = mHashes; 434 final Object[] oarray = mArray; 435 allocArrays(n); 436 437 if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) { 438 throw new ConcurrentModificationException(); 439 } 440 441 if (mHashes.length > 0) { 442 if (DEBUG) System.out.println(TAG + " put: copy 0-" + osize + " to 0"); 443 System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length); 444 System.arraycopy(oarray, 0, mArray, 0, oarray.length); 445 } 446 447 freeArrays(ohashes, oarray, osize); 448 } 449 450 if (index < osize) { 451 if (DEBUG) System.out.println(TAG + " put: move " + index + "-" + (osize-index) 452 + " to " + (index+1)); 453 System.arraycopy(mHashes, index, mHashes, index + 1, osize - index); 454 System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1); 455 } 456 457 if (CONCURRENT_MODIFICATION_EXCEPTIONS) { 458 if (osize != mSize || index >= mHashes.length) { 459 throw new ConcurrentModificationException(); 460 } 461 } 462 463 mHashes[index] = hash; 464 mArray[index<<1] = key; 465 mArray[(index<<1)+1] = value; 466 mSize++; 467 return null; 468 } 469 470 /** 471 * Perform a {@link #put(Object, Object)} of all key/value pairs in <var>array</var> 472 * @param array The array whose contents are to be retrieved. 473 */ putAll(SimpleArrayMap<? extends K, ? extends V> array)474 public void putAll(SimpleArrayMap<? extends K, ? extends V> array) { 475 final int N = array.mSize; 476 ensureCapacity(mSize + N); 477 if (mSize == 0) { 478 if (N > 0) { 479 System.arraycopy(array.mHashes, 0, mHashes, 0, N); 480 System.arraycopy(array.mArray, 0, mArray, 0, N<<1); 481 mSize = N; 482 } 483 } else { 484 for (int i=0; i<N; i++) { 485 put(array.keyAt(i), array.valueAt(i)); 486 } 487 } 488 } 489 490 /** 491 * Remove an existing key from the array map. 492 * @param key The key of the mapping to remove. 493 * @return Returns the value that was stored under the key, or null if there 494 * was no such key. 495 */ remove(Object key)496 public V remove(Object key) { 497 final int index = indexOfKey(key); 498 if (index >= 0) { 499 return removeAt(index); 500 } 501 502 return null; 503 } 504 505 /** 506 * Remove the key/value mapping at the given index. 507 * @param index The desired index, must be between 0 and {@link #size()}-1. 508 * @return Returns the value that was stored at this index. 509 */ removeAt(int index)510 public V removeAt(int index) { 511 final Object old = mArray[(index << 1) + 1]; 512 final int osize = mSize; 513 final int nsize; 514 if (osize <= 1) { 515 // Now empty. 516 if (DEBUG) System.out.println(TAG + " remove: shrink from " + mHashes.length + " to 0"); 517 freeArrays(mHashes, mArray, osize); 518 mHashes = ContainerHelpers.EMPTY_INTS; 519 mArray = ContainerHelpers.EMPTY_OBJECTS; 520 nsize = 0; 521 } else { 522 nsize = osize - 1; 523 if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) { 524 // Shrunk enough to reduce size of arrays. We don't allow it to 525 // shrink smaller than (BASE_SIZE*2) to avoid flapping between 526 // that and BASE_SIZE. 527 final int n = osize > (BASE_SIZE*2) ? (osize + (osize>>1)) : (BASE_SIZE*2); 528 529 if (DEBUG) System.out.println(TAG + " remove: shrink from " + mHashes.length + " to " + n); 530 531 final int[] ohashes = mHashes; 532 final Object[] oarray = mArray; 533 allocArrays(n); 534 535 if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) { 536 throw new ConcurrentModificationException(); 537 } 538 539 if (index > 0) { 540 if (DEBUG) System.out.println(TAG + " remove: copy from 0-" + index + " to 0"); 541 System.arraycopy(ohashes, 0, mHashes, 0, index); 542 System.arraycopy(oarray, 0, mArray, 0, index << 1); 543 } 544 if (index < nsize) { 545 if (DEBUG) System.out.println(TAG + " remove: copy from " + (index+1) + "-" + nsize 546 + " to " + index); 547 System.arraycopy(ohashes, index + 1, mHashes, index, nsize - index); 548 System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1, 549 (nsize - index) << 1); 550 } 551 } else { 552 if (index < nsize) { 553 if (DEBUG) System.out.println(TAG + " remove: move " + (index+1) + "-" + nsize 554 + " to " + index); 555 System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index); 556 System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1, 557 (nsize - index) << 1); 558 } 559 mArray[nsize << 1] = null; 560 mArray[(nsize << 1) + 1] = null; 561 } 562 } 563 if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) { 564 throw new ConcurrentModificationException(); 565 } 566 mSize = nsize; 567 return (V)old; 568 } 569 570 /** 571 * Return the number of items in this array map. 572 */ size()573 public int size() { 574 return mSize; 575 } 576 577 /** 578 * {@inheritDoc} 579 * 580 * <p>This implementation returns false if the object is not a Map or 581 * SimpleArrayMap, or if the maps have different sizes. Otherwise, for each 582 * key in this map, values of both maps are compared. If the values for any 583 * key are not equal, the method returns false, otherwise it returns true. 584 */ 585 @Override equals(Object object)586 public boolean equals(Object object) { 587 if (this == object) { 588 return true; 589 } 590 if (object instanceof SimpleArrayMap) { 591 SimpleArrayMap<?, ?> map = (SimpleArrayMap<?, ?>) object; 592 if (size() != map.size()) { 593 return false; 594 } 595 596 try { 597 for (int i=0; i<mSize; i++) { 598 K key = keyAt(i); 599 V mine = valueAt(i); 600 Object theirs = map.get(key); 601 if (mine == null) { 602 if (theirs != null || !map.containsKey(key)) { 603 return false; 604 } 605 } else if (!mine.equals(theirs)) { 606 return false; 607 } 608 } 609 } catch (NullPointerException ignored) { 610 return false; 611 } catch (ClassCastException ignored) { 612 return false; 613 } 614 return true; 615 } else if (object instanceof Map) { 616 Map<?, ?> map = (Map<?, ?>) object; 617 if (size() != map.size()) { 618 return false; 619 } 620 621 try { 622 for (int i=0; i<mSize; i++) { 623 K key = keyAt(i); 624 V mine = valueAt(i); 625 Object theirs = map.get(key); 626 if (mine == null) { 627 if (theirs != null || !map.containsKey(key)) { 628 return false; 629 } 630 } else if (!mine.equals(theirs)) { 631 return false; 632 } 633 } 634 } catch (NullPointerException ignored) { 635 return false; 636 } catch (ClassCastException ignored) { 637 return false; 638 } 639 return true; 640 } 641 return false; 642 } 643 644 /** 645 * {@inheritDoc} 646 */ 647 @Override hashCode()648 public int hashCode() { 649 final int[] hashes = mHashes; 650 final Object[] array = mArray; 651 int result = 0; 652 for (int i = 0, v = 1, s = mSize; i < s; i++, v+=2) { 653 Object value = array[v]; 654 result += hashes[i] ^ (value == null ? 0 : value.hashCode()); 655 } 656 return result; 657 } 658 659 /** 660 * {@inheritDoc} 661 * 662 * <p>This implementation composes a string by iterating over its mappings. If 663 * this map contains itself as a key or a value, the string "(this Map)" 664 * will appear in its place. 665 */ 666 @Override toString()667 public String toString() { 668 if (isEmpty()) { 669 return "{}"; 670 } 671 672 StringBuilder buffer = new StringBuilder(mSize * 28); 673 buffer.append('{'); 674 for (int i=0; i<mSize; i++) { 675 if (i > 0) { 676 buffer.append(", "); 677 } 678 Object key = keyAt(i); 679 if (key != this) { 680 buffer.append(key); 681 } else { 682 buffer.append("(this Map)"); 683 } 684 buffer.append('='); 685 Object value = valueAt(i); 686 if (value != this) { 687 buffer.append(value); 688 } else { 689 buffer.append("(this Map)"); 690 } 691 } 692 buffer.append('}'); 693 return buffer.toString(); 694 } 695 } 696