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

2020-03-21  本文已影响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

上一篇下一篇

猜你喜欢

热点阅读