| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 org.apache.commons.collections4.map; |
| |
| import java.io.IOException; |
| import java.io.ObjectInputStream; |
| import java.io.ObjectOutputStream; |
| import java.io.Serializable; |
| import java.util.AbstractCollection; |
| import java.util.AbstractSet; |
| import java.util.Collection; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.Set; |
| |
| import org.apache.commons.collections4.CollectionUtils; |
| import org.apache.commons.collections4.IterableMap; |
| import org.apache.commons.collections4.MapIterator; |
| import org.apache.commons.collections4.ResettableIterator; |
| import org.apache.commons.collections4.iterators.EmptyIterator; |
| import org.apache.commons.collections4.iterators.EmptyMapIterator; |
| |
| /** |
| * A {@code Map} implementation that stores data in simple fields until |
| * the size is greater than 3. |
| * <p> |
| * This map is designed for performance and can outstrip HashMap. |
| * It also has good garbage collection characteristics. |
| * </p> |
| * <ul> |
| * <li>Optimised for operation at size 3 or less. |
| * <li>Still works well once size 3 exceeded. |
| * <li>Gets at size 3 or less are about 0-10% faster than HashMap, |
| * <li>Puts at size 3 or less are over 4 times faster than HashMap. |
| * <li>Performance 5% slower than HashMap once size 3 exceeded once. |
| * </ul> |
| * <p> |
| * The design uses two distinct modes of operation - flat and delegate. |
| * While the map is size 3 or less, operations map straight onto fields using |
| * switch statements. Once size 4 is reached, the map switches to delegate mode |
| * and only switches back when cleared. In delegate mode, all operations are |
| * forwarded straight to a HashMap resulting in the 5% performance loss. |
| * </p> |
| * <p> |
| * The performance gains on puts are due to not needing to create a Map Entry |
| * object. This is a large saving not only in performance but in garbage collection. |
| * </p> |
| * <p> |
| * Whilst in flat mode this map is also easy for the garbage collector to dispatch. |
| * This is because it contains no complex objects or arrays which slow the progress. |
| * </p> |
| * <p> |
| * Do not use {@code Flat3Map} if the size is likely to grow beyond 3. |
| * </p> |
| * <p> |
| * <strong>Note that Flat3Map is not synchronized and is not thread-safe.</strong> |
| * If you wish to use this map from multiple threads concurrently, you must use |
| * appropriate synchronization. The simplest approach is to wrap this map |
| * using {@link java.util.Collections#synchronizedMap(Map)}. This class may throw |
| * exceptions when accessed by concurrent threads without synchronization. |
| * </p> |
| * |
| * @param <K> the type of the keys in this map |
| * @param <V> the type of the values in this map |
| * @since 3.0 |
| */ |
| public class Flat3Map<K, V> implements IterableMap<K, V>, Serializable, Cloneable { |
| |
| /** Serialization version */ |
| private static final long serialVersionUID = -6701087419741928296L; |
| |
| /** The size of the map, used while in flat mode */ |
| private transient int size; |
| /** Hash, used while in flat mode */ |
| private transient int hash1; |
| /** Hash, used while in flat mode */ |
| private transient int hash2; |
| /** Hash, used while in flat mode */ |
| private transient int hash3; |
| /** Key, used while in flat mode */ |
| private transient K key1; |
| /** Key, used while in flat mode */ |
| private transient K key2; |
| /** Key, used while in flat mode */ |
| private transient K key3; |
| /** Value, used while in flat mode */ |
| private transient V value1; |
| /** Value, used while in flat mode */ |
| private transient V value2; |
| /** Value, used while in flat mode */ |
| private transient V value3; |
| /** Map, used while in delegate mode */ |
| private transient AbstractHashedMap<K, V> delegateMap; |
| |
| /** |
| * Constructor. |
| */ |
| public Flat3Map() { |
| } |
| |
| /** |
| * Constructor copying elements from another map. |
| * |
| * @param map the map to copy |
| * @throws NullPointerException if the map is null |
| */ |
| public Flat3Map(final Map<? extends K, ? extends V> map) { |
| putAll(map); |
| } |
| |
| //----------------------------------------------------------------------- |
| /** |
| * Gets the value mapped to the key specified. |
| * |
| * @param key the key |
| * @return the mapped value, null if no match |
| */ |
| @Override |
| public V get(final Object key) { |
| if (delegateMap != null) { |
| return delegateMap.get(key); |
| } |
| if (key == null) { |
| switch (size) { |
| // drop through |
| case 3: |
| if (key3 == null) { |
| return value3; |
| } |
| case 2: |
| if (key2 == null) { |
| return value2; |
| } |
| case 1: |
| if (key1 == null) { |
| return value1; |
| } |
| } |
| } else { |
| if (size > 0) { |
| final int hashCode = key.hashCode(); |
| switch (size) { |
| // drop through |
| case 3: |
| if (hash3 == hashCode && key.equals(key3)) { |
| return value3; |
| } |
| case 2: |
| if (hash2 == hashCode && key.equals(key2)) { |
| return value2; |
| } |
| case 1: |
| if (hash1 == hashCode && key.equals(key1)) { |
| return value1; |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Gets the size of the map. |
| * |
| * @return the size |
| */ |
| @Override |
| public int size() { |
| if (delegateMap != null) { |
| return delegateMap.size(); |
| } |
| return size; |
| } |
| |
| /** |
| * Checks whether the map is currently empty. |
| * |
| * @return true if the map is currently size zero |
| */ |
| @Override |
| public boolean isEmpty() { |
| return size() == 0; |
| } |
| |
| //----------------------------------------------------------------------- |
| /** |
| * Checks whether the map contains the specified key. |
| * |
| * @param key the key to search for |
| * @return true if the map contains the key |
| */ |
| @Override |
| public boolean containsKey(final Object key) { |
| if (delegateMap != null) { |
| return delegateMap.containsKey(key); |
| } |
| if (key == null) { |
| switch (size) { // drop through |
| case 3: |
| if (key3 == null) { |
| return true; |
| } |
| case 2: |
| if (key2 == null) { |
| return true; |
| } |
| case 1: |
| if (key1 == null) { |
| return true; |
| } |
| } |
| } else { |
| if (size > 0) { |
| final int hashCode = key.hashCode(); |
| switch (size) { // drop through |
| case 3: |
| if (hash3 == hashCode && key.equals(key3)) { |
| return true; |
| } |
| case 2: |
| if (hash2 == hashCode && key.equals(key2)) { |
| return true; |
| } |
| case 1: |
| if (hash1 == hashCode && key.equals(key1)) { |
| return true; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Checks whether the map contains the specified value. |
| * |
| * @param value the value to search for |
| * @return true if the map contains the key |
| */ |
| @Override |
| public boolean containsValue(final Object value) { |
| if (delegateMap != null) { |
| return delegateMap.containsValue(value); |
| } |
| if (value == null) { // drop through |
| switch (size) { |
| case 3: |
| if (value3 == null) { |
| return true; |
| } |
| case 2: |
| if (value2 == null) { |
| return true; |
| } |
| case 1: |
| if (value1 == null) { |
| return true; |
| } |
| } |
| } else { |
| switch (size) { // drop through |
| case 3: |
| if (value.equals(value3)) { |
| return true; |
| } |
| case 2: |
| if (value.equals(value2)) { |
| return true; |
| } |
| case 1: |
| if (value.equals(value1)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| //----------------------------------------------------------------------- |
| /** |
| * Puts a key-value mapping into this map. |
| * |
| * @param key the key to add |
| * @param value the value to add |
| * @return the value previously mapped to this key, null if none |
| */ |
| @Override |
| public V put(final K key, final V value) { |
| if (delegateMap != null) { |
| return delegateMap.put(key, value); |
| } |
| // change existing mapping |
| if (key == null) { |
| switch (size) { // drop through |
| case 3: |
| if (key3 == null) { |
| final V old = value3; |
| value3 = value; |
| return old; |
| } |
| case 2: |
| if (key2 == null) { |
| final V old = value2; |
| value2 = value; |
| return old; |
| } |
| case 1: |
| if (key1 == null) { |
| final V old = value1; |
| value1 = value; |
| return old; |
| } |
| } |
| } else { |
| if (size > 0) { |
| final int hashCode = key.hashCode(); |
| switch (size) { // drop through |
| case 3: |
| if (hash3 == hashCode && key.equals(key3)) { |
| final V old = value3; |
| value3 = value; |
| return old; |
| } |
| case 2: |
| if (hash2 == hashCode && key.equals(key2)) { |
| final V old = value2; |
| value2 = value; |
| return old; |
| } |
| case 1: |
| if (hash1 == hashCode && key.equals(key1)) { |
| final V old = value1; |
| value1 = value; |
| return old; |
| } |
| } |
| } |
| } |
| |
| // add new mapping |
| switch (size) { |
| default: |
| convertToMap(); |
| delegateMap.put(key, value); |
| return null; |
| case 2: |
| hash3 = key == null ? 0 : key.hashCode(); |
| key3 = key; |
| value3 = value; |
| break; |
| case 1: |
| hash2 = key == null ? 0 : key.hashCode(); |
| key2 = key; |
| value2 = value; |
| break; |
| case 0: |
| hash1 = key == null ? 0 : key.hashCode(); |
| key1 = key; |
| value1 = value; |
| break; |
| } |
| size++; |
| return null; |
| } |
| |
| /** |
| * Puts all the values from the specified map into this map. |
| * |
| * @param map the map to add |
| * @throws NullPointerException if the map is null |
| */ |
| @Override |
| public void putAll(final Map<? extends K, ? extends V> map) { |
| final int size = map.size(); |
| if (size == 0) { |
| return; |
| } |
| if (delegateMap != null) { |
| delegateMap.putAll(map); |
| return; |
| } |
| if (size < 4) { |
| for (final Map.Entry<? extends K, ? extends V> entry : map.entrySet()) { |
| put(entry.getKey(), entry.getValue()); |
| } |
| } else { |
| convertToMap(); |
| delegateMap.putAll(map); |
| } |
| } |
| |
| /** |
| * Converts the flat map data to a map. |
| */ |
| private void convertToMap() { |
| delegateMap = createDelegateMap(); |
| switch (size) { // drop through |
| case 3: |
| delegateMap.put(key3, value3); |
| case 2: |
| delegateMap.put(key2, value2); |
| case 1: |
| delegateMap.put(key1, value1); |
| case 0: |
| break; |
| default: |
| throw new IllegalStateException("Invalid map index: " + size); |
| } |
| |
| size = 0; |
| hash1 = hash2 = hash3 = 0; |
| key1 = key2 = key3 = null; |
| value1 = value2 = value3 = null; |
| } |
| |
| /** |
| * Create an instance of the map used for storage when in delegation mode. |
| * <p> |
| * This can be overridden by subclasses to provide a different map implementation. |
| * Not every AbstractHashedMap is suitable, identity and reference based maps |
| * would be poor choices. |
| * |
| * @return a new AbstractHashedMap or subclass |
| * @since 3.1 |
| */ |
| protected AbstractHashedMap<K, V> createDelegateMap() { |
| return new HashedMap<>(); |
| } |
| |
| /** |
| * Removes the specified mapping from this map. |
| * |
| * @param key the mapping to remove |
| * @return the value mapped to the removed key, null if key not in map |
| */ |
| @Override |
| public V remove(final Object key) { |
| if (delegateMap != null) { |
| return delegateMap.remove(key); |
| } |
| if (size == 0) { |
| return null; |
| } |
| if (key == null) { |
| switch (size) { // drop through |
| case 3: |
| if (key3 == null) { |
| final V old = value3; |
| hash3 = 0; |
| key3 = null; |
| value3 = null; |
| size = 2; |
| return old; |
| } |
| if (key2 == null) { |
| final V old = value2; |
| hash2 = hash3; |
| key2 = key3; |
| value2 = value3; |
| hash3 = 0; |
| key3 = null; |
| value3 = null; |
| size = 2; |
| return old; |
| } |
| if (key1 == null) { |
| final V old = value1; |
| hash1 = hash3; |
| key1 = key3; |
| value1 = value3; |
| hash3 = 0; |
| key3 = null; |
| value3 = null; |
| size = 2; |
| return old; |
| } |
| return null; |
| case 2: |
| if (key2 == null) { |
| final V old = value2; |
| hash2 = 0; |
| key2 = null; |
| value2 = null; |
| size = 1; |
| return old; |
| } |
| if (key1 == null) { |
| final V old = value1; |
| hash1 = hash2; |
| key1 = key2; |
| value1 = value2; |
| hash2 = 0; |
| key2 = null; |
| value2 = null; |
| size = 1; |
| return old; |
| } |
| return null; |
| case 1: |
| if (key1 == null) { |
| final V old = value1; |
| hash1 = 0; |
| key1 = null; |
| value1 = null; |
| size = 0; |
| return old; |
| } |
| } |
| } else { |
| if (size > 0) { |
| final int hashCode = key.hashCode(); |
| switch (size) { // drop through |
| case 3: |
| if (hash3 == hashCode && key.equals(key3)) { |
| final V old = value3; |
| hash3 = 0; |
| key3 = null; |
| value3 = null; |
| size = 2; |
| return old; |
| } |
| if (hash2 == hashCode && key.equals(key2)) { |
| final V old = value2; |
| hash2 = hash3; |
| key2 = key3; |
| value2 = value3; |
| hash3 = 0; |
| key3 = null; |
| value3 = null; |
| size = 2; |
| return old; |
| } |
| if (hash1 == hashCode && key.equals(key1)) { |
| final V old = value1; |
| hash1 = hash3; |
| key1 = key3; |
| value1 = value3; |
| hash3 = 0; |
| key3 = null; |
| value3 = null; |
| size = 2; |
| return old; |
| } |
| return null; |
| case 2: |
| if (hash2 == hashCode && key.equals(key2)) { |
| final V old = value2; |
| hash2 = 0; |
| key2 = null; |
| value2 = null; |
| size = 1; |
| return old; |
| } |
| if (hash1 == hashCode && key.equals(key1)) { |
| final V old = value1; |
| hash1 = hash2; |
| key1 = key2; |
| value1 = value2; |
| hash2 = 0; |
| key2 = null; |
| value2 = null; |
| size = 1; |
| return old; |
| } |
| return null; |
| case 1: |
| if (hash1 == hashCode && key.equals(key1)) { |
| final V old = value1; |
| hash1 = 0; |
| key1 = null; |
| value1 = null; |
| size = 0; |
| return old; |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Clears the map, resetting the size to zero and nullifying references |
| * to avoid garbage collection issues. |
| */ |
| @Override |
| public void clear() { |
| if (delegateMap != null) { |
| delegateMap.clear(); // should aid gc |
| delegateMap = null; // switch back to flat mode |
| } else { |
| size = 0; |
| hash1 = hash2 = hash3 = 0; |
| key1 = key2 = key3 = null; |
| value1 = value2 = value3 = null; |
| } |
| } |
| |
| //----------------------------------------------------------------------- |
| /** |
| * Gets an iterator over the map. |
| * Changes made to the iterator affect this map. |
| * <p> |
| * A MapIterator returns the keys in the map. It also provides convenient |
| * methods to get the key and value, and set the value. |
| * It avoids the need to create an entrySet/keySet/values object. |
| * It also avoids creating the Map Entry object. |
| * |
| * @return the map iterator |
| */ |
| @Override |
| public MapIterator<K, V> mapIterator() { |
| if (delegateMap != null) { |
| return delegateMap.mapIterator(); |
| } |
| if (size == 0) { |
| return EmptyMapIterator.<K, V>emptyMapIterator(); |
| } |
| return new FlatMapIterator<>(this); |
| } |
| |
| /** |
| * FlatMapIterator |
| */ |
| static class FlatMapIterator<K, V> implements MapIterator<K, V>, ResettableIterator<K> { |
| private final Flat3Map<K, V> parent; |
| private int nextIndex; |
| private boolean canRemove; |
| |
| FlatMapIterator(final Flat3Map<K, V> parent) { |
| this.parent = parent; |
| } |
| |
| @Override |
| public boolean hasNext() { |
| return nextIndex < parent.size; |
| } |
| |
| @Override |
| public K next() { |
| if (hasNext() == false) { |
| throw new NoSuchElementException(AbstractHashedMap.NO_NEXT_ENTRY); |
| } |
| canRemove = true; |
| nextIndex++; |
| return getKey(); |
| } |
| |
| @Override |
| public void remove() { |
| if (canRemove == false) { |
| throw new IllegalStateException(AbstractHashedMap.REMOVE_INVALID); |
| } |
| parent.remove(getKey()); |
| nextIndex--; |
| canRemove = false; |
| } |
| |
| @Override |
| public K getKey() { |
| if (canRemove == false) { |
| throw new IllegalStateException(AbstractHashedMap.GETKEY_INVALID); |
| } |
| switch (nextIndex) { |
| case 3: |
| return parent.key3; |
| case 2: |
| return parent.key2; |
| case 1: |
| return parent.key1; |
| } |
| throw new IllegalStateException("Invalid map index: " + nextIndex); |
| } |
| |
| @Override |
| public V getValue() { |
| if (canRemove == false) { |
| throw new IllegalStateException(AbstractHashedMap.GETVALUE_INVALID); |
| } |
| switch (nextIndex) { |
| case 3: |
| return parent.value3; |
| case 2: |
| return parent.value2; |
| case 1: |
| return parent.value1; |
| } |
| throw new IllegalStateException("Invalid map index: " + nextIndex); |
| } |
| |
| @Override |
| public V setValue(final V value) { |
| if (canRemove == false) { |
| throw new IllegalStateException(AbstractHashedMap.SETVALUE_INVALID); |
| } |
| final V old = getValue(); |
| switch (nextIndex) { |
| case 3: |
| parent.value3 = value; |
| break; |
| case 2: |
| parent.value2 = value; |
| break; |
| case 1: |
| parent.value1 = value; |
| break; |
| default: |
| throw new IllegalStateException("Invalid map index: " + nextIndex); |
| } |
| return old; |
| } |
| |
| @Override |
| public void reset() { |
| nextIndex = 0; |
| canRemove = false; |
| } |
| |
| @Override |
| public String toString() { |
| if (canRemove) { |
| return "Iterator[" + getKey() + "=" + getValue() + "]"; |
| } |
| return "Iterator[]"; |
| } |
| } |
| |
| /** |
| * Gets the entrySet view of the map. |
| * Changes made to the view affect this map. |
| * <p> |
| * NOTE: from 4.0, the returned Map Entry will be an independent object and will |
| * not change anymore as the iterator progresses. To avoid this additional object |
| * creation and simply iterate through the entries, use {@link #mapIterator()}. |
| * |
| * @return the entrySet view |
| */ |
| @Override |
| public Set<Map.Entry<K, V>> entrySet() { |
| if (delegateMap != null) { |
| return delegateMap.entrySet(); |
| } |
| return new EntrySet<>(this); |
| } |
| |
| /** |
| * EntrySet |
| */ |
| static class EntrySet<K, V> extends AbstractSet<Map.Entry<K, V>> { |
| private final Flat3Map<K, V> parent; |
| |
| EntrySet(final Flat3Map<K, V> parent) { |
| this.parent = parent; |
| } |
| |
| @Override |
| public int size() { |
| return parent.size(); |
| } |
| |
| @Override |
| public void clear() { |
| parent.clear(); |
| } |
| |
| @Override |
| public boolean remove(final Object obj) { |
| if (obj instanceof Map.Entry == false) { |
| return false; |
| } |
| final Map.Entry<?, ?> entry = (Map.Entry<?, ?>) obj; |
| final Object key = entry.getKey(); |
| final boolean result = parent.containsKey(key); |
| parent.remove(key); |
| return result; |
| } |
| |
| @Override |
| public Iterator<Map.Entry<K, V>> iterator() { |
| if (parent.delegateMap != null) { |
| return parent.delegateMap.entrySet().iterator(); |
| } |
| if (parent.isEmpty()) { |
| return EmptyIterator.<Map.Entry<K, V>>emptyIterator(); |
| } |
| return new EntrySetIterator<>(parent); |
| } |
| } |
| |
| static class FlatMapEntry<K, V> implements Map.Entry<K, V> { |
| private final Flat3Map<K, V> parent; |
| private final int index; |
| private volatile boolean removed; |
| |
| FlatMapEntry(final Flat3Map<K, V> parent, final int index) { |
| this.parent = parent; |
| this.index = index; |
| this.removed = false; |
| } |
| |
| /** |
| * Used by the iterator that created this entry to indicate that |
| * {@link java.util.Iterator#remove()} has been called. |
| * <p> |
| * As a consequence, all subsequent call to {@link #getKey()}, |
| * {@link #setValue(Object)} and {@link #getValue()} will fail. |
| * |
| * @param flag the new value of the removed flag |
| */ |
| void setRemoved(final boolean flag) { |
| this.removed = flag; |
| } |
| |
| @Override |
| public K getKey() { |
| if (removed) { |
| throw new IllegalStateException(AbstractHashedMap.GETKEY_INVALID); |
| } |
| switch (index) { |
| case 3: |
| return parent.key3; |
| case 2: |
| return parent.key2; |
| case 1: |
| return parent.key1; |
| } |
| throw new IllegalStateException("Invalid map index: " + index); |
| } |
| |
| @Override |
| public V getValue() { |
| if (removed) { |
| throw new IllegalStateException(AbstractHashedMap.GETVALUE_INVALID); |
| } |
| switch (index) { |
| case 3: |
| return parent.value3; |
| case 2: |
| return parent.value2; |
| case 1: |
| return parent.value1; |
| } |
| throw new IllegalStateException("Invalid map index: " + index); |
| } |
| |
| @Override |
| public V setValue(final V value) { |
| if (removed) { |
| throw new IllegalStateException(AbstractHashedMap.SETVALUE_INVALID); |
| } |
| final V old = getValue(); |
| switch (index) { |
| case 3: |
| parent.value3 = value; |
| break; |
| case 2: |
| parent.value2 = value; |
| break; |
| case 1: |
| parent.value1 = value; |
| break; |
| default: |
| throw new IllegalStateException("Invalid map index: " + index); |
| } |
| return old; |
| } |
| |
| @Override |
| public boolean equals(final Object obj) { |
| if (removed) { |
| return false; |
| } |
| if (obj instanceof Map.Entry == false) { |
| return false; |
| } |
| final Map.Entry<?, ?> other = (Map.Entry<?, ?>) obj; |
| final Object key = getKey(); |
| final Object value = getValue(); |
| return (key == null ? other.getKey() == null : key.equals(other.getKey())) && |
| (value == null ? other.getValue() == null : value.equals(other.getValue())); |
| } |
| |
| @Override |
| public int hashCode() { |
| if (removed) { |
| return 0; |
| } |
| final Object key = getKey(); |
| final Object value = getValue(); |
| return (key == null ? 0 : key.hashCode()) ^ |
| (value == null ? 0 : value.hashCode()); |
| } |
| |
| @Override |
| public String toString() { |
| if (!removed) { |
| return getKey() + "=" + getValue(); |
| } |
| return ""; |
| } |
| |
| } |
| |
| abstract static class EntryIterator<K, V> { |
| private final Flat3Map<K, V> parent; |
| private int nextIndex; |
| private FlatMapEntry<K, V> currentEntry; |
| |
| /** |
| * Create a new Flat3Map.EntryIterator. |
| */ |
| EntryIterator(final Flat3Map<K, V> parent) { |
| this.parent = parent; |
| } |
| |
| public boolean hasNext() { |
| return nextIndex < parent.size; |
| } |
| |
| public Map.Entry<K, V> nextEntry() { |
| if (!hasNext()) { |
| throw new NoSuchElementException(AbstractHashedMap.NO_NEXT_ENTRY); |
| } |
| currentEntry = new FlatMapEntry<>(parent, ++nextIndex); |
| return currentEntry; |
| } |
| |
| public void remove() { |
| if (currentEntry == null) { |
| throw new IllegalStateException(AbstractHashedMap.REMOVE_INVALID); |
| } |
| parent.remove(currentEntry.getKey()); |
| currentEntry.setRemoved(true); |
| nextIndex--; |
| currentEntry = null; |
| } |
| |
| } |
| |
| /** |
| * EntrySetIterator and MapEntry |
| */ |
| static class EntrySetIterator<K, V> extends EntryIterator<K, V> implements Iterator<Map.Entry<K, V>> { |
| EntrySetIterator(final Flat3Map<K, V> parent) { |
| super(parent); |
| } |
| |
| @Override |
| public Map.Entry<K, V> next() { |
| return nextEntry(); |
| } |
| } |
| |
| /** |
| * Gets the keySet view of the map. |
| * Changes made to the view affect this map. |
| * To simply iterate through the keys, use {@link #mapIterator()}. |
| * |
| * @return the keySet view |
| */ |
| @Override |
| public Set<K> keySet() { |
| if (delegateMap != null) { |
| return delegateMap.keySet(); |
| } |
| return new KeySet<>(this); |
| } |
| |
| /** |
| * KeySet |
| */ |
| static class KeySet<K> extends AbstractSet<K> { |
| private final Flat3Map<K, ?> parent; |
| |
| KeySet(final Flat3Map<K, ?> parent) { |
| this.parent = parent; |
| } |
| |
| @Override |
| public int size() { |
| return parent.size(); |
| } |
| |
| @Override |
| public void clear() { |
| parent.clear(); |
| } |
| |
| @Override |
| public boolean contains(final Object key) { |
| return parent.containsKey(key); |
| } |
| |
| @Override |
| public boolean remove(final Object key) { |
| final boolean result = parent.containsKey(key); |
| parent.remove(key); |
| return result; |
| } |
| |
| @Override |
| public Iterator<K> iterator() { |
| if (parent.delegateMap != null) { |
| return parent.delegateMap.keySet().iterator(); |
| } |
| if (parent.isEmpty()) { |
| return EmptyIterator.<K>emptyIterator(); |
| } |
| return new KeySetIterator<>(parent); |
| } |
| } |
| |
| /** |
| * KeySetIterator |
| */ |
| static class KeySetIterator<K> extends EntryIterator<K, Object> implements Iterator<K> { |
| |
| @SuppressWarnings("unchecked") |
| KeySetIterator(final Flat3Map<K, ?> parent) { |
| super((Flat3Map<K, Object>) parent); |
| } |
| |
| @Override |
| public K next() { |
| return nextEntry().getKey(); |
| } |
| } |
| |
| /** |
| * Gets the values view of the map. |
| * Changes made to the view affect this map. |
| * To simply iterate through the values, use {@link #mapIterator()}. |
| * |
| * @return the values view |
| */ |
| @Override |
| public Collection<V> values() { |
| if (delegateMap != null) { |
| return delegateMap.values(); |
| } |
| return new Values<>(this); |
| } |
| |
| /** |
| * Values |
| */ |
| static class Values<V> extends AbstractCollection<V> { |
| private final Flat3Map<?, V> parent; |
| |
| Values(final Flat3Map<?, V> parent) { |
| this.parent = parent; |
| } |
| |
| @Override |
| public int size() { |
| return parent.size(); |
| } |
| |
| @Override |
| public void clear() { |
| parent.clear(); |
| } |
| |
| @Override |
| public boolean contains(final Object value) { |
| return parent.containsValue(value); |
| } |
| |
| @Override |
| public Iterator<V> iterator() { |
| if (parent.delegateMap != null) { |
| return parent.delegateMap.values().iterator(); |
| } |
| if (parent.isEmpty()) { |
| return EmptyIterator.<V>emptyIterator(); |
| } |
| return new ValuesIterator<>(parent); |
| } |
| } |
| |
| /** |
| * ValuesIterator |
| */ |
| static class ValuesIterator<V> extends EntryIterator<Object, V> implements Iterator<V> { |
| |
| @SuppressWarnings("unchecked") |
| ValuesIterator(final Flat3Map<?, V> parent) { |
| super((Flat3Map<Object, V>) parent); |
| } |
| |
| @Override |
| public V next() { |
| return nextEntry().getValue(); |
| } |
| } |
| |
| //----------------------------------------------------------------------- |
| /** |
| * Write the map out using a custom routine. |
| * |
| * @param out the output stream |
| * @throws IOException if an error occurs while writing to the stream |
| */ |
| private void writeObject(final ObjectOutputStream out) throws IOException { |
| out.defaultWriteObject(); |
| out.writeInt(size()); |
| for (final MapIterator<?, ?> it = mapIterator(); it.hasNext();) { |
| out.writeObject(it.next()); // key |
| out.writeObject(it.getValue()); // value |
| } |
| } |
| |
| /** |
| * Read the map in using a custom routine. |
| * |
| * @param in the input stream |
| * @throws IOException if an error occurs while reading from the stream |
| * @throws ClassNotFoundException if an object read from the stream can not be loaded |
| */ |
| @SuppressWarnings("unchecked") |
| private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { |
| in.defaultReadObject(); |
| final int count = in.readInt(); |
| if (count > 3) { |
| delegateMap = createDelegateMap(); |
| } |
| for (int i = count; i > 0; i--) { |
| put((K) in.readObject(), (V) in.readObject()); |
| } |
| } |
| |
| //----------------------------------------------------------------------- |
| /** |
| * Clones the map without cloning the keys or values. |
| * |
| * @return a shallow clone |
| * @since 3.1 |
| */ |
| @Override |
| @SuppressWarnings("unchecked") |
| public Flat3Map<K, V> clone() { |
| try { |
| final Flat3Map<K, V> cloned = (Flat3Map<K, V>) super.clone(); |
| if (cloned.delegateMap != null) { |
| cloned.delegateMap = cloned.delegateMap.clone(); |
| } |
| return cloned; |
| } catch (final CloneNotSupportedException ex) { |
| throw new InternalError(); |
| } |
| } |
| |
| /** |
| * Compares this map with another. |
| * |
| * @param obj the object to compare to |
| * @return true if equal |
| */ |
| @Override |
| public boolean equals(final Object obj) { |
| if (obj == this) { |
| return true; |
| } |
| if (delegateMap != null) { |
| return delegateMap.equals(obj); |
| } |
| if (obj instanceof Map == false) { |
| return false; |
| } |
| final Map<?, ?> other = (Map<?, ?>) obj; |
| if (size != other.size()) { |
| return false; |
| } |
| if (size > 0) { |
| Object otherValue = null; |
| switch (size) { // drop through |
| case 3: |
| if (other.containsKey(key3) == false) { |
| return false; |
| } |
| otherValue = other.get(key3); |
| if (value3 == null ? otherValue != null : !value3.equals(otherValue)) { |
| return false; |
| } |
| case 2: |
| if (other.containsKey(key2) == false) { |
| return false; |
| } |
| otherValue = other.get(key2); |
| if (value2 == null ? otherValue != null : !value2.equals(otherValue)) { |
| return false; |
| } |
| case 1: |
| if (other.containsKey(key1) == false) { |
| return false; |
| } |
| otherValue = other.get(key1); |
| if (value1 == null ? otherValue != null : !value1.equals(otherValue)) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Gets the standard Map hashCode. |
| * |
| * @return the hash code defined in the Map interface |
| */ |
| @Override |
| public int hashCode() { |
| if (delegateMap != null) { |
| return delegateMap.hashCode(); |
| } |
| int total = 0; |
| switch (size) { // drop through |
| case 3: |
| total += hash3 ^ (value3 == null ? 0 : value3.hashCode()); |
| case 2: |
| total += hash2 ^ (value2 == null ? 0 : value2.hashCode()); |
| case 1: |
| total += hash1 ^ (value1 == null ? 0 : value1.hashCode()); |
| case 0: |
| break; |
| default: |
| throw new IllegalStateException("Invalid map index: " + size); |
| } |
| return total; |
| } |
| |
| /** |
| * Gets the map as a String. |
| * |
| * @return a string version of the map |
| */ |
| @Override |
| public String toString() { |
| if (delegateMap != null) { |
| return delegateMap.toString(); |
| } |
| if (size == 0) { |
| return "{}"; |
| } |
| final StringBuilder buf = new StringBuilder(128); |
| buf.append('{'); |
| switch (size) { // drop through |
| case 3: |
| buf.append(key3 == this ? "(this Map)" : key3); |
| buf.append('='); |
| buf.append(value3 == this ? "(this Map)" : value3); |
| buf.append(CollectionUtils.COMMA); |
| case 2: |
| buf.append(key2 == this ? "(this Map)" : key2); |
| buf.append('='); |
| buf.append(value2 == this ? "(this Map)" : value2); |
| buf.append(CollectionUtils.COMMA); |
| case 1: |
| buf.append(key1 == this ? "(this Map)" : key1); |
| buf.append('='); |
| buf.append(value1 == this ? "(this Map)" : value1); |
| break; |
| // case 0: has already been dealt with |
| default: |
| throw new IllegalStateException("Invalid map index: " + size); |
| } |
| buf.append('}'); |
| return buf.toString(); |
| } |
| |
| } |