Upper segment After concurrency is introduced, starting with this section, we will explore some of the dynamic features of Java, including reflection, class loaders, annotations, and dynamic proxies. With these features, some flexible and general functions can be implemented in an elegant way, often used in various frameworks, libraries and system programs, such as:
- stay 63 quarter Jackson, a practical serialization library introduced in this paper, implements a universal serialization/deserialization mechanism using reflection and annotation
- There are many libraries such as Spring MVC, Jersey used to process Web requests. Using reflection and annotation, user's request parameters and content can be easily converted into Java objects, and Java objects into response content.
- There are many libraries, such as Spring, Guice, which use these features to implement object management containers to facilitate programmers to manage the life cycle of objects and their complex dependencies.
- Application servers such as Tomcat use class loaders to isolate different applications, and JSP technology uses class loaders to implement the features that modify code can take effect without restarting it.
- AOP-Aspect Oriented Programming separates common concerns in programming, such as logging, security checking, from the main logic of the business, reduces redundant code and improves the maintainability of the program. AOP needs to rely on these features to achieve.
In this section, we first look at the reflection mechanism.
In general, we all know and depend on the type of data when we manipulate data, such as:
- Create objects by type using new
- Define variables by type. Types may be basic types, classes, interfaces, or arrays
- Passing a particular type of object to a method
- Call the method of an object by accessing its attributes according to its type
The compiler also checks and compiles the code according to its type.
Reflection is different. It dynamically acquires the type of information, such as interface information, member information, method information, construction method information, etc., at run time, not at compile time. It creates objects, accesses/modifies members, invokes methods and so on according to the information dynamically acquired. So it's abstract. Now we'll specify that the entry of reflection is a class named "Class". Let's take a look at it.
Class class
Get the Class object
We are 17 quarter After introducing the basic principles of class and inheritance, we mentioned that every loaded class has a class information in memory, and every object has a reference to the class information it belongs to. In Java, the corresponding class of class information is java.lang.Class. Note that not lowercase class, class is the key to define the class. The root parent class Object of all classes has a method to obtain the class object of the object:
public final native Class<?> getClass()
Class is a generic class with a type parameter. getClass() does not know the specific type, so it returns Class <?>.
Class objects do not necessarily require instance objects. If you know the class name when you write a program, you can use <class name>.class to get Class objects, such as:
Class<Date> cls = Date.class;
Interfaces also have Class objects, and this approach is also applicable to interfaces, such as:
Class<Comparable> cls = Comparable.class;
There is no getClass method for the basic type, but there are also corresponding Class objects with type parameters corresponding to the wrapper type, such as:
Class<Integer> intCls = int.class; Class<Byte> byteCls = byte.class; Class<Character> charCls = char.class; Class<Double> doubleCls = double.class;
As a special return type, void also has corresponding Class:
Class<Void> voidCls = void.class;
For arrays, each type has a Class object corresponding to the type of array, and each dimension has a Class object corresponding to the type of array. That is, one dimension array has a Class object, and one dimension array has a Class object corresponding to the type of array.
String[] strArr = new String[10]; int[][] twoDimArr = new int[3][2]; int[] oneDimArr = new int[10]; Class<? extends String[]> strArrCls = strArr.getClass(); Class<? extends int[][]> twoDimArrCls = twoDimArr.getClass(); Class<? extends int[]> oneDimArrCls = oneDimArr.getClass();
Enumeration types also have corresponding Class es, such as:
enum Size { SMALL, MEDIUM, BIG } Class<Size> cls = Size.class;
Class has a static method for Name, which can load Class directly according to the class name and get Class objects, such as:
try { Class<?> cls = Class.forName("java.util.HashMap"); System.out.println(cls.getName()); } catch (ClassNotFoundException e) { e.printStackTrace(); }
Note that forName may throw an exception ClassNotFoundException.
With the Class object, we can learn a lot about the type of information, and take some actions based on this information. Class has many methods, most of which are simple, direct and easy to understand. Below, we divide into several groups to give a brief introduction.
Name information
Class has the following methods to obtain name-related information:
public String getName() public String getSimpleName() public String getCanonicalName() public Package getPackage()
getSimpleName does not contain package information. getName returns the real name used inside Java. getCanonical Name returns a more friendly name. getPackage returns package information. Their differences can be seen in the following table:
What needs to be explained is the getName return value of the array type, which uses the prefix [to denote an array, several [to denote a multi-dimensional array, the type of the array is represented by a character, I to denote an int, L to denote a class or interface, and the corresponding relations between other types and characters are boolean(Z), byte(B), char(C), double(D), float(F), long(J), short(S). For an array of reference types, notice that there is a semicolon at the end.
Field (instance and static variable) information
The static and instance variables defined in the class are called fields. They are represented by the class Field and are located under the package java. util. reflection. The reflective related classes involved later are all located under the package. Class has four methods to obtain Field information:
//Return to all public Field, including its parent class, returns an empty array if no field exists public Field[] getFields() //Returns all fields declared in this class, including non- public Of, but not of, the parent class. public Field[] getDeclaredFields() //Returns the name specified in this class or parent class. public Field, no exception thrown NoSuchFieldException public Field getField(String name) //Returns the field of the specified name declared in this class. No exception was thrown. NoSuchFieldException public Field getDeclaredField(String name)
Field also has many ways to obtain information about a field, or to access and manipulate the value of the field in a specified object through Field. The basic methods are as follows:
//Get the name of the field public String getName() //Determine whether the current program has access to this field public boolean isAccessible() //flag Set as true Indicate Ignorance Java Access checking mechanism to allow read and write non- public Fields of public void setAccessible(boolean flag) //Gets the specified object obj The value of the field in public Object get(Object obj) //Will specify the object obj Set the value of this field to value public void set(Object obj, Object value)
In the get/set method, for static variables, obj is ignored and can be null. If the field value is a basic type, get/set will automatically convert between the basic type and the corresponding packaging type. For private fields, calling get/set directly will throw an illegal access exception Illegal AccessException. SetAccessib should be called first. Le (true) to turn off Java's checking mechanism.
Look at a simple sample code:
List<String> obj = Arrays.asList(new String[]{"Old horse","programming"}); Class<?> cls = obj.getClass(); for(Field f : cls.getDeclaredFields()){ f.setAccessible(true); System.out.println(f.getName()+" - "+f.get(obj)); }
The code is relatively simple, let's not go into details. We are ThreadLocal section This article introduces how to use reflection to clear ThreadLocal. Repeat the code here, and the meaning is clear.
protected void beforeExecute(Thread t, Runnable r) { try { //Empty all with reflection ThreadLocal Field f = t.getClass().getDeclaredField("threadLocals"); f.setAccessible(true); f.set(t, null); } catch (Exception e) { e.printStackTrace(); } super.beforeExecute(t, r); }
In addition to the above methods, Field has many other methods, such as:
//Returns modifiers for fields public int getModifiers() //Type of return field public Class<?> getType() //Operate fields with basic types public void setBoolean(Object obj, boolean z) public boolean getBoolean(Object obj) public void setDouble(Object obj, double d) public double getDouble(Object obj) //Annotation information for query fields public <T extends Annotation> T getAnnotation(Class<T> annotationClass) public Annotation[] getDeclaredAnnotations()
getModifiers returns an int that can be interpreted by static methods of the Modifier class, such as assuming that the Student class has the following fields:
public static final int MAX_NAME_LEN = 255;
The modifier for this field can be viewed as follows:
Field f = Student.class.getField("MAX_NAME_LEN"); int mod = f.getModifiers(); System.out.println(Modifier.toString(mod)); System.out.println("isPublic: " + Modifier.isPublic(mod)); System.out.println("isStatic: " + Modifier.isStatic(mod)); System.out.println("isFinal: " + Modifier.isFinal(mod)); System.out.println("isVolatile: " + Modifier.isVolatile(mod));
The output is:
public static final isPublic: true isStatic: true isFinal: true isVolatile: false
As for the annotations, we will discuss them in more detail in the next section.
Method information
Static and instance methods defined in classes are called methods. Class has four methods to obtain Method information, represented by class Method.
//Return to all public Method, including its parent class, returns an empty array if no method exists public Method[] getMethods() //Returns all methods declared in this class, including non- public Of, but not of, the parent class. public Method[] getDeclaredMethods() //Returns the specified name and parameter type in this class or parent class public Method, no throw exception was found NoSuchMethodException public Method getMethod(String name, Class<?>... parameterTypes) //Returns the method of the specified name and parameter type declared in this class. No exception was thrown. NoSuchMethodException public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
Method also has many methods, which can obtain the information of the method or call the method of the object through the method. The basic methods are as follows:
//Get the name of the method public String getName() //flag Set as true Indicate Ignorance Java Access checking mechanism to allow non-invocation public Method public void setAccessible(boolean flag) //In the specified object obj Up call Method Represents the method and passes the list of parameters as follows args public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException
For invoke methods, if the Method is static, obj is ignored, null can be null, args can be null, or an empty array, the return value of the Method call is wrapped as Object, and if the actual Method call throws an exception, the exception is wrapped as Invocation Target Exception and re-thrown through getCaus. The original anomaly is obtained by e Method.
Look at a simple sample code:
Class<?> cls = Integer.class; try { Method method = cls.getMethod("parseInt", new Class[]{String.class}); System.out.println(method.invoke(null, "123")); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); }
Method also has many methods to get information about modifiers, parameters, return values, annotations, etc. for a method, such as:
//Gets the modifier for the method, and the return value can be passed through Modifier Interpretation of Class public int getModifiers() //Get the parameter type of the method public Class<?>[] getParameterTypes() //Gets the return value type of the method public Class<?> getReturnType() //Gets the exception type thrown by the method declaration public Class<?>[] getExceptionTypes() //Getting annotation information public Annotation[] getDeclaredAnnotations() public <T extends Annotation> T getAnnotation(Class<T> annotationClass) //Obtaining annotation information for method parameters public Annotation[][] getParameterAnnotations()
Create objects and construct methods
Class has a method that can be used to create objects:
public T newInstance() throws InstantiationException, IllegalAccessException
It calls the default constructor of the class (that is, the no-parameter public constructor), and throws an exception InstantiationException if the class does not have that constructor. Look at a simple example:
Map<String,Integer> map = HashMap.class.newInstance(); map.put("hello", 123);
Many libraries and frameworks that use reflection assume by default that classes have parametric public constructs, so remember to provide one when classes use these libraries and frameworks.
newInstance can only use the default constructor, Class has some methods, you can get all the constructors:
//Get all public Construct a method that returns an empty array with a possible length of 0 public Constructor<?>[] getConstructors() //Get all construction methods, including non- public Of public Constructor<?>[] getDeclaredConstructors() //Gets the specified parameter type public Constructing method, no throwing exception found NoSuchMethodException public Constructor<T> getConstructor(Class<?>... parameterTypes) //Gets the constructor for the specified parameter type, including non- public No exception was found. NoSuchMethodException public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
Class Constructor represents the construction method by which objects can be created.
public T newInstance(Object ... initargs) throws InstantiationException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException
For example:
Constructor<StringBuilder> contructor= StringBuilder.class .getConstructor(new Class[]{int.class}); StringBuilder sb = contructor.newInstance(100);
In addition to creating objects, Constructor has many ways to get a lot of information about the construction methods, such as:
//Get type information for parameters public Class<?>[] getParameterTypes() //The modifier of the constructor, the return value can be passed through Modifier Interpretation of Class public int getModifiers() //Annotation information for construction methods public Annotation[] getDeclaredAnnotations() public <T extends Annotation> T getAnnotation(Class<T> annotationClass) //Annotation information of parameters in construction method public Annotation[][] getParameterAnnotations()
Type Checking and Conversion
We are 16 quarter The instanceof keyword has been introduced, which can be used to determine the actual object type that the variable points to. The type after instanceof is determined in the code. If the type to be checked is dynamic, the following method of Class class can be used:
public native boolean isInstance(Object obj)
That is to say, the following code:
if(list instanceof ArrayList){ System.out.println("array list"); }
The output of the following code is the same:
Class cls = Class.forName("java.util.ArrayList"); if(cls.isInstance(list)){ System.out.println("array list"); }
In addition to judging types, mandatory type conversions are often required in programs, such as:
List list = .. if(list instanceof ArrayList){ ArrayList arrList = (ArrayList)list; }
In this code, the type to which the cast is forced is known when the code is written. If it is dynamic, you can use the following method of Class:
public T cast(Object obj)
For example:
public static <T> T toType(Object obj, Class<T> cls){ return cls.cast(obj); }
IsInstance/case describes the relationship between objects and classes. Class also has a way to judge the relationship between classes:
// Check parameter type cls Can we assign it to the present? Class Variables of type public native boolean isAssignableFrom(Class<?> cls);
For example, the results of the following expressions are true:
Object.class.isAssignableFrom(String.class) String.class.isAssignableFrom(String.class) List.class.isAssignableFrom(ArrayList.class)
Class Type Information
Class represents not only ordinary classes, but also internal classes, but also basic types, arrays and so on. What type is it for a given Class object? Examination can be carried out by the following methods:
//Is it an array? public native boolean isArray(); //Is it a basic type? public native boolean isPrimitive(); //Is it an interface? public native boolean isInterface(); //Is it an enumeration? public boolean isEnum() //Is it an annotation? public boolean isAnnotation() //Is it an anonymous inner class? public boolean isAnonymousClass() //Is it a member class? public boolean isMemberClass() //Is it a local class? public boolean isLocalClass()
The difference between anonymous inner classes, member classes and local classes is explained. Local classes are non-anonymous inner classes defined within methods, such as the following code:
public static void localClass(){ class MyLocal { } Runnable r = new Runnable() { @Override public void run(){ } }; System.out.println(MyLocal.class.isLocalClass()); System.out.println(r.getClass().isLocalClass()); }
MyLocal is defined within the localClass method as a local class. The object of r belongs to an anonymous class, but not a local class.
Membership classes are also internal classes, which are defined inside the class and outside the method. They are neither anonymous classes nor local classes.
Class declaration information
Class also has many ways to obtain declarative information of classes, such as modifiers, parent classes, interfaces implemented, annotations, etc. as follows:
//Gets the modifier, and the return value can be passed through Modifier Interpretation of Class public native int getModifiers() //Get the parent class if Object,The parent class is null public native Class<? super T> getSuperclass() //For classes, all interfaces declared for themselves, for interfaces, directly extended interfaces, excluding indirect inheritance through parent classes public native Class<?>[] getInterfaces(); //Annotations to self-statements public Annotation[] getDeclaredAnnotations() //All annotations, including inherited ones public Annotation[] getAnnotations() //Gets or checks annotations of a specified type, including inherited ones public <A extends Annotation> A getAnnotation(Class<A> annotationClass) public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
Internal class
With regard to internal classes, Class has some special methods, such as:
//Get all public Internal classes and interfaces, including those inherited from parent classes public Class<?>[] getClasses() //Get all the internal classes and interfaces you declare public Class<?>[] getDeclaredClasses() //If at present Class For internal classes, get the exterior that declares the class Class object public Class<?> getDeclaringClass() //If at present Class For an internal class, get the class that directly contains that class public Class<?> getEnclosingClass() //If at present Class For local or anonymous inner classes, return the method that contains it public Method getEnclosingMethod()
Class loading
Class has two static methods that can load classes according to class names:
public static Class<?> forName(String className) public static Class<?> forName(String name, boolean initialize, ClassLoader loader)
ClassLoader represents the class loader, as we will see in the next section, initialize indicates whether the class initialization code (such as the static statement block) is executed after loading. The first method does not pass these parameters, which is equivalent to calling:
Class.forName(className, true, currentLoader)
CurrtLoader represents the ClassLoader that loads the current class.
Here, the return value of className is the same as that of Classs. getName, for example, for String arrays:
String name = "[Ljava.lang.String;"; Class cls = Class.forName(name); System.out.println(cls == String[].class);
It should be noted that the basic type does not support the forName method, that is to say, as follows:
Class.forName("int");
The exception ClassNotFoundException is thrown, so how do I construct Class objects from strings of the original type? Class.forName can be wrapped, such as:
public static Class<?> forName(String className) throws ClassNotFoundException{ if("int".equals(className)){ return int.class; } //Other basic types... return Class.forName(className); }
Reflection and Array
For an array type, there is a special method to get its element type:
public native Class<?> getComponentType()
For example:
String[] arr = new String[]{}; System.out.println(arr.getClass().getComponentType());
The output is:
class java.lang.String
In the java.lang.reflect package, there is a special class Array for arrays (note that it is not Arrays in java.util), which provides some reflection support for arrays, so as to deal with multiple types of arrays in a unified way. The main methods are:
//Create an array of specified element types and length. public static Object newInstance(Class<?> componentType, int length) //Create multidimensional arrays public static Object newInstance(Class<?> componentType, int... dimensions) //Get arrays array Specified index location index The Value of Location public static native Object get(Object array, int index) //Modify arrays array Specified index location index The value of the place is value public static native void set(Object array, int index, Object value) //Returns the length of the array public static native int getLength(Object array)
Note that in the Array class, arrays are represented by Object rather than Object []. Why? This is to facilitate the processing of multiple types of arrays, int [], String [] can not be converted to Object [], but can be converted to Object, such as:
int[] intArr = (int[])Array.newInstance(int.class, 10); String[] strArr = (String[])Array.newInstance(String.class, 10);
In addition to manipulating array elements in Object type, Array also supports manipulating array elements in various basic types, such as:
public static native double getDouble(Object array, int index) public static native void setDouble(Object array, int index, double d) public static native void setLong(Object array, int index, long l) public static native long getLong(Object array, int index)
Reflection and Enumeration
Enumeration types also have a special method for obtaining all enumeration constants:
public T[] getEnumConstants()
Application examples
What's the use of introducing so many methods of Class? Let's look at a simple example of using reflection to implement a simple generic serialization/deserialization class, SimpleMapper, which provides two static methods:
public static String toString(Object obj) public static Object fromString(String str)
toString converts an object obj to a string, and fromString converts a string to an object. For simplicity, we only support the simplest class, that is, the default constructor, and the member type only has the base type, wrapper class, or String. In addition, the format of serialization is very simple. The name of the first behavior class, followed by each line representing a field, is separated by the character'='to represent the field name and the value in the form of a string. SimpleMapper can be used as follows:
public class SimpleMapperDemo { static class Student { String name; int age; Double score; public Student() { } public Student(String name, int age, Double score) { super(); this.name = name; this.age = age; this.score = score; } @Override public String toString() { return "Student [name=" + name + ", age=" + age + ", score=" + score + "]"; } } public static void main(String[] args) { Student zhangsan = new Student("Zhang San", 18, 89d); String str = SimpleMapper.toString(zhangsan); Student zhangsan2 = (Student) SimpleMapper.fromString(str); System.out.println(zhangsan2); } }
The code first calls the toString method to convert the object to String, and then calls the fromString method to convert the string to Student. The value of the new object is the same as that of the original object. The output is as follows:
Student [name = Zhang San, age=18, score=89.0]
Let's look at an example implementation of SimpleMapper (mainly for demonstration of principles and careful use in production). The code for toString is:
public static String toString(Object obj) { try { Class<?> cls = obj.getClass(); StringBuilder sb = new StringBuilder(); sb.append(cls.getName() + "\n"); for (Field f : cls.getDeclaredFields()) { if (!f.isAccessible()) { f.setAccessible(true); } sb.append(f.getName() + "=" + f.get(obj).toString() + "\n"); } return sb.toString(); } catch (IllegalAccessException e) { throw new RuntimeException(e); } }
The code for fromString is:
public static Object fromString(String str) { try { String[] lines = str.split("\n"); if (lines.length < 1) { throw new IllegalArgumentException(str); } Class<?> cls = Class.forName(lines[0]); Object obj = cls.newInstance(); if (lines.length > 1) { for (int i = 1; i < lines.length; i++) { String[] fv = lines[i].split("="); if (fv.length != 2) { throw new IllegalArgumentException(lines[i]); } Field f = cls.getDeclaredField(fv[0]); if(!f.isAccessible()){ f.setAccessible(true); } setFieldValue(f, obj, fv[1]); } } return obj; } catch (Exception e) { throw new RuntimeException(e); } }
It calls the setFieldValue method to set values for fields, and the code is:
private static void setFieldValue(Field f, Object obj, String value) throws Exception { Class<?> type = f.getType(); if (type == int.class) { f.setInt(obj, Integer.parseInt(value)); } else if (type == byte.class) { f.setByte(obj, Byte.parseByte(value)); } else if (type == short.class) { f.setShort(obj, Short.parseShort(value)); } else if (type == long.class) { f.setLong(obj, Long.parseLong(value)); } else if (type == float.class) { f.setFloat(obj, Float.parseFloat(value)); } else if (type == double.class) { f.setDouble(obj, Double.parseDouble(value)); } else if (type == char.class) { f.setChar(obj, value.charAt(0)); } else if (type == boolean.class) { f.setBoolean(obj, Boolean.parseBoolean(value)); } else if (type == String.class) { f.set(obj, value); } else { Constructor<?> ctor = type.getConstructor(new Class[] { String.class }); f.set(obj, ctor.newInstance(value)); } }
setFieldValue converts the value in string form to the value in corresponding type according to the type of field. For the type other than the basic type and String, it assumes that the type has a construction method with String type as its parameter.
Reflection and Generics
In the introduction generic paradigm When we mention that generic parameters will be erased at runtime, we need to add that there is still some information about generics in Class, which can be reflected. Generics involve more methods and classes, which are ignored in the introduction above. Here is a brief supplement.
Class has the following methods to obtain generic parameter information of a class:
public TypeVariable<Class<T>>[] getTypeParameters()
Field has the following methods:
public Type getGenericType()
Method has the following methods:
public Type getGenericReturnType() public Type[] getGenericParameterTypes() public Type[] getGenericExceptionTypes()
Constructor has the following methods:
public Type[] getGenericParameterTypes()
Type is an interface. Class implements Type. Other sub-interfaces of Type include:
- TypeVariable: Type parameters can have upper bounds, such as T extends Number
- ParameterizedType: Parametric type with original type and specific type parameters, such as List < String>.
- Wildcard Type: wildcard types, such as:?,? Extends Number,? Super Integer
Let's look at a simple example:
public class GenericDemo { static class GenericTest<U extends Comparable<U>, V> { U u; V v; List<String> list; public U test(List<? extends Number> numbers) { return null; } } public static void main(String[] args) throws Exception { Class<?> cls = GenericTest.class; // Type parameters of classes for (TypeVariable t : cls.getTypeParameters()) { System.out.println(t.getName() + " extends " + Arrays.toString(t.getBounds())); } // field - generic types Field fu = cls.getDeclaredField("u"); System.out.println(fu.getGenericType()); // field - Types of parameterization Field flist = cls.getDeclaredField("list"); Type listType = flist.getGenericType(); if (listType instanceof ParameterizedType) { ParameterizedType pType = (ParameterizedType) listType; System.out.println("raw type: " + pType.getRawType() + ",type arguments:" + Arrays.toString(pType.getActualTypeArguments())); } // Generic parameters of methods Method m = cls.getMethod("test", new Class[] { List.class }); for (Type t : m.getGenericParameterTypes()) { System.out.println(t); } } }
The output of the program is:
U extends [java.lang.Comparable<U>] V extends [class java.lang.Object] U raw type: interface java.util.List,type arguments:[class java.lang.String] java.util.List<? extends java.lang.Number>
The code is relatively simple, so we won't go into it.
Careful use of reflex
Reflections are flexible, but in general, they are not our priority. The main reason is that:
- Reflection is more prone to run-time errors. Using explicit classes and interfaces, compilers can help us do type checking and reduce errors, but using reflection, types are known only at run-time, and compilers can't help.
- Reflection has a lower performance. Before accessing fields and calling methods, reflection first looks for the corresponding Field/Method, which is slower.
Simply put, if you can achieve the same flexibility with an interface, don't use reflection.
Summary
This section introduces the main reflection-related classes and methods in Java. Through the entry class Class, you can access all kinds of information of classes, such as fields, methods, construction methods, parent classes, interfaces, generic information, etc. You can also create and operate objects, call methods, etc. Using these methods, you can write general, dynamic and flexible. This section demonstrates a simple generic serialization/deserialization class SimpleMapper. Reflection is flexible and versatile, but it is more prone to run-time errors, so when you can replace it with interfaces, you should try to use interfaces.
Many of the classes introduced in this section, such as Class/Field/Method/Constructor, can be annotated. What exactly are annotations?
(As in other chapters, all of the code in this section is located in https://github.com/swiftma/program-logic Located under package shuo.laoma.dynamic.c84)
----------------
To be continued, check the latest articles, please pay attention to the Wechat public number "Lao Ma Says Programming" (scanning the two-dimensional code below), from the entry to advanced, in-depth shallow, Lao Ma and you explore the essence of Java programming and computer technology. Be original and reserve all copyright.