美文网首页Java 之旅
中级13 - 类型与反射

中级13 - 类型与反射

作者: 晓风残月1994 | 来源:发表于2020-06-02 17:36 被阅读0次

Java的强大不仅体现在静态、面向对象范式上。Java还拥有强大的自省和运行时动态调用能力,我们称之为反射。没有反射,Java生态系统就像是无源之水。CRUD 小王子用不到,但想玩出花就要用到了。

灵魂三问:

  • 对象是如何创造出来的?【有类说明书】
  • 类说明书从哪来的?【Java 安装目录下的相关 jar 包、自己项目中 Java 源文件编译之后的 target 目录,甚至从网络中,从任何地方】
  • 类说明书被谁加载进来的?【ClassLoader】

1. Java 的类与 Class

可以通过对象的 getClass() 可以拿到 Class 对象,其代表了运行时真实的类型信息。Java 中所有的对象都在堆(heap)中。

  • RTTI(Run-Time Type Identification)运行时类型识别。
  • 一个 Class 对象就是一个类的说明书,JVM 根据这个说明书创建出来一个类的实例。
  • 静态变量本质是归属于 class 对象本身的(依附于说明书本身),不和任何实例绑定。
  • instatnceof ,既然存在运行时类型识别以及 getClass() 方法,所以 instanceof 就像是语法糖而已。
  • 强制类型转化,运行时 JVM 知道对象实际是什么类型,所以在运行强制类型转化时如果很牵强,则会发生运行时异常 ClassCastException

2. 类加载与 ClassLoader

2.1 类加载

加载前是 .class 文件,加载后是 Class 对象。该对象表示了一个正在运行的 Java 程序中的 class 或者接口。
代表了每个类的说明书的 class 对象都存放在 JVM 的元空间(永久代)中。

image.pngimage.png
Class 类对象(在 JVM 中代表了编译后的 .class 文件)在第一次使用时被加载。

例如继承体系是 Object <- Anical <- Cat <- WhiteCat。在第一次 new WhiteCat 时,实际上先要加载和调用相关父类。

把 IDEA 自动拼接后的命令复制出来,手动添加 -verbose:class 参数,顺便使用 grep 过滤一下,观察类的加载顺序:

$ "C:\Program Files\Java\jdk1.8.0_202\bin\java.exe" -verbose:class "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2.1\lib\idea_rt.jar=55950:C:\Program Files\JetBrains\IntelliJ IDEA 2019
.2.1\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_202\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib
\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\jacces
s.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\nashorn.jar;C:\Program F
iles\Java\jdk1.8.0_202\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\j
dk1.8.0_202\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jce.
jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_202\
jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar;D:\Pro
jects\tmp\map-bean-converter\target\classes" com.github.hcsp.reflection.WhiteCat | grep hcsp

# 输出如下
[Loaded com.github.hcsp.reflection.Animal from file:/D:/Projects/tmp/map-bean-converter/target/classes/]
[Loaded com.github.hcsp.reflection.Cat from file:/D:/Projects/tmp/map-bean-converter/target/classes/]
[Loaded com.github.hcsp.reflection.WhiteCat from file:/D:/Projects/tmp/map-bean-converter/target/classes/]

# 不要过滤,输出全量,会发现 Object 类是最先加载的
[Opened C:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]
[Loaded java.lang.Object from C:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]

2.2 ClassLoader

Classloader负责从外部系统中加载一个类,而这个被加载的类:

  • 这个 .class 文件对应的 Java 文件并不需要一定存在【真正需要的只是编译后的 .class 文件】
  • 这个 class 文件也不需要一定存在【文件本质是字节流,可以从网络中加载进来,查看 ClassLoader 中的 defineClass 方法】
  • 可以动态生成,在内存中凭空“捏造”【术语:动态字节码增强,这是丰富多彩的基石】
  • 双亲委派加载模型

