Java反射机制详解

  Java1.1中引入反射(Reflection),被视为动态语言的关键,反射机制允许程序在运行期通过ReflectionAPI获取任何类的内部信息,并能操作任何对象的属性及方法。

反射机制原理
  • 反射的实现是借助于Class类,在Java中“万物皆对象”,类也不例外,类自身也是对象,每个类都是Class类的对象。
  • 当某个类被加载后,在方法区中就会创建一个代表该类的Class类对象,该对象保存有该类的所有信息,作为访问该类的入口。
  • 所以在运行期间,想用发射获取某个类的信息的话,首先JVM检查该类是否已被加载,没有的话先加载,然后通过调用该类对应的Class对象获取该类的信息。

类的加载过程

理解类是如何被加载的,才能对反射有更清楚的认识。


我们运行编写的.java程序,需要经历这几个步骤:

1.执行javac.exe命令对.java进行编译,生成一个或多个字节码文件(即.class文件);

2.执行java.exe命令对字节码文件进行解释运行,首先要进行类的加载,加载到内存的类称为运行时类,即Class类型的一个对象;

3.系统将控制权从调用者移给运行时类或其对象;


其中第2步中类的加载分三个步骤:
在这里插入图片描述

  • 加载:类加载器将.class文件加载到内存,具体来说是Java内存空间的方法区,将字节码内容转换为方法区的运行时数据结构,然后生成一个代表该类的Class类型对象,作为方法区中类数据的访问入口,所有对类数据的访问都要通过这个Class类型对象(反射机制也就是从内存中找到了这个Class类型对象,再通过这个对象获取类的属性、方法、构造器等所有信息)。

  • 链接:将运行时类的二进制代码合并到JVM运行状态中

    • 验证:确保加载的类信息符合JVM规范,如以cafe开头
    • 准备:为类的静态变量分配内存并赋默认初始值,如int为0、String为null,这些内存都在方法区中
    • 解析:将常量池的符号引用替换为直接地址
  • 初始化

    • 执行类构造器,其是由编译器自动收集类变量的赋值动作和静态代码块的语句合成的,注意这是类的构造器,不是类对象的构造器
    • 如果类的父类还没初始化,需要先初始化父类
    • 虚拟机会保证类构造器在多线程环境中被正确加锁和同步

类何时被加载进内存?

这里扩展下,类是什么时候被加载进内存的?答案是当类被需要时,那类何时是被需要的,上面说过通过反射调用类是一种情况,此外还有下面这些情况类也是被需要的:

  • 类被实例化时;
  • 调用类中的静态方法时;
  • 使用类中的静态属性时;
  • 调用反射中的方法时,如Class.forName("xxx")
  • 对子类进行以上操作时,父类及父类的父类等都需要加载;

可以发现以上情况很多时候是在Java程序运行期间发生的,所以Java程序启动时并不是完全载入的,这与许多传统语言不同,Java中只有当JVM检测到类被需要才被加载。

获取Class类对象的几种方式:

1.Class.forName(全类名)

我们通过Class.forName("xxx")验证下xxx类是在运行期被加载的。

package com.codezhao.reflect;

/**
 * @author codeZhao
 * @date 2021/1/16  16:19
 * @Description Class.forName测试
 */
