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