反射机制就是将类的各个组成部分(属性,方法,构造器)封装为其他对象
相关的核心类
注意这个源代码阶段并不是我们的.java文件而是我们的class文件
所以我们知道有三个阶段,分别是源码阶段,类对象阶段,和运行时阶段。源码阶段的源码肯定是全局唯一存在的。那么Class对象是用这个源码来用ClassLoader进行加载的,所以应该也是全局唯一的,但是后面创建对象用的new,new 出来的对象肯定就不是全局唯一的了。
因为我们之前说过,我们对于反射,我们是需要获取到类对象的属性方法和构造器然后我们获取到了之后就可以很容易的去使用他们。所以我们需要获取Class对象。
Class对象获取的三种方式分别对应了前面我们说过的Java代码在计算机中经历的三个阶段。源代码阶段,Class对象阶段,运行时阶段。我们直接来一个代码来演示一下。
首先我们先创建一个类Person
package cn.cuit.bo.entity;
public class Person {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Person(){
System.out.println("构造方法执行!");
}
}
然后我们现在直接来一个案例等下解释一波
package cn.cuit.bo;
import cn.cuit.bo.entity.Person;
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
// 源代码阶段获取类加载器
Class<?> c1 = Class.forName("cn.cuit.bo.entity.Person");
System.out.println(c1.hashCode());
// 类对象阶段加载
Class<Person> c2 = Person.class;
System.out.println(c2.hashCode());
// 运行时阶段加载
Person person = new Person();
Class<? extends Person> c3 = person.getClass();
System.out.println(c3.hashCode());
}
}
首先我们来看看代码啊,首先我们第一个阶段,我们在这里写的时候并没有引入这个包,或者说这个类,我们完全就不知道这个类是哪一个,我们就直接用了Class.forName也就是我们通过这个类的包路径来找到源代码,并且加载进内存然后转化为一个对象。然后看第二阶段,这个时候我们用到了这个类对象,也就是类名的哪个Person对象然后.class,这个时候其实我们是需要导包的,也就是我们知道了我们正在操作的那个类是哪一个,然后最后一个运行时阶段加载,很简单我们的对象有一个方法,.getClass()方法可以很容易获得对象的类。
然后我们来看看运行的结果
我们可以看到都获取成功了,并且无论在哪一个阶段进行获取,我们获得到的Class对象的hashCode都是一样的。所以这也证明了Class对象贯穿整个程序周期,是唯一存在的。
获取成员变量对象
方法名称 | 用法 |
---|---|
newInstance | 这个方法调用就类似我们拿到了这个构造器,然后我们执行这个构造器初始化一个对象出来一样。记住这个方法后面的括号里面可以加上参数,如果获取到的构造器是一个有参数的构造器,那么就必须加上构造器里面的参数才可以正常调用newInstance的方法 |
setAccessiable(bool accessable) | 这个方法用来设置是否打破权限进行调用,如果我们现在利用getDeclaredConstructor方法获取到一个非公有的构造方法,如果我们立马对其进行调用,那么会失败,因为权限是不够的。所以我们需要设置是否打破权限进行调用,那么我们设置了true之后那么就可以进行调用了,所以对于私有的构造器我们可以利用反射机制进行调用 |
getModifiers() | 该方法返回的是构造器的权限访问字,返回的是int类型,返回的数字对应的权限如下private 2 protected 4 public 1 无修饰符 0 |
getParameterCount | 返回构造器的参数个数 |
getParameterTypes | 返回构造器参数类型数组 |
方法名称 | 用处 |
---|---|
getName | 获取属性名称 |
getModifiers | 获取当前属性的访问权限返回值也是int,和前面的一样 |
getType() | 获取属性的类型返回值是一个Class对象 |
set(Object obj, Object val) | 给对象obj的这个属性赋值为val,使用这个的时候权限不足会抛出异常,所以需要配合是否打破权限进行 |
setAccessible(bool accessible) | 是否打破权限访问 |
相关方法
方法定义 | 方法使用 |
---|---|
getModifiers | 获取当前方法的返回权限【int】 |
getName() | 得到方法名称 |
getParameterCount() | 得到方法的参数个数 |
getParameterTypes() | 按照顺序获取方法的参数类型,返回Class的数组 |
getReturnType() | 得到返回值的类型 |
setAccessible(boolean flag) | 是否打破访问权限 |
invoke(Object obj, Object… args) | 执行obj对象中的这个方法,并且如果有参数需要传入这个参数 |
我们可以思考一下,为什么我们的方法重写里面返回值没有被纳入考虑范围呢?想一下,因为我们利用反射的时候只需要方法名称和参数就可以进行一个调用了,也就确定了一个方法,如果要执行这个方法和返回值压根不沾边,所以返回值没有纳入。
注解就是对应用程序的某一个部分的特殊说明,这个说明只针对关注这个说明的程序,如果其他程序不关注这个说明,那么注解对于这个程序来说是无效的。可以理解为一个标签
package cn.cuit.bo.annotation;
public @interface TestAnnotation {
}
作用在注解上面的注解就是元注解
标记这个注解的作用目标
默认来说注解都是可以加在任何的地方的,但是如果想让注解只能加到某些特殊的地方那么就需要用到target了。
@Target(value = {})
这里面的参数是ElementType的枚举类型,常用的有
这几种
Retention的英文意思是保留期的意思。当@Retention应用到一个注解上的时候,它解释说明这个注解的过期时间
过期为
名称 | 时间 |
---|---|
RetentionPolicy.SOURCE | 只有在源码阶段保留,其他时候被丢弃 |
RetentionPolicy.CLASS | 注解制备保留到编译进行的时候,不会被加载到JVM中 |
RetentionPolicy.RUNTIME | 注解可以保留到程序运行的时候,会被加载到JVM中 |
顾名思义,这个元注解是和文档有关系的,用来生成文档的。
是继承的意思,但是不是注解可以被继承的意思,意思是,注解到了一个类上,如果这个类有子类,那么子类也有注解。
用来标记一个注解是否可以多次出现
举一个例子
可以看到上面有一个Person注解上面申明了一个@Repeatable注解并且value是Persons这个注解的字节码,这个表示我们可以看到首先Person这个注解可以出现多次,就像下面的Test类上面注释的部分,我可以写多个@Person注解,或者我可以在Persons里面定义一个person注解数组,这样我也可以写一个@Persons注解里面写多个Person注解。这个persons注解一般叫做容器注解,里面放注解并且里面自己也是一个注解。
前面说过,我们写了注解,注解是一个标签,只对对这个标签感兴趣的代码才有效,所以我们其实还需要写一些代码来提取这个标签,获取标签里面的内容才可以让这个注解产生效果。
我们的注解可以加载类上,属性上,构造器上,方法上,或者方法参数上,我们写一个小小的demo来看看
首先我们定义一个注解,因为没有加上@Target元注解,所以这个注解是可以加到任何地方的。
package cn.cuit.bo.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
String value();
}
然后我们写一个Student类,在Student类上我们在各个地方加上注解
package cn.cuit.bo.entity;
import cn.cuit.bo.annotation.TestAnnotation;
@TestAnnotation("我是加到类上的注解")
public class Student {
@TestAnnotation("我是加到属性上的注解")
String name;
@TestAnnotation("我是加到构造方法上的注解")
public Student(){}
@TestAnnotation("我是加到方法上的注解")
public void sayHello(@TestAnnotation("我是加到方法参数上的注解") String objName, String word){ System.out.println(objName ":" word); }
}
然后我们来一段测试demo
package cn.cuit.bo;
import cn.cuit.bo.annotation.TestAnnotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Test{
public static void main(String[] args) throws Exception {
// 获取到Student类的Class对象
Class> c1 = Class.forName("cn.cuit.bo.entity.Student");
/*获取类上的注解*/
TestAnnotation anno1 = c1.getDeclaredAnnotation(TestAnnotation.class);
System.out.println(anno1.value());
// Annotation[] anno1s = c1.getDeclaredAnnotations(); 获取这个类上的所有注解
/*获取构造器上的注解*/
Constructor> constructor = c1.getDeclaredConstructor();
TestAnnotation anno2 = constructor.getDeclaredAnnotation(TestAnnotation.class);
System.out.println(anno2.value());
// constructor.getDeclaredAnnotations(); 获取这个类的构造器上的所有注解
/*获取属性上的注解*/
Field f = c1.getDeclaredField("name");
TestAnnotation anno3 = f.getDeclaredAnnotation(TestAnnotation.class);
System.out.println(anno3.value());
// Annotation[] declaredAnnotations = f.getDeclaredAnnotations(); 获取这个属性上的所有注解
/*获取方法上的注解*/
Method sayHello = c1.getDeclaredMethod("sayHello", String.class, String.class);
TestAnnotation anno4 = sayHello.getDeclaredAnnotation(TestAnnotation.class);
System.out.println(anno4.value());
// sayHello.getDeclaredAnnotations(); 获取这个方法上的所有注解;
/*获取方法参数上的注解*/
Annotation[][] annoss = sayHello.getParameterAnnotations();
// 遍历
for(Annotation[] anns : annoss) {
for(Annotation ann : anns){
if (ann instanceof TestAnnotation) {
TestAnnotation ta = (TestAnnotation)ann;
System.out.println(ta.value());
}
}
}
}
}
然后我们来观察一下结果
很好啊,我们可以看到结果也是非常的符合我们的预期,所有的注解都被获取到了里面的值我们也得到了。这里顺便说一个值得注意的事情,我们的注解一定一定要设置作用时间,不然等我们获取的时候它已经超过了过期时间了。
用来加载字节码文件
Class --> getClassLoader()
ClassLoader类 --> ClassLoader.getSystemClassLoader()
方法名称 | 作用 |
---|---|
getParent() | 得到父加载器 |
Class> loadClass(String name) | 根据类的完全限定名获取字节码文件 |
Class> findClass(String name) | 一句类的完全限定名来查找内存里面的class对象 |
findLoadedClass | 查找名称为name的,已经被加载过的类,返回的结果是Class |
defineClass(String name, byte[] b, int off, int len) | 这个是用来加载类的一个方法,我们的JVM加载类的时候可以根据完全限定名来查找文件然后利用io流加载为一个byte数组,然后使用这个defineClass方法把这个byte信息注册成为一个类,并且加载到内存中 |
resolveClass(Class> c) | 链接指定的Java类 |
JVM将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(initialize)
查找并加载类的二进制数据
确保加载类信息符合JVM规范,没有安全方面的问题
为类的静态变量分类内存,并将其初始化为默认值
把虚拟机常量池中的符号引用转换为直接引用
为类的静态变量赋予正确的初始值
解析这一部分需要说明,在java中,虚拟机会为每个加载的类维护一个常量池【不同字符串常量池,这个常量池知识该类的字面值(例如类名,方法名)和符号引用的有序集合。而字符串常量池,是整个JVM共享的】这些符号(例如int a = 5,中的a)就是符号引用,解析过程就是转换为堆中的对象的相对地址
Java中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由Java应用开发人员编写的。
它用来加载Java的核心库(jre/lib/rt.jar), 是用原生C 代码来实现的,并不继承自java.lang.ClassLoader类。
加载扩展类和应用程序类加载器,并置顶他们的父类加载器,在java中获取不到。
它用来加载java的扩展库(jre/ext/*.jar)Java虚拟机的实现会提供一个扩展库目录。该类加载器再次目录里面查找并加载Java类。
它根据Java应用的类路径(CLASSPATH)来加载Java类。一般来说,Java应用的类都是由他来加载完成的。可以通过ClassLoader.getSystemClassLoader()来获取。我们自己编写的类的加载器就是这个系统类加载器。
除了系统提供的类加载器以外,开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。
其实上面的前面三种加载器都是有树状的一个结构的,有父子关系。我们可以来试试看。写一个小demo
package cn.cuit.bo;
public class Test{
public static void main(String[] args) throws Exception {
ClassLoader c1 = Test.class.getClassLoader();
ClassLoader c2 = c1.getParent();
ClassLoader c3 = c2.getParent();
System.out.println(c1);
System.out.println(c2);
System.out.println(c3);
}
}
好的首先我们获取了当前这个我们自己写的Test类的加载器,然后可以通过getParent方法来获取这个加载器的父加载器,然后我们一共往上查找了三次
我们来看下结果
可以看到我们自己的Test类的加载器是AppClassLoader,然后它的父加载器就是扩展加载器PlatFormClassLoader最后我们可以看到再往上就是null了,这个原因其实就是再往上就是引导类加载器了,这个时候获取不到的所以是null
我们的类加载器,首先碰到一个类之后,如果需要用到那么就会让当前的类的加载器进行加载,一般来说,当前类加载器会直接把这个任务抛给上一级,一直往上面抛,最后到达最后的类加载器,这个时候最顶部的类加载器又会往下分任务,如果下面的加载器已经加载了这个类的话那么就不用再去加载这个类了。这就是双亲委派机制。
我们可以来试试一个东西,一个小小的demo
可以看到这个demo我写了一个和String类一摸一样的类,包名和类名都是一样的,这个时候如果我们运行这个入口函数,那么会出现什么问题呢?
可以看到,它说没有main方法,但是图片上面明显有main方法,这是为什么呢?其实是这个样子的,首先我们需要运行这个方法,那么我们自己写的String方法是必须要进入到内存中的,然后就需要类加载器去进行一个加载,然后一直网上抛这个任务,最后顶部的加载器遇到之后,发现自己已经加载了这个String类了(加载了官方的String类)然后就告诉你不需要加载了可以直接运行,然后就调用官方的String中的main入口函数,这样的话原本的String中是没有这个入口函数的,所以就出现了上面的那种情况。
所以我们可以知道其实双亲委派机制可以保护我们的类不要被后面的类干扰,保护核心内部的Java类。
我们只需要用一个类去继承ClassLoader就可以实现一个加载器了,我们之前说过,将一个类加载到内存中我们需要用到类加载器中的一个方法,defineClass
我们直接来看一个小案例,写了一个自定义的类加载器
package cn.cuit.bo.classloader;
import java.io.*;
public class MyFileClassLoader extends ClassLoader{
public MyFileClassLoader(String rootPath){
this.rootPath = rootPath;
}
private String rootPath;
// 获取类全限定名找到字节码文件变为字节数组
private byte[] getData(String name) throws ClassNotFoundException {
ByteArrayOutputStream out = null;
byte[] data = null;
String path = rootPath name.replace(".", File.separator) ".class";
BufferedInputStream bis = null;
try{
bis = new BufferedInputStream(new FileInputStream(path));
byte[] buffer = new byte[1024 * 8];
out = new ByteArrayOutputStream();
// 循环读取到 out 里面
int len = -1;
while((len = bis.read(buffer)) != -1) {
out.write(buffer,0,len);
}
out.flush();
data = out.toByteArray();
}catch (FileNotFoundException e) {
e.printStackTrace();
throw new ClassNotFoundException();
}catch (IOException e){
e.printStackTrace();
} finally {
try {
if(out != null) out.close();
if(bis != null) bis.close();
}catch (Exception e){
e.printStackTrace();
}
}
return data;
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
// 判断一下有没有被其他类加载器加载过了
Class> loadedClass = this.findLoadedClass(name);
if(loadedClass != null) return loadedClass;
else {// 需要加载
byte[] data = this.getData(name);
Class> aClass = this.defineClass(name, data, 0, data.length);
return aClass;
}
}
}
然后我们解释一下这个代码,主要重写的方法是findClass这个方法,传入一个类的全限定名我们就可以开始解析了,首先我们可以判断一下这个类是不是已经被别的加载器加载进来了,如果已经加载进来了的话,那么就直接返回就完事了,如果没有那么就需要我们自己去进行加载操作了。我们这个案例是从文件中加载类,这个时候肯定需要提取到文件的路径了,一般来说我们的包名其实就是一个相对路径,所以我们有一个根路径,我们需要知道所以我们定义了一个属性叫做rootPath然后我们写一个构造方法来给这个rootPath传值,这样路径有了我们就可以知道从哪里提取出字节码文件了,前面我们说过我们加载一个类就要用到defineClass方法,这个方法需要的全限定名我们有,还需要一个字节数组,所以我们现在当务之急是获取到这个数组,所以我们又编写了一个方法,传入全限定类名提取出字节数组,这个方法里面,我们先初始化了一个BufferedInputStream对象,然后又创建了一个ByteArrayOutPutStream对象,这个对象我们可以写到这个输出流里面然后获取byte数组,然后循环输出就完事了,然后获取到之后直接返回,然后直接调用findClass就可以了。非常的简单
测试一下
package cn.cuit.bo.classloader;
public class Test{
public static void main(String[] args) throws Exception {
MyFileClassLoader myFileClassLoader = new MyFileClassLoader("C:\\Users\\1902001047\\Desktop\\DemoTest\\");
Class> aClass = myFileClassLoader.findClass("cn.cuit.bo.wuhu.HelloJava");
aClass.newInstance();
ClassLoader c1 = aClass.getClassLoader();
ClassLoader c2 = c1.getParent();
ClassLoader c3 = c2.getParent();
ClassLoader c4 = c3.getParent();
System.out.println(c1);
System.out.println(c2);
System.out.println(c3);
System.out.println(c4);
}
}
操作结果
首先这个结果确实是是成功加载了这个类,并且我们获取它的类加载器,确实是我们的MyFileClassLoader加载器,这个时候我们继续向上获取加载器,发现它的上面是AppClassLoader,哦!原来我们自己写的类加载器其实是最底层的加载器
原本我们知道,如果我们定义了一个List集合,我们需要加上泛型,如果这个泛型我们是String类型,那么我们尝试去add一个Integer会怎么样呢?
很明显是不可以的,因为泛型是String但是我们非要加上一个Integer这怎么去弄呢?其实还是有办法的,我们可以利用反射机制去擦除我们的泛型。来看一个案例
public static void main(String[] args) throws Exception {
List strs = new ArrayList<>();
strs.add("AA");
strs.add("BB");
// 获取Class对象
Class extends List> cs = strs.getClass();
Method add = cs.getMethod("add", Object.class);
add.invoke(strs,123);
System.out.println(strs);
}
因为我们不可以直接进行add操作,但是我们通过反射拿到了这个method,然后传入参数,因为泛型操作一般参数都是Object,只有再编译的时候这个泛型T才会被检测,检测传入的参数是不是T这种类型的,但是我们利用反射机制,其实是再运行阶段才会进行运行,在编译之前没有什么异常的操作。所以通过了编译阶段之后就可以放飞自我了。
本次学习参考Blibli视频
链接:JAVA高级特性_反射_注解_类加载器【雷哥】