浅谈Java的反射机理

1.什么是反射?

Java之所以被称为动态语言,其中有个特点就是因为反射机制。Java的反射是指在运行时检查类、方法、属性等结构的能力,以及在运行时实例化对象、调用方法、获取或设置属性等。通过反射,可以在编译时未知类的情况下操作类的方法和属性。这使得程序可以动态地加载、检查和使用类,以及在运行时修改类的行为。

2.反射的例子

import java.lang.reflect.Constructor;

class MyClass {
    private String message;

    public MyClass() {
        this.message = "你好,反射!";
    }

    public void showMessage() {
        System.out.println(message);
    }
}

public class Main {
    public static void main(String[] args) {
        try {
            Class<?> myClass = Class.forName("MyClass");
            Constructor<?> constructor = myClass.getConstructor();
            Object myObject = constructor.newInstance();

            System.out.println("类名: " + myClass.getName());
            myClass.getMethod("showMessage").invoke(myObject);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.类加载原理

相信大家都经历不少了面向对象编程的语言,会发现大多数语言中定义类的都是大同小异:

  • 所有的类都有类名;
  • 所有的类都有0或者n个属性,每个属性都有一个数据类型和名称;
  • 所有的类都有0或者n个方法,每个方法都有对应的参数列表和返回值。 
  • 所有的类都有静态方法、静态成员变量。
  • 所有的类都有对应修饰符修饰。

所有的类在以上几点都是相同,只是根据实际的需求,导致他们的方法名、属性、类名有不同。所以针对这样一个公共特性,java用一个特殊的类Class去表示这些自定义类的行为。

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement,
                              TypeDescriptor.OfField<Class<?>>,
                              Constable {
    private static final int ANNOTATION= 0x00002000;
    private static final int ENUM      = 0x00004000;
    private static final int SYNTHETIC = 0x00001000;

    private static native void registerNatives();
    static {
        registerNatives();
    }
    private Class(ClassLoader loader, Class<?> arrayComponentType);
    public String toString();
    public String toGenericString();
    static String typeVarBounds(TypeVariable<?> typeVar);
    @CallerSensitive
    public static Class<?> forName(String className);
    @CallerSensitive
    public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader);
    ...
}

这里源码来自于Class.java,可以使用IDEA进行跳转查看。

Class也有相应的数据结构去存储这些的类的公共特性。首先看看Field,这里面主要存储类的属性里面的名称和类型。那么如何获得Fields呢,首先看看下面的两个方法:getFields和getDeclaredFields。

两个方法的不同点在于:

  1. getFields 方法用于获取类及其父类中所有的公共(public)字段。这包括类声明的公共字段以及从父类继承而来的公共字段。

  2. getDeclaredFields 方法用于获取类中声明的所有字段,包括公共、受保护、默认(包内可见)和私有字段,但不包括从父类继承而来的字段。

    @CallerSensitive
    public Field[] getFields() throws SecurityException {
        @SuppressWarnings("removal")
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkMemberAccess(sm, Member.PUBLIC, Reflection.getCallerClass(), true);
        }
        return copyFields(privateGetPublicFields());
    }

    @CallerSensitive
    public Field[] getDeclaredFields() throws SecurityException {
        @SuppressWarnings("removal")
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkMemberAccess(sm, Member.DECLARED, Reflection.getCallerClass(), true);
        }
        return copyFields(privateGetDeclaredFields(false));
    }

Field这块代码在Field.java中,只截取我们感兴趣的部分的:

public final
class Field extends AccessibleObject implements Member {
    private final Class<?>            clazz;
    private final int                 slot;
    // This is guaranteed to be interned by the VM in the 1.4
    // reflection implementation
    private final String              name;
    private final Class<?>            type;
}

这里我们主要关注的是如何利用field去获得类的属性的名字和类型,关注下两个方法:getName,getType。同样的在getType以后我们还可以利用getName拿到类型的名称:

        try {
            Class<?> myClass = Class.forName("MyClass");
            Field[] fields = myClass.getDeclaredFields();
            for(Field field : fields){
                String fieldName = field.getName();
                System.out.println(fieldName);
                Class<?> fieldType = field.getType();
                System.out.println(fieldType.getName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

其次是方法,方法主要是由Method.java里的Method类进行封装的,同样我们也只截取源码中关键的部分:

public final class Method extends Executable {
    private final String              name;
    private final Class<?>            returnType;
    private final Class<?>[]          parameterTypes;
    private final Class<?>[]          exceptionTypes;
}

那么method总共包含以下基本信息:name,returnType,paramterTypes,exceptionTypes,对于这些信息我们同样可以get的方式提取出这些关键信息,这里不再赘述。

主要说明一下如何提取method,同样存在两个方法,一个是getDeclaredMethods,第二个是getMethods,这两个函数的区别和Field是类似的。

整个类的生命周期如下图所示:

1)类的加载阶段

根据类的全限定名找到对应的二进制字节流。可以在ZIP文件、网络的Applet、运算时生成、JSP、数据库读取。

2)验证阶段

确保Class文件包含的字节流形式符合虚拟机要求,并且不会危害虚拟机自身健康。

其中包括以下验证方式:

        a.文件格式验证。(验证是否当前版本的文件格式要求)

        b和c.元数据验证和字节码验证。(语义上的验证)

        d.符号引用验证。

3)准备阶段

JVM开始首先为类变量(静态变量)分配内存。

例如下面的类成员变量:

                       static String str="abc";

首先为它分配一段空间,并将其置为NULL。而不是"abc"

但若该类变量(静态变量)被final修饰的话

                        static final String str="abc";

就会在一开始赋成你的想要的值。

4)初始化阶段

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

顺序:

父类静态变量声明时 -> 父类静态代码块 -> 子类静态变量声明时 -> 子类静态代码块 -> 父类普通成员变量声明时 -> 父类代码块 -> 父类构造器 -> 子类普通成员变量声明时 -> 子类代码块 -> 子类构造器

class Grandpa
{
    static
    {
        System.out.println("爷爷在静态代码块");
    }
}    
class Father extends Grandpa
{
    static
    {
        System.out.println("爸爸在静态代码块");
    }

    public static int factor = 25;

    public Father()
    {
        System.out.println("我是爸爸~");
    }
}
class Son extends Father
{
    static 
    {
        System.out.println("儿子在静态代码块");
    }

    public Son()
    {
        System.out.println("我是儿子~");
    }
}
public class InitializationDemo
{
    public static void main(String[] args)
    {
        System.out.println("爸爸的岁数:" + Son.factor);	//入口
    }
}

5)使用阶段

从入口方法中使用代码

6)卸载

代码执行完毕,销货Class对象,释放对应的内存空间。

  • 17
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值