
com.cedarsoftware.util.CaseInsensitiveSet Maven / Gradle / Ivy
package com.cedarsoftware.util;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ConcurrentSkipListSet;
/**
* A {@link java.util.Set} implementation that performs case-insensitive comparisons for {@link String} elements,
* while preserving the original case of the strings. This set can contain both {@link String} and non-String elements,
* providing support for homogeneous and heterogeneous collections.
*
* Key Features
*
* - Case-Insensitive String Handling: For {@link String} elements, comparisons are performed
* in a case-insensitive manner, but the original case is preserved when iterating or retrieving elements.
* - Homogeneous and Heterogeneous Collections: Supports mixed types within the set, treating non-String
* elements as in a normal {@link Set}.
* - Customizable Backing Map: Allows specifying the underlying {@link java.util.Map} implementation,
* providing flexibility for use cases requiring custom performance or ordering guarantees.
* - Compatibility with Java Collections Framework: Fully implements the {@link Set} interface,
* supporting standard operations like {@code add()}, {@code remove()}, and {@code retainAll()}.
*
*
* Usage Examples
* {@code
* // Create a case-insensitive set
* CaseInsensitiveSet set = new CaseInsensitiveSet<>();
* set.add("Hello");
* set.add("HELLO"); // No effect, as "Hello" already exists
* System.out.println(set); // Outputs: [Hello]
*
* // Mixed types in the set
* CaseInsensitiveSet
*
* Backing Map Selection
*
* The backing map for this set can be customized using various constructors:
*
*
* - The default constructor uses a {@link CaseInsensitiveMap} with a {@link java.util.LinkedHashMap} backing
* to preserve insertion order.
* - Other constructors allow specifying the backing map explicitly or initializing the set from
* another collection.
*
*
* Deprecated Methods
*
* The following methods are deprecated and retained for backward compatibility:
*
*
* - {@code plus()}: Use {@link #addAll(Collection)} instead.
* - {@code minus()}: Use {@link #removeAll(Collection)} instead.
*
*
* Additional Notes
*
* - String comparisons are case-insensitive but preserve original case for iteration and output.
* - Thread safety depends on the thread safety of the chosen backing map.
* - Set operations like {@code contains()}, {@code add()}, and {@code remove()} rely on the
* behavior of the underlying {@link CaseInsensitiveMap}.
*
*
* @param the type of elements maintained by this set
* @see java.util.Set
* @see CaseInsensitiveMap
* @see java.util.LinkedHashMap
* @see java.util.TreeMap
* @see java.util.concurrent.ConcurrentSkipListSet
*
* @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 CaseInsensitiveSet implements Set {
private final Map map;
private static final Object PRESENT = new Object();
/**
* Constructs an empty {@code CaseInsensitiveSet} backed by a {@link CaseInsensitiveMap} with a default
* {@link java.util.LinkedHashMap} implementation.
*
* This constructor is useful for creating a case-insensitive set with predictable iteration order
* and default configuration.
*
*/
public CaseInsensitiveSet() {
map = new CaseInsensitiveMap<>();
}
/**
* Constructs a {@code CaseInsensitiveSet} containing the elements of the specified collection.
*
* The backing map is chosen based on the type of the input collection:
*
* - If the input collection is a {@code ConcurrentNavigableSetNullSafe}, the backing map is a {@code ConcurrentNavigableMapNullSafe}.
* - If the input collection is a {@code ConcurrentSkipListSet}, the backing map is a {@code ConcurrentSkipListMap}.
* - If the input collection is a {@code ConcurrentSet}, the backing map is a {@code ConcurrentHashMapNullSafe}.
* - If the input collection is a {@code SortedSet}, the backing map is a {@code TreeMap}.
* - For all other collection types, the backing map is a {@code LinkedHashMap} with an initial capacity based on the size of the input collection.
*
*
*
* @param collection the collection whose elements are to be placed into this set
* @throws NullPointerException if the specified collection is {@code null}
*/
public CaseInsensitiveSet(Collection extends E> collection) {
if (collection instanceof ConcurrentNavigableSetNullSafe) {
map = new CaseInsensitiveMap<>(new ConcurrentNavigableMapNullSafe<>());
} else if (collection instanceof ConcurrentSkipListSet) {
map = new CaseInsensitiveMap<>(new ConcurrentSkipListMap<>());
} else if (collection instanceof ConcurrentSet) {
map = new CaseInsensitiveMap<>(new ConcurrentHashMapNullSafe<>());
} else if (collection instanceof SortedSet) {
map = new CaseInsensitiveMap<>(new TreeMap<>()); // covers SortedSet or NavigableSet
} else {
map = new CaseInsensitiveMap<>(collection.size());
}
addAll(collection);
}
/**
* Constructs a {@code CaseInsensitiveSet} containing the elements of the specified collection,
* using the provided map as the backing implementation.
*
* This constructor allows full control over the underlying map implementation, enabling custom behavior
* for the set.
*
*
* @param source the collection whose elements are to be placed into this set
* @param backingMap the map to be used as the backing implementation
* @throws NullPointerException if the specified collection or map is {@code null}
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public CaseInsensitiveSet(Collection extends E> source, Map backingMap) {
map = backingMap;
addAll(source);
}
/**
* Constructs an empty {@code CaseInsensitiveSet} with the specified initial capacity.
*
* This constructor is useful for creating a set with a predefined capacity to reduce resizing overhead
* during population.
*
*
* @param initialCapacity the initial capacity of the backing map
* @throws IllegalArgumentException if the specified initial capacity is negative
*/
public CaseInsensitiveSet(int initialCapacity) {
map = new CaseInsensitiveMap<>(initialCapacity);
}
/**
* Constructs an empty {@code CaseInsensitiveSet} with the specified initial capacity and load factor.
*
* This constructor allows fine-grained control over the performance characteristics of the backing map.
*
*
* @param initialCapacity the initial capacity of the backing map
* @param loadFactor the load factor of the backing map, which determines when resizing occurs
* @throws IllegalArgumentException if the specified initial capacity is negative or if the load factor is
* non-positive
*/
public CaseInsensitiveSet(int initialCapacity, float loadFactor) {
map = new CaseInsensitiveMap<>(initialCapacity, loadFactor);
}
/**
* {@inheritDoc}
*
* For {@link String} elements, the hash code computation is case-insensitive, as it relies on the
* case-insensitive hash codes provided by the underlying {@link CaseInsensitiveMap}.
*
*/
@Override
public int hashCode() {
return map.keySet().hashCode();
}
/**
* {@inheritDoc}
*
* For {@link String} elements, equality is determined in a case-insensitive manner, ensuring that
* two sets containing equivalent strings with different cases (e.g., "Hello" and "hello") are considered equal.
*
*
* @param other the object to be compared for equality with this set
* @return {@code true} if the specified object is equal to this set
* @see Object#equals(Object)
*/
@SuppressWarnings("unchecked")
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (!(other instanceof Set)) {
return false;
}
Set that = (Set) other;
return that.size() == size() && containsAll(that);
}
/**
* {@inheritDoc}
*
* Returns the number of elements in this set. For {@link String} elements, the count is determined
* in a case-insensitive manner, ensuring that equivalent strings with different cases (e.g., "Hello" and "hello")
* are counted as a single element.
*
*
* @return the number of elements in this set
*/
@Override
public int size() {
return map.size();
}
/**
* {@inheritDoc}
*
* Returns {@code true} if this set contains no elements. For {@link String} elements, the check
* is performed in a case-insensitive manner, ensuring that equivalent strings with different cases
* are treated as a single element.
*
*
* @return {@code true} if this set contains no elements, {@code false} otherwise
*/
@Override
public boolean isEmpty() {
return map.isEmpty();
}
/**
* {@inheritDoc}
*
* Returns {@code true} if this set contains the specified element. For {@link String} elements,
* the check is performed in a case-insensitive manner, meaning that strings differing only by case
* (e.g., "Hello" and "hello") are considered equal.
*
*
* @param o the element whose presence in this set is to be tested
* @return {@code true} if this set contains the specified element, {@code false} otherwise
*/
@Override
public boolean contains(Object o) {
return map.containsKey(o);
}
/**
* {@inheritDoc}
*
* Returns an iterator over the elements in this set. For {@link String} elements, the iterator
* preserves the original case of the strings, even though the set performs case-insensitive
* comparisons.
*
*
* @return an iterator over the elements in this set
*/
@Override
public Iterator iterator() {
return map.keySet().iterator();
}
/**
* {@inheritDoc}
*
* Returns an array containing all the elements in this set. For {@link String} elements, the array
* preserves the original case of the strings, even though the set performs case-insensitive
* comparisons.
*
*
* @return an array containing all the elements in this set
*/
@Override
public Object[] toArray() {
return map.keySet().toArray();
}
/**
* {@inheritDoc}
*
* Returns an array containing all the elements in this set. The runtime type of the returned array
* is that of the specified array. For {@link String} elements, the array preserves the original
* case of the strings, even though the set performs case-insensitive comparisons.
*
*
* @param a the array into which the elements of the set are to be stored, if it is big enough;
* otherwise, a new array of the same runtime type is allocated for this purpose
* @return an array containing all the elements in this set
* @throws ArrayStoreException if the runtime type of the specified array is not a supertype of the runtime type
* of every element in this set
* @throws NullPointerException if the specified array is {@code null}
*/
@Override
public T[] toArray(T[] a) {
return map.keySet().toArray(a);
}
/**
* {@inheritDoc}
*
* Adds the specified element to this set if it is not already present. For {@link String} elements,
* the addition is case-insensitive, meaning that strings differing only by case (e.g., "Hello" and
* "hello") are considered equal, and only one instance is added to the set.
*
*
* @param e the element to be added to this set
* @return {@code true} if this set did not already contain the specified element
*/
@Override
public boolean add(E e) {
return map.putIfAbsent(e, PRESENT) == null;
}
/**
* {@inheritDoc}
*
* Removes the specified element from this set if it is present. For {@link String} elements, the
* removal is case-insensitive, meaning that strings differing only by case (e.g., "Hello" and "hello")
* are treated as equal, and removing any of them will remove the corresponding entry from the set.
*
*
* @param o the object to be removed from this set, if present
* @return {@code true} if this set contained the specified element
*/
@Override
public boolean remove(Object o) {
return map.remove(o) != null;
}
/**
* {@inheritDoc}
*
* Returns {@code true} if this set contains all of the elements in the specified collection. For
* {@link String} elements, the comparison is case-insensitive, meaning that strings differing only by
* case (e.g., "Hello" and "hello") are treated as equal.
*
*
* @param c the collection to be checked for containment in this set
* @return {@code true} if this set contains all of the elements in the specified collection
* @throws NullPointerException if the specified collection is {@code null}
*/
@Override
public boolean containsAll(Collection> c) {
for (Object o : c) {
if (!map.containsKey(o)) {
return false;
}
}
return true;
}
/**
* {@inheritDoc}
*
* Adds all the elements in the specified collection to this set if they're not already present.
* For {@link String} elements, the addition is case-insensitive, meaning that strings differing
* only by case (e.g., "Hello" and "hello") are treated as equal, and only one instance is added
* to the set.
*
*
* @param c the collection containing elements to be added to this set
* @return {@code true} if this set changed as a result of the call
* @throws NullPointerException if the specified collection is {@code null} or contains {@code null} elements
*/
@Override
public boolean addAll(Collection extends E> c) {
boolean modified = false;
for (E elem : c) {
if (add(elem)) { // Reuse the efficient add() method
modified = true;
}
}
return modified;
}
/**
* {@inheritDoc}
*
* Retains only the elements in this set that are contained in the specified collection.
* For {@link String} elements, the comparison is case-insensitive, meaning that strings
* differing only by case (e.g., "Hello" and "hello") are treated as equal.
*
*
* @param c the collection containing elements to be retained in this set
* @return {@code true} if this set changed as a result of the call
* @throws NullPointerException if the specified collection is {@code null}
*/
@Override
public boolean retainAll(Collection> c) {
Map other = new CaseInsensitiveMap<>();
for (Object o : c) {
@SuppressWarnings("unchecked")
E element = (E) o; // Safe cast because Map allows adding any type
other.put(element, PRESENT);
}
Iterator iterator = map.keySet().iterator();
boolean modified = false;
while (iterator.hasNext()) {
E elem = iterator.next();
if (!other.containsKey(elem)) {
iterator.remove();
modified = true;
}
}
return modified;
}
/**
* {@inheritDoc}
*
* Removes from this set all of its elements that are contained in the specified collection.
* For {@link String} elements, the removal is case-insensitive, meaning that strings differing
* only by case (e.g., "Hello" and "hello") are treated as equal, and removing any of them will
* remove the corresponding entry from the set.
*
*
* @param c the collection containing elements to be removed from this set
* @return {@code true} if this set changed as a result of the call
* @throws NullPointerException if the specified collection is {@code null}
*/
@Override
public boolean removeAll(Collection> c) {
boolean modified = false;
for (Object elem : c) {
@SuppressWarnings("unchecked")
E element = (E) elem; // Cast to E since map keys match the generic type
if (map.remove(element) != null) {
modified = true;
}
}
return modified;
}
/**
* {@inheritDoc}
*
* Removes all elements from this set. After this call, the set will be empty.
* For {@link String} elements, the case-insensitive behavior of the set has no impact
* on the clearing operation.
*
*/
@Override
public void clear() {
map.clear();
}
@Deprecated
public Set minus(Iterable removeMe) {
for (Object me : removeMe) {
remove(me);
}
return this;
}
@Deprecated
public Set minus(E removeMe) {
remove(removeMe);
return this;
}
@Deprecated
public Set plus(Iterable right) {
for (E item : right) {
add(item);
}
return this;
}
@Deprecated
public Set plus(Object right) {
add((E) right);
return this;
}
/**
* {@inheritDoc}
*
* Returns a string representation of this set. The string representation consists of a list of
* the set's elements in their original case, enclosed in square brackets ({@code "[]"}). For
* {@link String} elements, the original case is preserved, even though the set performs
* case-insensitive comparisons.
*
*
*
* The order of elements in the string representation matches the iteration order of the backing map.
*
*
* @return a string representation of this set
*/
@Override
public String toString() {
return map.keySet().toString();
}
}