目录
前言
11.21Java-类加载和1.22Java-反射以及之后的1.23Java-JVM关系紧密,这三章也是java重点难点,平常开发中用到的深度并不大,但是面试很重要。如果想要深度了解底层,请认真学习、思考。如果只是开发中使用而不深入了解,前几章节可以简单浏览,重点学习1.22.5反射常用方法。
本章借鉴B站韩顺平老师的反射课程。
1.22.1引入
在学习反射前,我们先思考一个问题,为什么有反射?
为了解决这个问题,我们从程序本身入手,来讨论这个问题。
首先,我们有个需求:定义student类,类里面有study方法,创建student实例对象并调用study方法。
请问有几种方法可以实现上述需求?
方法一:
public class Student{
public void study(){
sout("学习中"); //sout是System.out.println快捷键
}
}
在main函数中
public void main(String[] args){
Student student=new Student();
student.study();
}
方法一是我们常用的方法,但是在实际中很多的框架用的是外部配置文件的方式,这样的优点是,可以不修改源码的情况下来操作程序。试想,在大型项目已运行的时候,我们难道因为要修改一个student的study方法,就停掉整个服务吗?显然不可能。但是外部配置文件就不一样了,我们在配置文件上修改,之后只需要替换配置文件就可以,不会影响整个服务的运行。
那么问题又来了,这和反射有什么关系?先不急,我们只是抛出个引子,说明刚才的需求,我们也可以通过配置文件来实现。
方法二
文件结构
student.properties配置文件
Classpath=com.lb.reflect.Student
method=study
Reflection方法
package com.lb.reflect;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
public class Reflection {
public static void main(String[] args) throws IOException {
//通过properties来读取配置文件
Properties properties=new Properties();
//加载文件目录
properties.load(new FileInputStream("src\\student.properties"));
//获取配置文件中对应的值
String classpath = properties.get("Classpath").toString();
String method=properties.get("method").toString();
System.out.println(classpath);
System.out.println(method);
}
}
输出结果
properties:加载配置文件
我们已经通过配置文件获取到类的信息,之后我们尝试创建对象。现在问题又来了,怎么创建?
既然 classpath 里面是 com.lb.reflect.Student method 里面是 study 难道是new classpath();
那肯定不对,classpath是个string类,它只是里面保存的是类的全限定名。
那么如果我写的是类的全限定名,发现没有报错
但是
Student student = new com.lb.reflect.Student();等价于
Student student = new Student();
你只不过是把它的全限定名写了出来,这俩没区别。
所以我们现阶段的知识,根本无法实现利用配置文件去操作类和程序。但是很多框架又需要我们通过配置文件的来控制程序。ocp原则,开闭原则
因此,引入了反射机制来解决问题,我们先来简单试试。
public class Reflection {
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
//通过properties来读取配置文件
Properties properties=new Properties();
//加载文件目录
properties.load(new FileInputStream("src\\student.properties"));
//获取配置文件中对应的值
String classpath = properties.get("Classpath").toString();
String method=properties.get("method").toString();
System.out.println(classpath);
System.out.println(method);
/* classpath 里面是 com.lb.reflect.Student
method 里面是 study
*/
//之后创建对象
//1.加载类,通过类全限定名返回一个Class类型的对象cla
Class cla = Class.forName(classpath);
//2.通过cla得到加载类 com.lb.reflect.Student 的对象实例
Student student = (Student)cla.newInstance();
//3.
}
}
反射的步骤
我们先加载类,获得cla类,之后就要通过这个cla类获取com.lb.reflect.Student 的对象实例student。
注意,这个地方会有一个理解上的误区:我既然已经获取到了student,直接student.study()不就结束了,so 简单!!
//之后创建对象
//1.加载类,返回一个Class类型的对象cla
Class cla = Class.forName(classpath);
//2.通过cla得到加载类 com.lb.reflect.Student 的对象实例
Student student = (Student)cla.newInstance();
//3.调用方法
student.study();
But,停下来,我们思考一个问题
1.现在student类里面有study()方法,你怎么确定以后study()方法不会被删除呢?
2. 前阶段我们累死累活获取的
String classpath = properties.get("Classpath").toString();
String method=properties.get("method").toString();
里面的method是干吗用的?
如果这里直接用student.study(),就写死了,student只会调用study()
因此正确的写法是
//之后创建对象 //1.加载类,返回一个Class类型的对象cla Class cla = Class.forName(classpath); //2.通过cla得到加载类 com.lb.reflect.Student 的对象实例 Student student = (Student)cla.newInstance(); //3.通过cla得到 加载的类 com.lb.reflect.Student 的 method 的方法对象 Method method1 = cla.getMethod(method); //4.通过调用method1 method1.invoke(student); //传统方法:对象.方法() 反射机制 方法.invoke(对象) }
运行结果
反射全程没有使用student类和方法,实现了解耦。
1.22.2反射机制
1.反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息(比如成员变量,构造器,成员方法等等),并能操作对象的属性及方法。反射在设计模式和框架底层都会用到。
2.加载完类之后,在堆中就产生了一个Class类型的对象(1)(一个类只有一个Class对象),这个对象包含了类的完整结构信息。通过这个对象得到类的结构(2)。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,形象的称之为:反射。
解释
(1)这里建议大家先去学习1.21Java-类加载 :
加载(Loading):classpath、jar包、网络、某个磁盘位置下的类的class二进制字节流读进来,在内存中生成一个代表这个类的java.lang.Class对象放入元空间(JDK1.8前叫方法区、永久代),此阶段程序员可以干预,自定义类加载器
注意:没有new出来之前,加载的时候ClassLoader会在java堆中生成一个代表这个类的Class对象,作为访问方法区(元空间)中这些数据的入口。new一个类的话,类的引用会在栈里,类的实例会在堆里。
这里的Class类,名字就叫Class。比如一个人,他的名字是“名字”。
(2)
1.我们先编写代码,如代码阶段的Cat类。
2.编写完成后,通过Javac编译成.class文件(字节码文件)
3.new 对象时,会导致类(.class这种/字节码文件)的加载,所以当我们在new Cat时,类加载器会将字节码文件里的成员变量,构造器,方法等信息,在堆中生成对应的Class对象。成员变量当成一种对象,这个对象的类型是Field,同理构造器当成一种对象,对象类型是Constructor等。
4.Cat对象知道自己属于堆中的哪个Class对象。
疑惑
1.这个类的java.lang.Class对象不是会放入元空间(方法区)为什么会在堆中?
答:在JDK1.8完全废除永久代之前的JDK版本中,方法区(永久代)是一个逻辑分区,实际是java堆的一部分,但是有Non-heap的标记,以便区分。
2.什么是类的引用?什么是类的实例?
1.22.2.1反射机制可以完成哪些
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时得到任意一个类所具有的成员变量和方法
- 在运行时调用任意一个对象的成员变量和方法
- 生成动态代理
1.22.2.2反射机制相关的类
- java.lang.Class:代表一个类,Class对象表示某个类加载后在堆中的对象
- java.lang.reflect.Method:代表类的方法
- java.lang.reflect.Field:代表类的成员变量
- java.lang.reflecnstructor代表类的构造器
我们接着1.22.1.1引入章节中的需求问题,来熟悉一下这些类。
Student类
public class Student {
private String name;
public void study(){
String s="学习中";
System.out.println(s);
}
public void Student(){
System.out.println("无参的构造方法");
}
public void Student(String name ){
System.out.println("有参的构造方法");
}
}
Reflection方法
public class Reflection {
public static void main(String[] args) throws Exception{
//通过properties来读取配置文件
Properties properties=new Properties();
//加载文件目录
properties.load(new FileInputStream("src\\student.properties"));
//获取配置文件中对应的值
String classpath = properties.get("Classpath").toString();
String method=properties.get("method").toString();
// System.out.println(classpath);
// System.out.println(method);
/* classpath 里面是 com.lb.reflect.Student
method 里面是 study
*/
//之后创建对象
//1.加载类,返回一个Class类型的对象cla
Class cla = Class.forName(classpath);
//2.通过cla得到加载类 com.lb.reflect.Student 的对象实例
Student student = (Student)cla.newInstance();
//3.通过cla得到 加载的类 com.lb.reflect.Student 的 method 的方法对象
Method method1 = cla.getMethod(method);
//4.通过调用method1
method1.invoke(student); //传统方法:对象.方法() 反射机制 方法.invoke(对象)
--------------------------------前面不用看,和之前一样---------------------------------
//通过
Field field = cla.getField("name");
}
这里,我们通过getField()方法尝试获取student的私有成员变量,报错,NoSuchFieldException:
1.本身就没有该Field;2.有该Field,但是该Field是使用private修饰的
所以我们不能通过getField方法来获取私有成员变量(想要获取该Field的时候,需要使用getDeclaredField这个方法。)具体的我们后面讲解。
之后我们也尝试获取构造器
//获得有参和无参的构造方法
Constructor constructor = cla.getConstructor();
System.out.println(constructor);
Constructor constructor1 = cla.getConstructor(String.class);
//String.class 表示 String类的class对象
System.out.println(constructor1);
但是却也报错
*1.22.3 反射机制-Class类
1.22.3.1 Class类
!我们先给出七个结论:
- Class也是类,继承Object类
- Class类对象不是new出来的,而是由系统创建的
- 对于某个类的Class类对象,在内存中只有一份,因为类只加载一次
- 每个类的实例都会记得自己是由哪个Class 实例所生成
- 通过Class可以完整地得到一个类的完整结构,通过一系列API
- Class对象是存放在堆的
- 类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据(包括方法代码,变量名,方法名,访问权限等等)
验证:
1.Class也是类,继承Object类
我们来看看这个Class类的详细信息,我们打开Class的 简图,上面的都是它的父类
也就是实现了这些接口
我们再思考一个问题,接口是否父类也是Object,如果接口是Object那么,这个Class的父类也是Object,这和我们之前的猜想是一致的。
我们先搞个小插曲——验证接口的父类是不是Object
为什么我们要单独验证这个呢,
这里我们继续借用韩老师的课件,看到韩老师给出的Class的简图中,表明了Class继承了Object类
可以得出结论:Class是类,同样继承于Object
2.Class类对象不是new出来的,而是系统创建的
前面提及到,Class类对象由类加载器加载到堆中,确切来说是类加载器中的loadClass()方法
public class Class01 {
public static void main(String[] args) throws Exception {
//查看class类对应的类图
断点 Class<?> aClass = Class.forName("com.lb.reflect.Student");
}
}
我们在 Class<?> aClass = Class.forName("com.lb.reflect.Student"); 这行加断点然后DEBGU。会发现,最后追寻到ClassLoader里面的loadClass方法
可以得出结论:Class类对象是由系统创建的。
3.对于某个类的Class类对象,在内存中只有一份,因为类只加载一次
法一:这里我们在前面加入
Student student = new Student();
public class Class01 {
public static void main(String[] args) throws Exception {
//查看class类对应的类图
Student student = new Student();
断点 Class<?> aClass = Class.forName("com.lb.reflect.Student");
}
}
我们再去DEBUG,在类加载中,程序执行到getClassLoader()并return caller.getClassLoader(),之后就返回到Class和CLass01了。没有执行ClassLoader里面的loadClass方法。
我们可以分析这个getClassLoader()方法:
caller就是个标记,当没有加载过相应的Class类时,caller就为null,不为null就返回已经加载过的Class类。而且这里的判断只有一个条件,就是if(是否为null),它没有进行其余的条件判断,也可以说明,类加载只加载一次。
法二:
Class<?> class1 = Class.forName("com.lb.reflect.Student");
Class<?> class2 = Class.forName("com.lb.reflect.Student");
System.out.println(class1.hashCode());
System.out.println(class2.hashCode());
我们创建两个Student类的Class对象,发现它们的地址相同。
可以得出结论:同一个类的Class类对象只加载一次。
4.每个类的实例都会记得自己是由哪个Class 实例所生成
这里涉及JVM底层-OOP-KLASS模型,之后在1.23Java-JVM更新
//牙视频深入理解类加载机制等1.21和1.22章节更新完毕后再计划
5.通过Class可以完整地得到一个类的完整结构,(利用API)
反射提供了很多方法,详见《1.22.5反射常用方法》
6.Class对象是存放在堆的
7.类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据(包括方法代码,变量名,方法名,访问权限等等)
我们把6和7放在一起,方法区主要是存放类字节码文件二进制数据,如果看过《2.21Java-类加载器》(里面讲了类字节码文件是什么),这里可能会有点犯迷糊,方法区存放的类字节码文件二进制数据,类字节码文件里面存的也是二进制数据啊?这和方法区一样?
这里我们需要抠字眼
这里方法区存入的是 类的字节码 这个文件 的二进制数据形式
类的字节码文件 里面是 魔术因子+二进制数据
方法区引用Class
方法区里面的类字节码二进制数据也称为元数据。
方法区里面是二进制数据,而堆中里产生的Class类对象是一种数据结构,这种数据结构可以把成员变量、构造器等映射/当成对象来进行操作。操作数据结构远远比操作二进制数据方便。
回顾1.22.2反射机制里面有一段:
3.new 对象时,会导致类(.class这种/字节码文件)的加载,所以当我们在new Cat时,类加载器会将字节码文件里的成员变量,构造器,方法等信息,在堆中生成对应的Class对象。成员变量当成一种对象,这个对象的类型是Field,同理构造器当成一种对象,对象类型是Constructor等。
1.22.3.2Class类的方法
结合代码来了解一些Class的方法。
Student类
package com.lb.reflect;
import java.awt.*;
public class Student {
public String name="tom";
public int age=20;
public String sex="man";
private String parent="lili and jim";
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
'}';
}
}
Class01主函数
package com.lb.reflect;
import java.lang.reflect.Field;
public class Class01 {
public static void main(String[] args) throws Exception {
String classpath="com.lb.reflect.Student";
Class<?> cla= Class.forName(classpath);
//1.获取Stusent类 对应的Class对象
System.out.println("-------1-----");
//显示是哪一个类的Class对象
System.out.println(cla);
//显示class的运行类型
System.out.println(cla.getClass());
//2.得到包名
System.out.println("-------2-----");
System.out.println(cla.getPackage().getName());
//3.得到全类名
System.out.println("-------3-----");
System.out.println(cla.getName());
//4.通过cla创建对象实例
System.out.println("-------4-----");
Student student= (Student) cla.newInstance(); //这里获得的就是真真正正的Student对象
System.out.println(student);
//5.通过反射获取属性 结合前面的讲解,Filed类主要保存属性/成员变量
System.out.println("-------5-----");
Field name = cla.getField("name");
System.out.println(name.get(student));
//注意私有属性无法通过getField获取,会报错
Field parent = cla.getField("parent");
System.out.println(parent.get(student));
//6.通过反射给属性赋值
System.out.println("-------6-----");
name.set(student,"jack");
System.out.println(name.get(student));
//7.遍历得到所有的属性(字段/成员变量)
System.out.println("-------7-----");
for (Field field : cla.getFields()) {
//属性的名称
System.out.println(field.getName());
}
}
}
1.22.3.3获取Class类对象的方式
- Class.ForName() //代码/编译阶段
- 类.class() //Class类阶段
- 对象名.getClass() //运行阶段
- 类加载器得到class对象 //类加载阶段
1.22.3.3.1 Class.ForName()
适用条件:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException,
实例:Class cls1 =Class.forName( "com.lb.reflect.Student” );
应用场景:多用于配置文件,读取类全路径,加载类。
1.22.3.3.2 类.class()
适用条件:若已知具体的类,通过类的class 获取,该方式最为安全可靠,程序性能
实例:Class cls2 = Cat.class;
应用场景:多用于参数传递,比如通过反射得到对应构造器对象.
1.22.3.3.3 对象名.getClass()
适用条件:已知某个类的实例,调用该实例的getClass()方法获取Class对象,
实例:Class clazz =对象.getClass();
应用场景:通过创建好的对象,获取Class对象.
1.22.3.3.4 类加载器得到class对象
ClassLoader cl =对象.getClass().getClassLoader();
Class class = cl.loadClass(“类的全类名”);
1.22.4类加载
反射机制可以实现Java类的动态加载。
静态加载:编译时加载相关的类,没有就报错,依赖性强。
动态加载:运行时加载需要的类,如果运行时不使用该类,则不报错,降低了依赖性
类加载的时机:
- 创建对象时(new)
- 子类被加载时
- 调用类中的静态成员时
- 通过反射
类加载的过程
1.22.5反射常用方法
获取成员变量对象的方法
方法名 | 说明 |
---|---|
Field[] getFields() | 返回所有公共成员变量对象的数组 |
Field[] getDeclaredFields() | 返回所有成员变量对象的数组 |
Field getField(String name) | 返回单个公共成员变量对象 |
Field getDeclaredField(String name) | 返回单个成员变量对象 |
void set(Object obj,Object value) | 给obj对象的成员变量赋值为value |
如果属性为私有,需要设置
X.setAccessible(true);
Field f=class对象.getDeclared(属性名);
f.setAccessible(true);
取成员方法对象的方法
方法名 | 说明 |
---|---|
Method[] getMethods() | 返回所有公共成员方法对象的数组,包括继承的 |
Method[] getDeclaredMethods() | 返回所有成员方法对象的数组,不包括继承的 |
Method getMethod(String name, Class<?>... parameterTypes) | 返回单个公共成员方法对象 |
Method getDeclaredMethod(String name, Class<?>... parameterTypes) | 返回单个成员方法对象 |
Object invoke(Object obj,Object... args) | 调用obj对象的成员方法,参数是args,返回值是Object类型 |
获取构造方法对象
方法名 | 说明 |
---|---|
Constructor<?>[ ] getConstructors() | 返回所有公共构造方法对象的数组 |
Constructor<?>[ ] getDeclaredConstructors() | 返回所有构造方法对象的数组 |
Constructor<T> getConstructor(Class<?>... parameterTypes) | 返回单个公共构造方法对象 |
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) | 返回单个构造方法对象 |
如果属性、构造器为私有的,需要加
X.setAccessible(true) ;X:属性、构造器
破坏了封装性。
创建对象的方法
方法名 | 说明 |
---|---|
T newInstance(Object...initargs) | 根据指定的构造方法创建对象 |
1.22.6
1.22.6.1 创建对象的过程
new一下(其实还有很多途径,比如反射、反序列化、clone等,这里拿最简单的new来讲):
①构造对象
首先main线程会在栈中申请一个属于自己的栈空间,然后我们调用main方法的时候,会生成一个main方法的栈帧,然后执行new Man(),
这里会根据Man类的元信息先确定对象的大小,
如果找不到元信息意味着类还没有被加载,此时需要类加载操作。
②类加载
重点回顾
栈内存 :存放基本类型数据和对象的引用变量,数据可以直接拿来访问,速度比堆快。
堆内存 :存放创建的对象和数组,会由java虚拟机的自动垃圾回收来管理(GC),创建一个对象放入堆内的同时也会在栈中创建一个指向该对象堆内存中的地址引用变量,下面说的对象就是存在该内存中。
一个java文件会在编译期间被初始化生成.class字节码文件,这个文件后面会被类加载器加载到内存。
类加载器 ClassLoader
加载class文件时,会把类里的一些数值常量、方法、类信息等加载到内存中,称之为类的元数据,最终目的是为了生成一个Class对象用来描述类,这个对象会被保存在.class文件里
在虚拟机遇到一条new的指令时,会去检查一遍在静态常量池中能否定位到一个类的符号引用 (就这个类的路径+名字),并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果不是第一次使用,那必须先执行相应的类加载过程,这个过程由类加载器来完成。
这个Class对象就像是镜子一样:
- 可以描述对应的类:描述该class的所有属性,比如类名、方法、接口等
- 可以生成对象,通过newInstance()
①构造对象-续
当类元信息被加载之后,我们就可以通过类元信息来确定对象信息和需要申请的内存大小。
然后在JVM堆里申请一块内存区域并构建对象,同时对Man对象成员变量信息,并赋予默认值。
③初始化对象
执行对象内部的初始化方法,初始化成员变量值,即执行对象的构造方法(或调用父类的构造方法进行赋值)
④引用对象
实例化对象之后,将栈中的Animal对象引用地址指向Cat对象在堆内存中的地址