Generics and Reflection in Java

Generics

What are generics and what are their advantages

  • The Java generics mechanism was only added in JDK1.5. Therefore, in order to be compatible with previous jdk versions, the implementation of Java generics adopts a “pseudo-generic” strategy, that is, Java supports generics in syntax, but will Perform “type erasure” and then the virtual machine will mark it for us and convert it to the type we specified.

  • Generics can be understood as an undefined data type. It allows a class or method to adapt to multiple types. We can think of it as a container that can hold different types of data.

  • The advantage of generics is mainly reflected in type safety. The correctness of generic types can be checked when the Java compiler compiles, avoiding type conversion exceptions that may be encountered at runtime. It also avoids redundant type checking and conversion. Generic code will be erased at compile time without introducing additional runtime overhead.

How to declare a generic class?

  • The grammatical format of declaring a generic class is to use angle brackets <> after the class name, and specify the generic parameters within the brackets. The generic parameters can be any legal identifier. We usually use the capital letter T to represent it. Let’s take a look. Consider the following example
public class Generics<T>{

    //The variable type is declared as a generic variable name
    private T item;

    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }
}

The Generics class has a generic parameter T. By using generic parameters, we can define member variables and methods in the class, and use this generic parameter to represent specific types of data when needed. By declaring a generic class, we can specify a specific type, such as Generics or Generics, when creating an object. In this way, we can use this generic class to handle different types of data without having to write a separate class for each type.

What are generic methods

  • Generic methods are methods that use generic parameters in methods. By declaring generic type parameters in a method, the method can be applied to multiple data types, improving the flexibility and reusability of the code.
public class GenericMethod {

    public static <T> void print(T value) {
        System.out.println(value);
    }

    public static void main(String[] args) {
        Integer intValue = 10;
        Double doubleValue = 3.1415926;
        String stringValue = "The weather is really nice today";

        // Call the generic method and pass parameters of different types
        print(intValue); // Output: 10
        print(doubleValue); // Output: 3.1415926
        print(stringValue); // Output: The weather is really nice today
    }
}

In the above example, the print method is a generic method. It has a parameter of a generic type and prints out the value of the parameter. Then we call the print method three times, passing in three different types of values, because the print method is a generic method and can accept parameters of any type. So we don’t need to overload methods to handle different types of data.

What is a generic wildcard

Generic wildcard is a special symbol used in Java to represent unknown types. The wildcard character is represented by “?”, which can be used in the definition of generic classes, interfaces, and methods. Wildcards come in two forms: unbounded wildcards and bounded wildcards. let’s take a look

  • Unbounded wildcard:

We can see that List represents a list of unknown element types and can store elements of any type. However, we cannot directly add elements to such a list because we cannot determine the exact type of element to be added.

List<?> list = new ArrayList<>();
  • Bounded wildcards:

Bounded wildcards are represented by ? extends type or ?super type, which means that they can match the specified type or its subtype or its parent type.

  • ? extends type: indicates a subtype that matches the specified type.
List<? extends Number> list = new ArrayList<>();

In this example, List represents a list whose element type is Number or its subclasses, such as Integer, Double, etc. We can read elements from such a list because we know the element is Number or its subclass and can operate accordingly. However, we cannot add elements to such a list because we do not know the exact type of element to be added.

  • ? super type: indicates the parent type that matches the specified type.
List<? super Integer> list = new ArrayList<>();

In this example, List represents a list whose element type is Integer or its parent class, such as Number, Object, etc. We can add Integer or subclass elements of Integer to such a list because they are all parent classes of Integer.

What is type erasure

  • Type erasure refers to the process of erasing the actual type information of a generic type during compilation. For example, the type of List after erasure is, and the type of List after erasure is List. In the compiled bytecode, Type parameters of generic types are replaced by their upper bounds. It should be noted that the erasure and translation of generics are performed at compile time, not at runtime. The Java virtual machine does not know the existence of generics, and all generic types are erased to their original types. This is also the reason why when using generics, the specific type information of the generic cannot be obtained.

