![JAR search and dependency download from the Maven repository](/logo.png)
org.jhotdraw8.icollection.ChampMap Maven / Gradle / Ivy
Show all versions of org.jhotdraw8.icollection Show documentation
/*
* @(#)SimpleImmutableMap.java
* Copyright © 2023 The authors and contributors of JHotDraw. MIT License.
*/
package org.jhotdraw8.icollection;
import org.jhotdraw8.icollection.facade.ReadOnlySetFacade;
import org.jhotdraw8.icollection.immutable.ImmutableMap;
import org.jhotdraw8.icollection.impl.champmap.BitmapIndexedNode;
import org.jhotdraw8.icollection.impl.champmap.ChangeEvent;
import org.jhotdraw8.icollection.impl.champmap.EntryIterator;
import org.jhotdraw8.icollection.impl.champmap.Node;
import org.jhotdraw8.icollection.impl.iteration.IteratorSpliterator;
import org.jhotdraw8.icollection.impl.iteration.MappedIterator;
import org.jhotdraw8.icollection.readonly.ReadOnlyMap;
import org.jhotdraw8.icollection.readonly.ReadOnlySet;
import org.jhotdraw8.icollection.serialization.MapSerializationProxy;
import org.jspecify.annotations.Nullable;
import java.io.ObjectStreamException;
import java.io.Serial;
import java.io.Serializable;
import java.util.AbstractMap;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Spliterator;
/**
* Implements the {@link ImmutableMap} interface using a Compressed Hash-Array
* Mapped Prefix-tree (CHAMP).
*
* Features:
*
* - supports up to 231 - 1 entries
* - allows null keys and null values
* - is immutable
* - is thread-safe
* - does not guarantee a specific iteration order
*
*
* Performance characteristics:
*
* - put: O(log₃₂ N)
* - remove: O(log₃₂ N)
* - containsKey: O(log₃₂ N)
* - toMutable: O(1) + O(log₃₂ N) distributed across subsequent updates in the mutable copy
* - clone: O(1)
* - iterator.next(): O(1)
*
*
* Implementation details:
*
* This map performs read and write operations of single elements in O(log₃₂ N) time,
* and in O(log₃₂ N) space.
*
* The CHAMP trie contains nodes that may be shared with other maps.
*
* If a write operation is performed on a node, then this map creates a
* copy of the node and of all parent nodes up to the root (copy-path-on-write).
*
* This map can create a mutable copy of itself in O(1) time and O(1) space
* using method {@link #toMutable()}. The mutable copy shares its nodes
* with this map, until it has gradually replaced the nodes with exclusively
* owned nodes.
*
* All operations on this map can be performed concurrently, without a need for
* synchronisation.
*
* The immutable version of this map extends from the non-public class
* {@code ChampBitmapIndexNode}. This design safes 16 bytes for every instance,
* and reduces the number of redirections for finding an element in the
* collection by 1.
*
* References:
*
* Portions of the code in this class has been derived from 'The Capsule Hash Trie Collections Library'.
*
* - Michael J. Steindorfer (2017).
* Efficient Immutable Collections.
* - michael.steindorfer.name
*
- The Capsule Hash Trie Collections Library.
*
Copyright (c) Michael Steindorfer. BSD-2-Clause License
* - github.com
*
*
* @param the key type
* @param the value type
*/
@SuppressWarnings("exports")
public class ChampMap
implements ImmutableMap, Serializable {
private static final ChampMap, ?> EMPTY = new ChampMap<>(BitmapIndexedNode.emptyNode(), 0);
@Serial
private static final long serialVersionUID = 0L;
/**
* We do not guarantee an iteration order. Make sure that nobody accidentally relies on it.
*/
static final int SALT = new Random().nextInt();
final transient BitmapIndexedNode root;
final int size;
/**
* Creates a new instance with the provided privateData data object.
*
* This constructor is intended to be called from a constructor
* of the subclass, that is called from method {@link #newInstance(PrivateData)}.
*
* @param privateData an privateData data object
*/
@SuppressWarnings("unchecked")
protected ChampMap(PrivateData privateData) {
this(((Map.Entry, ?>) privateData.get()).getKey(), ((Map.Entry, Integer>) privateData.get()).getValue());
}
/**
* Creates a new instance with the provided privateData object as its internal data structure.
*
* Subclasses must override this method, and return a new instance of their subclass!
*
* @param privateData the internal data structure needed by this class for creating the instance.
* @return a new instance of the subclass
*/
protected ChampMap newInstance(PrivateData privateData) {
return new ChampMap<>(privateData);
}
private ChampMap newInstance(BitmapIndexedNode root, int size) {
return newInstance(new PrivateData(new AbstractMap.SimpleImmutableEntry<>(root, size)));
}
ChampMap(BitmapIndexedNode root, int size) {
this.root = root;
this.size = size;
}
@Override
public int characteristics() {
return Spliterator.IMMUTABLE | Spliterator.SIZED | Spliterator.DISTINCT;
}
/**
* Returns an immutable copy of the provided map.
*
* @param c a map
* @param the key type
* @param the value type
* @return an immutable copy
*/
@SuppressWarnings("unchecked")
public static ChampMap copyOf(Iterable extends Map.Entry extends K, ? extends V>> c) {
return ChampMap.of().putAll(c);
}
/**
* Returns an immutable copy of the provided map.
*
* @param map a map
* @param the key type
* @param the value type
* @return an immutable copy
*/
public static ChampMap copyOf(Map extends K, ? extends V> map) {
return ChampMap.of().putAll(map);
}
static boolean entryKeyEquals(SimpleImmutableEntry a, SimpleImmutableEntry b) {
return Objects.equals(a.getKey(), b.getKey());
}
static int keyHash(Object e) {
return SALT ^ Objects.hashCode(e);
}
static int entryKeyHash(SimpleImmutableEntry e) {
return SALT ^ keyHash(e.getKey());
}
/**
* Returns an empty immutable map.
*
* @param the key type
* @param the value type
* @return an empty immutable map
*/
@SuppressWarnings("unchecked")
public static ChampMap of() {
return (ChampMap) ChampMap.EMPTY;
}
/**
* {@inheritDoc}
*/
@Override
public ChampMap clear() {
return isEmpty() ? this : of();
}
/**
* {@inheritDoc}
*/
@Override
public boolean containsKey(@Nullable Object o) {
@SuppressWarnings("unchecked") final K key = (K) o;
return root.findByKey(key, keyHash(key), 0) != Node.NO_DATA;
}
@Override
public boolean equals(@Nullable Object other) {
if (other == this) {
return true;
}
if (other instanceof ChampMap, ?> that) {
return size == that.size && root.equivalent(that.root);
}
return ReadOnlyMap.mapEquals(this, other);
}
@Override
@SuppressWarnings("unchecked")
public @Nullable V get(Object o) {
Object result = root.findByKey((K) o, keyHash(o), 0);
return result == Node.NO_DATA ? null : (V) result;
}
/**
* Update function for a map: we keep the old entry if it has the same
* value as the new entry.
*
* @param oldv the old entry
* @param newv the new entry
* @param the key type
* @param the value type
* @return the old or the new entry
*/
@Nullable
static SimpleImmutableEntry updateEntry(@Nullable SimpleImmutableEntry oldv, @Nullable SimpleImmutableEntry newv) {
return Objects.equals(oldv.getValue(), newv.getValue()) ? oldv : newv;
}
@Override
public int hashCode() {
return ReadOnlyMap.iteratorToHashCode(iterator());
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public Iterator> iterator() {
return new EntryIterator<>(root, null, null);
}
@Override
public int maxSize() {
return Integer.MAX_VALUE;
}
@Override
public ChampMap put(K key, @Nullable V value) {
var details = new ChangeEvent();
var newRootNode = root.put(null, key, value,
keyHash(key), 0, details, ChampMap::keyHash);
if (details.isModified()) {
return newInstance(newRootNode, details.isReplaced() ? size : size + 1);
}
return this;
}
@Override
public ChampMap putAll(Map extends K, ? extends V> m) {
return (ChampMap) ImmutableMap.super.putAll(m);
}
@SuppressWarnings("unchecked")
@Override
public ChampMap putAll(Iterable extends Map.Entry extends K, ? extends V>> c) {
var m = toMutable();
return m.putAll(c) ? m.toImmutable() : this;
}
@Override
public ChampMap remove(K key) {
int keyHash = keyHash(key);
var details = new ChangeEvent();
var newRootNode = root.remove(null, key, keyHash, 0, details);
if (details.isModified()) {
return size == 1 ? ChampMap.of() : newInstance(newRootNode, size - 1);
}
return this;
}
@SuppressWarnings("unchecked")
@Override
public ChampMap removeAll(Iterable extends K> c) {
var m = toMutable();
return m.removeAll(c) ? m.toImmutable() : this;
}
@SuppressWarnings("unchecked")
@Override
public ChampMap retainAll(Iterable extends K> c) {
var m = toMutable();
return m.retainAll(c) ? m.toImmutable() : this;
}
@Override
public ReadOnlySet readOnlyKeySet() {
return new ReadOnlySetFacade<>(
() -> new MappedIterator<>(new EntryIterator<>(root, null, null), Map.Entry::getKey),
this::size,
this::containsKey,
Spliterator.IMMUTABLE);
}
@Override
public int size() {
return size;
}
public Spliterator> spliterator() {
return new IteratorSpliterator<>(iterator(), size(), characteristics(), null);
}
/**
* Creates a mutable copy of this map.
*
* @return a mutable CHAMP map
*/
@Override
public MutableChampMap toMutable() {
return new MutableChampMap<>(this);
}
@Override
public MutableChampMap asMap() {
return new MutableChampMap<>(this);
}
/**
* Returns a string representation of this map.
*
* The string representation is consistent with the one produced
* by {@link AbstractMap#toString()}.
*
* @return a string representation
*/
@Override
public String toString() {
return ReadOnlyMap.mapToString(this);
}
@Serial
private Object writeReplace() throws ObjectStreamException {
return new SerializationProxy<>(this.toMutable());
}
static class SerializationProxy extends MapSerializationProxy {
@Serial
private static final long serialVersionUID = 0L;
SerializationProxy(Map target) {
super(target);
}
@Serial
@Override
protected Object readResolve() {
return ChampMap.of().putAll(deserializedEntries);
}
}
}