加载一个类时,调用 ClassLoader 类的实例方法 loadClass ,该方法按以下逻辑进行加载:
先看类是否已经加载过了,如尚未加载,则再看当前 ClassLoader 是否存在 parent (parent class loader,用于委派。该字段由 JVM 提供),存在则调用 parentloadClass 方法进行加载,不存在则尝试调用 JVM 内置的 ClassLoader。
最后该 Class 对象如果还是没加载到、也没找到,依然是 null,那么会调用 findClass (在 ClassLoader 抽象类中的实现只是抛了个异常,因此需要继承该抽象类的时候进行完善,参见下方实战)。
出于安全性考虑,JDK 内置的类都是由最顶端的类加载器进行加载。

  • Java 规范和 JVM 规范,二者之间是独立的,唯一的联系是字节码,这种分离提供了在 JVM 上可以运行其他语言的可能。

3. 反射与动态调用

反射,顾名思义,镜子里看到的东西。根据参数的不同,可以运行时动态决定响应的过程。反射是一个主动阅读 class 说明书的过程。
Class 对象上的 get* 方法可以获取当前及其父类的 public 方法或属性,想要获取所有非继承的属性或方法,可使用 getDeclaredMethodgetDeclaredField 等。

• Class

  • Class.forName()

• Method

  • Method.invoke

• Field

  • Field.get
  • 根据参数动态创建一个对象:
// 动态创建对象
public class Main {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        String className = args[0];
        Class c = Class.forName(className); // 传入全限定类名(FQCN),比如 java.lang.String
        Object obj = c.getConstructor().newInstance();
    }
}
  • 根据参数动态调用一个方法:
package com.github.hcsp.string;

import java.lang.reflect.InvocationTargetException;

public class Cat {

    public void catchMouse() {
        System.out.println("catchMouse!");
    }

    public void beCute() {
        System.out.println("beCute!");
    }

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Cat cat = new Cat();
        // 刻意这样换行写,方便打断点在调试器中观察
        cat
            .getClass()
            .getMethod(args[0]) // 传入方法名,比如 beCute
            .invoke(cat);
    }
}
  • 根据参数动态获取一个属性:
package com.github.hcsp.reflection;

public class Cat extends Animal {
    public String[] leg;
    public String head = "head";
    public String tail = "tail";

    public static int count;

    public void catchMouse() {
        System.out.println("catchMouse!");
    }

    public void beCute() {
        System.out.println("beCute!");
    }

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Cat cat = new Cat();
        System.out.println(cat
                .getClass()
                .getField(args[0]) // 传入字段名,比如 head
                .get(cat)
        );
    }
}

Tips:可以直接使用 IDEA 为 java 命令传参,或者自己手动拼接。


image.pngimage.png

4. 实战

4.1 使用反射实现一个 Java Bean 到 Map 的转换器

