一、类的加载时机
当程序要使用某个类时,如果该类还未被加载到内存中,系统会通过加载,连接,初始化三步来实现对这个类进行初始化:
(1)加载:
就是指将class文件读入内存,并为之创建一个Class对象。任何类被使用时系统都会建立一个Class对象。
(2)连接:
验证是否有正确的内部结构,并和其他类协调一致,准备 负责为类的静态成员分配内存,并设置默认初始化值
(3)初始化:
初始化成员变量等等
类的加载时机分为以下几种:
(1)创建类的实例
(2)访问类的静态变量,或者为静态变量赋值
(3)调用类的静态方法
(4)初始化某个类的子类
(5)使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
二、什么是反射
众所周知,创建一个对象分为三个阶段:
1.源文件阶段 .java的文件
2.字节码阶段 .class
3.创建对象阶段 new 对象名称
java的反射机制就是指:
在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个方法和属性;
这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。想要使用反射,就必须得要获取字节码文件
用途
在日常的第三方应用开发过程中,经常会遇到某个类的某个成员变量、方法或是属性是私有的或是只对系统应用开放,这时候就可以利用Java的反射机制通过反射来获取所需的私有成员或是方法。
接下来通过代码来进行演示,首先我们创建一个Person类:
package com.zhangyi.demo;
/**
* @author zhangyi
*/
public class Person {
public String name;
public Integer age;
private String food;
//无参的构造方法
public Person() {
}
//有参的构造方法
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public void show() {
System.out.println("我是" + this.name + ",今年" + this.age + "岁,我今天吃了"+this.food);
}
private void eat(String food){
System.out.println("我今天吃了"+ food);
}
}
之前提到,想要使用反射,就必须得要获取字节码文件,获取类的字节码文件有以下三种方式,以Person类为例,通过Junit4单元测试来进行演示:
public class PersonTest {
@Test
public void test1() throws ClassNotFoundException {
/*获取类的字节码的三种方式*/
/* 第一种 Class类中静态方法forName() 一般用于读取配置文件*/
Class<?> clazz1 = Class.forName("com.zhangyi.demo.Person");
/* 第二种静态属性class 一般用于当作静态方法的锁对象*/
Class<?> clazz2 = Person.class;
/*第三种 Object类的getClass()方法 一般用于判断两个对象是否是同一个字节码文件*/
Person person = new Person();
Class<?> clazz3 = person.getClass();
/*判断三者是否相等 */
System.out.println(clazz1 == clazz2);
System.out.println(clazz2 == clazz3);
System.out.println(clazz1 == clazz3);
}
}
输出结果:
true
true
true
因此,我们可以看到,通过这三种方式创建Person类的字节码文件都是一样的。
三、反射的部分用法
(一)通过字节码创建对象
1.通过无参的构造器创建对象:
@Test
public void test2() throws Exception {
/* 1.获取字节码*/
Class<?> clazz = Class.forName("com.zhangyi.demo.Person");
/*2.调用字节码的newInstance()方法*/
Person person = (Person) clazz.newInstance();
person.name = "张仪";
person.age = 21;
//food为私有的变量,没提供get、set方法,不可直接调用,后续会通过反射机制进行暴力赋值
person.show();
}
输出结果:
我是张仪,今年21岁,今天吃了null
** 2.通过有参的构造器创建对象**
@Test
public void test3() throws Exception {
/* 1.获取字节码的构造器 clazz.getConstructor(type.class) 因为在反射阶段操作的都是字节码,不知道具体的类型,只有在创建对象的时候才去给实际参数*/
Class<?> clazz = Class.forName("com.zhangyi.demo.Person");
Constructor constructor = clazz.getConstructor(String.class, Integer.class);
/* 2.通过构造器创建对象 调用构造器的newInstance方法并传入参数*/
Person person = (Person) constructor.newInstance("张仪",21);
person.show();
}
输出结果:
我是张仪,今年21岁,今天吃了null
(二)获取字段
1.获取公共的字段:
@Test
public void test4() throws Exception{
/*1.根据反射创建对象*/
Class<?> clazz = Class.forName("com.zhangyi.demo.Person");
Person person = (Person) clazz.newInstance();
/*2.获取公共的字段*/
Field name = clazz.getField("name");
Field age = clazz.getField("age");
/*3.设置person对象的属性*/
name.set(person,"张仪");
age.set(person,21);
person.show();
}
输出结果:
我是张仪,今年21岁,今天吃了null
2.获取私有的字段
@Test
public void test5() throws Exception{
/*1.根据反射创建对象*/
Class<?> clazz = Class.forName("com.zhangyi.demo.Person");
Person person = (Person) clazz.newInstance();
/*2.获取公共的字段 并且设置person对象的相应属性*/
Field name = clazz.getField("name");
Field age = clazz.getField("age");
name.set(person,"张仪");
age.set(person,21);
/*3.通过暴力反射获取私有的字段 并且通过setAccessible方法去除私有权限 */
Field food = clazz.getDeclaredField("food");
food.setAccessible(true);
//赋值
food.set(person,"面条");
person.show();
}
输出结果:
我是张仪,今年21岁,今天吃了面条
(三)获取方法
1.获取公共的方法:
@Test
public void test6() throws Exception{
/*1.根据反射创建对象*/
Class<?> clazz = Class.forName("com.zhangyi.demo.Person");
Person person = (Person) clazz.newInstance();
/*2.获取公共的方法*/
Method show = clazz.getMethod("show");
/*3.调用invoke方法 若无参数则可以不用传值*/
show.invoke(person);
}
输出结果:
我是null,今年null岁,今天吃了null
2.获取私有的方法:
@Test
public void test7() throws Exception{
/*1.根据反射创建对象*/
Class<?> clazz = Class.forName("com.zhangyi.demo.Person");
Person person = (Person) clazz.newInstance();
/*2.通过暴力反射获取私有的方法 并且通过setAccessible方法去除私有权限 若有参数记得传参*/
Method eat = clazz.getDeclaredMethod("eat",String.class);
eat.setAccessible(true);
/*3.调用invoke方法 若无参数则可以不用传值,有则记得传值*/
eat.invoke(person,"面条");
}
输出结果:
我今天吃了面条
(四)经典小案例
需求:如何绕过数组泛型检测
科普一个小知识点:java的泛型又被称之为假泛型,java的泛型仅在编译期有效,在运行期则会被擦除,即编译的.class文件中并没有泛型,也就是说所有的泛型参数类型在编译后都会被清除掉。这就是所谓的类型擦除。感兴趣可以自行编译一下查看class文件。
需求实现代码如下:
@Test
public void test8() throws Exception {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
// list.add("添加字符串会报错");
/*获取ArrayList的字节码*/
Class<?> clazz = Class.forName("java.util.ArrayList");
/*获取add方法*/
Method add = clazz.getMethod("add", Object.class);
/*此时调用方法add 给list添加字符串*/
add.invoke(list,"此时添加字符串不会报错");
System.out.println(list);
}
输出结果:
[1, 此时添加字符串不会报错]
以上就是我的部分见解。