java反射机制

来源:互联网 发布:乐动力没有数据 编辑:程序博客网 时间:2024/06/03 14:16

Java 的类加载器除了根类加载器之外,其他类加载器都是使用 Java 语言编写的,意思就是说我们也可以开发自己的类加载器,通过自定义类加载器来完成一些特殊的功能。
1、类的加载、连接、初始化
1.1 JVM和类
JVM 是 Java Virtual Machine(Java 虚拟机)的缩写,每次运行一个 Java 程序时,都会启动一个 JVM 进程,同一个JVM的的所有线程、所有变量都在同一个进程中,都使用该进程的内存去,以下几种情况会终止 JVM 进程:
1).程序运行到最后,正常结束。
2).程序运行中执行了 System.exit() 或者 Runtime.getRuntime().exit() 代码时会结束程序。
3).程序执行过程中遇到未捕获的异常或者错误而结束。
4).程序运行的所在平台强制结束了 JVM 进程。
当 Java 程序运行结束,JVM 进程也就结束,该进程在内存中的状态也将丢失。

1.2 类加载
当程序主动使用某个类时,如果该类还没有被加载到内存中,则系统进通过加载、连接、初始化三个步骤将该类初始化,一般这三个步骤是连续执行的。类加载是指将类的 class 文件读入内存,并为其创建一个 java.lang.Class 对象,使用任何类都会创建这个 Class 对象。
类加载由类加载器完成,通常由 JVM 提供,JVM 提供的类加载器也成为系统类加载器。上面提到类加载器可以自定义,我们可以通过继承 ClassLoader 类来创建自定义类加载器。
类文件的来源一般分为一下几种:
1).从本地系统文件加载 class 文件,大部分程序都是这种加载方式。
2).从 jar 包中加载 class 文件。
3).从网络加载 class 文件。
4).把一个 java 文件动态编译并加载。
类文件不一定是“首次使用”才会进行加载,JVM 允许系统预加载某些类。

1.3 类连接
当类加载完成之后,系统会为其生成一个 Class 对象,然后进入类连接阶段,这个阶段负责把类的二进制数据合并到 JRE 中,类连接一般分为三个步骤:
1).验证:用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。
2).准备:负责为类的类变量分配内存,并设置初始值。
3).解析:将类的二进制数据中的符号引用替换成直接引用。

1.4 类初始化
在类的初始化阶段,JVM 负责对类进行初始化,主要是对类变量进行初始化。为类变量指定初始值有两种方式:
1).声明类变量时指定初始值。
2).使用静态代码块为类变量指定初始值。
通过代码说明一下:
[java] view plain copy print?
class Test {
// 声明类变量时指定初始值
static int a = 5;
static int b;
// 使用静态代码块为类变量指定初始值
static {
b = 6;
}
}

public class GenericDemo {
public static void main(String[] args) {
System.out.println(new Test().a);
System.out.println(new Test().b);
}
}

可以看到两种方式都正确为类变量赋了值,如果不指定初始值,则采用默认值,int 类型的变量默认值是 0 ,需要注意的是,声明变量时指定初始值和使用静态代码块指定初始值都可以为类变量赋值,它们的执行顺序是依次执行的。
[java] view plain copy print?
class Test {
// 声明类变量时指定初始值
static int a = 5;
static int b = 7;
// 使用静态代码块为类变量指定初始值
static {
b = 6;
}
}

public class GenericDemo {
public static void main(String[] args) {
System.out.println(“a—>” + new Test().a);
System.out.println(“b—>” + new Test().b);
}
}
上面的程序现在声明变量时指定了初始值为 7 ,静态代码块又为 b 赋值为 6 ,所以最后 b 的值为6,同理,如果静态代码块执行在前,那么 b 的值最后将为7。
JVM 对类初始化一般分为 3 个步骤:
1).如果这个类没有被加载和连接,则先加载并连接该类。
2).如果该类的直接父类没有被初始化,则先初始化直接父类。
3).如果类中有初始化语句,则 JVM 依次执行这些初始化语句。
在执行步骤 2) 时,对JVM 对直接父类的初始化也会执行上述 3 个步骤,如果父类还有父类,则再向上初始化,依此类推,所以 JVM 最先初始化的一定是 java.lang.Object 类,在使用一个类时,JVM 一定会保证该类及其所有父类都被初始化。

