💁 个人主页:黄小黄的博客主页
❤️ 支持我:👍 点赞 🌷 收藏 🤘关注
🎏 格言:All miracles start from sometime somewhere, make it right now.
本文来自专栏:JavaSE从入门到精通
文章目录
1 类加载
1.1 动态加载与静态加载
通过上一节内容,我们了解到,反射机制是Java实现动态语言的关键,也就是通过反射实现类动态加载。
现在,我们需要了解两个概念:
- 静态加载: 编译时加载相关的类,如果没有则报错,依赖性强;
- 动态加载: 运行时加载需要的类,如果运行时没有使用到该类,即使不存在该类,也不报错,降低了依赖性。
是不是还是有些懵?不用慌,我们来看下面的例子(假设没有写好Cat类的代码):静态加载的方式
import java.util.Scanner;
/**
* @author 兴趣使然黄小黄
* @version 1.0
* 演示静态加载
*/
public class ClassLoadTest {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入1 or 2: ");
String key = scanner.next();
switch (key){
case "1":
Cat cat = new Cat();
cat.hi();
break;
case "2":
System.out.println("程序运行结束!");
break;
default:
System.out.println("输入有误!");
break;
}
}
}
分析上述代码可知:如果用户键入的值是2或者是其他,则压根使用不到Cat类,但是,该方式是静态加载的,所以,程序运行直接报错,无论是否使用到该类。
接着,我们尝试使用 动态加载的方式 改进程序:
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Scanner;
/**
* @author 兴趣使然黄小黄
* @version 1.0
* 演示动态加载
*/
public class ClassLoadTest {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入1 or 2: ");
String key = scanner.next();
switch (key){
case "1":
//动态加载
Class<?> cls = Class.forName("reflection.Cat");
Object cat = cls.newInstance();
Method hi = cls.getMethod("hi");
hi.invoke(cat);
break;
case "2":
System.out.println("程序运行结束!");
break;
default:
System.out.println("输入有误!");
break;
}
}
}
当用户输入2 或者其他的时候,程序可以正常运行,动态加载降低了依赖性!
1.2 类加载时机与类加载流程
类何时被加载?
- new时,即创建对象时。(静态加载)
- 当子类被加载时,父类也被加载。(静态加载)
- 调用类中的静态成员时。(静态加载)
- 通过反射。(动态加载)
类加载的流程
回到上一节我们看过的图:
类加载是通过类加载器实现的,不仅会生成Class类对象,还会在方法区生成对应类的字节码二进制数据/元数据。
这次,我们展开讲讲类加载究竟经历了哪些过程,看下图:类加载过程图
- 在Loading加载阶段,将类的class文件读入内存,并为之创建一个java.lang.Class对象,此过程由类加载器完成;
- 在Linking链接阶段,将类的二进制数据合并到了JRE中,实际上是进入了一个可运行的状态;
- 而在初始化阶段,JVM负责对类进行初始化,这里主要指静态成员。
类加载后,内存布局情况如下:
1.3 类加载的五个阶段
1.3.1 加载阶段
JVM在该阶段主要目的是将字节码从不同的数据源(可能是class文件、也可能是jar包,甚至网络)转化为二进制字节流加载到内存中, 并生成一个代表该类的java.lang.Class对象。
1.3.2 链接阶段(1)验证阶段
验证阶段的目的是为了 确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
包括:文件格式验证、元数据验证、字节码验证、符号引用验证。
可以考虑使用 -Xverify:one
参数来关闭大部分类验证措施,缩短虚拟机类加载的时间。
1.3.3 链接阶段(2)准备阶段
JVM 会在该阶段 对静态变量分配内存并进行默认初始化(对应数据类型默认初始值,如0、0L、null、false等)。这些变量所使用的内存都将在方法区中进行分配。
示例代码:
class T{
public int n1 = 10;
public static int n2 = 20;
public static final int n3 = 30;
}
- n1是实例属性,不是静态变量,因此在准备阶段,不会分配内存;
- n2是静态变量,分配内存,n2初始化为0,而不是20。值分配成20是在初始化阶段做的事情;
- n3是static final修饰的,是常量,与静态变量不同,会一次性分配。(一旦赋值,就不变化),所以n3 = 30。
1.3.4 链接阶段(3)解析阶段
即虚拟机将常量池内的符号引用替换为直接引用的过程。
可以理解成程序的逻辑地址和内存的物理地址
1.3.5 初始化阶段
到了初始化阶段,才真正开始执行类中定义的 Java程序代码,此阶段是执行 < clinit >() 方法的过程。
何为 < clinit >() 方法?
< clinit >() 方法是由编译器 按语句在源文件中出现的顺序,依次自动收集类中所有 静态变量的赋值动作和静态代码块中的语句,并进行合并。
演示类加载的初始化阶段:
/**
* @author 兴趣使然黄小黄
* @version 1.0
* 演示类加载的初始化阶段
*/
public class ClassLoadTest {
public static void main(String[] args){
System.out.println(A.num);
}
}
class A{
static {
System.out.println("A的静态代码块被执行");
num = 500;
}
static int num = 100;
public A(){
System.out.println("A的构造器被执行");
}
}
过程分析:
- 加载A类,并生成A的Class对象;
- 链接阶段:num = 0;
- 初始化阶段:依次自动收集静态变量的赋值动作和静态代码块中的语句,并合并, 等价于如下代码:
clinit(){
System.out.println("A的静态代码块被执行");
num = 500;
num = 100;
}
补充知识: 虚拟机 会保证一个类的 < clinit >() 方法在多线程的环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 < clinit >() 方法,其他线程都需要阻塞等待,直到活动线程执行 < clinit >() 方法完毕。
保证了某个类在内存中只有一份 Class对象。
2 反射暴破
2.1 通过反射创建对象
有如下两种方式:
- 调用类中的public修饰的无参构造器;
- 调用类中的指定构造器。
Class类包含了相关的方法:
方法名 | 作用 |
---|---|
newInstance | 调用类中的无参构造器,获取相应类的对象 |
getConstructor(Class…clazz) | 根据参数列表,获取相应的public构造器对象 |
getDecalaredConstructor(Class…clazz) | 根据参数列表,获取对应的所有构造器对象 |
Constructor类相关方法:
方法名 | 作用 |
---|---|
setAccessible | 暴破 |
newInstance(Object…obj) | 调用构造器 |
在上一节中,我们已经尝试过使用第一种方式:调用类中的public修饰的无参构造器 创建过对象。本文不再赘述。
调用类中指定构造器实例:
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* @author 兴趣使然黄小黄
* @version 1.0
* 演示使用特定构造器创建对象
*/
public class CreateInstance {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//加载Class对象,并获取指定的构造器对象
Class<?> cls = Class.forName("reflection.Cat");
Constructor<?> strConstructor = cls.getConstructor(String.class);
//创建Cat对象实例
Object o = strConstructor.newInstance("猫咪");
System.out.println(o);
}
}
class Cat {
public String name = "name属性";
public Cat(){}
public Cat(String name){
this.name = name;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
'}';
}
}
但是,当我们通过这样的方式获取私有构造器、属性或者方法对象时,再操作,则会报异常。
下面我们通过反射暴破的方式,来避开private限制
2.2 反射暴破访问私有构造器
//加载Class对象,并获取指定的构造器对象
Class<?> cls = Class.forName("reflection.Cat");
//得到private构造器对象
Constructor<?> strConstructor = cls.getDeclaredConstructor(String.class);
//创建Cat对象实例
//暴力破解
strConstructor.setAccessible(true);
Object o = strConstructor.newInstance("猫咪");
2.3 反射暴破操作属性
反射暴破操作属性的步骤如下:
- 根据属性名获取Field对象
Field f = clazz对象.getDeclareField(属性名);
- 暴破
f.setAccessible(true);
- 访问
f.set(o,值);
f.get(o);
- 如果是静态属性,则set和get中的参数o,可以写成null
实例如下: 具体见代码注释
import java.lang.reflect.Field;
/**
* @author 兴趣使然黄小黄
* @version 1.0
* 演示反射操作属性
*/
public class AccessProperty {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//1.得到Student类对应的对象
Class<?> stuClass = Class.forName("reflection.Student");
//2.创建对象
Object o = stuClass.newInstance();
//3.使用反射得到age属性对象
Field age = stuClass.getField("age");
age.set(o, 20);//通过反射设置属性
System.out.println(o);
//4.使用反射暴破操作私有静态属性name
Field name = stuClass.getDeclaredField("name");
name.setAccessible(true);
name.set(o, "黄小黄");
System.out.println(o);
}
}
class Student{
public int age;
private static String name;
public Student(){
}
@Override
public String toString() {
return "Student{" +
"age=" + age + ", name=" + name +
'}';
}
}
2.4 反射暴破操作方法
反射暴破操作方法的步骤如下:
- 根据方法名和参数列表获取Method方法对象:
Method m = clazz.getDeclaredMethod(方法名,XX.class);
- 获取对象
Object o = clazz.newInstance();
- 暴破
m.setAccessible(true);
- 访问
Object returnValue = m.invoke(o,实参列表);
- 注意,如果是静态方法,则invoke参数o,可以写成null。
实例如下: 具体见代码注释
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @author 兴趣使然黄小黄
* @version 1.0
* 演示通过反射调用方法
*/
public class AccessibleMethod {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException,
IllegalAccessException, NoSuchMethodException, InvocationTargetException {
//1.得到Teacher类对应的Class对象
Class<?> teacherClass = Class.forName("reflection.Teacher");
//2.创建对象
Object o = teacherClass.newInstance();
//3.调用方法 public
Method hi = teacherClass.getMethod("hi", String.class);
hi.invoke(o, "黄小黄");
//4.调用方法 private
Method say = teacherClass.getDeclaredMethod("say", int.class, String.class, char.class);
say.setAccessible(true);//暴破
Object returnString = say.invoke(null, 1, "黄小黄", 'Y');
System.out.println(returnString);
}
}
class Teacher{
public int age;
private static String name;
public Teacher(){
}
private static String say(int n, String s, char c){
return n + " " + s + " " + c;
}
public void hi(String s){
System.out.println("hi " + s);
}
}
在反射中,如果方法有返回值,统一返回为Object,编译类型为Object,运行类型还是方法的return类型
写在最后
🌟以上便是本文的全部内容啦,本专栏到此也就告一段落啦,如果文章对你有所帮助,麻烦动动小手点个赞 + 关注,非常感谢 ❤️ ❤️ ❤️ !
如果有问题,欢迎私信或者评论区!
共勉:“你间歇性的努力和蒙混过日子,都是对之前努力的清零。”