From 476c3ce9ffe4fb19d17f3ef863995f53bbd7e821 Mon Sep 17 00:00:00 2001 From: John DeRegnaucourt Date: Thu, 9 Jan 2025 01:03:34 -0500 Subject: [PATCH] - CompactMap now properly caches the classes it generates. It also locks at a per classname level, increasing parallel execution. - More Javadoc updates. --- README.md | 27 +--- .../com/cedarsoftware/util/CompactMap.java | 118 +++++++++----- .../cedarsoftware/util/ReflectionUtils.java | 36 ++--- .../cedarsoftware/util/CompactMapTest.java | 20 +-- .../cedarsoftware/util/CompactSetTest.java | 22 +-- userguide.md | 149 ++++++++++++++++++ 6 files changed, 255 insertions(+), 117 deletions(-) create mode 100644 userguide.md diff --git a/README.md b/README.md index 6e1ae000..47e2426b 100644 --- a/README.md +++ b/README.md @@ -48,31 +48,8 @@ implementation 'com.cedarsoftware:java-util:2.18.0' ### Sets -- **[CompactSet](/src/main/java/com/cedarsoftware/util/CompactSet.java)** - Memory-efficient Set that dynamically adapts its storage structure based on size
Details - - A memory-efficient `Set` implementation that dynamically adapts its internal storage structure based on size: - - **Key Features:** - - Dynamic Storage Transitions: - - Empty state: Minimal memory footprint - - Single element: Optimized single-reference storage - - Small sets (2 to N elements): Efficient array-based storage - - Large sets: Automatic transition to full Set implementation - - **Example Usage:** - ```java - // Basic usage - CompactSet set = new CompactSet<>(); - - // With custom transition size - CompactSet set = new CompactSet<>(10); - - // Case-insensitive set - CompactSet caseInsensitive = CompactSet.createCaseInsensitiveSet(); - ``` -
- -- **[CaseInsensitiveSet](/src/main/java/com/cedarsoftware/util/CaseInsensitiveSet.java)** - Set implementation with case-insensitive String handling +- **[CompactSet](userguide.md#compactset)** - Memory-efficient Set that dynamically adapts its storage structure based on size +- **[CaseInsensitiveSet](userguide.md#caseinsensitiveset)** - Set implementation with case-insensitive String handling - **[ConcurrentSet](/src/main/java/com/cedarsoftware/util/ConcurrentSet.java)** - Thread-safe Set supporting null elements - **[ConcurrentNavigableSetNullSafe](/src/main/java/com/cedarsoftware/util/ConcurrentNavigableSetNullSafe.java)** - Thread-safe NavigableSet supporting null elements diff --git a/src/main/java/com/cedarsoftware/util/CompactMap.java b/src/main/java/com/cedarsoftware/util/CompactMap.java index 30caacb0..b6715ad6 100644 --- a/src/main/java/com/cedarsoftware/util/CompactMap.java +++ b/src/main/java/com/cedarsoftware/util/CompactMap.java @@ -34,6 +34,8 @@ import java.util.SortedMap; import java.util.TreeMap; import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; /** @@ -250,6 +252,8 @@ public class CompactMap implements Map { private static final Class DEFAULT_MAP_TYPE = HashMap.class; private static final String DEFAULT_SINGLE_KEY = "id"; private static final String INNER_MAP_TYPE = "innerMapType"; + private static final TemplateClassLoader templateClassLoader = new TemplateClassLoader(ClassUtilities.getClassLoader(CompactMap.class)); + private static final Map CLASS_LOCKS = new ConcurrentHashMap<>(); // The only "state" and why this is a compactMap - one member variable protected Object val = EMPTY_MAP; @@ -2167,12 +2171,11 @@ private static final class TemplateGenerator { private static Class getOrCreateTemplateClass(Map options) { String className = generateClassName(options); try { - return ClassUtilities.getClassLoader().loadClass(className); + return templateClassLoader.loadClass(className); } catch (ClassNotFoundException e) { return generateTemplateClass(options); } } - /** * Generates a unique class name encoding the configuration options. *

@@ -2237,7 +2240,8 @@ private static String generateClassName(Map options) { /** * Creates a new template class for the specified configuration options. *

- * This synchronized method: + * This method effectively synchronizes on the class name to ensure that only one thread can be + * compiling a particular class, but multiple threads can compile different classes concurrently. *

    *
  • Double-checks if class was created while waiting for lock
  • *
  • Generates source code for the template class
  • @@ -2249,21 +2253,34 @@ private static String generateClassName(Map options) { * @return the newly generated and compiled template Class * @throws IllegalStateException if compilation fails or class cannot be loaded */ - private static synchronized Class generateTemplateClass(Map options) { - // Double-check if class was created while waiting for lock + private static Class generateTemplateClass(Map options) { + // Determine the target class name String className = generateClassName(options); + + // Acquire (or create) a lock dedicated to this className + ReentrantLock lock = CLASS_LOCKS.computeIfAbsent(className, k -> new ReentrantLock()); + + lock.lock(); try { - return ClassUtilities.getClassLoader().loadClass(className); - } catch (ClassNotFoundException ignored) { - // Generate source code + // --- Double-check if class was created while waiting for lock --- + try { + return ClassUtilities.getClassLoader(CompactMap.class).loadClass(className); + } catch (ClassNotFoundException ignored) { + // Not found, proceed with generation + } + + // --- Generate source code --- String sourceCode = generateSourceCode(className, options); - // Compile source code using JavaCompiler + // --- Compile the source code using JavaCompiler --- Class templateClass = compileClass(className, sourceCode); return templateClass; } + finally { + lock.unlock(); + } } - + /** * Generates Java source code for a CompactMap template class. *

    @@ -2679,14 +2696,7 @@ public OutputStream openOutputStream() { * @throws LinkageError if class definition fails */ private static Class defineClass(String className, byte[] classBytes) { - // Use ClassUtilities to get the most appropriate ClassLoader - ClassLoader parentLoader = ClassUtilities.getClassLoader(CompactMap.class); - - // Create our template class loader - TemplateClassLoader loader = new TemplateClassLoader(parentLoader); - - // Define the class using our custom loader - return loader.defineTemplateClass(className, classBytes); + return templateClassLoader.defineTemplateClass(className, classBytes); } } @@ -2703,10 +2713,32 @@ private static Class defineClass(String className, byte[] classBytes) { * Internal implementation detail of the template generation system. */ private static final class TemplateClassLoader extends ClassLoader { + private final Map> definedClasses = new ConcurrentHashMap<>(); + private final Map classLoadLocks = new ConcurrentHashMap<>(); + private TemplateClassLoader(ClassLoader parent) { super(parent); } + public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + // 1. Check if we already loaded it + Class c = findLoadedClass(name); + if (c == null) { + try { + // 2. Parent-first + c = getParent().loadClass(name); + } + catch (ClassNotFoundException e) { + // 3. If the parent can't find it, attempt local + c = findClass(name); + } + } + if (resolve) { + resolveClass(c); + } + return c; + } + /** * Defines or retrieves a template class in this ClassLoader. *

    @@ -2720,15 +2752,25 @@ private TemplateClassLoader(ClassLoader parent) { * @throws LinkageError if class definition fails */ private Class defineTemplateClass(String name, byte[] bytes) { - // First try to load from parent + ReentrantLock lock = classLoadLocks.computeIfAbsent(name, k -> new ReentrantLock()); + lock.lock(); try { - return findClass(name); - } catch (ClassNotFoundException e) { - // If not found, define it - return defineClass(name, bytes, 0, bytes.length); + // Check if already defined + Class cached = definedClasses.get(name); + if (cached != null) { + return cached; + } + + // Define new class + Class definedClass = defineClass(name, bytes, 0, bytes.length); + definedClasses.put(name, definedClass); + return definedClass; + } + finally { + lock.unlock(); } } - + /** * Finds the specified class using appropriate ClassLoader. *

    @@ -2745,20 +2787,24 @@ private Class defineTemplateClass(String name, byte[] bytes) { */ @Override protected Class findClass(String name) throws ClassNotFoundException { - // First try parent classloader for any non-template classes - if (!name.startsWith("com.cedarsoftware.util.CompactMap$")) { - // Use the thread context classloader for test classes - ClassLoader classLoader = ClassUtilities.getClassLoader(); - if (classLoader != null) { - try { - return classLoader.loadClass(name); - } catch (ClassNotFoundException e) { - // Fall through to try parent loader - } + // For your "template" classes: + if (name.startsWith("com.cedarsoftware.util.CompactMap$")) { + // Check if we have it cached + Class cached = definedClasses.get(name); + if (cached != null) { + return cached; } - return getParent().loadClass(name); + // If we don't, we can throw ClassNotFoundException or + // your code might dynamically generate the class at this point. + // Typically, you'd have a method to define it: + // return defineTemplateClassDynamically(name); + + throw new ClassNotFoundException("Not found: " + name); } - throw new ClassNotFoundException(name); + + // Fallback: if it's not a template, let the system handle it + // (i.e. you can call super, or also do TCCL checks if you want). + return super.findClass(name); } } diff --git a/src/main/java/com/cedarsoftware/util/ReflectionUtils.java b/src/main/java/com/cedarsoftware/util/ReflectionUtils.java index 4b6489bb..a04cdcb0 100644 --- a/src/main/java/com/cedarsoftware/util/ReflectionUtils.java +++ b/src/main/java/com/cedarsoftware/util/ReflectionUtils.java @@ -593,8 +593,8 @@ public static Field getField(Class c, String fieldName) { } /** - * Retrieves the declared fields of a class using a custom field filter, with caching for performance. - * This method provides direct field access with customizable filtering criteria. + * Retrieves the declared fields of a class (not it's parent) using a custom field filter, with caching for + * performance. This method provides direct field access with customizable filtering criteria. *

    * Key features: *

      @@ -667,9 +667,9 @@ public static List getDeclaredFields(final Class c, Predicate f } /** - * Retrieves the declared fields of a class using the default field filter, with caching for performance. - * This method provides the same functionality as {@link #getDeclaredFields(Class, Predicate)} but uses - * the default field filter. + * Retrieves the declared fields of a class (not it's parent) using the default field filter, with caching for + * performance. This method provides the same functionality as {@link #getDeclaredFields(Class, Predicate)} + * but uses the default field filter. *

      * The default filter excludes: *

        @@ -679,8 +679,6 @@ public static List getDeclaredFields(final Class c, Predicate f *
      • Groovy's metaClass field
      • *
      *

      - * This method is equivalent to calling {@link #getDeclaredFields(Class, Predicate)} with the default - * field filter. * * @param c The class whose complete field hierarchy is to be retrieved * @return An unmodifiable list of all fields in the class hierarchy that pass the default filter @@ -774,7 +772,7 @@ public static List getAllDeclaredFields(final Class c) { } /** - * Returns all Fields from a class (including inherited) as a Map, filtered by the provided predicate. + * Returns all Fields from a class (including inherited) as a Map filtered by the provided predicate. *

      * The returned Map uses String field names as keys and Field objects as values, with special * handling for name collisions across the inheritance hierarchy. @@ -853,11 +851,12 @@ public static Map getAllDeclaredFieldsMap(Class c) { * Note that getAllDeclaredFields() includes transient fields and synthetic fields * (like "this$"). If you need the old behavior, filter the additional fields: *

      {@code
      -            // Combine DEFAULT_FIELD_FILTER with additional criteria for legacy behavior
      -            Predicate legacyFilter = field ->
      -            DEFAULT_FIELD_FILTER.test(field) &&
      -            !Modifier.isTransient(field.getModifiers()) &&
      -            !field.isSynthetic();
      +     * // Get fields excluding transient and synthetic fields
      +     * List fields = getAllDeclaredFields(MyClass.class, field ->
      +     *     DEFAULT_FIELD_FILTER.test(field) &&
      +     *     !Modifier.isTransient(field.getModifiers()) &&
      +     *     !field.isSynthetic()
      +     * );
            * }
      * This method will may be removed in 3.0.0. */ @@ -880,11 +879,12 @@ public static Collection getDeepDeclaredFields(Class c) { * Note that getAllDeclaredFieldsMap() includes transient fields and synthetic fields * (like "this$"). If you need the old behavior, filter the additional fields: *
      {@code
      -            // Combine DEFAULT_FIELD_FILTER with additional criteria for legacy behavior
      -            Predicate legacyFilter = field ->
      -            DEFAULT_FIELD_FILTER.test(field) &&
      -            !Modifier.isTransient(field.getModifiers()) &&
      -            !field.isSynthetic();
      +     * // Get fields excluding transient and synthetic fields
      +     * List fields = getAllDeclaredFieldsMap(MyClass.class, field ->
      +     *     DEFAULT_FIELD_FILTER.test(field) &&
      +     *     !Modifier.isTransient(field.getModifiers()) &&
      +     *     !field.isSynthetic()
      +     * );
            * }
      * This method will may be removed in 3.0.0. */ diff --git a/src/test/java/com/cedarsoftware/util/CompactMapTest.java b/src/test/java/com/cedarsoftware/util/CompactMapTest.java index b3ea50f6..1ec0926c 100644 --- a/src/test/java/com/cedarsoftware/util/CompactMapTest.java +++ b/src/test/java/com/cedarsoftware/util/CompactMapTest.java @@ -3507,25 +3507,7 @@ public void testPerformance() for (int i = lower; i < upper; i++) { compactSize[0] = i; - CompactMap map = new CompactMap() - { - protected String getSingleValueKey() - { - return "key1"; - } - protected Map getNewMap() - { - return new HashMap<>(); - } - protected boolean isCaseInsensitive() - { - return false; - } - protected int compactSize() - { - return compactSize[0]; - } - }; + CompactMap map = CompactMap.builder().compactSize(i).caseSensitive(true).noOrder().singleValueKey("key1").build(); long start = System.nanoTime(); // ===== Timed diff --git a/src/test/java/com/cedarsoftware/util/CompactSetTest.java b/src/test/java/com/cedarsoftware/util/CompactSetTest.java index 55324cd3..55e75f8e 100644 --- a/src/test/java/com/cedarsoftware/util/CompactSetTest.java +++ b/src/test/java/com/cedarsoftware/util/CompactSetTest.java @@ -381,31 +381,15 @@ public void testCompactCILinkedSet() public void testPerformance() { int maxSize = 1000; - final int[] compactSize = new int[1]; - int lower = 5; - int upper = 140; + int lower = 50; + int upper = 80; long totals[] = new long[upper - lower + 1]; for (int x = 0; x < 2000; x++) { for (int i = lower; i < upper; i++) { - compactSize[0] = i; - CompactSet set = new CompactSet() - { - protected Set getNewSet() - { - return new HashSet<>(); - } - protected boolean isCaseInsensitive() - { - return false; - } - protected int compactSize() - { - return compactSize[0]; - } - }; + CompactSet set = CompactSet.builder().caseSensitive(true).compactSize(i).build(); long start = System.nanoTime(); // ===== Timed diff --git a/userguide.md b/userguide.md new file mode 100644 index 00000000..ed5023ba --- /dev/null +++ b/userguide.md @@ -0,0 +1,149 @@ +# User Guide + +## CompactSet + +[View Source](/src/main/java/com/cedarsoftware/util/CompactSet.java) + +A memory-efficient `Set` implementation that internally uses `CompactMap`. This implementation provides the same memory benefits as `CompactMap` while maintaining proper Set semantics. + +### Key Features + +- Configurable case sensitivity for String elements +- Flexible element ordering options: + - Sorted order + - Reverse order + - Insertion order + - No oOrder +- Customizable compact size threshold +- Memory-efficient internal storage + +### Usage Examples + +```java +// Create a case-insensitive, sorted CompactSet +CompactSet set = CompactSet.builder() + .caseSensitive(false) + .sortedOrder() + .compactSize(70) + .build(); + +// Create a CompactSet with insertion ordering +CompactSet ordered = CompactSet.builder() + .insertionOrder() + .build(); +``` + +### Configuration Options + +#### Case Sensitivity +- Control case sensitivity for String elements using `.caseSensitive(boolean)` +- Useful for scenarios where case-insensitive string comparison is needed + +#### Element Ordering +Choose from three ordering strategies: +- `sortedOrder()`: Elements maintained in natural sorted order +- `reverseOrder()`: Elements maintained in reverse sorted order +- `insertionOrder()`: Elements maintained in the order they were added +- `noOrder()`: Elements maintained in an arbitrary order + +#### Compact Size +- Set custom threshold for compact storage using `.compactSize(int)` +- Allows fine-tuning of memory usage vs performance tradeoff + +### Implementation Notes + +- Built on top of `CompactMap` for memory efficiency +- Maintains proper Set semantics while optimizing storage +- Thread-safe when properly synchronized externally +--- +## CaseInsensitiveSet + +[View Source](/src/main/java/com/cedarsoftware/util/CaseInsensitiveSet.java) + +A specialized `Set` implementation that performs case-insensitive comparisons for String elements while preserving their original case. This collection can contain both String and non-String elements, making it versatile for mixed-type usage. + +### Key Features + +- **Case-Insensitive String Handling** + - Performs case-insensitive comparisons for String elements + - Preserves original case when iterating or retrieving elements + - Treats non-String elements as a normal Set would + +- **Flexible Collection Types** + - Supports both homogeneous (all Strings) and heterogeneous (mixed types) collections + - Maintains proper Set semantics for all element types + +- **Customizable Backing Storage** + - Supports various backing map implementations for different use cases + - Automatically selects appropriate backing store based on input collection type + +### Usage Examples + +```java +// Create a basic 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-type usage +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] +``` + +### Construction Options + +1. **Default Constructor** + ```java + CaseInsensitiveSet set = new CaseInsensitiveSet<>(); + ``` + Creates an empty set with default initial capacity and load factor. + +2. **Initial Capacity** + ```java + CaseInsensitiveSet set = new CaseInsensitiveSet<>(100); + ``` + Creates an empty set with specified initial capacity. + +3. **From Existing Collection** + ```java + Collection source = List.of("A", "B", "C"); + CaseInsensitiveSet set = new CaseInsensitiveSet<>(source); + ``` + The backing map is automatically selected based on the source collection type: + - `ConcurrentNavigableSetNullSafe` → `ConcurrentNavigableMapNullSafe` + - `ConcurrentSkipListSet` → `ConcurrentSkipListMap` + - `ConcurrentSet` → `ConcurrentHashMapNullSafe` + - `SortedSet` → `TreeMap` + - Others → `LinkedHashMap` + +### Implementation Notes + +- Thread safety depends on the backing map implementation +- String comparisons are case-insensitive but preserve original case +- Set operations use the underlying `CaseInsensitiveMap` for consistent behavior +- Maintains proper `Set` contract while providing case-insensitive functionality for strings + +### Best Practices + +1. **Choose Appropriate Constructor** + - Use default constructor for general use + - Specify initial capacity when approximate size is known + - Use collection constructor to maintain specific ordering or concurrency properties + +2. **Performance Considerations** + - Consider backing map selection for concurrent access needs + - Initialize with appropriate capacity to minimize resizing + - Use appropriate load factor for memory/performance trade-offs + +3. **Type Safety** + - Prefer homogeneous collections when possible + - Be aware of case-insensitive behavior when mixing String and non-String elements + +### Related Components +- `CaseInsensitiveMap`: The underlying map implementation used by this set +- `ConcurrentSet`: For concurrent access needs +- `TreeMap`: For sorted set behavior \ No newline at end of file