This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git


The following commit(s) were added to refs/heads/master by this push:
     new 43154e016 Default unbounded WildcardType upper bound to Object in 
TypeUtils (#1807)
43154e016 is described below

commit 43154e016ae213a0da5b727cd1aa18e6e0c56adb
Author: alhuda <[email protected]>
AuthorDate: Mon Oct 5 01:23:31 2026 +0530

    Default unbounded WildcardType upper bound to Object in TypeUtils (#1807)
    
    * default unbounded WildcardType upper bound to Object in TypeUtils
    
    * drop changes.xml entry
    
    The maintainers keep src/changes/changes.xml themselves, so restore it to
    the base version and leave it out of this change.
    
    * match the JDK hashCode in TypeUtils WildcardTypeImpl
    
    A wildcard built by TypeUtils is now equal to the JDK's in both
    directions, but WildcardTypeImpl.hashCode() used its own shift/OR formula
    while the JDK computes Arrays.hashCode(lowerBounds) ^
    Arrays.hashCode(upperBounds), so two equal wildcards hashed differently.
    
    Use the JDK algorithm and assert the hash codes agree in
    testWildcardTypeImplicitUpperBound.
    
    * hash the bounds equals compares in TypeUtils WildcardTypeImpl
    
    WildcardTypeImpl.equals() compares normalised bounds: an absent lower
    bound and a lone null lower bound are the same, and redundant upper bounds
    are dropped. hashCode() hashed the raw arrays, so
    wildcardType().withLowerBounds((Type) null).withUpperBounds().build()
    equalled WILDCARD_ALL with a different hash code, and likewise a wildcard
    with a redundant upper bound.
    
    Hash the same forms equals() compares, keeping an absent lower bound as
    the JDK's empty array so the hash code still matches a JDK wildcard.
---
 .../apache/commons/lang3/reflect/TypeUtils.java    | 13 ++++++-----
 .../commons/lang3/reflect/TypeUtilsTest.java       | 27 ++++++++++++++++++++++
 2 files changed, 34 insertions(+), 6 deletions(-)

diff --git a/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java 
b/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java
index 5d90023c6..74feb8848 100644
--- a/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java
+++ b/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java
@@ -236,7 +236,9 @@ private static final class WildcardTypeImpl implements 
WildcardType {
          * @param lowerBounds of this type.
          */
         private WildcardTypeImpl(final Type[] upperBounds, final Type[] 
lowerBounds) {
-            this.upperBounds = upperBounds != null ? upperBounds.clone() : 
ArrayUtils.EMPTY_TYPE_ARRAY;
+            // A wildcard with no explicit upper bound has an implicit upper 
bound of Object, per
+            // WildcardType.getUpperBounds(); returning an empty array breaks 
equals() with a JDK wildcard.
+            this.upperBounds = ArrayUtils.isNotEmpty(upperBounds) ? 
upperBounds.clone() : new Type[] {Object.class};
             this.lowerBounds = lowerBounds != null ? lowerBounds.clone() : 
ArrayUtils.EMPTY_TYPE_ARRAY;
         }
 
@@ -269,11 +271,10 @@ public Type[] getUpperBounds() {
          */
         @Override
         public int hashCode() {
-            int result = 73 << 8;
-            result |= Arrays.hashCode(upperBounds);
-            result <<= 8;
-            result |= Arrays.hashCode(lowerBounds);
-            return result;
+            // Same algorithm as the JDK's WildcardType, over the bounds that 
equals() compares, so that equal wildcards share a hash code.
+            // A lone null lower bound means no lower bound, which the JDK 
reports as an empty array.
+            final Type[] lower = lowerBounds.length == 1 && lowerBounds[0] == 
null ? ArrayUtils.EMPTY_TYPE_ARRAY : lowerBounds;
+            return Arrays.hashCode(lower) ^ 
Arrays.hashCode(getImplicitUpperBounds(this));
         }
 
         /**
diff --git a/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java 
b/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java
index 291cfd45d..8b0749c34 100644
--- a/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java
@@ -1571,6 +1571,7 @@ void testTypeToString(final Type type) {
     void testUnboundedWildcardType() {
         final WildcardType unbounded = 
TypeUtils.wildcardType().withLowerBounds((Type) null).withUpperBounds().build();
         assertTrue(TypeUtils.equals(TypeUtils.WILDCARD_ALL, unbounded));
+        assertEquals(TypeUtils.WILDCARD_ALL.hashCode(), unbounded.hashCode());
         assertArrayEquals(new Type[] { Object.class }, 
TypeUtils.getImplicitUpperBounds(unbounded));
         assertArrayEquals(new Type[] { null }, 
TypeUtils.getImplicitLowerBounds(unbounded));
         assertEquals("?", TypeUtils.toString(unbounded));
@@ -1621,6 +1622,32 @@ void testWildcardTypeBuilderDefensiveCopy() {
         assertArrayEquals(new Type[] { String.class }, 
wildcardLower.getLowerBounds());
     }
 
+    @Test
+    void testWildcardTypeImplicitUpperBound() throws NoSuchFieldException {
+        final WildcardType built = TypeUtils.wildcardType().build();
+        // An unbounded wildcard has an implicit upper bound of Object, per 
WildcardType.getUpperBounds().
+        assertArrayEquals(new Type[] { Object.class }, built.getUpperBounds());
+        // equals() must be symmetric with a JDK unbounded wildcard.
+        final WildcardType jdk = (WildcardType) ((ParameterizedType) 
TypeUtilsTest.class.getField("wildcardComparable")
+                .getGenericType()).getActualTypeArguments()[0];
+        assertEquals(built, jdk);
+        assertEquals(jdk, built);
+        // Equal wildcards must share a hash code.
+        assertEquals(jdk.hashCode(), built.hashCode());
+        assertEquals(TypeUtils.WILDCARD_ALL, built);
+        assertEquals(TypeUtils.WILDCARD_ALL.hashCode(), built.hashCode());
+    }
+
+    @Test
+    void testWildcardTypeRedundantUpperBound() {
+        // equals() ignores a redundant upper bound, so hashCode() must ignore 
it too.
+        final WildcardType redundant = 
TypeUtils.wildcardType().withUpperBounds(Collection.class, List.class).build();
+        final WildcardType simple = 
TypeUtils.wildcardType().withUpperBounds(List.class).build();
+        assertEquals(simple, redundant);
+        assertEquals(redundant, simple);
+        assertEquals(simple.hashCode(), redundant.hashCode());
+    }
+
     @Test
     void testWrap() {
         final Type t = getClass().getTypeParameters()[0];

Reply via email to