18 What Pitfalls Arise When Reflection Annotations and Generics Meet Oop

18 What Pitfalls Arise When Reflection Annotations and Generics Meet OOP #

Today, we will discuss the topic of advanced features in Java, specifically, the pitfalls that may arise when reflection, annotations, and generics interact with overloading and inheritance.

You may argue that in business projects, there are few opportunities to use these advanced features such as reflection, annotations, and generics, so there isn’t much to learn. However, I want to emphasize that only by learning and utilizing these advanced features well can we develop code that is simpler and easier to read. Moreover, almost all frameworks use these three major advanced features. For example, to reduce duplicate code, we need to use reflection and annotations (see Lesson 21 for details).

If you have never used reflection, annotations, and generics before, you can get a general understanding through the official website:

Next, let’s look at some cases to see what pitfalls may arise when using these three major features in conjunction with OOP.

Reflectively Invoking Methods Does Not Determine Overloading Based on Parameters #

The functionality of reflection includes dynamically obtaining class and class member definitions at runtime, as well as dynamically reading properties and invoking methods. This means that when dynamically calling methods on a class, regardless of how the fields and methods in the class change, we can use the same rules to retrieve information and execute methods. Therefore, almost all ORM (Object-Relational Mapping), object mapping, and MVC frameworks use reflection.

The starting point for reflection is the Class class, which provides various methods to help us query its information. You can use this documentation to understand the purpose of each method.

Next, let’s look at a pitfall when using reflection to invoke methods that encounter overloading: there are two methods named age, with the parameters being the primitive type int and the wrapper type Integer respectively.

@Slf4j
public class ReflectionIssueApplication {
  private void age(int age) {
      log.info("int age = {}", age);
  }

  private void age(Integer age) {
      log.info("Integer age = {}", age);
  }
}

If we don’t use reflection to call the methods, it is clear which overloaded method will be invoked. For example, passing in 36 will invoke the overload method with the int parameter, and passing in Integer.valueOf("36") will invoke the Integer overload method:

ReflectionIssueApplication application = new ReflectionIssueApplication();
application.age(36);
application.age(Integer.valueOf("36"));

But a common misconception when using reflection is that the method overload is determined based on the parameters. For example, using getDeclaredMethod to obtain the age method, and then passing in Integer.valueOf("36"):

getClass().getDeclaredMethod("age", Integer.TYPE).invoke(this, Integer.valueOf("36"));

The output log proves that the int overload method is invoked:

14:23:09.801 [main] INFO org.geekbang.time.commonmistakes.advancedfeatures.demo1.ReflectionIssueApplication - int age = 36

In fact, the first step in invoking a method through reflection is to determine the method based on its method signature. In this particular case, the parameter Integer.TYPE passed to getDeclaredMethod represents int. Therefore, regardless of whether the parameter is a wrapper type or a primitive type, the age method with the int parameter will always be invoked.

By changing Integer.TYPE to Integer.class, the parameter type becomes the wrapper type Integer. At this time, regardless of whether Integer.valueOf("36") or the primitive type 36 is passed in:

getClass().getDeclaredMethod("age", Integer.class).invoke(this, Integer.valueOf("36"));
getClass().getDeclaredMethod("age", Integer.class).invoke(this, 36);

The age method with the Integer parameter will be invoked in both cases:

14:25:18.028 [main] INFO org.geekbang.time.commonmistakes.advancedfeatures.demo1.ReflectionIssueApplication - Integer age = 36
14:25:18.029 [main] INFO org.geekbang.time.commonmistakes.advancedfeatures.demo1.ReflectionIssueApplication - Integer age = 36

Now we are very clear that when invoking a method using reflection, the method to be invoked is determined by the method name and parameter types passed when obtaining the method through reflection. Next, let’s see what pitfalls arise when combining reflection, generics erasure, and inheritance.

Pitfalls of Bridge Methods in Generic Type Erasure #

Generics are a style or paradigm commonly used in strongly-typed programming languages, which allow developers to use type parameters instead of explicit types, and specify the specific types at instantiation. Generics are an effective means of code reuse, as they allow a set of code to be applied to multiple data types, avoiding the need to implement duplicate code for each data type.

