众所周知,java有三大特性: 封装继承和多态,封装是为了细化权限,继承是为了多态,多态是为了灵活复用。我们又知道,继承破坏了封装,但是,它破坏的不彻底,有没有破坏的更彻底的呢?有,反射! 反射效率很低,因为它工作在运行时,为什么工作在运行时呢?我们先来看个例子:
public class User {
// 创建一个私有字段name
private String name;
public User(String name) {
this.name = name;
}
}
public void test(){
// 创建一个User对象,name为:java
User user = new User("java");
// 反射的工作目标是.class对象
Class aclass = User.class;
// 获取声明的字段:name
Field nameField = aclass.getDeclaredField("name");
// 允许访问非public的字段
nameField.setAccessible(true);
// 获取此字段在user对象上的值
Object nameValue = nameField.get(user);
// 打印
System.out.println(nameValue);
}
打印结果:
java
Process finished with exit code 0
这个例子简单的演示了反射的使用,其中只有一点需要我们记得: 「反射的工作目标是.class对象」,也就是说,如果把反射比作一个函数,那么它的入参是.class对象,那么这个.class对象从哪里来呢?答曰: 类加载!
我们之前在JVM类加载机制里面提到过类加载的一些知识,这里再来看一下。JVM类加载流程:
- 1 通过全限定类名来获取定义此类的二进制字节流。
- 2 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 3 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
❝简而言之,就是: JVM类加载机制 接收的是: .class文件的二进制字节流,产出的是: java.lang.Class对象。
❞
等等!我们上面反射的例子,不就是针对Class对象工作的吗,所以可以说: 类加载机制产出了反射可以进行工作的目标,也就是Class对象,经过了类加载机制,反射才有了入参(Class对象),才可以工作,而「类加载工作在运行时,反射工作在类加载之后,所以: 反射工作在运行时!」,这就回答了刚刚的问题。那么,类加载为什么工作在运行时,java不是一门强类型静态语言吗?是,java是静态语言,但是它有动态特性,就是为了支持比如多态、JSP等技术引入的,比如:
A a = new B();
其中B是A的子类,这就是多态,只有在运行时,才知道它是什么类型,这就是java的动态特性,所以JVM的类加载是在运行时,这虽然一定程度上降低了效率,但是带来的作用是巨大的,这也是反射为什么工作在运行时,因为它需要的Class对象要在运行时才有。
那么既然JVM类加载机制 接收.class文件,产出java.lang.Class对象,那么它接收的.class文件从哪来呢?有人已经想到了,就是javac!也就是编译!
我们知道,写一个Hello.java文件,然后运行指令:javac Hello.java,就会生成一个Hello.class文件,这个文件是个二进制流,正是JVM类加载需要的东西!javac代表了编译过程,运行了这个指令,就会找到你设置的环境变量里面的相关程序,去进行编译,大家也可以直接在:com.sun.tools.javac.main.JavaCompiler找相关代码,看一下这个.class文件是怎么生成的,大概三个步骤:
- 1 解析与填充符号表,此阶段执行词法分析,语法分析,生成抽象语法树等。
- 2 注解处理,此阶段着重处理注解,也是apt的工作阶段,比如arouter。
- 3 分析与字节码生成,此阶段执行常量折叠,解语法糖等操作。