美文网首页
2020-03-20-Java的Class对象和反射

2020-03-20-Java的Class对象和反射

作者: 耿望 | 来源:发表于2020-03-21 21:35 被阅读0次
    Class的生命周期 (3).jpg

    Class的文件格式

    通过javap命令对class文件进行反解析,我们可以看到class文件包含了哪些内容:


    class.PNG

    比如以下命令对Main.class进行解析

    javap -verbose Main.class
    

    这里截取了部分内容:
    1.minor version副版本号
    2.major version主版本号
    3.access_flags访问标志,ACC_PUBLIC这是一个public类,ACC_SUPER默认都为true
    4.Constant pool常量池


    2.PNG

    常量池

    常量池主要存放字符串常量和符号引用。
    符号引用包括:
    类和接口的全限定名;
    字段的名称和描述符;
    方法的名称和描述符。
    常量池在一定程度上能避免对象的频繁创建。比如下面这段代码,有三个String类型,但是class的常量池中只会创建一个String对象“abc”。

            String aStr = "abc";
            String bStr = "abc";
            String cStr = new String("abc");
    

    类型信息

    类型信息包括访问表示,ACC_PUBLIC表示公共,ACC_SUPER,允许使用invokespecial字节码指令,这个指令会对类初始化。
    常量池和类型信息之外,class文件的组成就是属性表,字段表和方法表,这里就不详细写了。

    运行时常量池

    一般情况下,常量池存放在方法区,跟Java堆是分开的,但是java7有一个新特性,会在java堆维护一个字符串常量池。
    下面这段代码,会输出false和true。
    第一个情况,创建s1对象的时候会创建两个对象,一个在堆中,一个在常量池中,内容都是字符串“1”,因为s1和s2是两个对象,不同地址,所以输出false;
    第二个情况,创建s3的时候,因为是两个字符串相加,不会在常量池中创建,只有调用intern之后,才会去常量池中查找,没有找到就创建一个“11”对象。然后创建s4的时候就直接使用了这个对象的引用。所以会输出true。

            String s1 = new String("1");
            s1.intern();
            String s2 = "1";
            System.out.println(s1 == s2);
            
            String s3 = new String("1") + new String("1");
            s3.intern();
            String s4 = "11";
            System.out.println(s3 == s4);
    

    Class的生命周期

    使用一个类需要三个过程,加载——链接——初始化。

    1. 加载:由ClassLoader执行,从字节码中创建一个Class对象。
    2. 链接:验证字节码,为静态域分配存储空间。
      链接分为三个阶段:
      (1)验证:确保被导入类型的正确性
      (2)准备:为静态域分配字段,并用默认值初始化
      (3)解析:将常量池内的符号引用替换为直接引用
      这里有两个引用的概念:
      符号引用:一个java类可以引用另一个类,但是在编译时java类不知道所引用类的实际内存地址,就需要一个符号引用来代替。
      直接引用:在解析阶段,需要找到所引用类的实际地址,也就是将符号引用替换成直接引用。
    3. 初始化:对静态代码块和非常量静态变量初始化。
      如果一个变量是static final类型的,并且是一个常量,那么它不需要对类进行初始化就可以被使用。
      比如下面这个例子,在使用finalStr的时候,并不需要对Child类初始化。
      从打印信息也能看出,类的加载顺序是先加载父类,构造顺序是先构造父类。
    public class Main
    {
        public static void main(String[] args) throws ClassNotFoundException
        {
            System.out.println(Child.finalStr);
            System.out.println(Child.staticStr);
        }
    }
    
    class Parent {
        static {
            System.out.println("Parent initializing...");
        }
        public Parent() {
            System.out.println("Parent constructing...");
        }
    }
    class Child extends Parent {
        public static final String finalStr = "final value";
        public static String staticStr = "static value";
        static {
            System.out.println("Child initializing...");
        }
        public Child() {
            System.out.println("Child constructing...");
        }
    }
    打印信息是:
    final value
    Parent initializing...
    Child initializing...
    static value
    

    class对象的创建

    通常是通过new指令来完成对象的创建。创建的过程大概如下:
    1.判断对象是否被加载,链接和初始化
    2.为对象分配内存,需要在内存空间中找到一块跟对象大小相等的连续内存;
    3.处理并发安全问题,对象的创建是非常频繁的,需要在分配内存空间时进行同步操作。在新的java虚拟机上,在java堆内存区域会给每个线程分配一个本地缓冲区(ThreadLocalAllocationBuffer TLAB),线程创建对象的时候,首先在TLAB区域分配空间。
    4.将分配到的内存空间进行初始化;
    5.将对象所属的类,hashCode,GC年龄等数据存储到对象头;
    6.执行init方法进行初始化。

    class对象的引用

    有几种获取class对象引用的方法,可以通过类名.class,或者是通过Object实例.getClass()方法。
    也可以使用Class的静态方法forName(),它返回一个Class对象的引用。如果类还没有被加载,这个方法就会让JVM去加载它。如果找不到这个类,会抛出ClassNotFoundException异常。

        public static void main(String[] args) throws ClassNotFoundException
        {
            Class.forName("Child");
        }
    

    .class并不需要添加try/catch,因为编译时会做类型检查。有趣的是使用.class并不会对类做初始化。所以下面的语句不会有信息打印。

        public static void main(String[] args)
        {
            Class child = Child.class;
        }
    

    实际上,java5之后,不管是forName,getClass还是.class,都是返回一个Class的泛型引用,它会在编译时做类型检查,是一种更安全的方式。
    比如下面这种写法,就限定了引用的类型范围,必须是Parent的子类。

        public static void main(String[] args) throws ClassNotFoundException
        {
            Class<? extends Parent> child = Child.class;
        }
    

    总结一下,其实这三种方法获取到的是同一个对象的引用:

            Class<? extends Parent> child1 = Child.class;
            Class child2 = Class.forName("Child");
            Class child3 = new Child().getClass();
    

    然后可以通过newInstance方法来初始化这个类的实例。

    public class Main
    {
        public static void main(String[] args) throws ClassNotFoundException
        {
            Class<? extends Parent> child = Child.class;
            try {
                child.newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
    
    class Parent {
        static {
            System.out.println("Parent initializing...");
        }
        public Parent() {
            System.out.println("Parent constructing...");
        }
    }
    class Child extends Parent {
        public static final String finalStr = "final value";
        public static String staticStr = "static value";
        static {
            System.out.println("Child initializing...");
        }
        public Child() {
            System.out.println("Child constructing...");
        }
    }
    上面这段代码会打印:
    Parent initializing...
    Child initializing...
    Parent constructing...
    Child constructing...
    

    java5还提供了cast方法,来将Class引用进行类型转换。比如下面两种方式,实现的效果是一致的。

        public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException
        {
            Class<? extends Parent> child = Child.class;
            Child c = (Child) child.newInstance();
            Child cc = (Child) child.cast(new Child());
        }
    

    前面这些内容,获取Class对象的引用,或者是使用某个类的公有域,都是在我们已知它的确切类型的情况下。在编译之前我们就明确知道该类的信息。
    如果想要在运行时获取某个类的信息,应该怎么办呢?

    反射

    java提供了一种机制,可以动态地获取某个类的信息,能够创建一个编译时完全未知的对象,并且调用它的方法。
    如果该类有默认的无参构造函数,可以通过newInstrance方法,加上Method的invoke方法来调用它的内部方法。

    public class Main
    {
        public static void main(String[] args)
        {
            Class<? extends Child> child = Child.class;
            try {
                child.getDeclaredMethod("normalMethod", String.class).invoke(child.newInstance(), "invoke");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    class Child {
        static {
            System.out.println("Child initializing...");
        }
        public Child() {
            System.out.println("Child constructing... ");
        }
        public void normalMethod(String str) {
            System.out.println("normalMethod:" + str);
        }
    }
    

    但是,如果该类没有默认的无参构造方法,Class的newInstance方法就会报错

    java.lang.InstantiationException: at java.lang.Class.newInstance(Unknown Source)
    

    需要通过getConstructor方法获取构造函数:

        public static void main(String[] args)
        {
            Class<? extends Child> child = Child.class;
            try {
                child.getDeclaredMethod("normalMethod", String.class).invoke(
                        child.getConstructor(String.class).newInstance("construct"),
                        "invoke");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    参考

    https://blog.csdn.net/My_TrueLove/article/details/51289217
    https://blog.csdn.net/sinat_38259539/article/details/71799078
    https://www.jianshu.com/p/6a8997560b05
    https://zhuanlan.zhihu.com/p/25823310
    https://cloud.tencent.com/developer/article/1455559
    Java 内存之方法区和运行时常量池
    深入解析String#intern

    相关文章

      网友评论

          本文标题:2020-03-20-Java的Class对象和反射

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