import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class MapBeanConverter {
    // 传入一个遵守Java Bean约定的对象,读取它的所有属性,存储成为一个Map
    // 例如,对于一个DemoJavaBean对象 { id = 1, name = "ABC" }
    // 应当返回一个Map { id -> 1, name -> "ABC", longName -> false }
    // 提示:
    //  1. 读取传入参数bean的Class
    //  2. 通过反射获得它包含的所有名为getXXX/isXXX,且无参数的方法(即getter方法)
    //  3. 通过反射调用这些方法并将获得的值存储到Map中返回
    public static Map<String, Object> beanToMap(Object bean) {

        Map<String, Object> result = new HashMap<>();
        Class klass = bean.getClass();

        Arrays.stream(klass.getDeclaredMethods())
                .filter(m -> {
                    StringBuilder name = new StringBuilder(m.getName()); // 使用 StringBuilder 防止待会CharAt时数组越界
                    // 过滤有参方法
                    if (m.getParameters().length != 0) {
                        return false;
                    }
                    // 过滤非 get 或 is 开头, 并且其后跟随非大写字母的方法
                    if (name.toString().startsWith("get") || name.toString().startsWith("is")) {
                        return Character.isUpperCase(name.charAt(name.toString().startsWith("get") ? 3 : 2));
                    } else {
                        return false;
                    }
                })
                .forEach(m -> {
                    String name = m.getName();
                    String roughName = name.substring(name.startsWith("get") ? 3 : 2);
                    try {
                        result.put(
                                Character.toLowerCase(roughName.charAt(0)) + roughName.substring(1),
                                m.invoke(bean)
                        );
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        e.printStackTrace();
                    }
                });

        return result;
    }

    // 传入一个遵守Java Bean约定的Class和一个Map,生成一个该对象的实例
    // 传入参数DemoJavaBean.class和Map { id -> 1, name -> "ABC"}
    // 应当返回一个DemoJavaBean对象 { id = 1, name = "ABC" }
    // 提示:
    //  1. 遍历map中的所有键值对,寻找klass中名为setXXX,且参数为对应值类型的方法(即setter方法)
    //  2. 使用反射创建klass对象的一个实例
    //  3. 使用反射调用setter方法对该实例的字段进行设值
    public static <T> T mapToBean(Class<T> klass, Map<String, Object> map) {

        T result = null;
        try {
            result = klass.getConstructor().newInstance();
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            e.printStackTrace();
        }

        T finalResult = result;
        map.forEach((name, value) -> {
            String methodName = "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
            try {
                klass.getMethod(methodName, value.getClass()).invoke(finalResult, value);
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }
        });

        return result;
    }

    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException {
        DemoJavaBean bean = new DemoJavaBean();
        bean.setId(100);
        bean.setName("AAAAAAAAAAAAAAAAAAA");
        System.out.println(beanToMap(bean));

        Map<String, Object> map = new HashMap<>();
        map.put("id", 123);
        map.put("name", "ABCDEFG");
        System.out.println(mapToBean(DemoJavaBean.class, map));
    }

    public static class DemoJavaBean {
        private Integer id;
        private String name;
        private String privateField = "privateField";

        public int isolate() {
            System.out.println(privateField);
            return 0;
        }

        public Integer getId() {
            return id;
        }

        public void setId(Integer id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public String getName(int i) {
            return name + i;
        }

        public void setName(String name) {
            this.name = name;
        }

        public boolean isLongName() {
            return name.length() > 10;
        }

        @Override
        public String toString() {
            return "DemoJavaBean{"
                    + "id="
                    + id
                    + ", name='"
                    + name
                    + '\''
                    + ", longName="
                    + isLongName()
                    + '}';
        }
    }
}

// 测试用例
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class MapBeanConverterTest {
    @Test
    public void test() {
        MapBeanConverter.DemoJavaBean bean = new MapBeanConverter.DemoJavaBean();
        bean.setId(100);
        bean.setName("BBBBBBBBBBBBB");
        Map<String, Object> resultMap = MapBeanConverter.beanToMap(bean);

        Assertions.assertEquals(100, resultMap.get("id"));
        Assertions.assertEquals("BBBBBBBBBBBBB", resultMap.get("name"));
        Assertions.assertEquals(true, resultMap.get("longName"));
        Assertions.assertNull(resultMap.get("olate"));

        Map<String, Object> map = new HashMap<>();
        map.put("id", 456);
        map.put("name", "12345");
        MapBeanConverter.DemoJavaBean resultBean =
                MapBeanConverter.mapToBean(MapBeanConverter.DemoJavaBean.class, map);

        Assertions.assertEquals(456, resultBean.getId());
        Assertions.assertEquals("12345", resultBean.getName());
    }
}

4.2 实现一个自定义的 ClassLoader

看一下 IDEA 拼接的命令行参数 -classpath 告诉了 JVM 去哪里寻找 .class 文件进行加载。而自定义的 ClassLoader 中可以实现运行时动态地加载指定位置(本地文件系统、甚至是网络中)的 .class 文件。

实现一个 ClassLoader,文档中鼓励覆盖 findClass 方法,而不是 loadClass 方法。

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;

public class MyClassLoader extends ClassLoader {
    // 存放字节码文件的目录
    private final File bytecodeFileDirectory;

    public MyClassLoader(File bytecodeFileDirectory) {
        this.bytecodeFileDirectory = bytecodeFileDirectory;
    }

    // 还记得类加载器是做什么的么?
    // "从外部系统中,加载一个类的定义(即Class对象)"
    // 请实现一个自定义的类加载器,将当前目录中的字节码文件加载成为Class对象
    // 提示,一般来说,要实现自定义的类加载器,你需要覆盖以下方法,完成:
    //
    // 1.如果类名对应的字节码文件存在,则将它读取成为字节数组
    //   1.1 调用ClassLoader.defineClass()方法将字节数组转化为Class对象
    // 2.如果类名对应的字节码文件不存在,则抛出ClassNotFoundException
    //
    // 一个用于测试的字节码文件可以在本项目的根目录找到
    //
    // 请思考:双亲委派加载模型在哪儿?为什么我们没有处理?
    // 扩展阅读:ClassLoader类的Javadoc文档
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassData(name);
        if (b == null) {
            throw new ClassNotFoundException(name);
        } else {
            return defineClass(name, b, 0, b.length);
        }
    }

    /**
     * 跟进文件名将二进制文件读取成字节流
     *
     * @param name .class 二进制文件的文件名
     * @return 成功读取返回 byte[],未找到或者读取失败返回 null
     */
    private byte[] loadClassData(String name) {
        try {
            return Files.readAllBytes(bytecodeFileDirectory.toPath().resolve(name + ".class"));
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static void main(String[] args) throws Exception {
        File projectRoot = new File(System.getProperty("basedir", System.getProperty("user.dir")));
        MyClassLoader myClassLoader = new MyClassLoader(projectRoot);

        Class testClass = myClassLoader.loadClass("com.github.hcsp.MyTestClass");
        Object testClassInstance = testClass.getConstructor().newInstance();
        String message = (String) testClass.getMethod("sayHello").invoke(testClassInstance);
        System.out.println(message);
    }
}

根目录中提前准备一个 .class 文件:

public class MyTestClass {
    public MyTestClass() {
    }

    public String sayHello() {
        return "Hello";
    }
}
image.pngimage.png

5. 查看字节码的工具

  • javap 工具
  • IDEA 的 ASM ByteCode Viewer 插件
  • Classpy 工具

相关文章

  • 中级13 - 类型与反射

    Java的强大不仅体现在静态、面向对象范式上。Java还拥有强大的自省和运行时动态调用能力,我们称之为反射。没有反...

  • 类型与反射

    对于类型与反射,我的理解是在运行时可以动态地对类进行一些操作。 比如:XXX x = new XXX() 这个 X...

  • 面试官问go反射第一弹

    目录 反射概念 reflect包 反射类型(Type)和种类(Kind) 反射类型(Type)使用 反射类型对象(...

  • CoreJava笔记 - 范型程序设计(5)

    反射与范型 由于类型擦除,反射无法得到关于范型类型参数的信息。 范型的Class类在Java的反射库中,Class...

  • Java 类型与反射

    Java的类与Class RTTI(Run-Time Type Identification)运行时类型识别任何对...

  • 美国摄影用光教程

    反射与角度的控制 漫反射(大小光源造成软质阴影和硬质阴影,但物体的表面决定了反射类型,与光源无关) 平方反比定律:...

  • go语言反射的总结

    首先巴拉巴拉一下golang反射机制的三个定律 1.反射可以从接口类型到反射类型对象 2.反射可以从反射类型对象到...

  • java 的反射

    参考文档:深入理解Java类型信息(Class对象)与反射机制

  • 一份反射go reflect的API练习以及其坑点

    主要内容: 由对象获取反射类型,由对象获取反射值 由反射值获取反射类型 反射值重新转换成对象 遍历字段 遍历方法 ...

  • 第11章 2.反射

    1、 方法和类型的反射 2、结构的反射

网友评论

    本文标题:中级13 - 类型与反射

    本文链接:https://www.haomeiwen.com/subject/hscpzhtx.html