io.permazen.util.ImmutableNavigableMap Maven / Gradle / Ivy
Show all versions of permazen-util Show documentation
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package io.permazen.util;
import com.google.common.base.Preconditions;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.concurrent.ConcurrentNavigableMap;
/**
* An immutable {@link NavigableMap} implementation optimized for read efficiency.
*
*
* Because the keys and values are stored in arrays, it's also possible to get the key and/or value by index;
* see {@link #getKey getKey()}, {@link #getValue getValue()}, and {@link #getEntry getEntry()}.
*
* @param key type
* @param value type
*/
@SuppressWarnings("serial")
public class ImmutableNavigableMap extends AbstractNavigableMap implements ConcurrentNavigableMap {
private final K[] keys;
private final V[] vals;
private final int minIndex;
private final int maxIndex;
private final Comparator super K> comparator;
private final Comparator super K> actualComparator;
// Constructors
/**
* Constructor.
*
* @param source data source
* @throws IllegalArgumentException if {@code source} is null
*/
public ImmutableNavigableMap(NavigableMap source) {
this(source, checkNull(source).comparator());
}
/**
* Constructor.
*
* @param source data source
* @param comparator key comparator, or null for natural ordering
* @throws IllegalArgumentException if {@code source} is null
*/
@SuppressWarnings("unchecked")
public ImmutableNavigableMap(NavigableMap source, Comparator super K> comparator) {
this((K[])checkNull(source).keySet().toArray(), (V[])source.values().toArray(), comparator);
}
/**
* Convenience constructor.
*
*
* Equivalent to: {@code ImmutableNavigableMap(keys, vals, 0, Math.min(keys.length, vals.length), comparator)}.
*
* @param keys sorted key array; this array is not copied and must be already sorted
* @param vals value array corresponding to {@code keys}; this array is not copied
* @param comparator key comparator, or null for natural ordering
* @throws IllegalArgumentException if {@code keys} or {@code vals} is null
* @throws IllegalArgumentException if {@code keys} and {@code vals} have different lengths
*/
public ImmutableNavigableMap(K[] keys, V[] vals, Comparator super K> comparator) {
this(new Bounds<>(), checkNull(keys), checkNull(vals), 0, Math.min(keys.length, vals.length), comparator);
}
/**
* Primary constructor.
*
* @param keys sorted key array; this array is not copied and must be already sorted
* @param vals value array corresponding to {@code keys}; this array is not copied
* @param minIndex minimum index into arrays (inclusive)
* @param maxIndex maximum index into arrays (exclusive)
* @param comparator key comparator, or null for natural ordering
* @throws IllegalArgumentException if {@code keys} or {@code vals} is null
* @throws IllegalArgumentException if {@code keys} and {@code vals} has length less than {@code maxIndex}
* @throws IllegalArgumentException if {@code minIndex > maxIndex}
*/
public ImmutableNavigableMap(K[] keys, V[] vals, int minIndex, int maxIndex, Comparator super K> comparator) {
this(new Bounds<>(), keys, vals, minIndex, maxIndex, comparator);
}
@SuppressWarnings("unchecked")
private ImmutableNavigableMap(Bounds bounds, K[] keys, V[] vals,
int minIndex, int maxIndex, Comparator super K> comparator) {
super(bounds);
Preconditions.checkArgument(keys != null);
Preconditions.checkArgument(vals != null);
Preconditions.checkArgument(minIndex >= 0 && maxIndex >= minIndex);
Preconditions.checkArgument(keys.length >= maxIndex);
Preconditions.checkArgument(vals.length >= maxIndex);
this.keys = keys;
this.vals = vals;
this.minIndex = minIndex;
this.maxIndex = maxIndex;
this.comparator = comparator;
this.actualComparator = NavigableSets.comparatorOrNatural(this.comparator);
for (int i = minIndex + 1; i < maxIndex; i++)
assert this.actualComparator.compare(this.keys[i - 1], this.keys[i]) < 0;
}
// Extra Methods
/**
* Get the key at the specified index.
*
* @param index index into the ordered key array
* @return the key at the specified index
* @throws IndexOutOfBoundsException if {@code index} is negative or greater than or equal to {@link #size}
*/
public K getKey(int index) {
Objects.checkIndex(index, this.size());
return this.keys[this.minIndex + index];
}
/**
* Get the value at the specified index.
*
* @param index index into the ordered value array
* @return the value at the specified index
* @throws IndexOutOfBoundsException if {@code index} is negative or greater than or equal to {@link #size}
*/
public V getValue(int index) {
Objects.checkIndex(index, this.size());
return this.vals[this.minIndex + index];
}
/**
* Get the entry at the specified index.
*
* @param index index into the ordered entry array
* @return the entry at the specified index
* @throws IndexOutOfBoundsException if {@code index} is negative or greater than or equal to {@link #size}
*/
public Map.Entry getEntry(int index) {
Objects.checkIndex(index, this.size());
return this.createEntry(this.minIndex + index);
}
/**
* Search for the given element in the underlying array.
*
*
* This method works like {@link Arrays#binarySearch(Object[], Object) Arrays.binarySearch()}, returning
* either the index of {@code key} in the underlying array given to the constructor if found, or else
* the one's complement of {@code key}'s insertion point.
*
*
* The array searched is the array given to the constructor, or if {@link #ImmutableNavigableMap(NavigableMap)}
* was used, an array containing all of the keys in this map.
*
* @param key key to search for
* @return index of {@code key}, or {@code -(insertion point) - 1} if not found
*/
public int binarySearch(K key) {
return this.find(key);
}
// NavigableMap
@Override
public Comparator super K> comparator() {
return this.comparator;
}
@Override
public boolean isEmpty() {
return this.minIndex == this.maxIndex;
}
@Override
public int size() {
return this.maxIndex - this.minIndex;
}
@Override
public boolean containsKey(Object obj) {
return this.find(obj) >= 0;
}
@Override
@SuppressWarnings("unchecked")
public V get(Object obj) {
final int index = this.find(obj);
return index >= 0 ? this.vals[index] : null;
}
@Override
public List values() {
List list = Arrays.asList(this.vals);
if (this.minIndex > 0 || this.maxIndex < this.vals.length)
list = list.subList(this.minIndex, this.maxIndex);
return Collections.unmodifiableList(list);
}
@Override
public boolean containsValue(Object obj) {
for (int i = this.minIndex; i < this.maxIndex; i++) {
if (Objects.equals(obj, this.vals[i]))
return true;
}
return false;
}
@Override
public K firstKey() {
return this.keys[this.checkIndex(this.minIndex)];
}
@Override
public Map.Entry firstEntry() {
return !this.isEmpty() ? this.createEntry(this.minIndex) : null;
}
@Override
public K lastKey() {
return this.keys[this.checkIndex(this.maxIndex - 1)];
}
@Override
public Map.Entry lastEntry() {
return !this.isEmpty() ? this.createEntry(this.maxIndex - 1) : null;
}
@Override
public K lowerKey(K maxKey) {
return this.findKey(maxKey, -1, -1);
}
@Override
public Map.Entry lowerEntry(K maxKey) {
return this.findEntry(maxKey, -1, -1);
}
@Override
public K floorKey(K maxKey) {
return this.findKey(maxKey, -1, 0);
}
@Override
public Map.Entry floorEntry(K maxKey) {
return this.findEntry(maxKey, -1, 0);
}
@Override
public K higherKey(K minKey) {
return this.findKey(minKey, 0, 1);
}
@Override
public Map.Entry higherEntry(K minKey) {
return this.findEntry(minKey, 0, 1);
}
@Override
public K ceilingKey(K minKey) {
return this.findKey(minKey, 0, 0);
}
@Override
public Map.Entry ceilingEntry(K minKey) {
return this.findEntry(minKey, 0, 0);
}
@Override
public Map.Entry pollFirstEntry() {
throw new UnsupportedOperationException();
}
@Override
public Map.Entry pollLastEntry() {
throw new UnsupportedOperationException();
}
@Override
public NavigableSet navigableKeySet() {
return new ImmutableNavigableSet<>(this.bounds, this.keys, this.minIndex, this.maxIndex, this.comparator);
}
@Override
@SuppressWarnings("unchecked")
public ImmutableNavigableSet> entrySet() {
final int size = this.size();
final AbstractMap.SimpleImmutableEntry[] entries
= (AbstractMap.SimpleImmutableEntry[])new AbstractMap.SimpleImmutableEntry, ?>[size];
for (int i = this.minIndex; i < this.maxIndex; i++)
entries[i - this.minIndex] = this.createEntry(i);
return new ImmutableNavigableSet<>(entries, Comparator.comparing(Map.Entry::getKey, this.actualComparator));
}
// ConcurrentNavigableMap
@Override
public V putIfAbsent(K key, V value) {
throw new UnsupportedOperationException();
}
@Override
public V replace(K key, V value) {
throw new UnsupportedOperationException();
}
@Override
public boolean replace(K key, V oldValue, V newValue) {
throw new UnsupportedOperationException();
}
@Override
public boolean remove(Object key, Object value) {
throw new UnsupportedOperationException();
}
// Narrowing Overrides
@Override
public ImmutableNavigableMap descendingMap() {
return (ImmutableNavigableMap)super.descendingMap();
}
@Override
public ImmutableNavigableMap subMap(K minKey, K maxKey) {
return (ImmutableNavigableMap)super.subMap(minKey, maxKey);
}
@Override
public ImmutableNavigableMap headMap(K maxKey) {
return (ImmutableNavigableMap)super.headMap(maxKey);
}
@Override
public ImmutableNavigableMap tailMap(K minKey) {
return (ImmutableNavigableMap)super.tailMap(minKey);
}
@Override
public ImmutableNavigableMap headMap(K newMaxKey, boolean inclusive) {
return (ImmutableNavigableMap)super.headMap(newMaxKey, inclusive);
}
@Override
public ImmutableNavigableMap tailMap(K newMinKey, boolean inclusive) {
return (ImmutableNavigableMap)super.tailMap(newMinKey, inclusive);
}
@Override
public ImmutableNavigableMap subMap(K newMinKey, boolean minInclusive, K newMaxKey, boolean maxInclusive) {
return (ImmutableNavigableMap)super.subMap(newMinKey, minInclusive, newMaxKey, maxInclusive);
}
// AbstractNavigableMap
@Override
protected ImmutableNavigableMap createSubMap(boolean reverse, Bounds newBounds) {
// Get upper and lower bounds; note: "newBounds" are already reversed
final K minBound = reverse ? newBounds.getUpperBound() : newBounds.getLowerBound();
final K maxBound = reverse ? newBounds.getLowerBound() : newBounds.getUpperBound();
final BoundType minBoundType = reverse ? newBounds.getUpperBoundType() : newBounds.getLowerBoundType();
final BoundType maxBoundType = reverse ? newBounds.getLowerBoundType() : newBounds.getUpperBoundType();
// Calculate the index range in our current array corresponding to the new bounds
final int newMinIndex;
switch (minBoundType) {
case INCLUSIVE:
newMinIndex = this.findNearby(minBound, 0, 0);
break;
case EXCLUSIVE:
newMinIndex = this.findNearby(minBound, 0, 1);
break;
case NONE:
newMinIndex = this.minIndex;
break;
default:
throw new RuntimeException("internal error");
}
final int newMaxIndex;
switch (maxBoundType) {
case INCLUSIVE:
newMaxIndex = this.findNearby(maxBound, 0, 1);
break;
case EXCLUSIVE:
newMaxIndex = this.findNearby(maxBound, 0, 0);
break;
case NONE:
newMaxIndex = this.maxIndex;
break;
default:
throw new RuntimeException("internal error");
}
// Create new instance
if (reverse) {
final int newSize = newMaxIndex - newMinIndex;
return new ImmutableNavigableMap(newBounds,
ImmutableNavigableSet.reverseArray(Arrays.copyOfRange(this.keys, newMinIndex, newMaxIndex)),
ImmutableNavigableSet.reverseArray(Arrays.copyOfRange(this.vals, newMinIndex, newMaxIndex)),
0, newSize, ImmutableNavigableSet.reversedComparator(this.comparator));
} else
return new ImmutableNavigableMap(newBounds, this.keys, this.vals, newMinIndex, newMaxIndex, this.comparator);
}
private K findKey(Object key, int notFoundOffset, int foundOffset) {
final int index = this.findNearby(key, notFoundOffset, foundOffset);
if (index < this.minIndex || index >= this.maxIndex)
return null;
return this.keys[index];
}
private Map.Entry findEntry(Object key, int notFoundOffset, int foundOffset) {
final int index = this.findNearby(key, notFoundOffset, foundOffset);
if (index < this.minIndex || index >= this.maxIndex)
return null;
return this.createEntry(index);
}
private int findNearby(Object key, int notFoundOffset, int foundOffset) {
final int index = this.find(key);
return index < 0 ? ~index + notFoundOffset : index + foundOffset;
}
@SuppressWarnings("unchecked")
private int find(Object key) {
return Arrays.binarySearch(this.keys, this.minIndex, this.maxIndex, (K)key, this.actualComparator);
}
private int checkIndex(final int index) {
if (index < this.minIndex || index >= this.maxIndex)
throw new NoSuchElementException();
return index;
}
private static T checkNull(T obj) {
if (obj == null)
throw new IllegalArgumentException();
return obj;
}
private AbstractMap.SimpleImmutableEntry createEntry(int index) {
assert index >= this.minIndex && index < this.maxIndex;
return new AbstractMap.SimpleImmutableEntry<>(this.keys[index], this.vals[index]);
}
}