Mega Code Archive

 
Categories / Java / Development Class
 

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());   } }