diff --git a/src/main/java/org/apache/commons/lang3/CharRange.java b/src/main/java/org/apache/commons/lang3/CharRange.java index d7a793db5ee..6ae74e181b9 100644 --- a/src/main/java/org/apache/commons/lang3/CharRange.java +++ b/src/main/java/org/apache/commons/lang3/CharRange.java @@ -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); } /** diff --git a/src/test/java/org/apache/commons/lang3/CharRangeTest.java b/src/test/java/org/apache/commons/lang3/CharRangeTest.java index 173843149e3..c1418ae557d 100644 --- a/src/test/java/org/apache/commons/lang3/CharRangeTest.java +++ b/src/test/java/org/apache/commons/lang3/CharRangeTest.java @@ -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 uniqueCharRanges = new HashSet<>(); + while (uniqueCharRanges.size() < TOTAL_INSTANCES) { + uniqueCharRanges.add(generateRandomCharRange(random)); + } + List testInstances = new ArrayList<>(uniqueCharRanges); + System.out.println("Generated " + testInstances.size() + " unique CharRange instances"); + + // 3. Stat collision metrics for Objects.hash-based implementation + Map> 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> 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> 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