com.cedarsoftware.util.CompactMap Maven / Gradle / Ivy
The newest version!
package com.cedarsoftware.util;
import java.lang.reflect.Constructor;
import java.util.AbstractCollection;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
/**
* Many developers do not realize than they may have thousands or hundreds of thousands of Maps in memory, often
* representing small JSON objects. These maps (often HashMaps) usually have a table of 16/32/64... elements in them,
* with many empty elements. HashMap doubles it's internal storage each time it expands, so often these Maps have
* barely 50% of these arrays filled.
*
* CompactMap is a Map that strives to reduce memory at all costs while retaining speed that is close to HashMap's speed.
* It does this by using only one (1) member variable (of type Object) and changing it as the Map grows. It goes from
* single value, to a single MapEntry, to an Object[], and finally it uses a Map (user defined). CompactMap is
* especially small when 0 and 1 entries are stored in it. When size() is from `2` to compactSize(), then entries
* are stored internally in single Object[]. If the size() is {@literal >} compactSize() then the entries are stored in a
* regular `Map`.
*
* Methods you may want to override:
*
* // If this key is used and only 1 element then only the value is stored
* protected K getSingleValueKey() { return "someKey"; }
*
* // Map you would like it to use when size() {@literal >} compactSize(). HashMap is default
* protected abstract Map{@literal <}K, V{@literal >} getNewMap();
*
* // If you want case insensitivity, return true and return new CaseInsensitiveMap or TreeMap(String.CASE_INSENSITIVE_ORDER) from getNewMap()
* protected boolean isCaseInsensitive() { return false; }
*
* // When size() {@literal >} than this amount, the Map returned from getNewMap() is used to store elements.
* protected int compactSize() { return 80; }
*
*
* **Empty**
* This class only has one (1) member variable of type `Object`. If there are no entries in it, then the value of that
* member variable takes on a pointer (points to sentinel value.)
*
* **One entry**
* If the entry has a key that matches the value returned from `getSingleValueKey()` then there is no key stored
* and the internal single member points to the value only.
*
* If the single entry's key does not match the value returned from `getSingleValueKey()` then the internal field points
* to an internal `Class` `CompactMapEntry` which contains the key and the value (nothing else). Again, all APIs still operate
* the same.
*
* **Two thru compactSize() entries**
* In this case, the single member variable points to a single Object[] that contains all the keys and values. The
* keys are in the even positions, the values are in the odd positions (1 up from the key). [0] = key, [1] = value,
* [2] = next key, [3] = next value, and so on. The Object[] is dynamically expanded until size() {@literal >} compactSize(). In
* addition, it is dynamically shrunk until the size becomes 1, and then it switches to a single Map Entry or a single
* value.
*
* **size() greater than compactSize()**
* In this case, the single member variable points to a `Map` instance (supplied by `getNewMap()` API that user supplied.)
* This allows `CompactMap` to work with nearly all `Map` types.
*
* This Map supports null for the key and values, as long as the Map returned by getNewMap() supports null keys-values.
*
* @author John DeRegnaucourt ([email protected])
*
* Copyright (c) Cedar Software LLC
*
* 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
*
* License
*
* 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.
*/
@SuppressWarnings("unchecked")
public class CompactMap implements Map
{
private static final String EMPTY_MAP = "_︿_ψ_☼";
private Object val = EMPTY_MAP;
public CompactMap()
{
if (compactSize() < 2)
{
throw new IllegalStateException("compactSize() must be >= 2");
}
}
public CompactMap(Map other)
{
this();
putAll(other);
}
public int size()
{
if (val instanceof Object[])
{ // 2 to compactSize
return ((Object[])val).length >> 1;
}
else if (val instanceof Map)
{ // > compactSize
return ((Map)val).size();
}
else if (val == EMPTY_MAP)
{ // empty
return 0;
}
// size == 1
return 1;
}
public boolean isEmpty()
{
return val == EMPTY_MAP;
}
private boolean compareKeys(Object key, Object aKey)
{
if (key instanceof String)
{
if (aKey instanceof String)
{
if (isCaseInsensitive())
{
return ((String)aKey).equalsIgnoreCase((String) key);
}
else
{
return aKey.equals(key);
}
}
return false;
}
return Objects.equals(key, aKey);
}
public boolean containsKey(Object key)
{
if (val instanceof Object[])
{ // 2 to compactSize
Object[] entries = (Object[]) val;
final int len = entries.length;
for (int i=0; i < len; i += 2)
{
if (compareKeys(key, entries[i]))
{
return true;
}
}
return false;
}
else if (val instanceof Map)
{ // > compactSize
Map map = (Map) val;
return map.containsKey(key);
}
else if (val == EMPTY_MAP)
{ // empty
return false;
}
// size == 1
return compareKeys(key, getLogicalSingleKey());
}
public boolean containsValue(Object value)
{
if (val instanceof Object[])
{ // 2 to Compactsize
Object[] entries = (Object[]) val;
final int len = entries.length;
for (int i=0; i < len; i += 2)
{
Object aValue = entries[i + 1];
if (Objects.equals(value, aValue))
{
return true;
}
}
return false;
}
else if (val instanceof Map)
{ // > compactSize
Map map = (Map) val;
return map.containsValue(value);
}
else if (val == EMPTY_MAP)
{ // empty
return false;
}
// size == 1
return getLogicalSingleValue() == value;
}
public V get(Object key)
{
if (val instanceof Object[])
{ // 2 to compactSize
Object[] entries = (Object[]) val;
final int len = entries.length;
for (int i=0; i < len; i += 2)
{
Object aKey = entries[i];
if (compareKeys(key, aKey))
{
return (V) entries[i + 1];
}
}
return null;
}
else if (val instanceof Map)
{ // > compactSize
Map map = (Map) val;
return map.get(key);
}
else if (val == EMPTY_MAP)
{ // empty
return null;
}
// size == 1
return compareKeys(key, getLogicalSingleKey()) ? getLogicalSingleValue() : null;
}
public V put(K key, V value)
{
if (val instanceof Object[])
{ // 2 to compactSize
Object[] entries = (Object[]) val;
final int len = entries.length;
for (int i=0; i < len; i += 2)
{
Object aKey = entries[i];
Object aValue = entries[i + 1];
if (compareKeys(key, aKey))
{ // Overwrite case
entries[i + 1] = value;
return (V) aValue;
}
}
// Not present in Object[]
if (size() < compactSize())
{ // Grow array
Object[] expand = new Object[len + 2];
System.arraycopy(entries, 0, expand, 0, len);
// Place new entry at end
expand[expand.length - 2] = key;
expand[expand.length - 1] = value;
val = expand;
}
else
{ // Switch to Map - copy entries
Map map = getNewMap(size() + 1);
entries = (Object[]) val;
final int len2 = entries.length;
for (int i=0; i < len2; i += 2)
{
Object aKey = entries[i];
Object aValue = entries[i + 1];
map.put((K) aKey, (V) aValue);
}
// Place new entry
map.put(key, value);
val = map;
}
return null;
}
else if (val instanceof Map)
{ // > compactSize
Map map = (Map) val;
return map.put(key, value);
}
else if (val == EMPTY_MAP)
{ // empty
if (compareKeys(key, getLogicalSingleKey()) && !(value instanceof Map || value instanceof Object[]))
{
val = value;
}
else
{
val = new CompactMapEntry(key, value);
}
return null;
}
// size == 1
if (compareKeys(key, getLogicalSingleKey()))
{ // Overwrite
V save = getLogicalSingleValue();
if (compareKeys(key, getSingleValueKey()) && !(value instanceof Map || value instanceof Object[]))
{
val = value;
}
else
{
val = new CompactMapEntry(key, value);
}
return save;
}
else
{ // CompactMapEntry to []
Object[] entries = new Object[4];
entries[0] = getLogicalSingleKey();
entries[1] = getLogicalSingleValue();
entries[2] = key;
entries[3] = value;
val = entries;
return null;
}
}
public V remove(Object key)
{
if (val instanceof Object[])
{ // 2 to compactSize
Object[] entries = (Object[]) val;
if (size() == 2)
{ // When at 2 entries, we must drop back to CompactMapEntry or val (use clear() and put() to get us there).
if (compareKeys(key, entries[0]))
{
Object prevValue = entries[1];
clear();
put((K)entries[2], (V)entries[3]);
return (V) prevValue;
}
else if (compareKeys(key, entries[2]))
{
Object prevValue = entries[3];
clear();
put((K)entries[0], (V)entries[1]);
return (V) prevValue;
}
}
else
{
final int len = entries.length;
for (int i = 0; i < len; i += 2)
{
Object aKey = entries[i];
if (compareKeys(key, aKey))
{ // Found, must shrink
Object prior = entries[i + 1];
Object[] shrink = new Object[len - 2];
System.arraycopy(entries, 0, shrink, 0, i);
System.arraycopy(entries, i + 2, shrink, i, shrink.length - i);
val = shrink;
return (V) prior;
}
}
}
return null; // not found
}
else if (val instanceof Map)
{ // > compactSize
Map map = (Map) val;
if (!map.containsKey(key))
{
return null;
}
V save = map.remove(key);
if (map.size() == compactSize())
{ // Down to compactSize, need to switch to Object[]
Object[] entries = new Object[compactSize() * 2];
Iterator> i = map.entrySet().iterator();
int idx = 0;
while (i.hasNext())
{
Entry entry = i.next();
entries[idx] = entry.getKey();
entries[idx + 1] = entry.getValue();
idx += 2;
}
val = entries;
}
return save;
}
else if (val == EMPTY_MAP)
{ // empty
return null;
}
// size == 1
if (compareKeys(key, getLogicalSingleKey()))
{ // found
V save = getLogicalSingleValue();
val = EMPTY_MAP;
return save;
}
else
{ // not found
return null;
}
}
public void putAll(Map extends K, ? extends V> map)
{
if (map == null)
{
return;
}
int mSize = map.size();
if (val instanceof Map || mSize > compactSize())
{
if (val == EMPTY_MAP)
{
val = getNewMap(mSize);
}
((Map) val).putAll(map);
}
else
{
for (Entry extends K, ? extends V> entry : map.entrySet())
{
put(entry.getKey(), entry.getValue());
}
}
}
public void clear()
{
val = EMPTY_MAP;
}
public int hashCode()
{
if (val instanceof Object[])
{
int h = 0;
Object[] entries = (Object[]) val;
final int len = entries.length;
for (int i=0; i < len; i += 2)
{
Object aKey = entries[i];
Object aValue = entries[i + 1];
h += computeKeyHashCode(aKey) ^ computeValueHashCode(aValue);
}
return h;
}
else if (val instanceof Map)
{
return val.hashCode();
}
else if (val == EMPTY_MAP)
{
return 0;
}
// size == 1
return computeKeyHashCode(getLogicalSingleKey()) ^ computeValueHashCode(getLogicalSingleValue());
}
public boolean equals(Object obj)
{
if (this == obj) return true;
if (!(obj instanceof Map)) return false;
Map, ?> other = (Map, ?>) obj;
if (size() != other.size()) return false;
if (val instanceof Object[])
{ // 2 to compactSize
for (Entry, ?> entry : other.entrySet())
{
final Object thatKey = entry.getKey();
if (!containsKey(thatKey))
{
return false;
}
Object thatValue = entry.getValue();
Object thisValue = get(thatKey);
if (thatValue == null || thisValue == null)
{ // Perform null checks
if (thatValue != thisValue)
{
return false;
}
}
else if (!thisValue.equals(thatValue))
{
return false;
}
}
}
else if (val instanceof Map)
{ // > compactSize
Map map = (Map) val;
return map.equals(other);
}
else if (val == EMPTY_MAP)
{ // empty
return other.isEmpty();
}
// size == 1
return entrySet().equals(other.entrySet());
}
public String toString()
{
Iterator> i = entrySet().iterator();
if (!i.hasNext())
{
return "{}";
}
StringBuilder sb = new StringBuilder();
sb.append('{');
for (;;)
{
Entry e = i.next();
K key = e.getKey();
V value = e.getValue();
sb.append(key == this ? "(this Map)" : key);
sb.append('=');
sb.append(value == this ? "(this Map)" : value);
if (!i.hasNext())
{
return sb.append('}').toString();
}
sb.append(',').append(' ');
}
}
public Set keySet()
{
return new AbstractSet()
{
public Iterator iterator()
{
if (useCopyIterator())
{
return new CopyKeyIterator();
}
else
{
return new CompactKeyIterator();
}
}
public int size() { return CompactMap.this.size(); }
public void clear() { CompactMap.this.clear(); }
public boolean contains(Object o) { return CompactMap.this.containsKey(o); } // faster than inherited method
public boolean remove(Object o)
{
final int size = size();
CompactMap.this.remove(o);
return size() != size;
}
public boolean removeAll(Collection c)
{
int size = size();
for (Object o : c)
{
CompactMap.this.remove(o);
}
return size() != size;
}
public boolean retainAll(Collection c)
{
// Create fast-access O(1) to all elements within passed in Collection
Map other = new CompactMap()
{ // Match outer
protected boolean isCaseInsensitive() { return CompactMap.this.isCaseInsensitive(); }
protected int compactSize() { return CompactMap.this.compactSize(); }
protected Map getNewMap() { return CompactMap.this.getNewMap(c.size()); }
};
for (Object o : c)
{
other.put((K)o, null);
}
final int size = size();
keySet().removeIf(key -> !other.containsKey(key));
return size() != size;
}
};
}
public Collection values()
{
return new AbstractCollection()
{
public Iterator iterator()
{
if (useCopyIterator())
{
return new CopyValueIterator();
}
else
{
return new CompactValueIterator();
}
}
public int size() { return CompactMap.this.size(); }
public void clear() { CompactMap.this.clear(); }
};
}
public Set> entrySet()
{
return new AbstractSet()
{
public Iterator> iterator()
{
if (useCopyIterator())
{
return new CopyEntryIterator();
}
else
{
return new CompactEntryIterator();
}
}
public int size() { return CompactMap.this.size(); }
public void clear() { CompactMap.this.clear(); }
public boolean contains(Object o)
{ // faster than inherited method
if (o instanceof Entry)
{
Entry entry = (Entry)o;
K entryKey = entry.getKey();
Object value = CompactMap.this.get(entryKey);
if (value != null)
{ // Found non-null value with key, return true if values are equals()
return Objects.equals(value, entry.getValue());
}
else if (CompactMap.this.containsKey(entryKey))
{
value = CompactMap.this.get(entryKey);
return Objects.equals(value, entry.getValue());
}
}
return false;
}
public boolean remove(Object o)
{
if (!(o instanceof Entry)) { return false; }
final int size = size();
Entry that = (Entry) o;
CompactMap.this.remove(that.getKey());
return size() != size;
}
/**
* This method is required. JDK method is broken, as it relies
* on iterator solution. This method is fast because contains()
* and remove() are both hashed O(1) look ups.
*/
public boolean removeAll(Collection c)
{
final int size = size();
for (Object o : c)
{
remove(o);
}
return size() != size;
}
public boolean retainAll(Collection c)
{
// Create fast-access O(1) to all elements within passed in Collection
Map other = new CompactMap()
{ // Match outer
protected boolean isCaseInsensitive() { return CompactMap.this.isCaseInsensitive(); }
protected int compactSize() { return CompactMap.this.compactSize(); }
protected Map getNewMap() { return CompactMap.this.getNewMap(c.size()); }
};
for (Object o : c)
{
if (o instanceof Entry)
{
other.put(((Entry)o).getKey(), ((Entry) o).getValue());
}
}
int origSize = size();
// Drop all items that are not in the passed in Collection
Iterator> i = entrySet().iterator();
while (i.hasNext())
{
Entry entry = i.next();
K key = entry.getKey();
V value = entry.getValue();
if (!other.containsKey(key))
{ // Key not even present, nuke the entry
i.remove();
}
else
{ // Key present, now check value match
Object v = other.get(key);
if (!Objects.equals(v, value))
{
i.remove();
}
}
}
return size() != origSize;
}
};
}
private Map getCopy()
{
Map copy = getNewMap(size()); // Use their Map (TreeMap, HashMap, LinkedHashMap, etc.)
if (val instanceof Object[])
{ // 2 to compactSize - copy Object[] into Map
Object[] entries = (Object[]) CompactMap.this.val;
final int len = entries.length;
for (int i=0; i < len; i += 2)
{
copy.put((K)entries[i], (V)entries[i + 1]);
}
}
else if (val instanceof Map)
{ // > compactSize - putAll to copy
copy.putAll((Map)CompactMap.this.val);
}
else if (val == EMPTY_MAP)
{ // empty - nothing to copy
}
else
{ // size == 1
copy.put(getLogicalSingleKey(), getLogicalSingleValue());
}
return copy;
}
private void iteratorRemove(Entry currentEntry, Iterator> i)
{
if (currentEntry == null)
{ // remove() called on iterator prematurely
throw new IllegalStateException("remove() called on an Iterator before calling next()");
}
remove(currentEntry.getKey());
}
public Map minus(Object removeMe)
{
throw new UnsupportedOperationException("Unsupported operation [minus] or [-] between Maps. Use removeAll() or retainAll() instead.");
}
public Map plus(Object right)
{
throw new UnsupportedOperationException("Unsupported operation [plus] or [+] between Maps. Use putAll() instead.");
}
protected enum LogicalValueType
{
EMPTY, OBJECT, ENTRY, MAP, ARRAY
}
protected LogicalValueType getLogicalValueType()
{
if (val instanceof Object[])
{ // 2 to compactSize
return LogicalValueType.ARRAY;
}
else if (val instanceof Map)
{ // > compactSize
return LogicalValueType.MAP;
}
else if (val == EMPTY_MAP)
{ // empty
return LogicalValueType.EMPTY;
}
else
{ // size == 1
if (CompactMapEntry.class.isInstance(val))
{
return LogicalValueType.ENTRY;
}
else
{
return LogicalValueType.OBJECT;
}
}
}
/**
* Marker Class to hold key and value when the key is not the same as the getSingleValueKey().
* This method transmits the setValue() changes to the outer CompactMap instance.
*/
public class CompactMapEntry extends AbstractMap.SimpleEntry
{
public CompactMapEntry(K key, V value)
{
super(key, value);
}
public V setValue(V value)
{
V save = this.getValue();
super.setValue(value);
CompactMap.this.put(getKey(), value); // "Transmit" (write-thru) to underlying Map.
return save;
}
public boolean equals(Object o)
{
if (!(o instanceof Map.Entry)) { return false; }
if (o == this) { return true; }
Map.Entry,?> e = (Map.Entry,?>)o;
return compareKeys(getKey(), e.getKey()) && Objects.equals(getValue(), e.getValue());
}
public int hashCode()
{
return computeKeyHashCode(getKey()) ^ computeValueHashCode(getValue());
}
}
protected int computeKeyHashCode(Object key)
{
if (key instanceof String)
{
if (isCaseInsensitive())
{
return StringUtilities.hashCodeIgnoreCase((String)key);
}
else
{ // k can't be null here (null is not instanceof String)
return key.hashCode();
}
}
else
{
int keyHash;
if (key == null)
{
return 0;
}
else
{
keyHash = key == CompactMap.this ? 37: key.hashCode();
}
return keyHash;
}
}
protected int computeValueHashCode(Object value)
{
if (value == CompactMap.this)
{
return 17;
}
else
{
return value == null ? 0 : value.hashCode();
}
}
private K getLogicalSingleKey()
{
if (CompactMapEntry.class.isInstance(val))
{
CompactMapEntry entry = (CompactMapEntry) val;
return entry.getKey();
}
return getSingleValueKey();
}
private V getLogicalSingleValue()
{
if (CompactMapEntry.class.isInstance(val))
{
CompactMapEntry entry = (CompactMapEntry) val;
return entry.getValue();
}
return (V) val;
}
/**
* @return String key name when there is only one entry in the Map.
*/
protected K getSingleValueKey() { return (K) "key"; };
/**
* @return new empty Map instance to use when size() becomes {@literal >} compactSize().
*/
protected Map getNewMap() { return new HashMap<>(compactSize() + 1); }
protected Map getNewMap(int size)
{
Map map = getNewMap();
try
{
Constructor> constructor = ReflectionUtils.getConstructor(map.getClass(), Integer.TYPE);
return (Map) constructor.newInstance(size);
}
catch (Exception e)
{
return map;
}
}
protected boolean isCaseInsensitive() { return false; }
protected int compactSize() { return 80; }
protected boolean useCopyIterator() {
Map newMap = getNewMap();
if (newMap instanceof CaseInsensitiveMap) {
newMap = ((CaseInsensitiveMap) newMap).getWrappedMap();
}
return newMap instanceof SortedMap;
}
/* ------------------------------------------------------------ */
// iterators
abstract class CompactIterator {
Iterator> mapIterator;
Object current; // Map.Entry if > compactsize, key <= compactsize
int expectedSize; // for fast-fail
int index; // current slot
CompactIterator() {
expectedSize = size();
current = EMPTY_MAP;
index = -1;
if (val instanceof Map) {
mapIterator = ((Map)val).entrySet().iterator();
}
}
public final boolean hasNext() {
if (mapIterator!=null) {
return mapIterator.hasNext();
}
else {
return (index+1) < size();
}
}
final void advance() {
if (expectedSize != size()) {
throw new ConcurrentModificationException();
}
if (++index>=size()) {
throw new NoSuchElementException();
}
if (mapIterator!=null) {
current = mapIterator.next();
}
else if (expectedSize==1) {
current = getLogicalSingleKey();
}
else {
current = ((Object [])val)[index*2];
}
}
public final void remove() {
if (current==EMPTY_MAP) {
throw new IllegalStateException();
}
if (size() != expectedSize)
throw new ConcurrentModificationException();
int newSize = expectedSize-1;
// account for the change in size
if (mapIterator!=null && newSize==compactSize()) {
current = ((Map.Entry)current).getKey();
mapIterator = null;
}
// perform the remove
if (mapIterator==null) {
CompactMap.this.remove(current);
} else {
mapIterator.remove();
}
index--;
current = EMPTY_MAP;
expectedSize--;
}
}
final class CompactKeyIterator extends CompactMap.CompactIterator implements Iterator
{
public final K next() {
advance();
if (mapIterator!=null) {
return ((Map.Entry)current).getKey();
} else {
return (K) current;
}
}
}
final class CompactValueIterator extends CompactMap.CompactIterator implements Iterator
{
public final V next() {
advance();
if (mapIterator != null) {
return ((Map.Entry) current).getValue();
} else if (expectedSize == 1) {
return getLogicalSingleValue();
} else {
return (V) ((Object[]) val)[(index*2) + 1];
}
}
}
final class CompactEntryIterator extends CompactMap.CompactIterator implements Iterator>
{
public final Map.Entry next() {
advance();
if (mapIterator != null) {
return (Map.Entry) current;
} else if (expectedSize == 1) {
if (val instanceof CompactMap.CompactMapEntry) {
return (CompactMapEntry) val;
}
else {
return new CompactMapEntry(getLogicalSingleKey(), getLogicalSingleValue());
}
} else {
Object [] objs = (Object []) val;
return new CompactMapEntry((K)objs[(index*2)],(V)objs[(index*2) + 1]);
}
}
}
abstract class CopyIterator {
Iterator> iter;
Entry currentEntry = null;
public CopyIterator() {
iter = getCopy().entrySet().iterator();
}
public final boolean hasNext() {
return iter.hasNext();
}
public final Entry nextEntry() {
currentEntry = iter.next();
return currentEntry;
}
public final void remove() {
iteratorRemove(currentEntry, iter);
currentEntry = null;
}
}
final class CopyKeyIterator extends CopyIterator
implements Iterator {
public K next() { return nextEntry().getKey(); }
}
final class CopyValueIterator extends CopyIterator
implements Iterator {
public V next() { return nextEntry().getValue(); }
}
final class CopyEntryIterator extends CompactMap.CopyIterator implements Iterator>
{
public Map.Entry next() { return nextEntry(); }
}
}