android.util.ArrayMap Maven / Gradle / Ivy
Show all versions of android-all Show documentation
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.util;
import libcore.util.EmptyArray;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Map;
import java.util.Set;
/**
* ArrayMap is a generic key->value mapping data structure that is
* designed to be more memory efficient than a traditional {@link java.util.HashMap}.
* It keeps its mappings in an array data structure -- an integer array of hash
* codes for each item, and an Object array of the key/value pairs. This allows it to
* avoid having to create an extra object for every entry put in to the map, and it
* also tries to control the growth of the size of these arrays more aggressively
* (since growing them only requires copying the entries in the array, not rebuilding
* a hash map).
*
* Note that this implementation is not intended to be appropriate for data structures
* that may contain large numbers of items. It is generally slower than a traditional
* HashMap, since lookups require a binary search and adds and removes require inserting
* and deleting entries in the array. For containers holding up to hundreds of items,
* the performance difference is not significant, less than 50%.
*
* Because this container is intended to better balance memory use, unlike most other
* standard Java containers it will shrink its array as items are removed from it. Currently
* you have no control over this shrinking -- if you set a capacity and then remove an
* item, it may reduce the capacity to better match the current size. In the future an
* explicit call to set the capacity should turn off this aggressive shrinking behavior.
*/
public final class ArrayMap implements Map {
private static final boolean DEBUG = false;
private static final String TAG = "ArrayMap";
/**
* Attempt to spot concurrent modifications to this data structure.
*
* It's best-effort, but any time we can throw something more diagnostic than an
* ArrayIndexOutOfBoundsException deep in the ArrayMap internals it's going to
* save a lot of development time.
*
* Good times to look for CME include after any allocArrays() call and at the end of
* functions that change mSize (put/remove/clear).
*/
private static final boolean CONCURRENT_MODIFICATION_EXCEPTIONS = true;
/**
* The minimum amount by which the capacity of a ArrayMap will increase.
* This is tuned to be relatively space-efficient.
*/
private static final int BASE_SIZE = 4;
/**
* Maximum number of entries to have in array caches.
*/
private static final int CACHE_SIZE = 10;
/**
* Special hash array value that indicates the container is immutable.
*/
static final int[] EMPTY_IMMUTABLE_INTS = new int[0];
/**
* @hide Special immutable empty ArrayMap.
*/
public static final ArrayMap EMPTY = new ArrayMap<>(-1);
/**
* Caches of small array objects to avoid spamming garbage. The cache
* Object[] variable is a pointer to a linked list of array objects.
* The first entry in the array is a pointer to the next array in the
* list; the second entry is a pointer to the int[] hash code array for it.
*/
static Object[] mBaseCache;
static int mBaseCacheSize;
static Object[] mTwiceBaseCache;
static int mTwiceBaseCacheSize;
final boolean mIdentityHashCode;
int[] mHashes;
Object[] mArray;
int mSize;
MapCollections mCollections;
private static int binarySearchHashes(int[] hashes, int N, int hash) {
try {
return ContainerHelpers.binarySearch(hashes, N, hash);
} catch (ArrayIndexOutOfBoundsException e) {
if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
throw new ConcurrentModificationException();
} else {
throw e; // the cache is poisoned at this point, there's not much we can do
}
}
}
int indexOf(Object key, int hash) {
final int N = mSize;
// Important fast case: if nothing is in here, nothing to look for.
if (N == 0) {
return ~0;
}
int index = binarySearchHashes(mHashes, N, hash);
// If the hash code wasn't found, then we have no entry for this key.
if (index < 0) {
return index;
}
// If the key at the returned index matches, that's what we want.
if (key.equals(mArray[index<<1])) {
return index;
}
// Search for a matching key after the index.
int end;
for (end = index + 1; end < N && mHashes[end] == hash; end++) {
if (key.equals(mArray[end << 1])) return end;
}
// Search for a matching key before the index.
for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
if (key.equals(mArray[i << 1])) return i;
}
// Key not found -- return negative value indicating where a
// new entry for this key should go. We use the end of the
// hash chain to reduce the number of array entries that will
// need to be copied when inserting.
return ~end;
}
int indexOfNull() {
final int N = mSize;
// Important fast case: if nothing is in here, nothing to look for.
if (N == 0) {
return ~0;
}
int index = binarySearchHashes(mHashes, N, 0);
// If the hash code wasn't found, then we have no entry for this key.
if (index < 0) {
return index;
}
// If the key at the returned index matches, that's what we want.
if (null == mArray[index<<1]) {
return index;
}
// Search for a matching key after the index.
int end;
for (end = index + 1; end < N && mHashes[end] == 0; end++) {
if (null == mArray[end << 1]) return end;
}
// Search for a matching key before the index.
for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) {
if (null == mArray[i << 1]) return i;
}
// Key not found -- return negative value indicating where a
// new entry for this key should go. We use the end of the
// hash chain to reduce the number of array entries that will
// need to be copied when inserting.
return ~end;
}
private void allocArrays(final int size) {
if (mHashes == EMPTY_IMMUTABLE_INTS) {
throw new UnsupportedOperationException("ArrayMap is immutable");
}
if (size == (BASE_SIZE*2)) {
synchronized (ArrayMap.class) {
if (mTwiceBaseCache != null) {
final Object[] array = mTwiceBaseCache;
mArray = array;
mTwiceBaseCache = (Object[])array[0];
mHashes = (int[])array[1];
array[0] = array[1] = null;
mTwiceBaseCacheSize--;
if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes
+ " now have " + mTwiceBaseCacheSize + " entries");
return;
}
}
} else if (size == BASE_SIZE) {
synchronized (ArrayMap.class) {
if (mBaseCache != null) {
final Object[] array = mBaseCache;
mArray = array;
mBaseCache = (Object[])array[0];
mHashes = (int[])array[1];
array[0] = array[1] = null;
mBaseCacheSize--;
if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes
+ " now have " + mBaseCacheSize + " entries");
return;
}
}
}
mHashes = new int[size];
mArray = new Object[size<<1];
}
private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
if (hashes.length == (BASE_SIZE*2)) {
synchronized (ArrayMap.class) {
if (mTwiceBaseCacheSize < CACHE_SIZE) {
array[0] = mTwiceBaseCache;
array[1] = hashes;
for (int i=(size<<1)-1; i>=2; i--) {
array[i] = null;
}
mTwiceBaseCache = array;
mTwiceBaseCacheSize++;
if (DEBUG) Log.d(TAG, "Storing 2x cache " + array
+ " now have " + mTwiceBaseCacheSize + " entries");
}
}
} else if (hashes.length == BASE_SIZE) {
synchronized (ArrayMap.class) {
if (mBaseCacheSize < CACHE_SIZE) {
array[0] = mBaseCache;
array[1] = hashes;
for (int i=(size<<1)-1; i>=2; i--) {
array[i] = null;
}
mBaseCache = array;
mBaseCacheSize++;
if (DEBUG) Log.d(TAG, "Storing 1x cache " + array
+ " now have " + mBaseCacheSize + " entries");
}
}
}
}
/**
* Create a new empty ArrayMap. The default capacity of an array map is 0, and
* will grow once items are added to it.
*/
public ArrayMap() {
this(0, false);
}
/**
* Create a new ArrayMap with a given initial capacity.
*/
public ArrayMap(int capacity) {
this(capacity, false);
}
/** {@hide} */
public ArrayMap(int capacity, boolean identityHashCode) {
mIdentityHashCode = identityHashCode;
// If this is immutable, use the sentinal EMPTY_IMMUTABLE_INTS
// instance instead of the usual EmptyArray.INT. The reference
// is checked later to see if the array is allowed to grow.
if (capacity < 0) {
mHashes = EMPTY_IMMUTABLE_INTS;
mArray = EmptyArray.OBJECT;
} else if (capacity == 0) {
mHashes = EmptyArray.INT;
mArray = EmptyArray.OBJECT;
} else {
allocArrays(capacity);
}
mSize = 0;
}
/**
* Create a new ArrayMap with the mappings from the given ArrayMap.
*/
public ArrayMap(ArrayMap map) {
this();
if (map != null) {
putAll(map);
}
}
/**
* Make the array map empty. All storage is released.
*/
@Override
public void clear() {
if (mSize > 0) {
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
final int osize = mSize;
mHashes = EmptyArray.INT;
mArray = EmptyArray.OBJECT;
mSize = 0;
freeArrays(ohashes, oarray, osize);
}
if (CONCURRENT_MODIFICATION_EXCEPTIONS && mSize > 0) {
throw new ConcurrentModificationException();
}
}
/**
* @hide
* Like {@link #clear}, but doesn't reduce the capacity of the ArrayMap.
*/
public void erase() {
if (mSize > 0) {
final int N = mSize<<1;
final Object[] array = mArray;
for (int i=0; iminimumCapacity
* items.
*/
public void ensureCapacity(int minimumCapacity) {
final int osize = mSize;
if (mHashes.length < minimumCapacity) {
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
allocArrays(minimumCapacity);
if (mSize > 0) {
System.arraycopy(ohashes, 0, mHashes, 0, osize);
System.arraycopy(oarray, 0, mArray, 0, osize<<1);
}
freeArrays(ohashes, oarray, osize);
}
if (CONCURRENT_MODIFICATION_EXCEPTIONS && mSize != osize) {
throw new ConcurrentModificationException();
}
}
/**
* Check whether a key exists in the array.
*
* @param key The key to search for.
* @return Returns true if the key exists, else false.
*/
@Override
public boolean containsKey(Object key) {
return indexOfKey(key) >= 0;
}
/**
* Returns the index of a key in the set.
*
* @param key The key to search for.
* @return Returns the index of the key if it exists, else a negative integer.
*/
public int indexOfKey(Object key) {
return key == null ? indexOfNull()
: indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
}
int indexOfValue(Object value) {
final int N = mSize*2;
final Object[] array = mArray;
if (value == null) {
for (int i=1; i>1;
}
}
} else {
for (int i=1; i>1;
}
}
}
return -1;
}
/**
* Check whether a value exists in the array. This requires a linear search
* through the entire array.
*
* @param value The value to search for.
* @return Returns true if the value exists, else false.
*/
@Override
public boolean containsValue(Object value) {
return indexOfValue(value) >= 0;
}
/**
* Retrieve a value from the array.
* @param key The key of the value to retrieve.
* @return Returns the value associated with the given key,
* or null if there is no such key.
*/
@Override
public V get(Object key) {
final int index = indexOfKey(key);
return index >= 0 ? (V)mArray[(index<<1)+1] : null;
}
/**
* Return the key at the given index in the array.
* @param index The desired index, must be between 0 and {@link #size()}-1.
* @return Returns the key stored at the given index.
*/
public K keyAt(int index) {
return (K)mArray[index << 1];
}
/**
* Return the value at the given index in the array.
* @param index The desired index, must be between 0 and {@link #size()}-1.
* @return Returns the value stored at the given index.
*/
public V valueAt(int index) {
return (V)mArray[(index << 1) + 1];
}
/**
* Set the value at a given index in the array.
* @param index The desired index, must be between 0 and {@link #size()}-1.
* @param value The new value to store at this index.
* @return Returns the previous value at the given index.
*/
public V setValueAt(int index, V value) {
index = (index << 1) + 1;
V old = (V)mArray[index];
mArray[index] = value;
return old;
}
/**
* Return true if the array map contains no items.
*/
@Override
public boolean isEmpty() {
return mSize <= 0;
}
/**
* Add a new value to the array map.
* @param key The key under which to store the value. If
* this key already exists in the array, its value will be replaced.
* @param value The value to store for the given key.
* @return Returns the old value that was stored for the given key, or null if there
* was no such key.
*/
@Override
public V put(K key, V value) {
final int osize = mSize;
final int hash;
int index;
if (key == null) {
hash = 0;
index = indexOfNull();
} else {
hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
index = indexOf(key, hash);
}
if (index >= 0) {
index = (index<<1) + 1;
final V old = (V)mArray[index];
mArray[index] = value;
return old;
}
index = ~index;
if (osize >= mHashes.length) {
final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
: (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
allocArrays(n);
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
if (mHashes.length > 0) {
if (DEBUG) Log.d(TAG, "put: copy 0-" + osize + " to 0");
System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
System.arraycopy(oarray, 0, mArray, 0, oarray.length);
}
freeArrays(ohashes, oarray, osize);
}
if (index < osize) {
if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (osize-index)
+ " to " + (index+1));
System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
}
if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
if (osize != mSize || index >= mHashes.length) {
throw new ConcurrentModificationException();
}
}
mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
mSize++;
return null;
}
/**
* Special fast path for appending items to the end of the array without validation.
* The array must already be large enough to contain the item.
* @hide
*/
public void append(K key, V value) {
int index = mSize;
final int hash = key == null ? 0
: (mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
if (index >= mHashes.length) {
throw new IllegalStateException("Array is full");
}
if (index > 0 && mHashes[index-1] > hash) {
RuntimeException e = new RuntimeException("here");
e.fillInStackTrace();
Log.w(TAG, "New hash " + hash
+ " is before end of array hash " + mHashes[index-1]
+ " at index " + index + " key " + key, e);
put(key, value);
return;
}
mSize = index+1;
mHashes[index] = hash;
index <<= 1;
mArray[index] = key;
mArray[index+1] = value;
}
/**
* The use of the {@link #append} function can result in invalid array maps, in particular
* an array map where the same key appears multiple times. This function verifies that
* the array map is valid, throwing IllegalArgumentException if a problem is found. The
* main use for this method is validating an array map after unpacking from an IPC, to
* protect against malicious callers.
* @hide
*/
public void validate() {
final int N = mSize;
if (N <= 1) {
// There can't be dups.
return;
}
int basehash = mHashes[0];
int basei = 0;
for (int i=1; i=basei; j--) {
final Object prev = mArray[j<<1];
if (cur == prev) {
throw new IllegalArgumentException("Duplicate key in ArrayMap: " + cur);
}
if (cur != null && prev != null && cur.equals(prev)) {
throw new IllegalArgumentException("Duplicate key in ArrayMap: " + cur);
}
}
}
}
/**
* Perform a {@link #put(Object, Object)} of all key/value pairs in array
* @param array The array whose contents are to be retrieved.
*/
public void putAll(ArrayMap array) {
final int N = array.mSize;
ensureCapacity(mSize + N);
if (mSize == 0) {
if (N > 0) {
System.arraycopy(array.mHashes, 0, mHashes, 0, N);
System.arraycopy(array.mArray, 0, mArray, 0, N<<1);
mSize = N;
}
} else {
for (int i=0; i= 0) {
return removeAt(index);
}
return null;
}
/**
* Remove the key/value mapping at the given index.
* @param index The desired index, must be between 0 and {@link #size()}-1.
* @return Returns the value that was stored at this index.
*/
public V removeAt(int index) {
final Object old = mArray[(index << 1) + 1];
final int osize = mSize;
final int nsize;
if (osize <= 1) {
// Now empty.
if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0");
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
mHashes = EmptyArray.INT;
mArray = EmptyArray.OBJECT;
freeArrays(ohashes, oarray, osize);
nsize = 0;
} else {
nsize = osize - 1;
if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
// Shrunk enough to reduce size of arrays. We don't allow it to
// shrink smaller than (BASE_SIZE*2) to avoid flapping between
// that and BASE_SIZE.
final int n = osize > (BASE_SIZE*2) ? (osize + (osize>>1)) : (BASE_SIZE*2);
if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n);
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
allocArrays(n);
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
if (index > 0) {
if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0");
System.arraycopy(ohashes, 0, mHashes, 0, index);
System.arraycopy(oarray, 0, mArray, 0, index << 1);
}
if (index < nsize) {
if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + nsize
+ " to " + index);
System.arraycopy(ohashes, index + 1, mHashes, index, nsize - index);
System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,
(nsize - index) << 1);
}
} else {
if (index < nsize) {
if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + nsize
+ " to " + index);
System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
(nsize - index) << 1);
}
mArray[nsize << 1] = null;
mArray[(nsize << 1) + 1] = null;
}
}
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
mSize = nsize;
return (V)old;
}
/**
* Return the number of items in this array map.
*/
@Override
public int size() {
return mSize;
}
/**
* {@inheritDoc}
*
* This implementation returns false if the object is not a map, or
* if the maps have different sizes. Otherwise, for each key in this map,
* values of both maps are compared. If the values for any key are not
* equal, the method returns false, otherwise it returns true.
*/
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object instanceof Map) {
Map map = (Map) object;
if (size() != map.size()) {
return false;
}
try {
for (int i=0; iThis implementation composes a string by iterating over its mappings. If
* this map contains itself as a key or a value, the string "(this Map)"
* will appear in its place.
*/
@Override
public String toString() {
if (isEmpty()) {
return "{}";
}
StringBuilder buffer = new StringBuilder(mSize * 28);
buffer.append('{');
for (int i=0; i 0) {
buffer.append(", ");
}
Object key = keyAt(i);
if (key != this) {
buffer.append(key);
} else {
buffer.append("(this Map)");
}
buffer.append('=');
Object value = valueAt(i);
if (value != this) {
buffer.append(value);
} else {
buffer.append("(this Map)");
}
}
buffer.append('}');
return buffer.toString();
}
// ------------------------------------------------------------------------
// Interop with traditional Java containers. Not as efficient as using
// specialized collection APIs.
// ------------------------------------------------------------------------
private MapCollections getCollection() {
if (mCollections == null) {
mCollections = new MapCollections() {
@Override
protected int colGetSize() {
return mSize;
}
@Override
protected Object colGetEntry(int index, int offset) {
return mArray[(index<<1) + offset];
}
@Override
protected int colIndexOfKey(Object key) {
return indexOfKey(key);
}
@Override
protected int colIndexOfValue(Object value) {
return indexOfValue(value);
}
@Override
protected Map colGetMap() {
return ArrayMap.this;
}
@Override
protected void colPut(K key, V value) {
put(key, value);
}
@Override
protected V colSetValue(int index, V value) {
return setValueAt(index, value);
}
@Override
protected void colRemoveAt(int index) {
removeAt(index);
}
@Override
protected void colClear() {
clear();
}
};
}
return mCollections;
}
/**
* Determine if the array map contains all of the keys in the given collection.
* @param collection The collection whose contents are to be checked against.
* @return Returns true if this array map contains a key for every entry
* in collection, else returns false.
*/
public boolean containsAll(Collection collection) {
return MapCollections.containsAllHelper(this, collection);
}
/**
* Perform a {@link #put(Object, Object)} of all key/value pairs in map
* @param map The map whose contents are to be retrieved.
*/
@Override
public void putAll(Map map) {
ensureCapacity(mSize + map.size());
for (Map.Entry entry : map.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
/**
* Remove all keys in the array map that exist in the given collection.
* @param collection The collection whose contents are to be used to remove keys.
* @return Returns true if any keys were removed from the array map, else false.
*/
public boolean removeAll(Collection collection) {
return MapCollections.removeAllHelper(this, collection);
}
/**
* Remove all keys in the array map that do not exist in the given collection.
* @param collection The collection whose contents are to be used to determine which
* keys to keep.
* @return Returns true if any keys were removed from the array map, else false.
*/
public boolean retainAll(Collection collection) {
return MapCollections.retainAllHelper(this, collection);
}
/**
* Return a {@link java.util.Set} for iterating over and interacting with all mappings
* in the array map.
*
* Note: this is a very inefficient way to access the array contents, it
* requires generating a number of temporary objects and allocates additional state
* information associated with the container that will remain for the life of the container.
*
* Note:
the semantics of this
* Set are subtly different than that of a {@link java.util.HashMap}: most important,
* the {@link java.util.Map.Entry Map.Entry} object returned by its iterator is a single
* object that exists for the entire iterator, so you can not hold on to it
* after calling {@link java.util.Iterator#next() Iterator.next}.
*/
@Override
public Set> entrySet() {
return getCollection().getEntrySet();
}
/**
* Return a {@link java.util.Set} for iterating over and interacting with all keys
* in the array map.
*
* Note: this is a fairly inefficient way to access the array contents, it
* requires generating a number of temporary objects and allocates additional state
* information associated with the container that will remain for the life of the container.
*/
@Override
public Set keySet() {
return getCollection().getKeySet();
}
/**
* Return a {@link java.util.Collection} for iterating over and interacting with all values
* in the array map.
*
* Note: this is a fairly inefficient way to access the array contents, it
* requires generating a number of temporary objects and allocates additional state
* information associated with the container that will remain for the life of the container.
*/
@Override
public Collection values() {
return getCollection().getValues();
}
}