javajavase

反射机制概述

Reflection 是被视为动态语言的关键,反射机制允许程序在执行期借助于 Reflection API 取得任何类的内部信息,并能直接操作任意对象的内部属性及方法

  • 加载完类之后,在堆内存的方法区中就产生了一个 Class 类型的对象(一个类只有一个 Class 对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射

image.png

动态语言 vs 静态语言

动态语言

  • 是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构
  • 主要动态语言:Object-C、C#、JavaScript、PHP、Python、Erlang

静态语言

  • 与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java、C、C++
  • Java不是动态语言,但Java可以称之为“准动态语言”。即Java有一定的动态性,我们可以利用反射机制、字节码操作获得类似动态语言的特性。Java的动态性让编程的时候更加灵活

反射动态性体现
只有当程序运行时我们才能知道调用的类
反射机制提供的功能

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时获取泛型信息
  • 在运行时调用任意一个对象的成员变量和方法
  • 在运行时处理注解
  • 生成动态代理

相关API

  • java.lang.Class——反射的源头
  • java.lang.reflect.Constructor:代表类的构造器
  • java.lang.reflect.Field:代表类的属性
  • java.lang.reflect.Method:代表类的方法
  • ….

    1. public class Person {
    2. private String name;
    3. public int age;
    4. public Person() {
    5. System.out.println("Person()");
    6. }
    7. private Person(String name) {
    8. this.name = name;
    9. }
    10. public Person(String name, int age) {
    11. this.name = name;
    12. this.age = age;
    13. }
    14. public void show() {
    15. System.out.println("你好,我是一个人");
    16. }
    17. private String showNation(String nation) {
    18. System.out.println("我的国籍是:" + nation);
    19. return nation;
    20. }
    21. getXxx()/setXxx()/toString()略
    22. }
    1. // 反射之前,对于Person的操作
    2. @Test
    3. public void test1() {
    4. // 1.创建Person类的对象
    5. Person p1 = new Person("Tom", 12);
    6. // 2.通过对象,调用其内部的属性、方法
    7. p1.age = 10;
    8. System.out.println(p1.toString());
    9. p1.show();
    10. //在Person类外部,不可以通过Person类的对象调用其内部私有结构。
    11. //比如:name、showNation()以及私有的构造器
    12. }
    1. // 反射之后,对于Person的操作
    2. @Test
    3. public void test2() throws Exception {
    4. Class clazz = Person.class;
    5. // 1.通过反射,创建Person类的对象
    6. Constructor cons = clazz.getConstructor(String.class, int.class);
    7. Object obj = cons.newInstance("Tom", 12);
    8. Person p = (Person) obj;
    9. System.out.println(p.toString());
    10. // 2.通过反射,调用对象指定的属性、方法
    11. // 调用属性
    12. Field age = clazz.getDeclaredField("age");
    13. age.set(p, 10);
    14. System.out.println(p.toString());
    15. // 调用方法
    16. Method show = clazz.getDeclaredMethod("show");
    17. show.invoke(p);
    18. System.out.println("*******************************");
    19. // 通过反射,可以调用Person类的私有结构的。比如:私有的构造器、方法、属性
    20. // 调用私有的构造器
    21. Constructor cons1 = clazz.getDeclaredConstructor(String.class);
    22. cons1.setAccessible(true);
    23. Person p1 = (Person) cons1.newInstance("Jerry");
    24. System.out.println(p1);
    25. // 调用私有的属性
    26. Field name = clazz.getDeclaredField("name");
    27. name.setAccessible(true);
    28. name.set(p1, "HanMeimei");
    29. System.out.println(p1);
    30. // 调用私有的方法
    31. Method showNation = clazz.getDeclaredMethod("showNation", String.class);
    32. showNation.setAccessible(true);
    33. //相当于String nation = p1.showNation("中国")
    34. String nation = (String) showNation.invoke(p1, "中国");
    35. System.out.println(nation);
    36. }

    疑问1:通过直接new的方式或反射的方式都可以调用公共的结构,开发中到底用那个?
    建议:直接new的方式。
    什么时候会使用反射的方式?
    编译的时候不确定创建哪个对象
    反射的特征:动态性
    疑问2:反射机制与面向对象中的封装性是不是矛盾的?如何看待两个技术?
    不矛盾,封装性是不建议调用,反射是能不能,非要用私有的也行但往往没必要


Class类

简述

  • 在 Object 类中定义了以下的方法,此方法将被所有子类继承
    **public final Class getClass()**
  • 以上的方法返回值的类型是一个 Class 类,此类是 Java 反射的源头,实际上所谓反射从程序的运行结果来看也很好理解,即可以通过对象反射求出类的名称
  • 对象使用反射后可以得到的信息:某个类的属性、方法、构造器、实现了哪些接口。对于每个类而言,JRE都为其保留一个不变的Class类型的对象。一个Class对象包含了特定某个结构(class/interface/enum/annotation/primitive type/void/[])的有关信息

    • Class本身也是一个类
    • Class对象只能由系统建立对象
    • 一个加载的类在JVM中只会有一个Class实例
    • 一个Class对象对应的是一个加载到JVM中的一个.class文件
    • 每个类的实例都会记得自己是由哪个Class实例所生成
    • 通过Class可以完整地得到一个类中的所有被加载的结构
    • Class 类是 Reflection 的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class对象

      常用方法

      **static Class forName(String name)**
      类名
      **String getName()** 返回 Class 对象所表示的实体名称
      **String getSimpleName()**
      创建无参对象
      **T newInstance()** 调用的是无参构造方法,这就是为什么 javabean 一定要有空参构造器
      获取类加载器
      **ClassLoader getClassLoader()**
      类的加载过程
  • 程序经过javac.exe命令以后,会生成一个或多个字节码文件(.class结尾)。接着我们使用java.exe命令对某个字节码文件进行解释运行,相当于将某个字节码文件加载到内存中,此过程就称为类的加载。加载到内存中的类,我们就称为运行时类,此运行时类,就作为 Class 的一个实例

  • 换句话说,Class 的实例就对应着一个运行时类
  • 加载到内存中的运行时类会缓存一定的时间,在此时间之内我们可以通过不同的方式来获取此运行时类

    获取Class实例的4种方式

    已知具体的类,通过类的class属性获取,该方法最为安全可靠,程序性能最高
    Class clazz = String.class;
    已知某个类的实例,调用该实例的 **getclass()** 方法获取 Class 对象
    Class clazz=person.getclass();
    已知一个类的全类名,且该类在类路径下,可通过 Class 类的静态方法 **forName()** 获取, 可能抛出 ClassNotFoundException(比较常用)
    Class clazz = Class.forName(String classPath)
    通过类加载器
    ClassLoader cl = this.getclass().getClassLoader();
    Class clazz = cl.loadClass(“类的全类名”);

    1. @Test
    2. public void test3() throws ClassNotFoundException {
    3. // 方式一:调用运行时类的属性:.class
    4. Class<Person> clazz1 = Person.class;
    5. System.out.println(clazz1); // class com.cess.java.Person
    6. // 方式二:通过运行时类的对象,调用getClass()
    7. Person p1 = new Person();
    8. Class<? extends Person> clazz2 = p1.getClass();
    9. System.out.println(clazz2); // class com.cess.java.Person
    10. // 方式三:调用Class的静态方法:forName(String classPath)
    11. Class<?> clazz3 = Class.forName("com.cess.java.Person");
    12. System.out.println(clazz3); // class com.cess.java.Person
    13. System.out.println(clazz1 == clazz2); // true
    14. System.out.println(clazz1 == clazz3); // true
    15. // 方式四:使用类的加载器:ClassLoader (了解)
    16. ClassLoader classLoader = ReflectionTest.class.getClassLoader();
    17. Class clazz4 = classLoader.loadClass("com.cess.java.Person");
    18. System.out.println(clazz4); // class com.cess.java.Person
    19. System.out.println(clazz1 == clazz4); // true
    20. }

    总结

    创建类的对象的方式

  1. new + 构造器
  2. 要创建Xxx类的对象,可以考虑:Xxx、Xxxs、XxxFactory、XxxBuilder类中查看是否有静态方法的存在。可以调用其静态方法,创建Xxx对象
  3. 通过反射

Class实例可以代表的结构

  1. class:外部类,内部类(成员内部类、静态内部类、局部内部类、匿名内部类)
  2. interface:接口
  3. []:数组
  4. enum:枚举
  5. annotation:注解@interface
  6. primitive type:基本数据类型
  7. void

在Java中万事万物皆对象

  1. @Test
  2. public void test4(){
  3. Class<Object> c1 = Object.class;
  4. Class<Comparable> c2 = Comparable.class;
  5. Class<String[]> c3 = String[].class;
  6. Class<int[][]> c4 = int[][].class;
  7. Class<ElementType> c5 = ElementType.class; // 这是个枚举类
  8. Class<Override> c6 = Override.class;
  9. Class<Integer> c7 = int.class;
  10. Class<Void> c8 = void.class;
  11. Class<Class> c9 = Class.class;
  12. int[] i1 = new int[10];
  13. int[] i2 = new int[100];
  14. Class<? extends int[]> c10 = i1.getClass();
  15. Class<? extends int[]> c11 = i2.getClass();
  16. // 只要数组的元素类型与维度一样,就是同一个Class
  17. System.out.println(c10 == c11); // true
  18. }

类的加载

类的加载过程

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过以下三个步骤对该类进行初始化
反射机制概述、Class类、类的加载 - 图2

  • 加载
    • 将 class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的 java.lang.Class 对象,作为方法区中类数据的访问入口(即引用地址)。所有需要访问和使用类数据只能通过这个Class对象。这个加载的过程需要类加载器参与
  • 链接

将Java类的二进制代码合并到JVM的运行状态之中的过程

  • 验证:确保加载的类信息符合JVM规范,例如:以cafe开头,没有安全方面的问题
  • 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配
  • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程
    • 类初始化
  • 执行类构造器()方法的过程。类构造器()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)
  • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化
  • 虚拟机会保证一个类的 () 方法在多线程环境中被正确加锁和同步 ```java public class ClassLoadingTest { public static void main (String [] args) { System.out.println(test.m); } }

class test{ static { m = 300; } static int m = 100; } // 第一步:加载 // 第二步:链接结束后m=0 // 第三步:初始化结束后,m的值由()方法执行决定 / 这个test构造器()方法由类变量的赋值和静态代码块中的语句按照顺序合并产生,类似于 () { m = 300; m = 100; } /

  1. **Java类编译、运行的执行的流程**<br />![](https://cdn.nlark.com/yuque/0/2020/png/1379492/1590769779627-a348aed9-60cd-4ff9-a4dd-86efff27544e.png#height=221&id=X8gAh&originHeight=294&originWidth=673&originalType=binary&ratio=1&size=0&status=done&style=shadow&width=505)<br />**类加载器的作用**
  2. - class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区中类数据的访问入口
  3. - 类缓存:标准的 JavaSE 类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些 Class 对象
  4. **类加载器的分类**<br />![](https://cdn.nlark.com/yuque/0/2020/png/1379492/1590769779610-ef2fa1a3-bcd2-4988-a03b-59656798ad13.png#height=238&id=xH8Hh&margin=%5Bobject%20Object%5D&originHeight=263&originWidth=773&originalType=binary&ratio=1&size=0&status=done&style=shadow&width=700)<br />引导类加载器、扩展类加载器、App类加载器、自定义类加载器
  5. ```java
  6. @Test
  7. public void test1() {
  8. // 对于自定义类,使用系统类加载器进行加载
  9. ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
  10. System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
  11. // 调用系统类加载器的getParent():获取扩展类加载器
  12. ClassLoader classLoader1 = classLoader.getParent();
  13. System.out.println(classLoader1); // sun.misc.Launcher$ExtClassLoader@5b2133b1
  14. // 调用扩展类加载器的getParent():无法获取引导类加载器
  15. //引导类加载器主要负责加载java的核心类库,无法加载自定义类的。
  16. ClassLoader classLoader2 = classLoader1.getParent();
  17. System.out.println(classLoader2); // null
  18. ClassLoader classLoader3 = String.class.getClassLoader();
  19. System.out.println(classLoader3); // null
  20. }

使用Classloader加载src目录下的配置文件

  1. /*
  2. Properties:用来读取配置文件。
  3. */
  4. @Test
  5. public void test2() throws Exception {
  6. Properties pros = new Properties();
  7. // 此时的文件默认在当前的module下
  8. // 读取配置文件的方式一:
  9. // FileInputStream fis = new FileInputStream("jdbc.properties");
  10. // FileInputStream fis = new FileInputStream("src/jdbc1.properties");
  11. // pros.load(fis);
  12. // 读取配置文件的方式二:使用ClassLoader
  13. // 配置文件默认识别为:当前module的src下
  14. ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
  15. InputStream is = classLoader.getResourceAsStream("jdbc1.properties");
  16. pros.load(is);
  17. String user = pros.getProperty("user");
  18. String password = pros.getProperty("password");
  19. System.out.println("user = " + user + ",password = " + password);
  20. }