You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2026/10/10 19:30:47 UTC

(commons-lang) 01/02: Sort members

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

commit cf50fbc4969a84dc5185b1681224d8025b929fc2
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Sat Oct 10 15:23:33 2026 -0400

    Sort members
---
 .../apache/commons/lang3/reflect/TypeUtils.java    |  94 +++----
 .../apache/commons/lang3/AnnotationUtilsTest.java  |  68 ++---
 .../commons/lang3/reflect/TypeUtilsTest.java       | 306 ++++++++++-----------
 3 files changed, 234 insertions(+), 234 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 ecd4dfe64..82b80f1b4 100644
--- a/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java
+++ b/src/main/java/org/apache/commons/lang3/reflect/TypeUtils.java
@@ -383,15 +383,6 @@ public static boolean containsTypeVariables(final Type type) {
         return containsTypeVariables(type, newIdentitySet());
     }
 
-    /**
-     * Creates a set that compares by identity, so that cycle detection never calls {@code hashCode()} or {@code equals()} of a custom {@link Type}.
-     *
-     * @return A new identity-based set.
-     */
-    private static Set<Type> newIdentitySet() {
-        return Collections.newSetFromMap(new IdentityHashMap<>());
-    }
-
     private static boolean containsTypeVariables(final Type type, final Set<Type> visited) {
         if (type instanceof TypeVariable<?>) {
             return true;
@@ -587,7 +578,6 @@ private static Type[] extractTypeArgumentsFrom(final Map<TypeVariable<?>, Type>
         return result;
     }
 
-
     /**
      * Creates a generic array type instance.
      *
@@ -599,6 +589,7 @@ public static GenericArrayType genericArrayType(final Type componentType) {
         return new GenericArrayTypeImpl(Objects.requireNonNull(componentType, "componentType"));
     }
 
+
     /**
      * Formats a {@link GenericArrayType} as a {@link String}.
      *
@@ -952,6 +943,23 @@ private static Map<TypeVariable<?>, Type> getTypeArguments(final Type type, fina
         throw new IllegalStateException("found an unhandled type: " + type);
     }
 
+    /**
+     * Tests whether following the owner types of the given type leads back to a type that was already seen. Only a custom {@link ParameterizedType} can do that.
+     *
+     * @param type The type whose owner chain to check.
+     * @return Whether the owner chain of {@code type} is cyclic.
+     */
+    private static boolean hasCyclicOwnerChain(final ParameterizedType type) {
+        final Set<Type> seen = newIdentitySet();
+        seen.add(type);
+        for (Type owner = type.getOwnerType(); owner instanceof ParameterizedType; owner = ((ParameterizedType) owner).getOwnerType()) {
+            if (!seen.add(owner)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * Tests whether the specified type denotes an array type.
      *
@@ -1330,6 +1338,26 @@ public static boolean isInstance(final Object value, final Type type) {
         return value == null ? !(type instanceof Class<?>) || !((Class<?>) type).isPrimitive() : isAssignable(value.getClass(), type, null);
     }
 
+    /**
+     * Tests whether two type arrays hold the very same types, compared by identity so that no {@code equals()} or {@code hashCode()} of a custom {@link Type}
+     * is called.
+     *
+     * @param types      The first array.
+     * @param otherTypes The second array.
+     * @return Whether both arrays have the same length and the same types at each index.
+     */
+    private static boolean isSameTypes(final Type[] types, final Type[] otherTypes) {
+        if (types.length != otherTypes.length) {
+            return false;
+        }
+        for (int i = 0; i < types.length; i++) {
+            if (types[i] != otherTypes[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     /**
      * Maps type variables.
      *
@@ -1370,6 +1398,15 @@ private static <T> void mapTypeVariablesToArguments(final Class<T> cls, final Pa
         }
     }
 
+    /**
+     * Creates a set that compares by identity, so that cycle detection never calls {@code hashCode()} or {@code equals()} of a custom {@link Type}.
+     *
+     * @return A new identity-based set.
+     */
+    private static Set<Type> newIdentitySet() {
+        return Collections.newSetFromMap(new IdentityHashMap<>());
+    }
+
     /**
      * Strips out the redundant upper bound types in type variable types and wildcard types (or it would with wildcard types if multiple upper bounds were
      * allowed).
@@ -1747,23 +1784,6 @@ public static Type unrollVariables(Map<TypeVariable<?>, Type> typeArguments, fin
         return unrollVariables(typeArguments, type, new HashSet<>(), newIdentitySet());
     }
 
-    /**
-     * Tests whether following the owner types of the given type leads back to a type that was already seen. Only a custom {@link ParameterizedType} can do that.
-     *
-     * @param type The type whose owner chain to check.
-     * @return Whether the owner chain of {@code type} is cyclic.
-     */
-    private static boolean hasCyclicOwnerChain(final ParameterizedType type) {
-        final Set<Type> seen = newIdentitySet();
-        seen.add(type);
-        for (Type owner = type.getOwnerType(); owner instanceof ParameterizedType; owner = ((ParameterizedType) owner).getOwnerType()) {
-            if (!seen.add(owner)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     private static Type unrollVariables(final Map<TypeVariable<?>, Type> typeArguments, final Type type, final Set<TypeVariable<?>> visited, final Set<Type> unrolling) {
         if (type instanceof TypeVariable<?>) {
             final TypeVariable<?> var = (TypeVariable<?>) type;
@@ -1824,26 +1844,6 @@ private static Type unrollVariables(final Map<TypeVariable<?>, Type> typeArgumen
         return type;
     }
 
-    /**
-     * Tests whether two type arrays hold the very same types, compared by identity so that no {@code equals()} or {@code hashCode()} of a custom {@link Type}
-     * is called.
-     *
-     * @param types      The first array.
-     * @param otherTypes The second array.
-     * @return Whether both arrays have the same length and the same types at each index.
-     */
-    private static boolean isSameTypes(final Type[] types, final Type[] otherTypes) {
-        if (types.length != otherTypes.length) {
-            return false;
-        }
-        for (int i = 0; i < types.length; i++) {
-            if (types[i] != otherTypes[i]) {
-                return false;
-            }
-        }
-        return true;
-    }
-
     /**
      * Creates a new {@link WildcardTypeBuilder}.
      *
diff --git a/src/test/java/org/apache/commons/lang3/AnnotationUtilsTest.java b/src/test/java/org/apache/commons/lang3/AnnotationUtilsTest.java
index 3bc12fcff..9c6dca93d 100644
--- a/src/test/java/org/apache/commons/lang3/AnnotationUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/AnnotationUtilsTest.java
@@ -57,31 +57,6 @@ private static final class AnnotatedClass {
         String value() default "member";
     }
 
-    @Retention(RetentionPolicy.RUNTIME)
-    public @interface RunnableAnnotation {
-        Runnable RUNNABLE = () -> {
-            // Creates a void synthetic method for the regression test.
-        };
-
-        String value() default "member";
-    }
-
-    @Retention(RetentionPolicy.RUNTIME)
-    public @interface SupplierAnnotation {
-        Supplier<String> SUPPLIER = () -> "constant";
-
-        String value() default "member";
-    }
-
-    @Retention(RetentionPolicy.RUNTIME)
-    public @interface ThrowingSupplierAnnotation {
-        Supplier<String> SUPPLIER = () -> {
-            throw new AssertionError("Annotation member access must not invoke a lambda");
-        };
-
-        String value() default "member";
-    }
-
     @Retention(RetentionPolicy.RUNTIME)
     public @interface NestAnnotation {
         boolean booleanValue();
@@ -108,10 +83,26 @@ private static final class AnnotatedClass {
         Class<?>[] types();
     }
 
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface RunnableAnnotation {
+        Runnable RUNNABLE = () -> {
+            // Creates a void synthetic method for the regression test.
+        };
+
+        String value() default "member";
+    }
+
     public enum Stooge {
         MOE, LARRY, CURLY, JOE, SHEMP
     }
 
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface SupplierAnnotation {
+        Supplier<String> SUPPLIER = () -> "constant";
+
+        String value() default "member";
+    }
+
     @Target(ElementType.FIELD)
     @Retention(RetentionPolicy.RUNTIME)
     public @interface TestAnnotation {
@@ -154,6 +145,15 @@ final class None extends Throwable {
         long timeout() default 0L;
     }
 
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface ThrowingSupplierAnnotation {
+        Supplier<String> SUPPLIER = () -> {
+            throw new AssertionError("Annotation member access must not invoke a lambda");
+        };
+
+        String value() default "member";
+    }
+
     @TestAnnotation(
             booleanValue = false,
             booleanValues = { false },
@@ -532,6 +532,15 @@ void testHashCodeWithFunction() {
         assertEquals(annotation.hashCode(), AnnotationUtils.hashCode(annotation));
     }
 
+    @Test
+    void testHashCodeWithoutInvokingSupplier() {
+        final ThrowingSupplierAnnotation annotation =
+                AnnotatedClass.class.getAnnotation(ThrowingSupplierAnnotation.class);
+        final int expected = (127 * "value".hashCode()) ^ "member".hashCode();
+
+        assertEquals(expected, AnnotationUtils.hashCode(annotation));
+    }
+
     @Test
     void testHashCodeWithRunnable() {
         final RunnableAnnotation annotation = AnnotatedClass.class.getAnnotation(RunnableAnnotation.class);
@@ -544,15 +553,6 @@ void testHashCodeWithSupplier() {
         assertEquals(annotation.hashCode(), AnnotationUtils.hashCode(annotation));
     }
 
-    @Test
-    void testHashCodeWithoutInvokingSupplier() {
-        final ThrowingSupplierAnnotation annotation =
-                AnnotatedClass.class.getAnnotation(ThrowingSupplierAnnotation.class);
-        final int expected = (127 * "value".hashCode()) ^ "member".hashCode();
-
-        assertEquals(expected, AnnotationUtils.hashCode(annotation));
-    }
-
     @Test
     void testIsValidAnnotationMemberType() {
         for (final Class<?> type : new Class[] { byte.class, short.class, int.class, char.class,
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 f0e0e9d52..19efa56c5 100644
--- a/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/reflect/TypeUtilsTest.java
@@ -232,42 +232,6 @@ public static <U> Iterable<U> methodWithGenericReturnType() {
         }
     }
 
-    static class LexOrdering<T> extends MyOrdering<Iterable<T>> implements Serializable {
-        private static final long serialVersionUID = 1L;
-    }
-
-    interface MyComparator<T> {
-    }
-
-    static class MyException extends Exception implements Iterable<Throwable> {
-
-        private static final long serialVersionUID = 1L;
-
-        @Override
-        public Iterator<Throwable> iterator() {
-            return null;
-        }
-    }
-
-    /** This non-static inner class is parameterized. */
-    private class MyInnerClass<T> {
-
-        class MyInnerClass2<X> {
-            // empty
-        }
-    }
-
-    static class MyNonTransientException extends MyException {
-        private static final long serialVersionUID = 1L;
-    }
-
-    static class MyOrdering<T> implements MyComparator<T> {
-    }
-
-    public class Other<T> implements This<String, T> {
-        // empty
-    }
-
     /**
      * A custom {@link ParameterizedType} with a structural {@code hashCode()}, whose owner and type arguments can be set after construction so that tests can
      * create cycles.
@@ -303,11 +267,6 @@ public Type getRawType() {
             return rawType;
         }
 
-        @Override
-        public String toString() {
-            return "CyclicParameterizedType<" + rawType.getName() + ">";
-        }
-
         @Override
         public int hashCode() {
             // Structural like the JDK implementation: it follows the owner, so a cycle would overflow the stack.
@@ -321,6 +280,47 @@ void setOwnerType(final Type ownerType) {
         void setTypeArguments(final Type... typeArguments) {
             this.typeArguments = typeArguments;
         }
+
+        @Override
+        public String toString() {
+            return "CyclicParameterizedType<" + rawType.getName() + ">";
+        }
+    }
+
+    static class LexOrdering<T> extends MyOrdering<Iterable<T>> implements Serializable {
+        private static final long serialVersionUID = 1L;
+    }
+
+    interface MyComparator<T> {
+    }
+
+    static class MyException extends Exception implements Iterable<Throwable> {
+
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        public Iterator<Throwable> iterator() {
+            return null;
+        }
+    }
+
+    /** This non-static inner class is parameterized. */
+    private class MyInnerClass<T> {
+
+        class MyInnerClass2<X> {
+            // empty
+        }
+    }
+
+    static class MyNonTransientException extends MyException {
+        private static final long serialVersionUID = 1L;
+    }
+
+    static class MyOrdering<T> implements MyComparator<T> {
+    }
+
+    public class Other<T> implements This<String, T> {
+        // empty
     }
 
     /** LANG-1836: non-static inner classes whose owner type is parameterized. */
@@ -565,6 +565,30 @@ void testContainsTypeVariables() throws NoSuchMethodException {
         assertTrue(TypeUtils.containsTypeVariables(Test1.class.getMethod("m17").getGenericReturnType()));
     }
 
+    @Test
+    void testContainsTypeVariablesCyclicOwnerType() {
+        // LANG-1836: a (custom) ParameterizedType that is its own owner must not cause a StackOverflowError.
+        final ParameterizedType[] self = new ParameterizedType[1];
+        self[0] = new ParameterizedType() {
+
+            @Override
+            public Type[] getActualTypeArguments() {
+                return new Type[0];
+            }
+
+            @Override
+            public Type getOwnerType() {
+                return self[0];
+            }
+
+            @Override
+            public Type getRawType() {
+                return Outer.Inner.class;
+            }
+        };
+        assertFalse(TypeUtils.containsTypeVariables(self[0]));
+    }
+
     @Test
     void testContainsTypeVariablesMultiBoundWildcard() {
         final TypeVariable<?> t = getClass().getTypeParameters()[0];
@@ -576,6 +600,17 @@ void testContainsTypeVariablesMultiBoundWildcard() {
         assertFalse(TypeUtils.containsTypeVariables(wtNone));
     }
 
+    @Test
+    void testContainsTypeVariablesMutuallyReferencingTypeArguments() {
+        // LANG-1836
+        final CyclicParameterizedType a = new CyclicParameterizedType(List.class);
+        final CyclicParameterizedType b = new CyclicParameterizedType(List.class);
+        a.setTypeArguments(b);
+        b.setTypeArguments(a);
+        assertFalse(TypeUtils.containsTypeVariables(a));
+        assertFalse(TypeUtils.containsTypeVariables(b));
+    }
+
     @Test
     void testContainsTypeVariablesOwnerType() throws NoSuchFieldException {
         // LANG-1836: Outer<T>.Inner
@@ -631,6 +666,31 @@ abstract class Test2<G> {
         assertTrue(TypeUtils.containsTypeVariables(Test2.class.getMethod("m17").getGenericReturnType()));
     }
 
+    @Test
+    void testContainsTypeVariablesSelfOwnedTypeWithStructuralHashCode() {
+        // LANG-1836: detecting the cycle must not call hashCode(), which would follow the owner again.
+        final CyclicParameterizedType self = new CyclicParameterizedType(Outer.Inner.class);
+        self.setOwnerType(self);
+        assertFalse(TypeUtils.containsTypeVariables(self));
+    }
+
+    @Test
+    void testContainsTypeVariablesSelfReferencingTypeArgument() {
+        // LANG-1836: a (custom) ParameterizedType that is its own type argument must not cause a StackOverflowError.
+        final CyclicParameterizedType self = new CyclicParameterizedType(List.class);
+        self.setTypeArguments(self);
+        assertFalse(TypeUtils.containsTypeVariables(self));
+    }
+
+    @Test
+    void testContainsTypeVariablesSelfReferencingTypeArgumentBesideVariable() {
+        // LANG-1836: cutting the cycle must not hide a variable that is found elsewhere.
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final CyclicParameterizedType self = new CyclicParameterizedType(Map.class);
+        self.setTypeArguments(self, t);
+        assertTrue(TypeUtils.containsTypeVariables(self));
+    }
+
     @Test
     void testCyclicGenericArrayTypeToString() {
         final GenericArrayType[] holder = new GenericArrayType[1];
@@ -1681,104 +1741,15 @@ void testUnrollVariables() {
     }
 
     @Test
-    void testUnrollVariablesOwnerType() throws NoSuchFieldException {
-        // LANG-1836
-        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
-        final Map<TypeVariable<?>, Type> mapping = Collections.<TypeVariable<?>, Type>singletonMap(t, String.class);
-        final ParameterizedType outerString = TypeUtils.parameterize(Outer.class, String.class);
-        // Outer<T>.Inner -> Outer<String>.Inner
-        assertEquals(TypeUtils.parameterizeWithOwner(outerString, Outer.Inner.class),
-                TypeUtils.unrollVariables(mapping, Outer.class.getDeclaredField("inner").getGenericType()));
-        // Outer<T>.InnerU<Integer> -> Outer<String>.InnerU<Integer>
-        assertEquals(TypeUtils.parameterizeWithOwner(outerString, Outer.InnerU.class, Integer.class),
-                TypeUtils.unrollVariables(mapping, Outer.class.getDeclaredField("innerUInteger").getGenericType()));
-        // Outer<T>.InnerU<T> -> Outer<String>.InnerU<String>
-        assertEquals(TypeUtils.parameterizeWithOwner(outerString, Outer.InnerU.class, String.class),
-                TypeUtils.unrollVariables(mapping, Outer.class.getDeclaredField("innerUT").getGenericType()));
-        // Map.Entry<T, Integer> -> Map.Entry<String, Integer>: the raw owner Map.class is left unchanged.
-        assertEquals(TypeUtils.parameterizeWithOwner(Map.class, Map.Entry.class, String.class, Integer.class),
-                TypeUtils.unrollVariables(mapping, Outer.class.getDeclaredField("entry").getGenericType()));
-    }
-
-    @Test
-    void testContainsTypeVariablesSelfOwnedTypeWithStructuralHashCode() {
-        // LANG-1836: detecting the cycle must not call hashCode(), which would follow the owner again.
-        final CyclicParameterizedType self = new CyclicParameterizedType(Outer.Inner.class);
-        self.setOwnerType(self);
-        assertFalse(TypeUtils.containsTypeVariables(self));
-    }
-
-    @Test
-    void testContainsTypeVariablesCyclicOwnerType() {
-        // LANG-1836: a (custom) ParameterizedType that is its own owner must not cause a StackOverflowError.
-        final ParameterizedType[] self = new ParameterizedType[1];
-        self[0] = new ParameterizedType() {
-
-            @Override
-            public Type[] getActualTypeArguments() {
-                return new Type[0];
-            }
-
-            @Override
-            public Type getOwnerType() {
-                return self[0];
-            }
-
-            @Override
-            public Type getRawType() {
-                return Outer.Inner.class;
-            }
-        };
-        assertFalse(TypeUtils.containsTypeVariables(self[0]));
-    }
-
-    @Test
-    void testContainsTypeVariablesSelfReferencingTypeArgument() {
-        // LANG-1836: a (custom) ParameterizedType that is its own type argument must not cause a StackOverflowError.
-        final CyclicParameterizedType self = new CyclicParameterizedType(List.class);
-        self.setTypeArguments(self);
-        assertFalse(TypeUtils.containsTypeVariables(self));
-    }
-
-    @Test
-    void testContainsTypeVariablesMutuallyReferencingTypeArguments() {
-        // LANG-1836
-        final CyclicParameterizedType a = new CyclicParameterizedType(List.class);
-        final CyclicParameterizedType b = new CyclicParameterizedType(List.class);
-        a.setTypeArguments(b);
-        b.setTypeArguments(a);
-        assertFalse(TypeUtils.containsTypeVariables(a));
-        assertFalse(TypeUtils.containsTypeVariables(b));
-    }
-
-    @Test
-    void testContainsTypeVariablesSelfReferencingTypeArgumentBesideVariable() {
-        // LANG-1836: cutting the cycle must not hide a variable that is found elsewhere.
-        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
-        final CyclicParameterizedType self = new CyclicParameterizedType(Map.class);
-        self.setTypeArguments(self, t);
-        assertTrue(TypeUtils.containsTypeVariables(self));
-    }
-
-    @Test
-    void testUnrollVariablesWildcardCycleThroughOwnerType() {
-        // LANG-1836: {T -> Outer<? extends T>.Inner} must not cause a StackOverflowError when T is unrolled.
-        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
-        final WildcardType extendsT = TypeUtils.wildcardType().withUpperBounds(t).build();
-        final Type outerExtendsT = TypeUtils.parameterizeWithOwner(TypeUtils.parameterize(Outer.class, extendsT), Outer.Inner.class);
-        final Map<TypeVariable<?>, Type> mapping = Collections.<TypeVariable<?>, Type>singletonMap(t, outerExtendsT);
-        assertEquals(outerExtendsT, TypeUtils.unrollVariables(mapping, t));
-    }
-
-    @Test
-    void testUnrollVariablesSelfReferencingTypeArgument() {
-        // LANG-1836: the cycle through the type argument is left as it is while the variable is still unrolled.
+    void testUnrollVariablesCycleThroughTypeArgumentAndOwner() {
+        // LANG-1836: Map<Inner, T> whose argument Inner has the Map as its owner. The cycle is left as it is instead of overflowing the stack, while T is unrolled.
         final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
-        final CyclicParameterizedType self = new CyclicParameterizedType(Map.class);
-        self.setTypeArguments(self, t);
+        final CyclicParameterizedType inner = new CyclicParameterizedType(Outer.Inner.class);
+        final CyclicParameterizedType map = new CyclicParameterizedType(Map.class, inner, t);
+        inner.setOwnerType(map);
         final Map<TypeVariable<?>, Type> mapping = Collections.<TypeVariable<?>, Type>singletonMap(t, String.class);
-        final Type[] unrolledArguments = ((ParameterizedType) TypeUtils.unrollVariables(mapping, self)).getActualTypeArguments();
-        assertSame(self, unrolledArguments[0]);
+        final Type[] unrolledArguments = ((ParameterizedType) TypeUtils.unrollVariables(mapping, map)).getActualTypeArguments();
+        assertSame(inner, unrolledArguments[0]);
         assertEquals(String.class, unrolledArguments[1]);
     }
 
@@ -1809,26 +1780,32 @@ void testUnrollVariablesCyclicOwnerChainFromEitherType() {
     }
 
     @Test
-    void testUnrollVariablesSelfOwnedTypeWithTypeVariable() {
+    void testUnrollVariablesOwnerType() throws NoSuchFieldException {
         // LANG-1836
         final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
-        final CyclicParameterizedType self = new CyclicParameterizedType(Outer.InnerU.class, t);
-        self.setOwnerType(self);
         final Map<TypeVariable<?>, Type> mapping = Collections.<TypeVariable<?>, Type>singletonMap(t, String.class);
-        assertSame(self, TypeUtils.unrollVariables(mapping, self));
+        final ParameterizedType outerString = TypeUtils.parameterize(Outer.class, String.class);
+        // Outer<T>.Inner -> Outer<String>.Inner
+        assertEquals(TypeUtils.parameterizeWithOwner(outerString, Outer.Inner.class),
+                TypeUtils.unrollVariables(mapping, Outer.class.getDeclaredField("inner").getGenericType()));
+        // Outer<T>.InnerU<Integer> -> Outer<String>.InnerU<Integer>
+        assertEquals(TypeUtils.parameterizeWithOwner(outerString, Outer.InnerU.class, Integer.class),
+                TypeUtils.unrollVariables(mapping, Outer.class.getDeclaredField("innerUInteger").getGenericType()));
+        // Outer<T>.InnerU<T> -> Outer<String>.InnerU<String>
+        assertEquals(TypeUtils.parameterizeWithOwner(outerString, Outer.InnerU.class, String.class),
+                TypeUtils.unrollVariables(mapping, Outer.class.getDeclaredField("innerUT").getGenericType()));
+        // Map.Entry<T, Integer> -> Map.Entry<String, Integer>: the raw owner Map.class is left unchanged.
+        assertEquals(TypeUtils.parameterizeWithOwner(Map.class, Map.Entry.class, String.class, Integer.class),
+                TypeUtils.unrollVariables(mapping, Outer.class.getDeclaredField("entry").getGenericType()));
     }
 
     @Test
-    void testUnrollVariablesCycleThroughTypeArgumentAndOwner() {
-        // LANG-1836: Map<Inner, T> whose argument Inner has the Map as its owner. The cycle is left as it is instead of overflowing the stack, while T is unrolled.
+    void testUnrollVariablesRepeatedVariable() throws NoSuchFieldException {
         final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
-        final CyclicParameterizedType inner = new CyclicParameterizedType(Outer.Inner.class);
-        final CyclicParameterizedType map = new CyclicParameterizedType(Map.class, inner, t);
-        inner.setOwnerType(map);
         final Map<TypeVariable<?>, Type> mapping = Collections.<TypeVariable<?>, Type>singletonMap(t, String.class);
-        final Type[] unrolledArguments = ((ParameterizedType) TypeUtils.unrollVariables(mapping, map)).getActualTypeArguments();
-        assertSame(inner, unrolledArguments[0]);
-        assertEquals(String.class, unrolledArguments[1]);
+        // Map<T, T> -> Map<String, String>: every occurrence of T is unrolled, not only the first.
+        assertEquals(TypeUtils.parameterize(Map.class, String.class, String.class),
+                TypeUtils.unrollVariables(mapping, Outer.class.getDeclaredField("map").getGenericType()));
     }
 
     @Test
@@ -1842,12 +1819,35 @@ void testUnrollVariablesReturnsTheSameTypeWhenNothingChanges() {
     }
 
     @Test
-    void testUnrollVariablesRepeatedVariable() throws NoSuchFieldException {
+    void testUnrollVariablesSelfOwnedTypeWithTypeVariable() {
+        // LANG-1836
         final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final CyclicParameterizedType self = new CyclicParameterizedType(Outer.InnerU.class, t);
+        self.setOwnerType(self);
         final Map<TypeVariable<?>, Type> mapping = Collections.<TypeVariable<?>, Type>singletonMap(t, String.class);
-        // Map<T, T> -> Map<String, String>: every occurrence of T is unrolled, not only the first.
-        assertEquals(TypeUtils.parameterize(Map.class, String.class, String.class),
-                TypeUtils.unrollVariables(mapping, Outer.class.getDeclaredField("map").getGenericType()));
+        assertSame(self, TypeUtils.unrollVariables(mapping, self));
+    }
+
+    @Test
+    void testUnrollVariablesSelfReferencingTypeArgument() {
+        // LANG-1836: the cycle through the type argument is left as it is while the variable is still unrolled.
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final CyclicParameterizedType self = new CyclicParameterizedType(Map.class);
+        self.setTypeArguments(self, t);
+        final Map<TypeVariable<?>, Type> mapping = Collections.<TypeVariable<?>, Type>singletonMap(t, String.class);
+        final Type[] unrolledArguments = ((ParameterizedType) TypeUtils.unrollVariables(mapping, self)).getActualTypeArguments();
+        assertSame(self, unrolledArguments[0]);
+        assertEquals(String.class, unrolledArguments[1]);
+    }
+
+    @Test
+    void testUnrollVariablesWildcardCycleThroughOwnerType() {
+        // LANG-1836: {T -> Outer<? extends T>.Inner} must not cause a StackOverflowError when T is unrolled.
+        final TypeVariable<?> t = Outer.class.getTypeParameters()[0];
+        final WildcardType extendsT = TypeUtils.wildcardType().withUpperBounds(t).build();
+        final Type outerExtendsT = TypeUtils.parameterizeWithOwner(TypeUtils.parameterize(Outer.class, extendsT), Outer.Inner.class);
+        final Map<TypeVariable<?>, Type> mapping = Collections.<TypeVariable<?>, Type>singletonMap(t, outerExtendsT);
+        assertEquals(outerExtendsT, TypeUtils.unrollVariables(mapping, t));
     }
 
     @SuppressWarnings("unlikely-arg-type")