
com.cedarsoftware.util.CompactSet Maven / Gradle / Ivy
package com.cedarsoftware.util;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Comparator;
/**
* A memory-efficient Set implementation that internally uses {@link CompactMap}.
*
* This implementation provides the same memory benefits as CompactMap while
* maintaining proper Set semantics. It can be configured for:
*
* - Case sensitivity for String elements
* - Element ordering (sorted, reverse, insertion)
* - Custom compact size threshold
*
*
*
* Creating a CompactSet
* Typically you will create one of the provided subclasses
* ({@link CompactLinkedSet}, {@link CompactCIHashSet}, or
* {@link CompactCILinkedSet}) or extend {@code CompactSet} with your own
* configuration. The builder pattern is available for advanced cases
* when running on a JDK.
* {@code
* CompactLinkedSet set = new CompactLinkedSet<>();
* set.add("hello");
*
* // Builder pattern (requires JDK)
* CompactSet custom = CompactSet.builder()
* .caseSensitive(false)
* .sortedOrder()
* .build();
* }
*
* @param the type of elements maintained by this set
*
* @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.
*/
public class CompactSet implements Set {
/**
* A special marker object stored in the map for each key.
* Using a single static instance to avoid per-entry overhead.
*/
private static final Object PRESENT = new Object();
/**
* The one and only data structure: a CompactMap whose keys represent the set elements.
*/
private final CompactMap map;
/**
* Constructs an empty CompactSet with the default configuration (i.e., default CompactMap).
*
* This uses the no-arg CompactMap constructor, which typically yields:
*
* - caseSensitive = true
* - compactSize = 50
* - unordered
*
*
* If you want custom config, use the {@link Builder} instead.
*
* @throws IllegalStateException if {@link #compactSize()} returns a value less than 2
*/
public CompactSet() {
CompactMap defaultMap;
if (ReflectionUtils.isJavaCompilerAvailable()) {
defaultMap = CompactMap.builder()
.compactSize(this.compactSize())
.caseSensitive(!isCaseInsensitive())
.build();
} else {
defaultMap = createSimpleMap(!isCaseInsensitive(), compactSize(), CompactMap.UNORDERED);
}
if (defaultMap.compactSize() < 2) {
throw new IllegalStateException("compactSize() must be >= 2");
}
this.map = defaultMap;
}
/**
* Constructs a CompactSet with a pre-existing CompactMap (usually from a builder).
*
* @param map the underlying CompactMap to store elements
*/
protected CompactSet(CompactMap map) {
if (map.compactSize() < 2) {
throw new IllegalStateException("compactSize() must be >= 2");
}
this.map = map;
}
/**
* Constructs a CompactSet containing the elements of the specified collection,
* using the default CompactMap configuration.
*
* @param c the collection whose elements are to be placed into this set
* @throws NullPointerException if the specified collection is null
*/
public CompactSet(Collection extends E> c) {
this();
addAll(c);
}
public boolean isDefaultCompactSet() {
// Delegate to the underlying map since the logic is identical
return map.isDefaultCompactMap();
}
/* ----------------------------------------------------------------- */
/* Implementation of Set methods */
/* ----------------------------------------------------------------- */
@Override
public int size() {
return map.size();
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean contains(Object o) {
return map.containsKey(o);
}
@Override
public boolean add(E e) {
// If map.put(e, PRESENT) returns null, the key was not in the map
// => we effectively added a new element => return true
// else we replaced an existing key => return false (no change)
return map.put(e, PRESENT) == null;
}
@Override
public boolean remove(Object o) {
// If map.remove(o) != null, the key existed => return true
// else the key wasn't there => return false
return map.remove(o) != null;
}
@Override
public void clear() {
map.clear();
}
@Override
public boolean containsAll(Collection> c) {
// We can just leverage map.keySet().containsAll(...)
return map.keySet().containsAll(c);
}
@Override
public boolean addAll(Collection extends E> c) {
boolean modified = false;
for (E e : c) {
if (add(e)) {
modified = true;
}
}
return modified;
}
@Override
public boolean retainAll(Collection> c) {
// Again, rely on keySet() to do the heavy lifting
return map.keySet().retainAll(c);
}
@Override
public boolean removeAll(Collection> c) {
return map.keySet().removeAll(c);
}
@Override
public Iterator iterator() {
// We can simply return map.keySet().iterator()
return map.keySet().iterator();
}
@Override
public Object[] toArray() {
return map.keySet().toArray();
}
@Override
public T[] toArray(T[] a) {
return map.keySet().toArray(a);
}
/* ----------------------------------------------------------------- */
/* Object overrides (equals, hashCode, etc.) */
/* ----------------------------------------------------------------- */
@Override
public boolean equals(Object o) {
// Let keySet() handle equality checks for us
return map.keySet().equals(o);
}
@Override
public int hashCode() {
return map.keySet().hashCode();
}
@Override
public String toString() {
return map.keySet().toString();
}
/**
* Returns a builder for creating customized CompactSet instances.
* This API generates subclasses at runtime and therefore requires
* the JDK compiler tools to be present.
*
* @param the type of elements in the set
* @return a new Builder instance
*/
public static Builder builder() {
return new Builder<>();
}
/**
* Builder for creating CompactSet instances with custom configurations.
*
* Internally, the builder configures a {@link CompactMap} (with <E, Object>).
*/
public static final class Builder {
private final CompactMap.Builder mapBuilder;
private boolean caseSensitive = CompactMap.DEFAULT_CASE_SENSITIVE;
private int compactSize = CompactMap.DEFAULT_COMPACT_SIZE;
private String ordering = CompactMap.UNORDERED;
private Builder() {
this.mapBuilder = CompactMap.builder();
}
/**
* Sets whether String elements should be compared case-sensitively.
* @param caseSensitive if false, do case-insensitive compares
*/
public Builder caseSensitive(boolean caseSensitive) {
this.caseSensitive = caseSensitive;
mapBuilder.caseSensitive(caseSensitive);
return this;
}
/**
* Sets the maximum size for compact array storage.
*/
public Builder compactSize(int size) {
this.compactSize = size;
mapBuilder.compactSize(size);
return this;
}
/**
* Configures the set to maintain elements in natural sorted order.
* Requires elements to be {@link Comparable}
*/
public Builder sortedOrder() {
this.ordering = CompactMap.SORTED;
mapBuilder.sortedOrder();
return this;
}
/**
* Configures the set to maintain elements in reverse sorted order.
* Requires elements to be {@link Comparable}
*/
public Builder reverseOrder() {
this.ordering = CompactMap.REVERSE;
mapBuilder.reverseOrder();
return this;
}
/**
* Configures the set to maintain elements in insertion order.
*/
public Builder insertionOrder() {
this.ordering = CompactMap.INSERTION;
mapBuilder.insertionOrder();
return this;
}
/**
* Configures the set to maintain elements in no specific order, like a HashSet.
*/
public Builder noOrder() {
this.ordering = CompactMap.UNORDERED;
mapBuilder.noOrder();
return this;
}
/**
* Creates a new CompactSet with the configured options.
*/
public CompactSet build() {
CompactMap builtMap;
if (ReflectionUtils.isJavaCompilerAvailable()) {
builtMap = mapBuilder.build();
} else {
builtMap = createSimpleMap(caseSensitive, compactSize, ordering);
}
return new CompactSet<>(builtMap);
}
}
/**
* Allow concrete subclasses to specify the compact size. Concrete subclasses are useful to simplify
* serialization.
*/
protected int compactSize() {
// Default is 50. Override if a different threshold is desired.
return CompactMap.DEFAULT_COMPACT_SIZE;
}
/**
* Allow concrete subclasses to specify the case-sensitivity. Concrete subclasses are useful to simplify
* serialization.
*/
protected boolean isCaseInsensitive() {
return false; // default to case-sensitive, for legacy
}
/**
* Returns the configuration settings of this CompactSet.
*
* The returned map contains the following keys:
*
* - {@link CompactMap#COMPACT_SIZE} - Maximum size before switching to backing map
* - {@link CompactMap#CASE_SENSITIVE} - Whether string elements are case-sensitive
* - {@link CompactMap#ORDERING} - Element ordering strategy
*
*
*
* @return an unmodifiable map containing the configuration settings
*/
public Map getConfig() {
// Get the underlying map's config but filter out map-specific details
Map mapConfig = map.getConfig();
// Create a new map with only the Set-relevant configuration
Map setConfig = new LinkedHashMap<>();
setConfig.put(CompactMap.COMPACT_SIZE, mapConfig.get(CompactMap.COMPACT_SIZE));
setConfig.put(CompactMap.CASE_SENSITIVE, mapConfig.get(CompactMap.CASE_SENSITIVE));
setConfig.put(CompactMap.ORDERING, mapConfig.get(CompactMap.ORDERING));
return Collections.unmodifiableMap(setConfig);
}
public CompactSet withConfig(Map config) {
Convention.throwIfNull(config, "config cannot be null");
// Start with a builder
Builder builder = CompactSet.builder();
// Get current configuration from the underlying map
Map currentConfig = map.getConfig();
// Handle compactSize with proper priority
Integer configCompactSize = (Integer) config.get(CompactMap.COMPACT_SIZE);
Integer currentCompactSize = (Integer) currentConfig.get(CompactMap.COMPACT_SIZE);
int compactSizeToUse = (configCompactSize != null) ? configCompactSize : currentCompactSize;
builder.compactSize(compactSizeToUse);
// Handle caseSensitive with proper priority
Boolean configCaseSensitive = (Boolean) config.get(CompactMap.CASE_SENSITIVE);
Boolean currentCaseSensitive = (Boolean) currentConfig.get(CompactMap.CASE_SENSITIVE);
boolean caseSensitiveToUse = (configCaseSensitive != null) ? configCaseSensitive : currentCaseSensitive;
builder.caseSensitive(caseSensitiveToUse);
// Handle ordering with proper priority
String configOrdering = (String) config.get(CompactMap.ORDERING);
String currentOrdering = (String) currentConfig.get(CompactMap.ORDERING);
String orderingToUse = (configOrdering != null) ? configOrdering : currentOrdering;
// Apply the determined ordering
applyOrdering(builder, orderingToUse);
// Build and populate the new set
CompactSet newSet = builder.build();
newSet.addAll(this);
return newSet;
}
private void applyOrdering(Builder builder, String ordering) {
if (ordering == null) {
builder.noOrder(); // Default to no order if somehow null
return;
}
switch (ordering) {
case CompactMap.SORTED:
builder.sortedOrder();
break;
case CompactMap.REVERSE:
builder.reverseOrder();
break;
case CompactMap.INSERTION:
builder.insertionOrder();
break;
default:
builder.noOrder();
}
}
static CompactMap createSimpleMap(boolean caseSensitive, int size, String ordering) {
return new CompactMap() {
@Override
protected boolean isCaseInsensitive() {
return !caseSensitive;
}
@Override
protected int compactSize() {
return size;
}
@Override
protected String getOrdering() {
return ordering;
}
@Override
protected Map getNewMap() {
int cap = size + 1;
boolean ci = !caseSensitive;
switch (ordering) {
case CompactMap.INSERTION:
return ci ? new CaseInsensitiveMap<>(Collections.emptyMap(), new LinkedHashMap<>(cap))
: new LinkedHashMap<>(cap);
case CompactMap.SORTED:
case CompactMap.REVERSE:
Comparator