1.5 类初始化的时机
当 Java 程序以以下 6 种方式使用某个类或接口时,JVM 会初始化该类或接口:
1).创建类的实例,包括使用 new 关键字创建实例;通过反射创建实例;通过反序列化创建实例。
2).调用某个类的静态方法。
3).访问某个类或接口的类变量,或为该类的类变量赋值。
4).通过反射方式强制创建某个类或接口对应的 java.lang.Class 对象。
5).初始化某个类的子类,父类也会被初始化。
6).直接使用 java.exe 命令来运行某个主类,会先初始化该主类。

2、类加载器
类加载器负责将 .class 文件加载到内存中,并为其生成 java.lang.Class 对象,在开发过程中我们无需关心类加载机制,但是我们如果了解其工作原理,则能更好的满足我们需要。

2.1 类加载器简介
类加载器负责加载所有的类,系统会为所有被载入内存的类生成一个 java.lang.Class 对象,一旦这个类被加载后,同一个类不会再次加载,那么怎么样算是同一个类。
Java 开发中一般是通过类名及包名来判断是否是同一个类,但是在 JVM 中,是通过类名及包名还有类加载器来判定的,如 pg 包中有一个 Person 类,被类加载器 ClassLoader的实例 cl1 加载,则其唯一标识为(Person 、 pg 、 cl1 ),如果被另一个 ClassLoader 的实例 cl2 加载,则唯一标识为(Person 、 pg 、 cl2 ),它们加载的类则被认为是不同的。

3、通过反射查看类信息
Java 程序中的很多对象在运行时都分为编译时类型和运行时类型。如 Student 类是 Person 类的子类,Person mPerson = new Student();这行代码会生成一个 mPerson 变量,mPerson 变量编译时类型为 Person,运行时类型则为 Student 。所以我们在使用 mPerson 变量时,如果要调用 Student 类中的方法,则有两种方式:
1).通过 instanceof 运算符判断 mPerson 是否是Student 类,然后再强制转换。
2).编译时无法预知该类属于哪些类,只能在运行时发现该对象和类的真实信息,这时就要用到反射了。

3.1 获取 Class 对象
每个类被加载后,系统会为其生成 Class 对象,通过该 Class 对象则可以访问到这个类,获取 Class 对象一般有三种方式:
1).使用 Class 类中的静态方法 forName(String className),参数需要传入某个类的全限定名(完整包名加类名)。
2).调用某个类的 class 属性,如上面的 Person 类,则调用 Person.class;。
3).调用某个对象的 getClass() 方法,如上面的 mPerson 对象,则调用 mPerson.getClass();。
三种方式相对而言第二种方式有两个优势:
1).代码更安全,可以在编译阶段就检查需要访问的 Class 对象是否存在。
2).程序性能更好,因为无需调用方法。
所以大部分情况都应该使用第二种方式来获取 Class 对象,第一种方式 forName(String className) 可能会抛出一个异常:ClassNotFoundException,表示这个类不存在。

3.2 从 Class 中获取信息
Class 类提供了大量方法来获取该 Class 对象所对应的类的详细信息,Class 类常用的方法如下:

1).获取构造函数

getConstructor(Class<?>  parameterTypes):返回此 Class 对象对应类的带指定形参列表的 public 访问权限修饰符修饰的构造方法。getConstructors():返回此 Class 对象对应类的所有的 public 访问权限修饰符修饰的构造方法。getDeclaredConstructor(Class<?>... parameterTypes):返回此 Class 对象对应类的带指定形参列表的构造方法,与访问权限无关。getDeclaredConstructors():返回此 Class 对象对应类的所有的构造方法,与访问权限无关。

2).获取方法:

getMethod(String name, Class<?>... parameterTypes):返回此 Class 对象对应类的带指定形参列表的 public 访问权限修饰符修饰的方法。getMethods():返回此 Class 对象对应类的所有的 public 访问权限修饰符修饰的方法,包括父类的 public 访问权限修饰符修饰方法。getDeclaredMethod(String name, Class<?>... parameterTypes):返回此 Class 对象对应类的带指定形参列表的方法,与访问权限无关。getDeclaredMethods():返回此 Class 对象对应类的所有的方法,与访问权限无关,不包括父类的方法。

3).获取成员变量:

