Skip to content

Commit

Permalink
- CompactMap now properly caches the classes it generates. It also lo…
Browse files Browse the repository at this point in the history
…cks at a per classname level, increasing parallel execution.

- More Javadoc updates.
  • Loading branch information
jdereg committed Jan 9, 2025
1 parent 862e274 commit 476c3ce
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 117 deletions.
27 changes: 2 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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><summary>Details</summary>

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<String> set = new CompactSet<>();

// With custom transition size
CompactSet<String> set = new CompactSet<>(10);

// Case-insensitive set
CompactSet<String> caseInsensitive = CompactSet.createCaseInsensitiveSet();
```
</details>

- **[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

Expand Down
118 changes: 82 additions & 36 deletions src/main/java/com/cedarsoftware/util/CompactMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -250,6 +252,8 @@ public class CompactMap<K, V> implements Map<K, V> {
private static final Class<? extends Map> 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<String, ReentrantLock> CLASS_LOCKS = new ConcurrentHashMap<>();

// The only "state" and why this is a compactMap - one member variable
protected Object val = EMPTY_MAP;
Expand Down Expand Up @@ -2167,12 +2171,11 @@ private static final class TemplateGenerator {
private static Class<?> getOrCreateTemplateClass(Map<String, Object> 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.
* <p>
Expand Down Expand Up @@ -2237,7 +2240,8 @@ private static String generateClassName(Map<String, Object> options) {
/**
* Creates a new template class for the specified configuration options.
* <p>
* 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.
* <ul>
* <li>Double-checks if class was created while waiting for lock</li>
* <li>Generates source code for the template class</li>
Expand All @@ -2249,21 +2253,34 @@ private static String generateClassName(Map<String, Object> 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<String, Object> options) {
// Double-check if class was created while waiting for lock
private static Class<?> generateTemplateClass(Map<String, Object> 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.
* <p>
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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<String, Class<?>> definedClasses = new ConcurrentHashMap<>();
private final Map<String, ReentrantLock> 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.
* <p>
Expand All @@ -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.
* <p>
Expand All @@ -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);
}
}

Expand Down
36 changes: 18 additions & 18 deletions src/main/java/com/cedarsoftware/util/ReflectionUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* Key features:
* <ul>
Expand Down Expand Up @@ -667,9 +667,9 @@ public static List<Field> getDeclaredFields(final Class<?> c, Predicate<Field> 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.
* <p>
* The default filter excludes:
* <ul>
Expand All @@ -679,8 +679,6 @@ public static List<Field> getDeclaredFields(final Class<?> c, Predicate<Field> f
* <li>Groovy's metaClass field</li>
* </ul>
* <p>
* 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
Expand Down Expand Up @@ -774,7 +772,7 @@ public static List<Field> 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.
* <p>
* The returned Map uses String field names as keys and Field objects as values, with special
* handling for name collisions across the inheritance hierarchy.
Expand Down Expand Up @@ -853,11 +851,12 @@ public static Map<String, Field> getAllDeclaredFieldsMap(Class<?> c) {
* Note that getAllDeclaredFields() includes transient fields and synthetic fields
* (like "this$"). If you need the old behavior, filter the additional fields:
* <pre>{@code
// Combine DEFAULT_FIELD_FILTER with additional criteria for legacy behavior
Predicate<Field> legacyFilter = field ->
DEFAULT_FIELD_FILTER.test(field) &&
!Modifier.isTransient(field.getModifiers()) &&
!field.isSynthetic();
* // Get fields excluding transient and synthetic fields
* List<Field> fields = getAllDeclaredFields(MyClass.class, field ->
* DEFAULT_FIELD_FILTER.test(field) &&
* !Modifier.isTransient(field.getModifiers()) &&
* !field.isSynthetic()
* );
* }</pre>
* This method will may be removed in 3.0.0.
*/
Expand All @@ -880,11 +879,12 @@ public static Collection<Field> getDeepDeclaredFields(Class<?> c) {
* Note that getAllDeclaredFieldsMap() includes transient fields and synthetic fields
* (like "this$"). If you need the old behavior, filter the additional fields:
* <pre>{@code
// Combine DEFAULT_FIELD_FILTER with additional criteria for legacy behavior
Predicate<Field> legacyFilter = field ->
DEFAULT_FIELD_FILTER.test(field) &&
!Modifier.isTransient(field.getModifiers()) &&
!field.isSynthetic();
* // Get fields excluding transient and synthetic fields
* List<Field> fields = getAllDeclaredFieldsMap(MyClass.class, field ->
* DEFAULT_FIELD_FILTER.test(field) &&
* !Modifier.isTransient(field.getModifiers()) &&
* !field.isSynthetic()
* );
* }</pre>
* This method will may be removed in 3.0.0.
*/
Expand Down
20 changes: 1 addition & 19 deletions src/test/java/com/cedarsoftware/util/CompactMapTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3507,25 +3507,7 @@ public void testPerformance()
for (int i = lower; i < upper; i++)
{
compactSize[0] = i;
CompactMap<String, Integer> map = new CompactMap()
{
protected String getSingleValueKey()
{
return "key1";
}
protected Map<String, Integer> getNewMap()
{
return new HashMap<>();
}
protected boolean isCaseInsensitive()
{
return false;
}
protected int compactSize()
{
return compactSize[0];
}
};
CompactMap<String, Integer> map = CompactMap.<String, Integer>builder().compactSize(i).caseSensitive(true).noOrder().singleValueKey("key1").build();

long start = System.nanoTime();
// ===== Timed
Expand Down
22 changes: 3 additions & 19 deletions src/test/java/com/cedarsoftware/util/CompactSetTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> set = new CompactSet<String>()
{
protected Set<String> getNewSet()
{
return new HashSet<>();
}
protected boolean isCaseInsensitive()
{
return false;
}
protected int compactSize()
{
return compactSize[0];
}
};
CompactSet<String> set = CompactSet.<String>builder().caseSensitive(true).compactSize(i).build();

long start = System.nanoTime();
// ===== Timed
Expand Down
Loading

0 comments on commit 476c3ce

Please sign in to comment.