• 面试官: 谈谈你对Java反射的理解


    转载:https://juejin.im/post/5dac4edff265da5b5f7588d1#heading-4
    Java提供的反射机制允许你在运行时检查类的信息

    Java的类加载
    Java在真正需要使用一个类时才会去加载类,而不是在启动程序时就载入所有的类,因为大多数使用者都只使用到程序的部分资源,在需要某些功能时再载入某些资源,可以让系统资源运用的更高效。

    类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在Jvm的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

    Java 中的所有类型包括基本类型(int, long, float等等),即使是数组都有与之关联的 Class 类的对象。
    Class对象是由Jvm自动生成的,每当一个类被载入时,Jvm就自动为其生成一个Class对象

    Class对象
    实例.getClass()
    通过Object的getClass()获取每一个实例对应的Class对象

    String name = "hello";
    Class stringClass = name.getClass();
    System.out.println("类的名称:" + stringClass.getName());
    System.out.println("是否为接口:" + stringClass.isInterface());
    System.out.println("是否为基本类型:" + stringClass.isPrimitive());
    System.out.println("是否为数组:" + stringClass.isArray());
    System.out.println("父类名称:" + stringClass.getSuperclass().getName());
    复制代码
    类的名称:java.lang.String
    是否为接口:false
    是否为基本类型:false
    是否为数组:false
    父类名称:java.lang.Object
    复制代码
    类名.class
    你也可以直接使用一下方式来获取String类的Class对象

    Class stringClass = String.class;
    复制代码
    Class.forName()
    在一些应用中,你无法事先知道使用者将载入什么类别,你可以使用Class的静态方法forName()来动态加载类别

    Class c = Class.forName(args[0]);
    System.out.println("类的名称:" + c.getName());
    System.out.println("是否为接口:" + c.isInterface());
    System.out.println("是否为基本类型:" + c.isPrimitive());
    System.out.println("是否为数组:" + c.isArray());
    System.out.println("父类名称:" + c.getSuperclass().getName());
    复制代码
    $ java Demo1 java.util.Scanner
    类的名称:java.util.Scanner
    是否为接口:false
    是否为基本类型:false
    是否为数组:false
    父类名称:java.lang.Object
    复制代码
    Class.forName()有两个版本,上面的版本只指定了全限定类名,而另一个版本可以让你指定类名,载入时是否执行静态代码块,执行类加载器(ClassLoader)

    static Class forName(String name, boolean initialize, ClassLoader loader)
    复制代码
    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    // Class.forName() 加载类 默认会执行初始化块
    Class.forName("Test2");
    // Class.forName() 加载类 第二个参数 可以控制是否执行初始化块
    Class.forName("Test2", false, loader);

    class Test2 {
    static {
    System.out.println("静态初始化块执行了!");
    }
    }
    复制代码
    从Class对象中获取信息
    Class对象表示所载入的类别,获取Class对象后,你就可以获取类别相关的信息,入 package, constructor, field, method等信息。
    而每一种信息,都有相对应的类别

    package: java.lang.reflect.Package
    constructor: java.lang.reflect.Constructor
    field: java.lang.reflect.Field
    method: java.lang.reflect.Method
    Class c = Class.forName(args[0]);
    System.out.println("包信息package:" + c.getPackage());
    System.out.println("类修饰符modifier:" + c.getModifiers());
    System.out.println("构造方法constructor:");
    Arrays.stream(c.getDeclaredConstructors()).forEach(System.out::println);
    System.out.println("成员变量fields:");
    Arrays.stream(c.getDeclaredFields()).forEach(System.out::println);
    复制代码
    $ java Demo1 java.util.ArrayList
    包信息package:package java.util
    类修饰符modifier:1
    构造方法constructor:
    public java.util.ArrayList(java.util.Collection)
    public java.util.ArrayList()
    public java.util.ArrayList(int)
    成员变量fields:
    private static final long java.util.ArrayList.serialVersionUID
    private static final int java.util.ArrayList.DEFAULT_CAPACITY
    private static final java.lang.Object[] java.util.ArrayList.EMPTY_ELEMENTDATA
    private static final java.lang.Object[] java.util.ArrayList.DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    transient java.lang.Object[] java.util.ArrayList.elementData
    private int java.util.ArrayList.size
    private static final int java.util.ArrayList.MAX_ARRAY_SIZE
    复制代码
    ClassLoader 类加载器
    Java在需要使用类的时候才会将类载入,Java中类的载入是由Class Loader来实现的.

    当你尝试执行java xxx命令时,java会尝试找到JRE的安装目录,然后寻找
    jvm.dll,接着启动JVM并进行初始化操作,接着产生
    BootstrapLoader,Bootstrap Loader会载入Extended Loader, 并设定Extended Loader 的parent 为 BootstrapLoader, 接着Bootstrap Loader 会载入 Application Loader, 并将Application Loader 的parent 设定为
    Extended Loader

    启动类加载器
    BootstrapLoader搜寻 sun.boot.library.path中指定的类, 你可以使用
    System.getProperty("sun.boot.library.path")来获取

    /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib
    复制代码
    扩展类加载器
    Extended Loader(sun.misc.Launcher$ExtClassLoader) 是由Java编写的,会搜寻系统参数java.ext.dirs中指定的类别,可以通过System.getProperty("java.ext.dirs")来获取

    /Users/dsying/Library/Java/Extensions:
    /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/ext:
    /Library/Java/Extensions:
    /Network/Library/Java/Extensions:
    /System/Library/Java/Extensions:/usr/lib/java
    复制代码
    应用程序类加载器
    Application Loader
    (sun.misc.Launcher$AppClassLoader) 是由Java编写的,会搜寻系统参数java.class.path中指定的类别,可以通过System.getProperty("java.class.path")来获取, 在使用java xxx命令执行.class字节码文件时,可以通过-cp参数设定classpath

    java –cp ./classes SomeClass
    复制代码
    类加载器之间的关系
    ClassLoader loader Thread.currentThread().getContextClassLoader();
    // sun.misc.Launcher$AppClassLoader@18b4aac2 应用类加载器
    System.out.println(loader);
    // sun.misc.Launcher$ExtClassLoader@610455d6 扩展类加载器
    System.out.println(loader.getParent());
    // Bootstrap ClassLoader 启动类加载器(用C语言实现,所以此处返回null)
    System.out.println(loader.getParent().getParent());
    复制代码
    sun.misc.Launcher$AppClassLoader@18b4aac2
    sun.misc.Launcher$ExtClassLoader@610455d6
    null
    复制代码

    类加载有三种方式:
    命令行启动应用时候由JVM初始化加载
    通过Class.forName()方法动态加载
    通过ClassLoader.loadClass()方法动态加载
    Class.forName()和ClassLoader.loadClass()区别
    Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
    ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
    Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象。
    JVM类加载机制
    全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
    父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
    缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
    双亲委派模型
    双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

    双亲委派机制:
    当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
    当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
    如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
    若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
    ClassLoader源码分析:
    public Class loadClass(String name)throws ClassNotFoundException { return loadClass(name, false); } protected synchronized Class loadClass(String name, boolean resolve)throws ClassNotFoundException {
    // 首先判断该类型是否已经被加载
    Class c = findLoadedClass(name);
    if (c == null) {
    //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
    try {
    if (parent != null) {
    //如果存在父类加载器,就委派给父类加载器加载
    c = parent.loadClass(name, false);
    } else {
    //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
    c = findBootstrapClass0(name);
    }
    } catch (ClassNotFoundException e) {
    //如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
    c = findClass(name);
    }
    }
    if (resolve) {
    resolveClass(c);
    }
    return c;
    }
    复制代码
    双亲委派模型意义:

    系统类防止内存中出现多份同样的字节码
    保证Java程序安全稳定运行
    自定义加载器
    自定义类加载器一般都是继承自ClassLoader类,从上面对loadClass方法来分析来看,我们只需要重写 findClass 方法即可。下面我们通过一个示例来演示自定义类加载器的流程:

    package com.github.hcsp.classloader;

    import java.io.ByteArrayOutputStream;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.IOException;

    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
    //
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getByteArrayFromFile(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name, classData, 0, classData.length);
    }
    
    byte[] getByteArrayFromFile(String className) throws ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        File file = new File(bytecodeFileDirectory, className + ".class");
        int len = 0;
        try {
            byte[] bufferSize = new byte[1024];
            FileInputStream fis = new FileInputStream(file);
            while ((len = fis.read(bufferSize)) != -1) {
                bos.write(bufferSize, 0, len);
            }
        } catch (FileNotFoundException e) {
            throw new ClassNotFoundException();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bos.toByteArray();
    }
    
    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文件进行加密,因此没有解密的过程.

    其它
    使用反射创建对象
    你可以使用Class的newInstance()方法来实例化

    Class c = Class.forName(className);
    Object obj = c.newInstance();
    复制代码
    调用方法
    使用反射可以取回类中的方法,方法对应的类为
    java.lang.reflect.Method, 你可以使用它的
    invoke()方法来调用指定的方法

    Class testClass = myClassLoader.loadClass("com.github.hcsp.MyTestClass");
    Object testClassInstance = testClass.getConstructor().newInstance();
    String message = (String) testClass.getMethod("sayHello").invoke(testClassInstance);
    复制代码

  • 相关阅读:
    看《长安十二时辰》可以了解哪些算法知识
    面试官,我会写二分查找法!对,没有 bug 的那种!
    毕业十年后,我忍不住出了一份程序员的高考试卷
    扫雷与算法:如何随机化的布雷(一)
    降维打击!为什么我认为数据结构与算法对前端开发很重要
    盖尔-沙普利算法告诉你,你的对象在哪里?
    这道算法题太太太太太简单啦
    有点难度,几道和「滑动窗口」有关的算法面试题
    几道和「黑洞照片」那种海量数据有关的算法问题
    LeetCode 上最难的链表算法题,没有之一!
  • 原文地址:https://www.cnblogs.com/zx-coder/p/13087496.html
Copyright © 2020-2023  润新知