getField(String name):返回此 Class 对象对应类的指定名称的 public 访问权限修饰符修饰的成员变量。
getFields():返回此 Class 对象对应类的所有的 public 访问权限修饰符修饰的成员变量。
getDeclaredField(String name):返回此 Class 对象对应类的指定名称的成员变量,与访问权限无关。
getDeclaredFields():返回此 Class 对象对应类的所有的成员变量,与访问权限无关。

4).获取注解(Annotation):
getAnnotation(Class annotationClass):返回此 Class 对象对应类的指定类型的 Annotation ,如果该类型的 Annotation 不存在,则返回null。

 getDeclaredAnnotation(Class<A> annotationClass):Java 8 新增的方法,返回此 Class 对象对应类的直接修饰的指定类型的 Annotation ,如果该类型的 Annotation 不存在,则返回null。getAnnotations():返回此 Class 对象对应类的所有的 Annotation 。getDeclaredAnnotations():返回此 Class 对象对应类的直接修饰的所有的 Annotation 。getAnnotationsByType(Class<A> annotationClass):Java 8 新增的方法,类似 getAnnotation() ,由于 Java 8 新增重复注解功能,所以需要该方法返回此 Class 对象对应类的指定类型的多个注解。getDeclaredAnnotationsByType(Class<A> annotationClass):Java 8 新增方法,类似 getDeclaredAnnotation() ,由于 Java 8 新增重复注解功能,所以需要该方法返回此 Class 对象对应类的直接修饰的指定类型的多个注解。

5).获取内部类:

getDeclaredClasses():返回此 Class 对象对应类中包含的所有内部类。

6).获取外部类:

getDeclaringClass():如果此 Class 对象对应类是另一个类的内部类,则返回此 Class 对象对应类的外部类

7).获取接口:

getInterfaces():返回此 Class 对象对应类实现的所有接口。

8).获取父类:

getSuperclass():返回此 Class 对象对应类所直接继承的父类。

9).获取其他信息:

getModifiers():返回此 Class 对象对应类的所有修饰符的和,需使用Modifier 工具类的方法解码,才能获取真实的修饰符。
getPackage():返回此 Class 对象对应类所在的包。
getName():返回此 Class 对象对应类的全限定名(包名加类名)。
getSimpleName():返回此 Class 对象对应类的类名。

10).判断方法:

