Memory-efficient map of keys to values with list-style random-access semantics.
/*
* Copyright (c) 2010 Google Inc.
*
* 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 com.google.api.client.util;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
/**
* Memory-efficient map of keys to values with list-style random-access semantics.
* <p>
* Conceptually, the keys and values are stored in a simpler array in order to minimize memory use
* and provide for fast access to a key/value at a certain index (for example {@link #getKey(int)}).
* However, traditional mapping operations like {@link #get(Object)} and
* {@link #put(Object, Object)} are slower because they need to look up all key/value pairs in the
* worst case.
*
* @since 1.0
* @author Yaniv Inbar
*/
public class ArrayMap<K, V> extends AbstractMap<K, V> implements Cloneable {
int size;
private Object[] data;
private EntrySet entrySet;
/**
* Returns a new instance of an array map with initial capacity of zero. Equivalent to calling the
* default constructor, except without the need to specify the type parameters. For example:
* {@code ArrayMap<String, String> map = ArrayMap.create();}.
*/
public static <K, V> ArrayMap<K, V> create() {
return new ArrayMap<K, V>();
}
/**
* Returns a new instance of an array map of the given initial capacity. For example: {@code
* ArrayMap<String, String> map = ArrayMap.create(8);}.
*/
public static <K, V> ArrayMap<K, V> create(int initialCapacity) {
ArrayMap<K, V> result = create();
result.ensureCapacity(initialCapacity);
return result;
}
/**
* Returns a new instance of an array map of the given key value pairs in alternating order. For
* example: {@code ArrayMap<String, String> map = ArrayMap.of("key1", "value1", "key2", "value2",
* ...);}.
* <p>
* WARNING: there is no compile-time checking of the {@code keyValuePairs} parameter to ensure
* that the keys or values have the correct type, so if the wrong type is passed in, any problems
* will occur at runtime. Also, there is no checking that the keys are unique, which the caller
* must ensure is true.
*/
public static <K, V> ArrayMap<K, V> of(Object... keyValuePairs) {
ArrayMap<K, V> result = create(1);
int length = keyValuePairs.length;
if (1 == length % 2) {
throw new IllegalArgumentException(
"missing value for last key: " + keyValuePairs[length - 1]);
}
result.size = keyValuePairs.length / 2;
Object[] data = result.data = new Object[length];
System.arraycopy(keyValuePairs, 0, data, 0, length);
return result;
}
/** Returns the number of key-value pairs set. */
@Override
public final int size() {
return this.size;
}
/** Returns the key at the given index or {@code null} if out of bounds. */
public final K getKey(int index) {
if (index < 0 || index >= this.size) {
return null;
}
@SuppressWarnings("unchecked")
K result = (K) this.data[index << 1];
return result;
}
/** Returns the value at the given index or {@code null} if out of bounds. */
public final V getValue(int index) {
if (index < 0 || index >= this.size) {
return null;
}
return valueAtDataIndex(1 + (index << 1));
}
/**
* Sets the key/value mapping at the given index, overriding any existing key/value mapping.
* <p>
* There is no checking done to ensure that the key does not already exist. Therefore, this method
* is dangerous to call unless the caller can be certain the key does not already exist in the
* map.
*
* @return previous value or {@code null} for none
* @throws IndexOutOfBoundsException if index is negative
*/
public final V set(int index, K key, V value) {
if (index < 0) {
throw new IndexOutOfBoundsException();
}
int minSize = index + 1;
ensureCapacity(minSize);
int dataIndex = index << 1;
V result = valueAtDataIndex(dataIndex + 1);
setData(dataIndex, key, value);
if (minSize > this.size) {
this.size = minSize;
}
return result;
}
/**
* Sets the value at the given index, overriding any existing value mapping.
*
* @return previous value or {@code null} for none
* @throws IndexOutOfBoundsException if index is negative or {@code >=} size
*/
public final V set(int index, V value) {
int size = this.size;
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
int valueDataIndex = 1 + (index << 1);
V result = valueAtDataIndex(valueDataIndex);
this.data[valueDataIndex] = value;
return result;
}
/**
* Adds the key/value mapping at the end of the list. Behaves identically to {@code set(size(),
* key, value)}.
*
* @throws IndexOutOfBoundsException if index is negative
*/
public final void add(K key, V value) {
set(this.size, key, value);
}
/**
* Removes the key/value mapping at the given index, or ignored if the index is out of bounds.
*
* @return previous value or {@code null} for none
*/
public final V remove(int index) {
return removeFromDataIndexOfKey(index << 1);
}
/** Returns whether there is a mapping for the given key. */
@Override
public final boolean containsKey(Object key) {
return -2 != getDataIndexOfKey(key);
}
/** Returns the index of the given key or {@code -1} if there is no such key. */
public final int getIndexOfKey(K key) {
return getDataIndexOfKey(key) >> 1;
}
/**
* Returns the value set for the given key or {@code null} if there is no such mapping or if the
* mapping value is {@code null}.
*/
@Override
public final V get(Object key) {
return valueAtDataIndex(getDataIndexOfKey(key) + 1);
}
/**
* Sets the value for the given key, overriding any existing value.
*
* @return previous value or {@code null} for none
*/
@Override
public final V put(K key, V value) {
int index = getIndexOfKey(key);
if (index == -1) {
index = this.size;
}
return set(index, key, value);
}
/**
* Removes the key-value pair of the given key, or ignore if the key cannot be found.
*
* @return previous value or {@code null} for none
*/
@Override
public final V remove(Object key) {
return removeFromDataIndexOfKey(getDataIndexOfKey(key));
}
/** Trims the internal array storage to minimize memory usage. */
public final void trim() {
setDataCapacity(this.size << 1);
}
/**
* Ensures that the capacity of the internal arrays is at least a given capacity.
*/
public final void ensureCapacity(int minCapacity) {
Object[] data = this.data;
int minDataCapacity = minCapacity << 1;
int oldDataCapacity = data == null ? 0 : data.length;
if (minDataCapacity > oldDataCapacity) {
int newDataCapacity = oldDataCapacity / 2 * 3 + 1;
if (newDataCapacity % 2 == 1) {
newDataCapacity++;
}
if (newDataCapacity < minDataCapacity) {
newDataCapacity = minDataCapacity;
}
setDataCapacity(newDataCapacity);
}
}
private void setDataCapacity(int newDataCapacity) {
if (newDataCapacity == 0) {
this.data = null;
return;
}
int size = this.size;
Object[] oldData = this.data;
if (size == 0 || newDataCapacity != oldData.length) {
Object[] newData = this.data = new Object[newDataCapacity];
if (size != 0) {
System.arraycopy(oldData, 0, newData, 0, size << 1);
}
}
}
private void setData(int dataIndexOfKey, K key, V value) {
Object[] data = this.data;
data[dataIndexOfKey] = key;
data[dataIndexOfKey + 1] = value;
}
private V valueAtDataIndex(int dataIndex) {
if (dataIndex < 0) {
return null;
}
@SuppressWarnings("unchecked")
V result = (V) this.data[dataIndex];
return result;
}
/**
* Returns the data index of the given key or {@code -2} if there is no such key.
*/
private int getDataIndexOfKey(Object key) {
int dataSize = this.size << 1;
Object[] data = this.data;
for (int i = 0; i < dataSize; i += 2) {
Object k = data[i];
if (key == null ? k == null : key.equals(k)) {
return i;
}
}
return -2;
}
/**
* Removes the key/value mapping at the given data index of key, or ignored if the index is out of
* bounds.
*/
private V removeFromDataIndexOfKey(int dataIndexOfKey) {
int dataSize = this.size << 1;
if (dataIndexOfKey < 0 || dataIndexOfKey >= dataSize) {
return null;
}
V result = valueAtDataIndex(dataIndexOfKey + 1);
Object[] data = this.data;
int moved = dataSize - dataIndexOfKey - 2;
if (moved != 0) {
System.arraycopy(data, dataIndexOfKey + 2, data, dataIndexOfKey, moved);
}
this.size--;
setData(dataSize - 2, null, null);
return result;
}
@Override
public void clear() {
this.size = 0;
this.data = null;
}
@Override
public final boolean containsValue(Object value) {
int dataSize = this.size << 1;
Object[] data = this.data;
for (int i = 1; i < dataSize; i += 2) {
Object v = data[i];
if (value == null ? v == null : value.equals(v)) {
return true;
}
}
return false;
}
@Override
public final Set<Map.Entry<K, V>> entrySet() {
EntrySet entrySet = this.entrySet;
if (entrySet == null) {
entrySet = this.entrySet = new EntrySet();
}
return entrySet;
}
@Override
public ArrayMap<K, V> clone() {
try {
@SuppressWarnings("unchecked")
ArrayMap<K, V> result = (ArrayMap<K, V>) super.clone();
result.entrySet = null;
Object[] data = this.data;
if (data != null) {
int length = data.length;
Object[] resultData = result.data = new Object[length];
System.arraycopy(data, 0, resultData, 0, length);
}
return result;
} catch (CloneNotSupportedException e) {
// won't happen
return null;
}
}
final class EntrySet extends AbstractSet<Map.Entry<K, V>> {
@Override
public Iterator<Map.Entry<K, V>> iterator() {
return new EntryIterator();
}
@Override
public int size() {
return ArrayMap.this.size;
}
}
final class EntryIterator implements Iterator<Map.Entry<K, V>> {
private boolean removed;
private int nextIndex;
public boolean hasNext() {
return this.nextIndex < ArrayMap.this.size;
}
public Map.Entry<K, V> next() {
int index = this.nextIndex;
if (index == ArrayMap.this.size) {
throw new NoSuchElementException();
}
this.nextIndex++;
return new Entry(index);
}
public void remove() {
int index = this.nextIndex - 1;
if (this.removed || index < 0) {
throw new IllegalArgumentException();
}
ArrayMap.this.remove(index);
this.removed = true;
}
}
final class Entry implements Map.Entry<K, V> {
private int index;
Entry(int index) {
this.index = index;
}
public K getKey() {
return ArrayMap.this.getKey(this.index);
}
public V getValue() {
return ArrayMap.this.getValue(this.index);
}
public V setValue(V value) {
return ArrayMap.this.set(this.index, value);
}
}
}
-------------------------
/*
* Copyright (c) 2010 Google Inc.
*
* 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 com.google.api.client.util;
import com.google.api.client.util.ArrayMap;
import junit.framework.TestCase;
/**
* Tests {@link ArrayMap}.
*
* @author Yaniv Inbar
*/
public class ArrayMapTest extends TestCase {
public ArrayMapTest() {
}
public ArrayMapTest(String testName) {
super(testName);
}
public void testOf_zero() {
ArrayMap<String, Integer> map = ArrayMap.of();
assertTrue(map.isEmpty());
}
public void testOf_one() {
ArrayMap<String, Integer> map = ArrayMap.of("a", 1);
assertEquals(1, map.size());
assertEquals("a", map.getKey(0));
assertEquals((Integer) 1, map.getValue(0));
}
public void testOf_two() {
ArrayMap<String, Integer> map = ArrayMap.of("a", 1, "b", 2);
assertEquals(2, map.size());
assertEquals("a", map.getKey(0));
assertEquals((Integer) 1, map.getValue(0));
assertEquals("b", map.getKey(1));
assertEquals((Integer) 2, map.getValue(1));
}
public void testRemove1() {
ArrayMap<String, Integer> map = ArrayMap.of("a", 1, "b", 2);
map.remove("b");
assertEquals(ArrayMap.of("a", 1), map);
}
public void testRemove2() {
ArrayMap<String, Integer> map = ArrayMap.of("a", 1, "b", 2);
map.remove("a");
assertEquals(ArrayMap.of("b", 2), map);
}
public void testRemove3() {
ArrayMap<String, Integer> map = ArrayMap.of("a", 1);
map.remove("a");
assertEquals(ArrayMap.of(), map);
}
public void testRemove4() {
ArrayMap<String, Integer> map = ArrayMap.of("a", 1, "b", 2, "c", 3);
map.remove("b");
assertEquals(ArrayMap.of("a", 1, "c", 3), map);
}
public void testClone_changingEntrySet() {
ArrayMap<String, String> map = ArrayMap.of();
assertEquals("{}", map.toString());
ArrayMap<String, String> clone = map.clone();
clone.add("foo", "bar");
assertEquals("{foo=bar}", clone.toString());
}
}
Related examples in the same category