2020-03-20-Java的Class对象和反射
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的生命周期
使用一个类需要三个过程,加载——链接——初始化。
- 加载:由ClassLoader执行,从字节码中创建一个Class对象。
- 链接:验证字节码,为静态域分配存储空间。
链接分为三个阶段:
(1)验证:确保被导入类型的正确性
(2)准备:为静态域分配字段,并用默认值初始化
(3)解析:将常量池内的符号引用替换为直接引用
这里有两个引用的概念:
符号引用:一个java类可以引用另一个类,但是在编译时java类不知道所引用类的实际内存地址,就需要一个符号引用来代替。
直接引用:在解析阶段,需要找到所引用类的实际地址,也就是将符号引用替换成直接引用。 - 初始化:对静态代码块和非常量静态变量初始化。
如果一个变量是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