The Java compiler applies powerful type checking to generics, allowing most generic coding errors to be exposed at compile time. However, there are still some coding errors, such as pitfalls in generic type erasure, that are only exposed at runtime. In this article, I will share with you a case study related to this issue.

In a project, there was a requirement to log changes in class fields whenever their contents changed. To achieve this, the developers came up with the idea of defining a generic parent class and a unified logging method within it, so that the child classes could inherit and reuse this method. Although the code went live without issues, there was always a problem of duplicate log entries. Initially, we suspected it was an issue with the logging framework, but after thorough investigation, we discovered that it was actually a problem with generics. It took multiple modifications before we finally solved the problem.

Here is the definition of the parent class: it has a generic placeholder T and an AtomicInteger counter used to record the number of updates to the value field, where the value field has the type T. The setValue method increments the counter by 1 each time a value is assigned to the value field. I also override the toString method to output the value and the counter:

class Parent<T> {

    // Used to record the number of value updates, simulating the logic of logging
    AtomicInteger updateCount = new AtomicInteger();

    private T value;

    // Override toString to output the value and the update count
    @Override
    public String toString() {
        return String.format("value: %s updateCount: %d", value, updateCount.get());
    }

    // Set the value
    public void setValue(T value) {
        this.value = value;
        updateCount.incrementAndGet();
    }
}

Here is the implementation of the child class Child1: it inherits from the parent class but does not provide a generic parameter for the parent class; it defines a setValue method that takes a parameter of type String, and uses super.setValue to invoke the parent class method for logging. It is evident that the developer designed it this way in order to override the setValue implementation of the parent class:

class Child1 extends Parent {
    public void setValue(String value) {
        System.out.println("Child1.setValue called");
        super.setValue(value);
    }
}

In the implementation, the method calls in the child class are performed using reflection. After instantiating an object of type Child1, all the methods are obtained using getClass().getMethods(), then the setValue methods are filtered by name and invoked by passing the string “test” as a parameter:

Child1 child1 = new Child1();