What are generic boundaries

  • Generic boundaries refer to the declaration of a generic type parameter that is used to limit the range of specific types that the parameter can accept. By using generic boundaries, we can make more precise restrictions on type parameters when writing generic code. As we mentioned above, we can use the extends keyword to limit the upper bound of generic type parameters, and use the super keyword. To limit the lower bound of generic type parameters. After specifying the upper bound, it means that the generic type parameter must be the specified class or its subclass, for example, it means that T must be the Number class or a subclass of Number. When the lower bound is specified, it means that the generic type parameter must be a superclass of the specified class, including the specified class itself. For example, the superclass indicating that T must be Integer can be Integer itself or the Object class.

Reflection

What is reflection

  • Reflection means that the program can obtain all the information of any object in the virtual machine through bytecode and objects during runtime, including attributes, methods, etc. Using reflection, we can easily obtain and manipulate various information about objects.

Function of the reflection mechanism

The reflection mechanism refers to dynamically obtaining inspection and modification class information and calling object methods when the program is running. It provides a series of functions

  • Obtain class information: You can obtain relevant information about a class at runtime, such as class name, parent class, implemented interfaces, constructors, methods and fields, etc.

  • Create object instance: You can dynamically create an object instance of a class at runtime. Regardless of whether there is a parameterless constructor or not, an instance of a class can be created through the newInstance() method of reflection.

  • Calling methods: You can dynamically call methods of a class at runtime. By obtaining the Method object, you can flexibly call public or private methods and pass the corresponding parameters.

  • Operation fields: You can dynamically obtain and modify the values of class fields at runtime. By getting the Field object, you can get and set the value of the field, whether it is public or private

  • Dynamic proxy: The reflection mechanism can implement dynamic proxy, dynamically generate a proxy class at runtime, and call the methods of the proxy object through the proxy class. Through dynamic proxy, we can add additional logic without modifying the original code.

  • Loading external classes and resources: External classes and resource files can be dynamically loaded while the program is running, achieving better flexibility and scalability.

Anyway, to put it simply, the reflection mechanism provides the ability to obtain and operate classes while the program is running, allowing us to dynamically create objects, call methods, modify properties, etc. at runtime. Make the program more scalable and adaptable.

Manipulate runtime classes through reflection

Let’s take a look at the following case first and then introduce it.

MyClass:

public class MyClass {

    private String name;

    public MyClass() {
    }

    public MyClass(String name) {
        this.name = name;
    }

    public void sayHello() {
        System.out.println("Hello, " + name);
    }
}

ReflectionExample:

public class ReflectionExample {

    public static void main(String[] args) {
        // Get class information
        Class<?> clazz = MyClass.class;
        String className = clazz.getName();
        System.out.println("Class Name: " + className);

        Constructor<?>[] constructors = clazz.getConstructors();
        System.out.println("Constructors:");
        for (Constructor<?> constructor : constructors) {
            System.out.println(constructor);
        }

        Method[] methods = clazz.getMethods();
        System.out.println("Methods:");
        for (Method method : methods) {
            System.out.println(method);
        }

        Field[] fields = clazz.getDeclaredFields();
        System.out.println("Fields:");
        for (Field field : fields) {
            System.out.println(field);
        }
    }
}

Let’s look at the above example. First, we get the Class object of the MyClass class through MyClass.class. Then use the getName() method to get the name of the class and print it out

Then use the getConstructors() method to get all the public constructors of the class and print them out. By traversing the constructor array, you can obtain detailed information about each constructor

Continue to use the getMethods() method to obtain all public methods of the class and print them out. Similarly, by traversing the array, you can get the detailed information of each method

Finally, use the getDeclaredFields() method to get all the fields of the class and print them out. Then you can get the detailed information of each field by traversing the field array

Basic principles of reflection

In Java, each class will generate a corresponding bytecode file when compiling, which contains the structural information of the class. When the program runs, the JVM will load these bytecode files into memory and create a Class object. to represent this class. The Class object is the core of the reflection mechanism. It saves all the information of the class and provides some APIs to access and operate this information.