Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/main/java/org/apache/commons/lang3/CharRange.java
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,8 @@ public char getStart() {
*/
@Override
public int hashCode() {
return Objects.hash(end, negated, start);
final int result = (start << 16) | (end & 0xFFFF);
return result ^ (negated ? 0x00010000 : 0);
}

/**
Expand Down
217 changes: 215 additions & 2 deletions src/test/java/org/apache/commons/lang3/CharRangeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,229 @@
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.lang.reflect.Modifier;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

/**
* Tests {@link CharRange}.
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // Enable @BeforeAll on non-static methods
class CharRangeTest extends AbstractLangTest {

// ========== Added for comparing CharRange hashCode implementations (Objects.hash vs bitwise) ==========
// Number of test iterations (larger values improve accuracy; recommended ? 100 million)
private static final long TEST_COUNT = 100_000_000L;
// CharRange test instances covering typical scenarios
private CharRange normalRange; // Standard range: a-z (non-negated)
private CharRange negatedRange; // Negated range: 0-9
private CharRange singleCharRange;// Single character range: x-x
private CharRange extremeRange; // Extreme value range: near Character.MAX_VALUE

/**
* Initialize test data for hashCode implementation comparison (executed once before all tests)
*/
@BeforeAll
void initHashCodeTestData() {
// Create test instances using official CharRange static factory methods
normalRange = CharRange.isIn('a', 'z');
negatedRange = CharRange.isNotIn('0', '9');
singleCharRange = CharRange.isIn('x', 'x');
extremeRange = CharRange.isIn(Character.MAX_VALUE, (char) (Character.MAX_VALUE - 100));
// JVM warm-up: Eliminate JIT compilation bias for first-time execution
warmUpJvmForHashCodeTests();
}

/**
* JVM warm-up to eliminate performance bias from first-time JIT compilation
* (executes both hashCode implementations to ensure fair performance comparison)
*/
private void warmUpJvmForHashCodeTests() {
for (long i = 0; i < 1_000_000; i++) {
hashCodeWithObjectsHash(normalRange);
hashCodeWithBitwise(normalRange);
hashCodeWithObjectsHash(negatedRange);
hashCodeWithBitwise(negatedRange);
hashCodeWithObjectsHash(singleCharRange);
hashCodeWithBitwise(singleCharRange);
hashCodeWithObjectsHash(extremeRange);
hashCodeWithBitwise(extremeRange);
}
}

private int hashCodeWithObjectsHash(CharRange range) {
return Objects.hash(range.getEnd(), range.isNegated(), range.getStart());
}

private int hashCodeWithBitwise(CharRange range) {
final int charCombined = (range.getStart() << 16) | (range.getEnd() & 0xFFFF);
return charCombined ^ (range.isNegated() ? 0x00010000 : 0);
}

// ========== HashCode implementation comparison tests ==========
@Test
void testHashCode_ContractCompliance() {
// Scenario 1: Equal CharRange instances must have equal hash codes (for both implementations)
CharRange range1 = CharRange.isIn('a', 'z');
CharRange range2 = CharRange.isIn('a', 'z');
assertTrue(range1.equals(range2));
assertEquals(hashCodeWithObjectsHash(range1), hashCodeWithObjectsHash(range2));
assertEquals(hashCodeWithBitwise(range1), hashCodeWithBitwise(range2));

// Scenario 2: Unequal instances should (probabilistically) have different hash codes
CharRange range3 = CharRange.isNotIn('a', 'z');
assertFalse(range1.equals(range3));
// Verify hash code dispersion (ignore rare probabilistic collisions)
assertNotEquals(hashCodeWithObjectsHash(range1), hashCodeWithObjectsHash(range3),
"Hash collision occurred in Objects.hash implementation (probabilistic)");
assertNotEquals(hashCodeWithBitwise(range1), hashCodeWithBitwise(range3),
"Hash collision occurred in bitwise implementation (extreme scenario)");

// Scenario 3: Verify hash consistency for ranges with reversed start/end (auto-swapped by CharRange)
CharRange range4 = CharRange.isIn('z', 'a'); // Auto-swapped to a-z internally
assertTrue(range1.equals(range4));
assertEquals(hashCodeWithObjectsHash(range1), hashCodeWithObjectsHash(range4));
assertEquals(hashCodeWithBitwise(range1), hashCodeWithBitwise(range4));
}

@Test
void testHashCode_PerformanceComparison() {
// ---------- Benchmark: Objects.hash-based implementation ----------
long startTimeObjectsHash = System.nanoTime();
long sumObjectsHash = 0; // Accumulate results to prevent JIT dead-code elimination
for (long i = 0; i < TEST_COUNT; i++) {
sumObjectsHash += hashCodeWithObjectsHash(normalRange);
sumObjectsHash += hashCodeWithObjectsHash(negatedRange);
sumObjectsHash += hashCodeWithObjectsHash(singleCharRange);
sumObjectsHash += hashCodeWithObjectsHash(extremeRange);
}
long endTimeObjectsHash = System.nanoTime();
long costTimeObjectsHash = (endTimeObjectsHash - startTimeObjectsHash) / 1_000_000; // Convert to milliseconds

// ---------- Benchmark: Bitwise-optimized implementation ----------
long startTimeBitwise = System.nanoTime();
long sumBitwise = 0; // Accumulate results to prevent JIT dead-code elimination
for (long i = 0; i < TEST_COUNT; i++) {
sumBitwise += hashCodeWithBitwise(normalRange);
sumBitwise += hashCodeWithBitwise(negatedRange);
sumBitwise += hashCodeWithBitwise(singleCharRange);
sumBitwise += hashCodeWithBitwise(extremeRange);
}
long endTimeBitwise = System.nanoTime();
long costTimeBitwise = (endTimeBitwise - startTimeBitwise) / 1_000_000;

// ---------- Output performance comparison results ----------
System.out.println("===== CharRange HashCode Performance Comparison (" + TEST_COUNT + " iterations/scenario) =====");
System.out.println("Objects.hash implementation total time: " + costTimeObjectsHash + " ms (sum: " + sumObjectsHash + ")");
System.out.println("Bitwise-optimized implementation total time: " + costTimeBitwise + " ms (sum: " + sumBitwise + ")");
// Calculate performance improvement ratio (avoid division by zero)
double performanceImprovement = costTimeObjectsHash == 0 ? 0 :
(double) (costTimeObjectsHash - costTimeBitwise) / costTimeObjectsHash * 100;
System.out.println("Bitwise implementation performance improvement: " + String.format("%.2f%%", performanceImprovement));

// ---------- Core assertion: Bitwise implementation must be faster ----------
assertTrue(costTimeBitwise < costTimeObjectsHash,
"Bitwise-optimized hashCode should outperform Objects.hash-based implementation!");
}

/**
* Tests collision rate of two hashCode implementations for CharRange:
* 1. Objects.hash-based implementation (baseline reference)
* 2. Bitwise operation-based implementation (optimized variant)
*
* Collision Definition: Two distinct CharRange instances (!equals()) with identical hashCode values
*/
@Test
void testHashCodeCollisionRate() {
// 1. Test configuration
final int TOTAL_INSTANCES = 1_000_000; // Generate 1 million unique CharRange instances (higher = more accurate stats)
final Random random = new Random(42); // Fixed random seed for reproducible test results

// 2. Generate a large set of distinct CharRange instances (ensure no duplicates/equality)
Set<CharRange> uniqueCharRanges = new HashSet<>();
while (uniqueCharRanges.size() < TOTAL_INSTANCES) {
uniqueCharRanges.add(generateRandomCharRange(random));
}
List<CharRange> testInstances = new ArrayList<>(uniqueCharRanges);
System.out.println("Generated " + testInstances.size() + " unique CharRange instances");

// 3. Stat collision metrics for Objects.hash-based implementation
Map<Integer, List<CharRange>> objectsHashToRanges = new HashMap<>();
for (CharRange range : testInstances) {
int hashCode = hashCodeWithObjectsHash(range);
objectsHashToRanges.computeIfAbsent(hashCode, k -> new ArrayList<>()).add(range);
}
long objectsHashTotalCollisions = calculateTotalCollisionCount(objectsHashToRanges);
double objectsHashCollisionRate = (double) (testInstances.size() - objectsHashToRanges.size()) / testInstances.size();

// 4. Stat collision metrics for bitwise operation-based implementation
Map<Integer, List<CharRange>> bitwiseHashToRanges = new HashMap<>();
for (CharRange range : testInstances) {
int hashCode = hashCodeWithBitwise(range);
bitwiseHashToRanges.computeIfAbsent(hashCode, k -> new ArrayList<>()).add(range);
}
long bitwiseTotalCollisions = calculateTotalCollisionCount(bitwiseHashToRanges);
double bitwiseCollisionRate = (double) (testInstances.size() - bitwiseHashToRanges.size()) / testInstances.size();

// 5. Print comparison results
System.out.println("===== HashCode Collision Rate Comparison =====");
System.out.println("Objects.hash implementation:");
System.out.println(" Number of unique hash codes: " + objectsHashToRanges.size());
System.out.println(" Total colliding instances: " + objectsHashTotalCollisions);
System.out.println(" Collision rate: " + String.format("%.6f%%", objectsHashCollisionRate * 100));
System.out.println("Bitwise implementation:");
System.out.println(" Number of unique hash codes: " + bitwiseHashToRanges.size());
System.out.println(" Total colliding instances: " + bitwiseTotalCollisions);
System.out.println(" Collision rate: " + String.format("%.6f%%", bitwiseCollisionRate * 100));
}

/**
* Generates a random, unique CharRange instance covering all core scenarios:
* - Normal/negated ranges
* - Single character ranges
* - Extreme value ranges (near Character.MAX_VALUE)
*
* @param random Random instance with fixed seed for reproducibility
* @return Unique CharRange instance (no equality with other generated instances)
*/
private CharRange generateRandomCharRange(Random random) {
// Randomly generate start/end (range: 0 ~ Character.MAX_VALUE)
char start = (char) random.nextInt(Character.MAX_VALUE + 1);
char end = (char) random.nextInt(Character.MAX_VALUE + 1);
// Randomly determine if the range is negated
boolean negated = random.nextBoolean();
// Ensure start <= end (avoid auto-swapping by CharRange which causes equal instances)
if (start > end) {
char temp = start;
start = end;
end = temp;
}
// Create CharRange instance (negated or non-negated)
return negated ? CharRange.isNotIn(start, end) : CharRange.isIn(start, end);
}

/**
* Calculates total collision count:
* Sum of (list size - 1) for all hash code lists with size > 1
* (list size - 1 = number of collisions for that specific hash code)
*
* @param hashCodeMap Map of hashCode values to associated CharRange instances
* @return Total number of colliding instances across all hash codes
*/
private long calculateTotalCollisionCount(Map<Integer, List<CharRange>> hashCodeMap) {
AtomicLong collisionCount = new AtomicLong(0);
hashCodeMap.values().forEach(list -> {
if (list.size() > 1) {
collisionCount.addAndGet(list.size() - 1);
}
});
return collisionCount.get();
}
// ========== End of hashCode implementation comparison code ==========

@Test
void testClass() {
// class changed to non-public in 3.0
Expand Down