isAnnotation():判断此 Class 对象对应类是否是一个 Annotation (@interface),是则返回 true ,不是则返回 false 。
isAnnotationPresent(Class

[java] view plain copy print?@Deprecated  public final class Student extends Person implements Action,Appearance {      // 两个变量,一个访问权限为private,另一个为public      private String name;      public int age;      // 三个构造方法,一个访问权限为public,另外两个为private      public Student() {      }      private Student(String name) {          this.name = name;      }      private Student(String name, int age) {          this.name = name;          this.age = age;      }      // 两个方法,一个访问权限为private,另外一个为public      public void say() {          // TODO Auto-generated method stub          System.out.println("我是一个学生");      }      private void say(String content) {          System.out.println(content);      }      // 内部类      class Boy {      }  }  

父类和接口中什么都没有,所以父类和接口就不贴出代码,main() 方法:

[java] view plain copy print?public class ReflectDemo {      public static void main(String[] args) {          try {              Student mStudent = new Student();              Class clazz = Class.forName("com.qinshou.reflectdemo.Student");              // 获取构造方法              System.out.println("getConstructors--->" + clazz.getConstructors().length);              System.out.println("getDeclaredConstructors--->" + clazz.getDeclaredConstructors().length);              // 获取成员变量              System.out.println("getFields--->" + clazz.getFields().length);              System.out.println("getDeclaredFields--->" + clazz.getDeclaredFields().length);              // 获取方法              System.out.println("getMethods--->" + clazz.getMethods().length);              System.out.println("getDeclaredMethods--->" + clazz.getDeclaredMethods().length);              // 获取Annotation              System.out.println("getAnnotations--->" + clazz.getAnnotations().length);              System.out.println("getDeclaredAnnotation--->" + clazz.getAnnotation(Deprecated.class));              // 获取内部类              System.out.println("getDeclaredClasses--->" + clazz.getDeclaredClasses().length);              // 获取父类              System.out.println("getSuperclass--->" + clazz.getSuperclass());              // 获取实现的接口              System.out.println("getInterfaces--->" + clazz.getInterfaces().length);              // 获取修饰符              System.out.println("getModifiers--->" + clazz.getModifiers() + ",final--->" + Modifier.FINAL + ",public--->"                      + Modifier.PUBLIC);              // 获取包名              System.out.println("getPackage--->" + clazz.getPackage());              // 获取全限定名              System.out.println("getName--->" + clazz.getName());              // 获取类名              System.out.println("getSimpleName--->" + clazz.getSimpleName());              // 是否是注解              System.out.println("isAnnotation--->" + clazz.isAnnotation());              // 是否有某个注解              System.out.println("isAnnotationPresent--->" + clazz.isAnnotationPresent(Deprecated.class));              // 是否是匿名类              System.out.println("isAnonymousClass--->" + clazz.isAnonymousClass());              // 是否是数组类              System.out.println("isArray--->" + clazz.isArray());              // 是否是枚举              System.out.println("isEnum--->" + clazz.isEnum());              // obj是否是该类的实例              System.out.println("isInstance--->" + clazz.isInstance(mStudent));              // 是否是接口              System.out.println("isInterface--->" + clazz.isInterface());          } catch (ClassNotFoundException e) {              // TODO Auto-generated catch block              e.printStackTrace();          }      }  }  

打印结果如下,可以看看返回结果:

关于获取构造方法时,如果要获取指定参数列表的构造方法,比如 Student 类中的这个构造方法:
[java] view plain copy print?
private Student(String name, int age) {
this.name = name;
this.age = age;
}
我们应该这么写:
[java] view plain copy print?
clazz.getConstructor(String.class,Integer.class);
而不是:
[java] view plain copy print?
clazz.getConstructor(name,age);
因为形参名是没有任何意义的,它可以任意变化,所以需要传入形参的类型,获取指定列表的方法同理,只是前面需要多加一个参数(方法名)。通过获取到的这些信息,我们可以完成很多功能,例如调用方法、创建实例等。

3.3 方法参数反射
Java 8 在 java.lang.reflect 包下新增了 Executable 抽象基类,该对象代表可执行的类成员,并派生了 Constructor 、 Method 两个子类。
Executable 抽象基类提供了大量方法来获取修饰该方法或构造方法的的注解信息,还有以下常用方法:
isVarArgs():判断该方法或者构造方法是否包含可变长度的形参。
getModifiers():返回修饰该方法或构造方法的修饰符。
getParameterCount():返回该方法或构造方法的形参个数。
getParameters():返回该方法或构造方法的所有形参。

getParameters() 方法返回一个 Parameter[] 数组,Parameter 也是 Java 8 新增的 API,它提供了大量方法来获取修饰该参数的泛型信息,还有一下常用方法:
getModifiers():返回修饰该形参的修饰符。
getName():返回形参名。
getParameterizedType():返回带泛型的参数类型。
getType():返回形参类型。
isNamePresent():判断该类的 class 文件中是否包含了方法的形参名信息。
isVarArgs():判断该参数是否是可变长度的参数。
需要注意的是默认生成的 class 文件并不包含方法的形参名信息,所以 isNamePresent() 方法会返回 false,貌似可以通过 javac 命令来保存形参信息,没有尝试,暂不做评论。这些方法也不是很常用,所以我们只要了解即可。

4、使用反射生成并操作对象
通过上面的方法我们已经可以获得构造方法(Constructor)、方法(Method)、还有成员变量(Field),所以我们可以通过这些对象来实现对应功能,利用 Constructor 对象创建实例,利用 Method 对象调用方法,利用 Field 对象访问并修改成员变量。
4.1 创建对象
通过反射创建对象有两种方式:
1).使用 Class 对象的 newInstance() 方法来创建此 Class 对象对应类的实例,这种方式是调用默认的无参构造方法来创建实例的,所以如果没有无参构造方法的话会抛出 java.lang.InstantiationException 和 java.lang.NoSuchMethodException的异常,这也是为什么要求一般创建一个类的时候至少要有一个无参构造方法的原因。
2).先获取 Constructor 对象,再调用 Constructor 对象的 newInstance() 方法来创建实例,这种方式可以使用指定的构造方法来创建实例。
通过一个简单例子看一下:

[java] view plain copy print?class Student {      private String name;      public int age;      public Student() {      }      public Student(String name) {          this.name = name;      }      public Student(String name, int age) {          this.name = name;          this.age = age;      }      public void say() {      }      public void say(String content) {      }      @Override      public String toString() {          // TODO Auto-generated method stub          return "姓名:" + name + ",年龄:" + age;      }  }  
public class ReflectDemo {      public static void main(String[] args) {          try {              Class clazz = Class.forName("com.qinshou.reflectdemo.Student");              Student mStudent1 = (Student) clazz.newInstance();              System.out.println(mStudent1.toString());              for (Constructor constructor : clazz.getConstructors()) {                  if (constructor.getParameterCount() == 2) {                      Student mStudent2 = (Student) constructor.newInstance("张三", 24);                      System.out.println(mStudent2.toString());                  }              }          } catch (Exception e) {              // TODO Auto-generated catch block              e.printStackTrace();          }      }  }  

程序运行结果如下:

4.2 调用方法
可以通过 getMethods() 来获取全部方法,也可以通过 getMethod() 获取指定方法。分别返回 Method 数组和 Method 对象,每一个 Method 对象都对应一个方法。Method 对象有一个 invoke() 方法:
invoke(Object obj, Object… args):其中 obj 是执行该方法的对象,Object… args 则是可传入的参数。
修改一下上面程序的 say() 方法:

[java] view plain copy print?public void say() {      System.out.println("啥也不传,我只能say Hello World");  }  public void say(String content) {      System.out.println("既然有内容,那我就say " + content);  }  

修改 main() 方法:

[java] view plain copy print?public static void main(String[] args) {      try {          Class clazz = Class.forName("com.qinshou.reflectdemo.Student");          Student mStudent1 = (Student) clazz.newInstance();          System.out.println(mStudent1.toString());          for (Method method : clazz.getMethods()) {              if (method.getName().equals("say") && method.getParameterCount() == 1) {                  method.invoke(mStudent1, "怎么还是Hello World");              }          }      } catch (Exception e) {          // TODO Auto-generated catch block          e.printStackTrace();      }  }  

程序运行结果如下:

需要注意的是 invoke() 方法调用对应方法时必须有访问权限,上面我们声明的权限是 public ,所以没有问题,如果是 private,则不能访问,比如将上面的带参数的 say() 方法修改为 private 修饰,会抛出异常:
java.lang.IllegalAccessException: Class com.qinshou.reflectdemo.ReflectDemo can not access a member of class com.qinshou.reflectdemo.Student with modifiers “private”
我们可以在调用前先取消访问权限检查,利用

setAccessible(boolean flags) 方法:[java] view plain copy print?<span style="white-space:pre">              </span>if (method.getName().equals("say") && method.getParameterCount() == 1) {                      if (!method.isAccessible()) {                          method.setAccessible(true);                          method.invoke(mStudent1, "怎么还是Hello World");                      }                  }  

这样就可以正常访问了。 setAccessible(boolean flags) 方法并不是 Method 类的,是父类 AccessibleObject 的,所以 Constructor 和 Field 类都可以调用该方法。

4.3访问成员变量
跟获取方法一样,可以通过 getFields() 来获取全部成员变量和 getField() 获取指定成员变量。然后可以通过 Field 对象的 get() 和 set() 方法获取和设置成员变量的值。
get(Object obj):获取 obj 对象的该成员变量的值。
set(Object obj, Object value):设置 obj 对象的该成员变量的值为 value。
当 get() 和 set() 方法操作的是基本类型的成员变量时有更具体的方法,如该成员变量是 boolean 类型,则可以通过 getBoolean(Object obj) 获取boolean,不用向下转型了,如该成员变量是 int 类型,则可以通过setInt(Object obj, int i) 设置值。基本类型包括 byte、char、int、float、double、short、long、boolean。

[java] view plain copy print?public static void main(String[] args) {      try {          Student mStudent = new Student("张三", 24);          Field field1 = mStudent.getClass().getDeclaredField("age");          System.out.println(field1.get(mStudent));          field1.set(mStudent, 21);          System.out.println(field1.get(mStudent));      } catch (Exception e) {          // TODO Auto-generated catch block          e.printStackTrace();      }  }  

运行结果如下:

Filed 跟 Method 一样,也需要访问权限,同调用方法一样,可以利用 setAccessible(boolean flags) 取消访问权限检查。

4.4操作数组
java.lang.reflect 包下有一个 Array 类,Array 对象可以动态创建数组,操作数组元素等,有以下几个常用方法:
newInstance(Class

[java] view plain copy print?public class ReflectDemo {      public static void main(String[] args) {              Object array = Array.newInstance(String.class, 5);              Array.set(array, 0, "第一个元素");              Array.set(array, 1, "第二个元素");              Array.set(array, 2, "第三个元素");              System.out.println(Array.get(array, 0));              System.out.println(Array.get(array, 1));              System.out.println(Array.get(array, 2));              System.out.println(Array.get(array, 3));              System.out.println(Array.get(array, 4));      }  } 

运行结果如下:

我们也可以创建多维数组:
[java] view plain copy print?
public class ReflectDemo {
public static void main(String[] args) {
Object array = Array.newInstance(String.class, 2, 2);
Array.set(array, 0, new String[] { “0,0”, “0,1” });
Array.set(array, 1, new String[] { “1,0”, “1,1” });
String[][] strings=(String[][]) array;
System.out.println(strings[0][0]);
System.out.println(strings[0][1]);
System.out.println(strings[1][0]);
System.out.println(strings[1][1]);
}
}
运行结果如下:

5、反射与泛型
5.1 反射时使用泛型
Java 5开始增加了泛型功能,泛型也可以限制 Class 类。在前面我们有这样的代码:
[java] view plain copy print?
Class clazz = Class.forName(“com.qinshou.reflectdemo.Student”);
Student mStudent1 = (Student) clazz.newInstance();

还有这样的:
[java] view plain copy print?
String[][] strings=(String[][]) array;
都用到了强制转型,我们知道向下转型是不安全的,所以可以在使用 Class 类的时候指定泛型:

[java] view plain copy print?
Class clazz = Student.class;
Student mStudent2 = clazz.newInstance();
这时候就不能使用 forName() 方法了,这样获得 Class 对象的方法也是我们刚开始说的比较常用的方法。对于创建数据,我们可以写一个简单工厂类:
[java] view plain copy print?
class ArrayFactory {
public static T[] getInstance(Class clazz, int length) {
return (T[]) Array.newInstance(clazz, length);
}
}

public class ReflectDemo {
public static void main(String[] args) {
String[] strings=ArrayFactory.getInstance(String.class, 5);
}
}

5.2 使用反射获取泛型信息
我们在获取到成员变量后,可以通过 getType() 方法获取到成员变量的类型,但是这只是普通类型,或者说是变量的原始类型,如果是设置了泛型的成员变量,则需要通过以下步骤来获取泛型信息:
1).利用 getGenericType() 方法获取带泛型的类型。
2).将获取后的类型强制转换为 ParameterizedType。
3).利用 getActualTypeArguments() 方法得到泛型参数的类型。
通过一个小例子来说明这几个方法的用法:
[java] view plain copy print?
public class ReflectDemo {
private List stringList = new ArrayList();

public static void main(String[] args) {      try {          Class<ReflectDemo> clazz = ReflectDemo.class;          Field field = clazz.getDeclaredField("stringList");          Class<?> a = field.getType();          System.out.println("stringList的类型:" + a);          Type mType = field.getGenericType();          System.out.println("stringList的泛型类型:" + mType);          if (mType instanceof ParameterizedType) {              ParameterizedType mParameterizedType = (ParameterizedType) mType;              Type rawType = mParameterizedType.getRawType();              System.out.println("stringList的原始类型:" + rawType);              Type[] actualTypes = mParameterizedType.getActualTypeArguments();              for (Type type : actualTypes) {                  System.out.println("stringList的泛型参数的类型:" + type);              }          }      } catch (Exception e) {          // TODO Auto-generated catch block          e.printStackTrace();      }  }  

}

可以看到我们能知道 stringList 成员变量的原始类型,带泛型的类型和泛型参数的类型,所以对待带泛型的成员变量,我们应该用 getGenericType() 来获取类型。

总结:反射的知识点很多,能做的事情也很多,通过学习泛型,差不多对 ButterKnife 这样的框架的实现原理有了个大概的猜测,所以学会用框架是一种本事,要了解它是如何实现的,又需要更多的基础知识来做垫脚石。泛型、反射和注解这三个知识点结合起来真的可以做很多事情。

原创粉丝点击