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

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

The newest version!
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 mixedSet = new CaseInsensitiveSet<>();
 * mixedSet.add("Apple");
 * mixedSet.add(123);
 * mixedSet.add("apple"); // No effect, as "Apple" already exists
 * System.out.println(mixedSet); // Outputs: [Apple, 123]
 * }
 *
 * 

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 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 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 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(); } }