All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.cedarsoftware.util.CompactSet Maven / Gradle / Ivy

The newest version!
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 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 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 comp = new CompactMap.CompactMapComparator(ci, CompactMap.REVERSE.equals(ordering)); Map tree = new TreeMap<>(comp); return ci ? new CaseInsensitiveMap<>(Collections.emptyMap(), tree) : tree; default: return ci ? new CaseInsensitiveMap<>(Collections.emptyMap(), new HashMap<>(cap)) : new HashMap<>(cap); } } }; } }