class Tom {
    //类中静态代码块在类首次被加载进内存时执行
    static {
        System.out.println("Tom come!");
    }
}
class Jack {
    //类中静态代码块在类首次被加载进内存时执行
    static {
        System.out.println("Jack come!");
    }
}
public class ClassforNameTest {
    public static void main(String[] args) {
        System.out.println("inside code");
        //创建Tom对象
        new Tom();
        try {
            //创建Jack对应的Class类对象
            Class.forName("com.codezhao.reflect.Jack");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

以上会打印出:

inside code
Tom come!
Jack come!

有些JVM下会打印出:

Tom come!
inside code
Jack come!

可见,有的JVM会检查到程序需要Tom类便在程序运行前将Tom类加载,但不管怎样通过Class.forName(“xxx”)调用的xxx类只能到运行期才被加载。

2.类名.class

这种方式更简单、安全,因为它在编译期就得到检查,同时没有方法调用所以更高效。

这种方法不仅适用于普通类,对于接口、数据及基本类型都可以用,基本类型.class会返回其封装类。

3.对象.getClass()

获取运行时类的结构的常用方法

对下面第5个方法的结果说明一下,Java中所有类有默认父类Object,所以看到获取的方法中很多Object中的方法。再具体地说,getMethods()先找到Student中的方法,再找到其父类Person中的方法,然后发现Person没有指定父类,这时会给Person指定一个默认父类Object,这一步是编译器或JVM帮我们实现的,最后找到Object的方法,所以说Java中所有类都有且只有一个父类。

public class ReflectTest {
    Class studentClass = Student.class;
    @Test
    public void test1() throws Exception {
        System.out.println("-------1.获取类的修饰符-------");
        int modifiers = studentClass.getModifiers();
        System.out.println(Modifier.isPublic(modifiers));
        System.out.println("-------2.获取类名-------");
        String className = studentClass.getName();
        System.out.println(className);
        System.out.println("-------3.获取类及父类中的public属性-------");
        Field[] fields = studentClass.getFields();
        Arrays.asList(fields).iterator().forEachRemaining(System.out::println);
        System.out.println("-------4.获取类中的所有属性-------");
        Field[] declaredFields = studentClass.getDeclaredFields();
        Arrays.asList(declaredFields).iterator().forEachRemaining(System.out::println);
        System.out.println("-------5.获取类及父类中的public方法-------");
        Method[] methods = studentClass.getMethods();
        Arrays.asList(methods).iterator().forEachRemaining(System.out::println);
        System.out.println("-------6.获取类中的所有方法-------");
        Method[] declaredMethods = studentClass.getDeclaredMethods();
        Arrays.asList(declaredMethods).iterator().forEachRemaining(System.out::println);
        System.out.println("-------7.获取类中的构造器,并用构造器创建对象-------");
        Constructor con = studentClass.getConstructor(new Class[]{Long.class, String.class});
        System.out.println(con.newInstance(11111L,"一年级"));
    }
}

结果:

-------1.获取类的修饰符-------
false
-------2.获取类名-------
com.codezhao.reflect.Student
-------3.获取类及父类中的public属性-------
public java.lang.Long com.codezhao.reflect.Student.studentNumber
public java.lang.String com.codezhao.reflect.Person.name
-------4.获取类中的所有属性-------
public java.lang.Long com.codezhao.reflect.Student.studentNumber
private java.lang.String com.codezhao.reflect.Student.grade
-------5.获取类及父类中的public方法-------
public void com.codezhao.reflect.Student.learn()
public java.lang.String com.codezhao.reflect.Student.toString()
public void com.codezhao.reflect.Person.run()
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
-------6.获取类中的所有方法-------
private void com.codezhao.reflect.Student.exam()
public void com.codezhao.reflect.Student.learn()
public java.lang.String com.codezhao.reflect.Student.toString()
-------7.获取类中的构造器-------
Student{studentNumber=11111, grade='一年级'}


调用运行时类的结构的常用方法
@Test
public void test2() throws Exception {
    //创建对象
    Person person = new Person();

    System.out.println("-------1.调用对象的属性-------");
    //获取运行时类指定名称的属性
    Field name = person.getClass().getField("name");
    //保证属性是可访问的
    name.setAccessible(true);
    //设置对象person的name属性
    name.set(person, "codezhao");
    //获取对象person的name属性
    System.out.println(name.get(person));

    System.out.println("-------2.调用对象的方法-------");
    //获取运行时类指定名称的方法
    Method run = person.getClass().getMethod("run");
    //保证方法是可访问的
    run.setAccessible(true);
    //通过invoke()调用方法,参数1为对象,参数2为方法实参(这里run()没有参数)
    //invoke()返回值为方法返回值
    Object result = run.invoke(person);

结果:

-------1.调用对象的属性-------
codezhao
-------2.调用对象的方法-------


RTTI

反射属于运行期类型鉴定(RTTI),传统的RTTI最常见的形式就是造型,如下代码,通过(Shape)将Object造型成Shape,造型会在运行期检查其正确性,而编译器不会检查。通过(Shape)造型后,使代码尽可能少知道对象的具体类型信息,只需关注某一类对象,这也就是多形性设计,但如果某些情况下必须知道对象的确切类型,就得用RTTI了。

package com.codezhao.javatest;

import java.util.Vector;

/**
 * @author codeZhao
 * @date 2021/1/10  16:17
 */
interface Shape {
    void draw();
}
class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Circle come");
    }
}
class Square implements Shape {
    @Override
    public void draw() {
        System.out.println("Square come");
    }
}
public class Shapes {
    public static void main(String[] args) {
        Vector shapes = new Vector();
        shapes.addElement(new Circle());
        shapes.addElement(new Square());
        for (Object shape:shapes) {
            ((Shape) shape).draw();
        }
    }
}

传统的RTTI与反射的区别在于:

传统的RTTI在编译器间要打开和检查.class文件,而对于反射来说,编译期间不会使用.class文件。比如:Class.forName(“xxx”),编译器不会检查xxx.class;而(xxx)造型的话编译器找不到xxx.class会报错。

为什么引入反射机制?

上面说过,反射是动态语言的关键,它可以使代码更加灵活,我们可以在实际应用中体会其妙处。

反射在实际应用场景中的应用:

  • 动态代理,AOP实现
  • 远程方法调用(RMI)



关于动态代理和RMI,我会在接下来的文章专门讲解。
欢迎关注我的公众号【codeZhao】,获取更多技术干货和职业思考。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值