Arrays.stream(child1.getClass().getMethods())
        .filter(method -> method.getName().equals("setValue"))
        .forEach(method -> {
            try {
                method.invoke(child1, "test");
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
System.out.println(child1.toString());

After running the code, we can see that although the value field of Parent is correctly set to “test”, the setValue method of the parent class is called twice, and the counter shows 2 instead of 1:

Child1.setValue called
Parent.setValue called
Parent.setValue called
value: test updateCount: 2

Clearly, the issue of the Parent’s setValue method being called twice is because the getMethods() method found two methods named setValue, corresponding to the parent class and the child class respectively.

In this case, there are two reasons why the child class method fails to override the parent class method:

Firstly, the child class does not specify the String generic parameter, so the generic method setValue(T value) of the parent class, after type erasure, becomes setValue(Object value), and the setValue method in the child class with an argument of type String is treated as a new method.

Secondly, the child class’s setValue method does not have the @Override annotation, so the compiler fails to detect the failed override. This shows that using the @Override annotation when overriding a method in a subclass is a good practice.

However, the developer believed that the problem lay in improper usage of the reflection API, and did not realize the failed override. After consulting the documentation, they found that the getMethods() method retrieves all the public methods of the current class and its parent class, while the getDeclaredMethods() method only retrieves all the public, protected, package, and private methods of the current class.

Therefore, they replaced getMethods() with getDeclaredMethods():

Arrays.stream(child1.getClass().getDeclaredMethods())
    .filter(method -> method.getName().equals("setValue"))
    .forEach(method -> {
        try {
            method.invoke(child1, "test");
        } catch (Exception e) {
            e.printStackTrace();
        }
    });

Although this solves the problem of duplicate log entries, it does not address the underlying issue of the child class method failing to override the parent class method. The output becomes:

Child1.setValue called
Parent.setValue called
value: test updateCount: 1

This is just treating the symptom and not the root cause. Other developers using Child1 will still see two setValue methods, which can be very confusing.

Fortunately, the architect discovered this issue before it was fixed and released, and asked the developer to reimplement Child2. In Child2, the generic type T is specified as String when inheriting from Parent, and the setValue method is annotated with the @Override keyword to achieve proper method override:

class Child2 extends Parent<String> {

    @Override
    public void setValue(String value) {
        System.out.println("Child2.setValue called");
        super.setValue(value);
    }
}
However, unfortunately, after the fixed code was deployed, the log duplication issue still occurred:

Child2.setValue called Parent.setValue called Child2.setValue called Parent.setValue called value: test updateCount: 2


As you can see, the `setValue` method of the `Child2` class was called twice this time. The developers were surprised and said that there must be a bug in the reflection. The methods found through `getDeclaredMethods` must come from the `Child2` class itself. Also, looking at the `Child2` class, there is only one `setValue` method, so why is it called twice?

If we debug, we can find that the `Child2` class actually has 2 `setValue` methods, one with a `String` parameter and the other with an `Object` parameter.

![img](../images/81116d6f11440f92757e4fe775df71b8.png)

If we don't invoke the method through reflection, it would be difficult to find this issue. In fact, this is caused by type erasure of generics. Let's analyze it.

As we know, in Java, the generic types are erased and replaced with `Object` at compile time. Although the subclass specifies that the generic type `T` of the superclass is `String`, `T` will be erased to `Object` at compile time, so the parameter of the superclass `setValue` method becomes `Object`, and the `value` is also an `Object`. If the `setValue` method of the `Child2` subclass wants to override the `setValue` method of the superclass, the parameter must also be `Object`. Therefore, the compiler will generate a so-called bridge method for us. You can use the `javap` command to decompile the compiled `Child2` class bytecode:

javap -c /Users/zhuye/Documents/common-mistakes/target/classes/org/geekbang/time/commonmistakes/advancedfeatures/demo3/Child2.class Compiled from “GenericAndInheritanceApplication.java” class org.geekbang.time.commonmistakes.advancedfeatures.demo3.Child2 extends org.geekbang.time.commonmistakes.advancedfeatures.demo3.Parent<java.lang.String> { org.geekbang.time.commonmistakes.advancedfeatures.demo3.Child2(); Code: 0: aload_0 1: invokespecial #1 // Method org/geekbang/time/commonmistakes/advancedfeatures/demo3/Parent."":()V 4: return public void setValue(java.lang.String); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Child2.setValue called 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: aload_0 9: aload_1 10: invokespecial #5 // Method org/geekbang/time/commonmistakes/advancedfeatures/demo3/Parent.setValue:(Ljava/lang/Object;)V 13: return

public void setValue(java.lang.Object); Code: 0: aload_0 1: aload_1 2: checkcast #6 // class java/lang/String 5: invokevirtual #7 // Method setValue:(Ljava/lang/String;)V 8: return }


As you can see, the `setValue` method with the `Object` parameter internally calls the `setValue` method with the `String` parameter (line 27), which is the method implemented in the code. If the compiler didn't generate this bridge method for us, then the `Child2` subclass would override the `setValue` method of the superclass after the type erasure, with the parameter being `Object`. These two methods, one with `String` and one with `Object` parameters, obviously don't comply with the semantics of Java:

class Parent {

AtomicInteger updateCount = new AtomicInteger();
private Object value;

public void setValue(Object value) {
    System.out.println("Parent.setValue called");
    this.value = value;
    updateCount.incrementAndGet();
}

}

class Child2 extends Parent {

@Override
public void setValue(String value) {
    System.out.println("Child2.setValue called");
    super.setValue(value);
}

}


Opening the `Child2` class with the `jclasslib` tool, you can also see that the bridge method with `Object` parameter is marked with `public + synthetic + bridge` attributes. `synthetic` represents invisible code generated by the compiler, and `bridge` represents the bridge code generated after type erasure of generics:

![img](../images/b5e30fb0ade19d71cd7fad1730e85808.png)

Knowing this issue, the way to fix it becomes clear. We can use the `isBridge` method of the `Method` class to determine whether a method is a bridge method:

After obtaining all methods using the `getDeclaredMethods` method, we need to filter them based on both the method name `setValue` and the `isBridge` condition in order to achieve an exclusive filter;

When using `Stream`, if we only want to match 0 or 1 item, we can use `ifPresent` in conjunction with `findFirst` method.

The fixed code is as follows:

Arrays.stream(child2.getClass().getDeclaredMethods()) .filter(method -> method.getName().equals(“setValue”) && !method.isBridge()) .findFirst() .ifPresent(method -> { try { method.invoke(child2, “test”); } catch (Exception e) { e.printStackTrace(); } });


This way, we can get the correct output:

Child2.setValue called Parent.setValue called value: test updateCount: 1


In conclusion, when querying the list of class methods using reflection, we need to pay attention to two points:

1. There is a difference between `getMethods` and `getDeclaredMethods`. The former can query the parent class methods, while the latter can only query the current class methods.

2. When using reflection to invoke methods, we need to be aware of filtering bridge methods.
## Can annotations be inherited?

Annotations can provide metadata for Java code, and various frameworks utilize annotations to expose functionality. For example, Spring framework has annotations such as `@Service`, `@Controller`, `@Bean`, and Spring Boot has `@SpringBootApplication`.

Frameworks can understand the functionality or characteristics of elements such as classes or methods marked with annotations, and enable or execute corresponding functionality based on them. Configuring frameworks through annotations instead of API calls is a declarative interaction, which can simplify framework configuration work and decouple it from the framework.

Developers may think that when a class is inherited, the annotations of the class can also be inherited, and when a subclass overrides a method in the superclass, the annotations on the superclass method can also apply to the subclass. However, these views are actually incorrect or incomplete. Let's verify this.

First, define an annotation `MyAnnotation` that contains a `value` attribute, which can be applied to methods or classes:

```java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String value();
}

Then, define a parent class Parent that is annotated with @MyAnnotation and set the value to “Class”. The foo method of this class is also marked with @MyAnnotation and the value is set to “Method”. Next, define a subclass Child that inherits from the Parent class and overrides the foo method, but neither the class nor the method in the subclass is annotated with @MyAnnotation.

@MyAnnotation(value = "Class")
@Slf4j
static class Parent {
    @MyAnnotation(value = "Method")
    public void foo() {
    }
}

@Slf4j
static class Child extends Parent {
    @Override
    public void foo() {
    }
}

Then, using reflection, retrieve the annotation information of the class and methods of both Parent and Child, and output the value of the value attribute of the annotation (if the annotation does not exist, output an empty string):

private static String getAnnotationValue(MyAnnotation annotation) {
    if (annotation == null) return "";
    return annotation.value();
}

public static void wrong() throws NoSuchMethodException {
    // Get annotations on the class and methods of the parent class
    Parent parent = new Parent();
    log.info("ParentClass:{}", getAnnotationValue(parent.getClass().getAnnotation(MyAnnotation.class)));
    log.info("ParentMethod:{}", getAnnotationValue(parent.getClass().getMethod("foo").getAnnotation(MyAnnotation.class)));
    // Get annotations on the class and methods of the subclass
    Child child = new Child();
    log.info("ChildClass:{}", getAnnotationValue(child.getClass().getAnnotation(MyAnnotation.class)));
    log.info("ChildMethod:{}", getAnnotationValue(child.getClass().getMethod("foo").getAnnotation(MyAnnotation.class)));
}

The output is as follows:

17:34:25.495 [main] INFO org.geekbang.time.commonmistakes.advancedfeatures.demo2.AnnotationInheritanceApplication - ParentClass:Class
17:34:25.501 [main] INFO org.geekbang.time.commonmistakes.advancedfeatures.demo2.AnnotationInheritanceApplication - ParentMethod:Method
17:34:25.504 [main] INFO org.geekbang.time.commonmistakes.advancedfeatures.demo2.AnnotationInheritanceApplication - ChildClass:
17:34:25.504 [main] INFO org.geekbang.time.commonmistakes.advancedfeatures.demo2.AnnotationInheritanceApplication - ChildMethod:

As seen, the annotations on the parent class and its methods can be correctly obtained, but not on the child class and its methods. This indicates that the subclass and its methods cannot automatically inherit annotations from the superclass and its methods.

If you are familiar with annotations, you may know that marking the @Inherited meta-annotation on an annotation can achieve annotation inheritance. So, if we mark @MyAnnotation with @Inherited, can we solve the problem easily?

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface MyAnnotation {
    String value();
}

After making this change and rerunning the code, the output is as follows:

17:44:54.831 [main] INFO org.geekbang.time.commonmistakes.advancedfeatures.demo2.AnnotationInheritanceApplication - ParentClass:Class
17:44:54.837 [main] INFO org.geekbang.time.commonmistakes.advancedfeatures.demo2.AnnotationInheritanceApplication - ParentMethod:Method
17:44:54.838 [main] INFO org.geekbang.time.commonmistakes.advancedfeatures.demo2.AnnotationInheritanceApplication - ChildClass:Class
17:44:54.838 [main] INFO org.geekbang.time.commonmistakes.advancedfeatures.demo2.AnnotationInheritanceApplication - ChildMethod:

As seen, the child class can obtain the annotation from the parent class; although the foo method of the subclass overrides the parent class method and the annotation supports inheritance, it still cannot obtain the annotation on the method.

If you carefully read the documentation of @Inherited, you will find that it can only achieve annotation inheritance on classes. To achieve annotation inheritance on methods, you can find the annotations on methods in the inheritance chain through reflection. However, this implementation is cumbersome and requires consideration for bridge methods.

Fortunately, Spring provides the AnnotatedElementUtils class to facilitate the handling of annotation inheritance issues. The findMergedAnnotation utility method of this class can help us find annotations on the superclass and interfaces, as well as on the methods of the superclass and interface methods, and can handle bridge methods, thus enabling us to easily find annotations in the inheritance chain:

Child child = new Child();
log.info("ChildClass:{}", getAnnotationValue(AnnotatedElementUtils.findMergedAnnotation(child.getClass(), MyAnnotation.class)));
log.info("ChildMethod:{}", getAnnotationValue(AnnotatedElementUtils.findMergedAnnotation(child.getClass().getMethod("foo"), MyAnnotation.class)));

After modifying and running the code, the output is as follows:

17:47:30.058 [main] INFO org.geekbang.time.commonmistakes.advancedfeatures.demo2.AnnotationInheritanceApplication - ChildClass:Class
17:47:30.059 [main] INFO org.geekbang.time.commonmistakes.advancedfeatures.demo2.AnnotationInheritanceApplication - ChildMethod:Method

As seen, the foo method of the subclass also obtained the annotation from the superclass method.

Key Summary #

Today, I shared with you some pitfalls you may encounter when using Java reflection, annotations, and generics in conjunction with OOP.

First, calling methods using reflection is not determined by the arguments passed at invocation, but rather by the method name and parameter types obtained during method retrieval. When dealing with overloaded methods that have wrapper types and primitive types, you need to pay special attention to this.

Second, when obtaining class members using reflection, you need to be aware of the difference between getXXX and getDeclaredXXX methods, where XXX includes Methods, Fields, Constructors, and Annotations. These two categories of methods have some subtle differences in implementation for different member types XXX and objects. Please refer to the official documentation for more details. The getDeclaredMethods method mentioned today cannot retrieve methods defined in the superclass, while the getMethods method can. This is just one of the differences and cannot be applied to all XXX.

Third, due to type erasure, generics can cause the placeholder T in a generic method to be replaced with Object. If a subclass overrides a parent class implementation using a specific type, the compiler will generate a bridge method. This satisfies both the definition of subclass method overriding the parent class method and the requirement of having a specific type for the subclass-implemented method. When using reflection to retrieve a list of methods, you need to pay special attention to this.

Fourth, custom annotations can be inherited through the use of the meta-annotation @Inherited, but this only applies to classes. If you want to inherit annotations defined on interfaces or methods, you can use Spring’s utility class AnnotatedElementUtils, and pay attention to the differences between various getXXX methods and findXXX methods. For more details, please refer to Spring’s documentation.

Finally, I want to mention that compiled code is not exactly the same as the original code. The compiler may perform optimizations, and with compile-time enhancement frameworks like AspectJ, the dynamic retrieval of type metadata using reflection may differ from the source code we write. This needs to be particularly noted. You can add assertions in reflection and throw exceptions directly when encountering unexpected situations to ensure that the business logic implemented through reflection meets expectations.

The code used today is stored in my GitHub repository, which you can access by clicking on this link.

Reflection and Discussion #

After the erasure of generic types, a bridge method is generated, which is also a synthetic method. Besides the erasure of generic types, do you know any other situations where the compiler will generate synthetic methods?

Regarding the issue of annotation inheritance, do you think Spring’s common annotations like @Service and @Controller support inheritance?

Have you encountered any other pitfalls related to advanced Java features? I’m Zhu Ye, and I welcome you to leave a comment in the comment section to share your thoughts with me. You are also welcome to share today’s content with your friends or colleagues to have a discussion together.