第1章 类加载器(ClassLoader)
1.1概述(了解)
类加载器:
负责将.class文件加载到内存中,并为之生成对应的Class对象,也就是字节码文件对象。
问题:我们平时书写在idea中的Java程序是如何运行的呢?
1)首先将 .java 源文件编译为class类文件;
2)编译后的类文件是存在硬盘中的,那么我们运行需要在内存中看到效果,那么类文件是如何被加载到内存中的呢,就是jvm通过类加载器ClassLoader把硬盘中的class文件加载到内存中,这样就可以使用这个类中的成员变量和方法了。而被加载到内存中这个class文件就会变成一个Class类的对象。
常见的类加载器有三种,每个类加载器负责加载不同位置的类:
1)Bootstrap 根类加载器;
2)ExtClassLoader 扩展类加载器;
3)AppClassLoader 系统/应用类加载器;
那么这三种类加载器各有什么作用或者有什么区别呢?
他们三个加载的范围是不一样的。如下图所示:
说明:
1)Bootstrap是最顶级的类加载器。它加载类文件不是我们自己书写的,负责Java核心类的,比如System,String等。只有所有类加载到内存中,我们才可以使用。
2)ExtClassLoader 扩展类加载器,加载的是扩展类的,我们是用不到的,都是jdk内部自己使用的。
3)AppClassLoader 系统/应用类加载器,是用来加载ClassPath 指定的所有jar或目录,ClassPath表示存放类路径的,我们如果不配置ClassPath,那么就表示当前文件夹,在idea环境下的ClassPath是out目录。在out目录存放的都是我们书写好的class文件,也就是说 AppClassLoader 类加载器是用来加载我们书写的out目录下的class文件。
需求:演示类加载器的父子关系。
代码演示如下所示:
分析:如何获取一个类的类加载器呢?
如果想获得当前类的加载器,那么首先必须获得当前类的字节码文件对象,而这个字节码文件对象属于Class类型,我们可以使用 Class类中的getClassLoader()函数来获得类加载器:
ClassLoader getClassLoader() 返回该类的类加载器
AppClassLoader:加载classPath中的所有的类,也就是我们自己写的那些类!
注意:类加载器,也是一个类,也需要被加载。一般类加载器都是被父类加载器加载的!
获取父类加载器的方法:使用ClassLoader 类中的getParent()返回委托的父类加载器 。
说明:AppClassLoader是被ExtClassLoader加载的!
ExtClassLoader肯定也是一个类,需要被父加载,它的父亲是BootStrap。
那么问题来了:如果这个类加载器也需要被人加载,那么就没有尽头了!因此,BootStrap是不需要被加载的。
因为它不是一个Java类。它是用C++实现的一段代码。
也就是说,jvm虚拟机一启动就会运行C++实现的这段代码,那么BootStrap类一旦被启动就会开始加载他下面的子类了。
注意:最顶级的类加载器不是Java类,而是C++实现的代码。
/*
* 演示类加载器的父子关系:
* 获取一个类加载器:使用Class类中的函数:ClassLoader getClassLoader() 返回该类的类加载器
* 获取Class类的对象 :类名.class
* AppClassLoader:加载classPath中的所有的类,也就是我们自己写的那些类!
* 类加载器,也是一个类,也需要被加载。一般类加载器都是被父类加载器加载的!
* 获取父类加载器的方法:使用ClassLoader类中的函数:
* ClassLoader getParent()返回委托的父类加载器
*
* AppClassLoader类加载器是被ExtClassLoader类加载器加载的!
*
* ExtClassLoader肯定也是一个类,需要被父类加载器加载,它的父类是BootStrap类加载器。
* 而ExtClassLoader确实被他的父类BootStrap类加载器加载的,
* 那么问题来了:如果BootStrap类加载器也需要被人加载,那么就没有尽头了!因此,BootStrap类加载器是不需要被加载的。
* 因为它不是一个Java类。它是用C++语言实现的一段代码。
* 所以这里获取不到BootStrap类加载器,就是因为他是一段C++代码实现的。
*/
public class ClassLoaderDemo1 {
public static void main(String[] args) {
// 获取当前类的加载器
ClassLoader loader = ClassLoaderDemo1.class.getClassLoader();
//输出当前类的类加载器
System.out.println(loader);//sun.misc.Launcher$AppClassLoader@b0014f0
//获取AppClassLoader类加载器的父类
ClassLoader parent = loader.getParent();
//输出AppClassLoader类加载器的父类加载器
System.out.println(parent);//sun.misc.Launcher$ExtClassLoader@325e9e34
//获取ExtClassLoader类加载器的父类
ClassLoader grandpa = parent.getParent();
//输出ExtClassLoader类加载器的父类加载器
System.out.println(grandpa);//null
}
}
注意:
通过上述学习我们发现三种类加载器都有自己要加载的类文件,各司其职,不能乱加载,比如Bootstrap类加载器只能加载sun公司定义好的类,他就不能加载自定义的类文件。所有的自定义类文件都是由AppClassLoader类加载器加载。
1.2委托机制(双亲委派机制)(了解)
通过上述学习我们发现三种类加载器都有自己要加载的类文件,各司其职,不能乱加载,比如Bootstrap类加载器只能加载JRE/lib/rt.jar 包下的类,他就不能加载JRE/lib/ext/*.jar包下的类文件,我们把上述这种关系叫做全盘负责委托机制(双亲委派机制)。
全盘负责委托机制:
ClassLoader(类加载器)加载类用的是全盘负责委托机制。
**1)全盘负责:**当一个ClassLoader(类加载器)加载一个类的时候,那么在这个类中所引用的所有其它的类通常也都由这个类加载器来加载。
举例:比如上述代码中我们在 ClassLoaderDemo1 类中书写如下代码:
public class ClassLoaderDemo1 {
public static void main(String[] args) {
// 获取当前类的加载器
ClassLoader loader = ClassLoaderDemo1.class.getClassLoader();
//输出当前类的类加载器
System.out.println(loader);//sun.misc.Launcher$AppClassLoader@b0014f0
//获取AppClassLoader类加载器的父类
ClassLoader parent = loader.getParent();
//输出AppClassLoader类加载器的父类加载器
System.out.println(parent);//sun.misc.Launcher$ExtClassLoader@325e9e34
}
}
说明:由于我们在ClassLoaderDemo1 类中使用了System类,那么System类也应该由ClassLoaderDemo1的类加载器加载到内存中。
换句话说,如果在A类中使用了B类,那么A类的加载器就会将B类也会加载到内存中,就是一个类的加载器同时把多个类都加载了。
2)委托机制:先让Parent(父)类加载器寻找。
但是呢,全盘负责要和委托机制一起使用,一个类加载器在加载一个类的时候不是上来就先加载类,而是先咨询这个类加载器的父亲,先看他的父类加载器有没有要加载的类,如果已经存在要加载的类了,那么子类加载器就不会加载,因为在加载就会重复,产生冲突了,只有在父类加载器中找不到的时候,才从自己的范围中寻找。
举例:还是上述的代码,由于ClassLoaderDemo1 类是被 AppClassLoader 类加载器加载内存中的,那么根据全盘负责机制,AppClassLoader 类加载器也会将System类加载到内存中,但是在加载的时候,根据委托机制AppClassLoader 类加载器会先去咨询他的父亲ExtClassLoader 类加载器,而这个类加载器中也没有System类,那么又会去咨询ExtClassLoader 类加载器的父类Bootstrap类加载器,而在这个类加载器中是可以加载System类的,所以作为子类加载器AppClassLoader 就不会加载了,这样才能保证一个类只会被加载一次,任何一个类同时只会被加载一次。
如果一个类在父类加载器中找到了,那么就会把这个类加载之后保存到cache(缓存)中。
3)类加载器的cache(缓存)机制:如果cache中保存了这个类就直接返回它,如果没有才加载这个类,然后存入cache中,下一次如果有其他类在使用的时候就不会在加载了,直接去cache缓存拿即可。这就是为什么每个类只加载一次,内存只有一份的原因。
举例:还是上述代码中,当第一次使用System类的时候,那么System类就会被加载了,那么System类就会存储到内存中了,当下面代码中我们再一次使用System类的时候,由于内存中已经有了,那么就不会在去加载了,这时会直接拿过来用即可。
因此方法区中每一个类的字节码文件只有一份的原因由全盘负责、委托机制和类加载器的cache(缓存)机制共同决定。也称为双亲委派机制
注意:双亲委派机制具有低效的传达机制,最后还是各司其职。所以从jdk9后对双亲委派机制已经破坏。才有如下方式:
1.经过破坏后的双亲委派模型更加高效,减少了很多类加载器之间不必要的委派操作
2.JDK9的模块化可以减少Java程序打包的体积,同时拥有更好的隔离性与封装性
3.每个moudle拥有专属的类加载器,程序在并发性上也会更加出色
总结:
1.全盘负责:就是在当前类中使用的其他类都是由当前类的类加载器加载。
2.委托机制:去询问父类加载器是否可以加载,如果能加载则由父类加载器加载。不能则由本类的类加载器加载
3.cache缓存机制:如果一个类.class被加载到了内存中使用一次就会被放到cache缓存中,然后下次使用直接从cache缓存中拿即可
全盘负责 委托机制 cache缓存机制 共同决定了内存中永远只有一份某个类的字节码文件对象(类名.class).
举例:内存只有一份System.class
1.3 使用类加载器加载配置文件的方式(掌握)
需求:用类加载器加载配置文件。
代码步骤:
1)新建一个类ClassLoaderDemo2 ,并书写main函数;
2)在当前项目下新建一个stu.ini文件,并在其中输入:
name=zhangsan
age=19
3)创建Properties 类的集合对象p;
4)创建字节输入流FileInputStream对象关联项目根目录文件stu.ini;
5)使用集合对象p调用load函数加载配置文件中的内容,并使用输出语句输出内容即可;
使用之前的FileInputStream方式加载配置文件方式加载文件内容:
//使用之前的加载配置文件方式加载文件内容
public void method_1() throws Exception{
// 创建Properties类的对象
Properties p = new Properties();
//调用函数加载配置文件中内容
p.load(new FileInputStream("stu.ini"));
//输出内容
System.out.println(p);
}
说明:直接使用FileInputStream的相对路径,是相对于项目。
注意:我们在开发中配置文件stu.ini不仅可以存放到项目的根目录,有时我们还会存放到src下面:
这样我们使用new FileInputStream(“stu.ini”)在读取就会出现如下问题:
错误原因:new FileInputStream(“stu.ini”)是去项目的根目录去查找stu.ini文件,而由于我们已经将该文件移动到src下面,所以会报找不到指定文件的异常。
既然使用之前的加载方式加载src下面的配置文件有问题,那么我们可以使用新的加载方式,就是用类加载器加载。
分析步骤:
1)使用当前类ClassLoaderDemo2获得Class对象并调用Class类中的getClassLoader()函数:
ClassLoader loader = ClassLoaderDemo2.class.getClassLoader();
2)使用类加载器对象loader 调用ClassLoader 类中的InputStream getResourceAsStream(String name)返回读取指定资源的输入流
InputStream in = loader.getResourceAsStream(“stu.ini”);
说明:这里的name是文件的路径:这个路径如果使用相对路径,相对的是src目录。
代码演示如下所示:
//使用之前的加载配置文件方式加载文件内容
public void method_1() throws Exception{
// 创建Properties类的对象
Properties p = new Properties();
//获取当前类的加载器
ClassLoader loader = ClassLoaderDemo2.class.getClassLoader();
//使用类加载器对象调用函数获取加载配置文件的字节输入流
InputStream in = loader.getResourceAsStream("stu.ini");
//调用函数加载配置文件中内容
// p.load(new FileInputStream("stu.ini"));
p.load(in);
//输出内容
System.out.println(p);
}
总结:这两种加载文件的区别在于加载的相对位置不一样,第一种方式相对的是当前项目,而第二种方式相对的是src。
第2章 反射-框架设计的灵魂
2.1 反射概述
问题:我们平时书写在idea中的Java程序是如何运行的呢?
1)首先将 .java 源文件编译为class类文件;
2)编译后的类文件是存在硬盘中的,那么我们运行需要在内存中看到效果,那么类文件是如何被加载到内存中的呢,就是jvm通过类加载器ClassLoader把硬盘中的class文件加载到内存中,这样就可以使用这个类中的成员变量和方法了。而被加载到内存中这个class文件就会变成一个Class类的对象。
反射要依赖于Class类。
由于Class表示类文件的字节码文件对象,类字节码文件就是在描述一个类,描述类的成员变量、成员函数和构造函数。
而反射就是从一个类的字节码文件中拿到成员变量、成员函数和构造函数。要想从一个类中拿东西必须拿到这个类的字节码文件对象,所以反射依赖于Class,因此我们在学习反射之前先了解下Class。
2.1.1 Class类介绍
在Java中使用类来描述所有的事物,而这些描述完的所有程序,在编译完之后统一都会生成各自class文件。
在Java中class文件是所有源代码(.java 文件)程序编译后统一的结果。class文件是一类可以被JVM直接执行的文件。class文件在Java世界中就是存在的一类事物。
Java使用Class类来描述和封装class文件这类事物。class文件也叫作字节码文件。
关于Class描述字节码文件如下图所示:
说明:
1)Java中使用Class类表示某个class文件;
2)任何一个class文件都是Class这个类的一个实例对象;
Class的API描述:
说明:
1)Class类它可以表示Java中的任何内容;
2)Java中使用Class类表示硬盘上的某个.class文件,启动JVM就会把文件加载到内存中,占用一片空间,称为一个字节码文件对象,这个对象就是Class类的一个实例。不同的类,有自己的字节码文件对象,这些对象都是Class类的实例;
3)说明:在Class类中专门提供了几个获取成员变量 、成员函数 、构造函数的函数。
Field getField(String name) 表示获取类中的成员变量的对象;
Method getMethod() 表示获取类中的成员函数的对象;
Constructor getConstructor() 表示获取类中的构造函数的对象;
2.1.2 获取Class的三种方式(掌握)
因为反射技术是通过Class对象来实现把一个类进行解剖的,所以需要先了解怎么样才可以获取到Class对象。
需求:演示获取Class的三种方式:
1)获取Class对象的第一种方式:使用类的class属性直接获取:类名.class。
说明:在任何的一个类中都有一个静态成员变量class,可以直接获取到class文件所属的Class对象。
2)获取Class对象的第二种方式:在Object类中,有个getClass方法,就可以获取到任何一个对象对应的Class对象。对象.getClass()。
3)获取Class对象的第三种方式:在Class类中有个静态的方法:static Class forName(String className),根据类的名称获取类的Class对象。
说明:这里的参数className必须是类的全名(就是带有包名的全名称)。如:Class.forName(“java.lang.String”);
补充:上述三种方式可以获得Class对象,获得完Class对象就可以获取类的基本信息:
获取类的基本信息:
String getName() 获取类的名称,就是获得包名.类名
String getSimpleName() 获取类的简称 类名
代码演示如下所示:
/*
* 演示:获取Class的三种方式
* 方式1:类名.class
* 方式2:对象.getClass()
* 方式3:static Class forName(String className)根据类的名称获取类的Class对象
* 注意:这里的className必须是类的全名!
*
* 获取类的基本信息:
* String getName() 获取类的名称 包名.类名
* String getSimpleName() 获取类的简称 类名
*/
public class ClassDemo1 {
public static void main(String[] args) throws ClassNotFoundException {
// 方式1:类名.class
Class clazz = String.class;
// 输出clazz全名 就是包名.类名 java.lang.String
System.out.println(clazz.getName());
// 输出clazz精简名字 就是类名 String
System.out.println(clazz.getSimpleName());
// 方式2:对象.getClass()
Class clazz2 = "柳岩".getClass();
// 输出clazz2全名 就是包名.类名 java.lang.String
System.out.println(clazz2.getName());
// 输出clazz2精简名字 就是类名 String
System.out.println(clazz2.getSimpleName());
// 方式3:static Class forName(String className)根据类的名称获取类的Class对象
// Class clazz3 = Class.forName("String");//这里必须是类的全名 包名.类名,否则报java.lang.ClassNotFoundException
Class clazz3 = Class.forName("java.lang.String");
// 输出clazz3全名 就是包名.类名 java.lang.String
System.out.println(clazz3.getName());
// 输出clazz3精简名字 就是类名 String
System.out.println(clazz3.getSimpleName());
//说明对于String类来说在内存中只有一个String.class文件
System.out.println(clazz==clazz2);//true
System.out.println(clazz==clazz3);//true
System.out.println(clazz2==clazz3);//true
}
}
总结:上述三种方式都是用来获取Class对象的,那么在开发中一般使用哪种获取方式呢?
在开发中我们会使用第三种获取方式。
说明:第三种的好处就是加载一个类却不需要知道这个类是什么,通过第一种方式获取前提是必须得先知道类名,然后才能通过类名.class获取。而第二种方式必须知道对象才能通过对象.getClass()来获取。
而第三种不需要知道类名或者对象就可以直接获取,或者可以这样理解,我们在真实开发中,类的名字是不知道的,都是通过IO流来读取配置文件读取回来的。
也就是说我们读取配置文件的时候根据key来获取String类型的value,这样就可以把String类型的value作为forName(String className)函数的参数,而不需要在程序代码中体现出类名字样就可以获得Class对象了。
2.1.3 预定义对象(了解)
通过查阅Class的API发现8种基本数据类型、void关键字和数组也表示为Class的对象,我们把8种基本数据类型、void叫做预定义对象,一共有9种。
说明:
1)基本数据类型要保存在内存中,那么他们也会有自己的字节码,获取的Class对象中保存的就是他们本身,因为基本数据类型不属于任何包下的。
如:Class c = int.class;对象c中保存的就是int。
2)对于函数返回值类型void,代表的是没有返回值类型,那么他也是一种数据类型,表示没有类型,那么输出对象也是void本身。
如:Class c1 = void.class;对象c1中保存的就是void。
代码演示如下:
/*
* 演示:9种预定义对象:
* 包含:8种基本数据类型以及void
*/
public class ClassDemo2 {
public static void main(String[] args) {
// 获取int类型的Class对象
Class c = int.class;
System.out.println(c);// int
// 获取void类型的Class对象
Class c1 = void.class;
System.out.println(c1);// void
}
}
总结:任何的一种数据都具有自己的字节码。
2.1.4 反射的概念
到目前为止我们已经了解了如何获得Class,那么接下来我们就会使用反射技术来剖析一个class文件。
那么到底什么是反射呢?
反射就是通过一个类的Class对象把类中的各种成员映射成对应的Java类。一个类中的:成员变量、构造函数、成员方法都有对应的Java类:Field、Contructor、Method; 就比如:一个汽车是一个大类,汽车中的发动机、轮胎等等都可以是一个个小的类。
一个类的Class对象可以获取其所有成员的信息,比如一个方法的名称、修饰符、参数类型、返回值等等信息封装成一个描述方法的类(Method)中。
换句话说反射通过Class类的对象可以获取一个类中的成员,比如函数,保存在Method类中。然后通过Method类的对象来获取一个成员函数的名称、修饰符、参数类型、返回值等等信息。
一个类中的所有成员,都可以通过Class对象获得,并封装为对应的对象。我们拿到这些对象以后,有什么用?怎么用?这正是我们学习反射的要点!
2.2 使用反射获取一个类中的构造函数
2.2.1使用Class类中的newInstance()函数创建某个类的对象
通过上述办法已经获取到了Class对象,即就是获取到某个类的class文件。现在我们不使用Java中的new关键字来创建这个class文件所描述的那个事物的真实对象, 而通过Class这个对象,动态的创建这个类的真实对象。
在Class类中有一个newInstance()方法,可以动态的创建出当前这个class文件的真实对象。
该方法如下所示:
代码演示如下所示:
Person类:
/*
* 描述人
*/
public class Person {
//属性
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person() {
}
}
测试类:
/*
* 使用Class类中的newInstance()函数获取某个class文件中的真实对象
*/
public class ReflectDemo1 {
public static void main(String[] args) throws Exception {
// 获取Class类的对象
Class clazz = Class.forName("cn.itcast.sh.reflect.demo.Person");
//使用对象调用函数创建Person类的对象
Person p = (Person) clazz.newInstance();
/*
* 上述两行代码等同于
*Person p= new Person();
*注意:这里是使用Person类中的无参构造函数在创建对象,所以要求Person类中必须具备无参构造函数,
*否则就会报 java.lang.InstantiationException:不能实例化异常
*/
System.out.println(p);//cn.itcast.sh.reflect.demo.Person@5a20d10a
//利用无参构造函数创建对象
String s = String.class.newInstance();
System.out.println(s.length());//0
}
}
注意:使用Class类中的newInstance()方法的时候,要求class文件中必须有空参数并且不能是private修饰的构造函数。如果没有,那么使用newInstance()方法就会报异常。
2.2.2 使用反射获取一个类中的所有构造函数(包括有参数和私有的构造函数)(Constructor类)
说明:
之前,我们都是使用Class类中的newInstance()调用无参的构造函数来创建对象的,对于有参数的构造方法无法实现调用并创建这个类的对象,所以sun公司专门给我们提供了如何获取一个类中的所有构造函数。
说明:
1)使用Class类的对象即字节码对象可以获取class文件中的所有构造函数,具体应该借助Class类中的如下函数:
a:Constructor[] getConstructors() 获取所有公共的构造函数
b:Constructor[] getDeclaredConstructors() 获取所有构造函数 包括私有的
c:Constructor getConstructor(Class ... parameterTypes)根据参数列表获取指定的公共的构造函数
说明:由于这里需要Class类的对象,所以在给参数的时候,直接使用实参类型的类获取Class对象即可。
举例:假设需要获取String类型的构造函数,那么这里直接使用String.class作为getConstructor(Class … parameterTypes)的参数。
d:Constructor getDeclaredConstructor(Class ... parameterTypes)根据参数列表获取指定的构造函数 包括私有的
2)获取到构造函数对象之后,就可以使用获取的构造函数对象创建某个类的真实对象。我们通过反射已经获取到构造函数,查阅Constructor类中的描述,发现Constructor类中的newInstance(Object… initargs) 方法,这个方法可以动态的创建出这个构造函数对象所表示的那个类的真实对象。
说明: Object… initargs 创建对象的时候需要传递的真实的数据,就是构造函数所需要的实际参数。
代码如下所示:
需求:获取File类的构造函数,并使用获取的构造函数创建对象。
D:\test\out.txt 作为newInstance函数的参数,创建对象之后并获取绝对路径。
/*
* 演示:反射获取构造函数。
* Constructor[] getConstructors() 获取所有公共的构造函数
* Constructor[] getDeclaredConstructors() 获取所有构造函数 包括私有的
* Constructor getConstructor(Class ... parameterTypes)根据参数列表获取指定的公共的构造函数
* 说明:由于这里需要Class类的对象,所以在给参数的时候,直接使用实参类型的类获取Class对象即可
* 举例:假设需要获取String类型的构造函数,那么这里直接使用String.class作为getConstructor(Class ... parameterTypes)的参数
* Constructor getDeclaredConstructor(Class ... parameterTypes)根据参数列表获取指定的构造函数 包括私有的
*
* 获取到构造函数对象之后,就可以使用获取的构造函数对象创建某个类的真实对象。我们通过反射已经获取到构造函数,
* 查阅Constructor类中的描述,发现Constructor类中的newInstance(Object... initargs) 方法,
* 说明: Object... initargs 创建对象的时候需要传递的 * 这个方法可以动态的创建出这个构造函数对象所表示的那个类的真实对象。
真实的数据,就是构造函数所需要的实际参数。
*/
public class ReflectDemo2 {
public static void main(String[] args) throws Exception {
// 获取字节码文件对象
Class clazz=File.class;
//Constructor[] getConstructors() 获取所有公共的构造函数
Constructor[] cons = clazz.getConstructors();
for (Constructor con : cons) {
/*
* public java.io.File(java.lang.String,java.lang.String)
* public java.io.File(java.lang.String)
* public java.io.File(java.io.File,java.lang.String)
* public java.io.File(java.net.URI)
*/
System.out.println(con);
}
System.out.println("--------------");
//Constructor[] getDeclaredConstructors() 获取所有构造函数
Constructor[] constructors = clazz.getDeclaredConstructors();
for (Constructor constructor : constructors) {
/*
* public java.io.File(java.lang.String,java.lang.String)
* public java.io.File(java.lang.String)
* private java.io.File(java.lang.String,java.io.File)
* public java.io.File(java.io.File,java.lang.String)
* public java.io.File(java.net.URI)
* private java.io.File(java.lang.String,int)
*/
System.out.println(constructor);
}
System.out.println("--------------");
//需求:我要获取File(String pathname)
//Constructor getConstructor(Class ... parameterTypes)根据参数列表获取指定的公共的构造函数
Constructor constructor = clazz.getConstructor(String.class);
//public java.io.File(java.lang.String)
System.out.println(constructor);
//需求:获取private File(String child, File parent)
//Constructor getDeclaredConstructor(Class ... parameterTypes)根据参数列表获取指定的构造函数 包括私有的
System.out.println("--------------");
//private java.io.File(java.lang.String,java.io.File)
Constructor constructor2 = clazz.getDeclaredConstructor(String.class,File.class);
System.out.println(constructor2);
//使用获得的构造函数创建对象 newInstance(Object... initargs)
File f = (File) constructor.newInstance("D:\\test\\out.txt");
System.out.println(f.getAbsolutePath());//D:\test\out.txt
}
}
注意:通过Class类中的Constructor[] getDeclaredConstructors()和Constructor getDeclaredConstructor(Class … parameterTypes)函数可以获得类中的所有的构造函数,包括私有的构造函数,但是私有的构造函数我们在其他类中是无法使用的,如果要想使用必须强制取消Java对私有成员的权限检测或者可以理解暴力访问。
需求:使用反射技术获得File类中的私有构造函数 private File(String child, File parent) 并创建File类的对象获得指定路径的绝对路径。
注:String child=”柳岩.jpg”,File parent=new File(“D:\test”)
代码实现如下所示:
public static void main(String[] args) throws Exception {
// 获取字节码文件对象
Class clazz=File.class;
/*
* 需求:使用反射技术获得File类中的私有构造函数 private File(String child, File parent)
* 并创建File类的对象获得指定路径的绝对路径。
* 注:String child="柳岩.jpg",File parent=new File("D:\\test")
*/
Constructor constructor2 = clazz.getDeclaredConstructor(String.class,File.class);
//创建File类的对象 使用获得的构造函数创建对象 newInstance(Object... initargs)
File f2 = (File) constructor2.newInstance("柳岩.jpg",new File("D:\\test"));
System.out.println(f2.getAbsolutePath());
}
上述代码发生了如下异常:
IllegalAccesssException异常是在没有访问权限时,就会引该异常。
解决上述异常,我们必须强制取消Java对私有成员的权限检测或者可以理解暴力访问,需要使用Constructor的父类AccessibleObject类中的函数:
AccessibleObject类如下所示:
使用如下函数即可:
上述函数表示强制取消Java对私有成员的权限检测。
或者可以理解暴力对私有成员进行访问。
改进的代码如下所示:
/*
* 需求:使用反射技术获得File类中的私有构造函数 private File(String child, File parent)
* 并创建File类的对象获得指定路径的绝对路径。
* 注:String child="柳岩.jpg",File parent=new File("D:\\test")
*/
Constructor constructor2 = clazz.getDeclaredConstructor(String.class,File.class);
//对于上述的构造函数是私有的,我们不能直接访问,只能暴力访问。
constructor2.setAccessible(true);
//创建File类的对象 使用获得的构造函数创建对象 newInstance(Object... initargs)
File f2 = (File) constructor2.newInstance("柳岩.jpg",new File("D:\\test"));
System.out.println(f2.getAbsolutePath());//D:\test\柳岩.jpg
小结:
当要访问Class对象中的私有的构造或成员时,需要使用getDeclaredXxxx()函数:
Xxxx表示:Constructor、Field、Method。
在访问Class对象中的私有的构造函数或成员时,需要取消java语言的默认访问权限检查
setAccessible(boolean) true表示强制取消Java对私有成员的权限检测。 false表示不会取消Java对私有成员的权限检测。
2.3 反射获取成员方法(Method)(掌握)
2.3.1 反射公开的非静态的成员方法
说明:
1)在Class类中提供的getMethod方法上接收一个String name,name表示的是需要反射的那个方法的名字。
因为在一个类中可以有多个不同名的方法。在反射的时候需要指定这个方法的名字,同时在一个类中还可能出现方法的重载,这时还需要指定具体反射的是哪个方法参数类型。
2)Method[] getMethods() 获取所有公共的成员方法。包括父类的公共方法。
3)让反射到的一个方法运行,需要使用Method类中的invoke方法 :
Object invoke(Object obj, Object… args)
invoke方法中的第一个参数 Object obj:表示的是当前需要调用这个方法的那个对象
invoke方法中的第二个参数Object… args:
表示的是真正需要运行的某个类中被反射的那个方法需要接收的真实参数
在调用Method类中的invoke方法的时候,其实底层是在运行被反射的那个方法,
既然是某个方法在运行,那么方法运行完之后可能会有返回值。
举例:需求:我们想通过反射技术获得Person类中的setName()函数,并让其执行。
/*
* 举例:需求:我们想通过反射技术获得Person类中的setName()函数,并让其执行。
* Method getMethod(String name, Class<?>... parameterTypes)
* 返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法。
* Method[] getMethods() 获取所有的公共方法 包括父类的公共方法
*
* Object invoke(Object obj, Object... args) 表示让反射到的一个方法运行
* obj:要执行哪个对象的方法
* args:方法需要的实际参数
*/
public class ReflectDemo {
public static void main(String[] args) throws Exception {
//获取Class对象
Class clazz = Class.forName("cn.itcast.sh.reflect.demo.Person");
/*
* 反射成员方法:
* public void setName(String name)
* 类中的非静态的成员方法,需要对象调用,我们反射到方法之后,最后肯定是要运行这个方法
* 这时肯定还是需要对象的
*
* Method getMethod(String name, Class<?>... parameterTypes)
* String name 反射的方法的名字
* Class<?>... parameterTypes 反射的方法接受的参数类型
*/
Method method = clazz.getMethod("setName", String.class);
//通过非反射的方式执行setName函数
/*
* Person p = new Person();
* p.setName("赵四");
* System.out.println(p.getName());//赵四
*/
//通过反射的方式执行setName函数
/*
* 让反射到的一个方法运行,需要使用Method类中的invoke方法
*
* Object invoke(Object obj, Object... args)
*
* invoke方法中的第一个参数 Object obj:表示的是当前需要调用这个方法的那个对象
* invoke方法中的第二个参数Object... args:
* 表示的是真正需要运行的某个类中被反射的那个方法需要接收的真实参数
* 在调用Method类中的invoke方法的时候,其实底层是在运行被反射的那个方法,
* 既然是某个方法在运行,那么方法运行完之后可能会有返回值
*/
//获取此时clazz所表示Person类的一个新的对象
// Object obj = clazz.newInstance();
Person p = (Person) clazz.newInstance();
//执行setName函数 这句代码就是在调用反射到Person类中的setName方法
Object obj2 = method.invoke(p, "赵四");
System.out.println(p);//cn.itcast.sh.reflect.demo.Person@7f21c5df
System.out.println(obj2);//null
System.out.println(p.getName());//赵四
System.out.println("=====================================");
//Method[] getMethods() 获取所有的公共方法 包括父类的公共方法
Method[] methods = clazz.getMethods();
for (Method m : methods) {
System.out.println(m);
}
}
}
总结:
反射过程如下图所示:
2.3.2反射类中的私有,静态的,不需要参数的,且有返回值的方法
Method[] getDeclaredMethods() 获取所有本类自己声明过的成员方法。不包括父类中的方法。
Method getDeclaredMethod(String name,Class … parameterTypes)获取某个方法。
案例:需求:反射Person类中的私有的 静态的成员方法。private static void say()。
在Person类中添加如下函数:
private static void say()
{
System.out.println("say....");
}
说明:
1)如果在反射的时候,反射的方法不需要接受参数的时候,在反射时参数的类型可以书写成null,由于第二个参数是可变参数,我们也可以什么都不写;
2)由于方法是私有的,这时需要取消Java的权限检查,method.setAccessible(true);
如果不取消权限检查会报异常:
3)调用invoke方法,让被反射的方法运行, 由于这个方法say是静态的,运行的时候是不需要对象的。这时在调用invoke方法的时候,对象书写null,由于不需要参数,真实的参数也书写成null。由于第二个参数是可变参数,我们也可以什么都不写。
具体代码如下所示:
/*
* 案例:需求:反射Person类中的私有的 静态的成员方法。private static void say()。
* Method[] getDeclaredMethods() 获取所有本类自己声明过的成员方法。不包括父类中的方法
* Method getDeclaredMethod(String name,Class ... parameterTypes)获取某个方法。
*/
public class ReflectDemo1 {
public static void main(String[] args) throws Exception {
//调用自定义函数
demo();
}
//Method getDeclaredMethod(String name,Class ... parameterTypes)获取某个方法。
//案例:需求:反射Person类中的私有的 静态的成员方法。private static void say()。
public static void demo() throws Exception {
//获取Class对象
Class clazz = Class.forName("cn.itcast.sh.reflect.demo.Person");
//Method[] getDeclaredMethods() 获取所有本类自己声明过的成员方法。不包括父类中的方法
//获取所有的方法
Method[] methods = clazz.getDeclaredMethods();
for (Method m : methods) {
System.out.println(m);
}
/*
* 反射方法say
* 反射的方法不需要接受参数的时候,在反射时参数的类型可以书写成null
* 由于第二个参数是可变参数,我们也可以什么都不写。
*/
// Method method = clazz.getDeclaredMethod("say", null);
Method method = clazz.getDeclaredMethod("say");
/*
* 由于方法是私有的,这时需要取消Java的权限检查
*/
method.setAccessible(true);
/*
* 调用invoke方法,让被反射的方法运行
* 由于这个方法是静态的,运行的时候是不需要对象的
* 这时在调用invoke方法的时候,对象书写null,由于不需要参数,真实的参数也书写成null
* 由于第二个参数是可变参数,我们也可以什么都不写
*/
// method.invoke(null, null);
Object obj = method.invoke(null);
System.out.println(obj);
}
}
代码运行结果如下所示:
2.4 反射获取成员变量(Field)属于扩展内容,自己学习
需求:演示:反射获取成员变量。
Field[] getFields() 获取所有公共的成员变量
Field[] getDeclaredFields() 获取所有成员变量 包括私有成员变量
Field getField(String name) 根据变量名获取指定的公共成员变量
Field getDeclaredField(String name)根据变量名获取指定的成员变量 包括私有成员变量
问题:拿到字段能干嘛?
使用Field 类中的函数获取或修改字段的值:
get(Object obj)获取指定对象上当前字段的值。
set(Object obj,Object value) 将obj对象上此 Field 表示的字段设置为指定的值
获取字段类型:
Class getType() 获取字段的数据类型的Class对象
需求:反射获取Person类中的name、age、address属性值并修改其值。
代码演示如下所示:
/*
* 演示:反射获取成员变量
* Field[] getFields() 获取所有公共的成员变量
* Field[] getDeclaredFields() 获取所有成员变量
* Field getField(String name) 根据变量名获取指定的公共成员变量
* Field getDeclaredField(String name)根据变量名获取指定的成员变量
* 问题:拿到字段能干嘛?
* 使用Field 类中的函数获取或修改字段的值:
* get(Object obj)获取指定对象上当前字段的值。
* set(Object obj,Object value) 将obj对象上此 Field 表示的字段设置为指定的值
* 获取字段类型:
* Class getType() 获取字段的数据类型的Class对象
*/
public class ReflectDemo3 {
public static void main(String[] args) throws Exception {
//创建Person类的对象
Person p = new Person("柳岩",19,"上海");
//根据对象p获得Class类的对象
Class clazz = p.getClass();
//Field[] getFields() 获取所有公共的成员变量
/*
* public int cn.itcast.sh.reflect.demo.Person.age
* public java.lang.String cn.itcast.sh.reflect.demo.Person.address
*/
Field[] fields = clazz.getFields();
for (Field field : fields) {
System.out.println(field);
}
System.out.println("------------------");
//Field[] getDeclaredFields() 获取所有成员变量
/*
* private java.lang.String cn.itcast.sh.reflect.demo.Person.name
* public int cn.itcast.sh.reflect.demo.Person.age
* public java.lang.String cn.itcast.sh.reflect.demo.Person.address
*/
Field[] fields2 = clazz.getDeclaredFields();
for (Field field : fields2) {
System.out.println(field);
}
System.out.println("------------------");
//Field getField(String name) 根据变量名获取指定的公共成员变量
//获取age
Field f = clazz.getField("age");
System.out.println(f);//public int cn.itcast.sh.reflect.demo.Person.age
System.out.println("------------------");
//Field getDeclaredField(String name)根据变量名获取指定的成员变量
Field f2 = clazz.getDeclaredField("name");
//private java.lang.String cn.itcast.sh.reflect.demo.Person.name
System.out.println(f2);
//get(Object obj)获取指定对象上当前字段的值。
int age = (int) f.get(p);
System.out.println(age);//19
//set(Object obj,Object value) 将obj对象上此 Field 表示的字段设置为指定的值
//需求,将年龄19改为18
f.set(p, 18);
int age1 = (int) f.get(p);
System.out.println(age1);//18
System.out.println("------------------");
//暴力修改
f2.setAccessible(true);
f2.set(p, "锁哥");
String name=(String) f2.get(p);
System.out.println(name);//锁哥
//获取字段类型
Class type = f.getType();
System.out.println(type);//int
}
}
2.5 反射的作用案例演示
-
作用
反射是框架的灵魂!框架的底层一定会用到反射技术。
-
需求:要把猫的睡觉方法 变成 狗的吃饭方法
- 效果:使用反射+Properties完成配置文件。把需要修改的灵活的内容写在配置文件中,代码不需要做任何的改动。
- 案例演示
public class Dog {
public void eat(){
System.out.println("狗爱吃肉");
}
public void sleep(){
System.out.println("狗睡觉流口水");
}
}
public class Cat {
public void eat(){
System.out.println("猫爱吃鱼");
}
public void sleep(){
System.out.println("猫睡觉打呼噜");
}
}
public class Demo {
public static void main(String[] args) throws Exception{
//不使用反射
//需求: 要把猫的睡觉方法 变成 狗的吃饭方法
//Dog d = new Dog();
//d.eat();
//使用反射
//properties
Properties pro = new Properties();
//load():可以把文件中的键值对读取到集合中
FileReader fr = new FileReader("day21\\aaa.txt");
pro.load(fr);
//通过键获取值
String cn = pro.getProperty("className");
String mn = pro.getProperty("methodName");
//获取字节码对象
Class c = Class.forName(cn);
//获取空参构造
Constructor con = c.getConstructor();
//执行构造方法
Object o = con.newInstance();
//获取方法
Method m = c.getMethod(mn);
//执行方法
m.invoke(o);
}
}
配置文件:
className=com.itheima_05.Cat
methodName=sleep
第3章 注解
3.1 注解概述
通过前面的演示,判断一个子类方法是否是重写父类的,或者判断一个接口是否是函数式接口,我们都已经使用过类似@Override或者@FunctionalInterface.那么这两个都是什么呢?这就是我们接下来要讲的注解。
-
什么是注解(Annotation)?
-
注解是JDK1.5的新特性,与类、接口是在同一个层次。
-
注解相当一种标记,是类的组成部分,可以给类携带一些额外的信息。
-
标记(注解)可以加在包,类,字段,方法,方法参数以及局部变量上。
-
注解是给编译器或JVM看的,编译器或JVM可以根据注解来完成对应的功能。
-
注解使用格式:@注解名
注解(Annotation)相当于一种标记,在程序中加入注解就等于为程序打上某种标记,以后,javac编译器、开发工具和其他程序可以通过反射来了解你的类及各种元素上有无何种 标记,看你的程序有什么标记,就去干相应的事,标记可以加在包、类,属性、方法,方法的参数以及局部变量上。
-
3.2注解的作用
-
生成帮助信息:通过代码里标识注解,辅助生成帮助文档对应的内容
/** 文档注释 * * @author wuyanzu 作者 * @version 1.0 版本 * */
-
编译检查:通过代码里标识注解,让编译器能够实现基本的编译检查
@Override 检查方法是一个重写方法 @FunctionalInterface 检查接口是一个函数式接口 @SuppressWarnings 表示抑制警告,不知道大家有没有发现,我们所书写的java代码中经常出现一些黄色的标识。 这是一个警告,不是一个错误。但是我们可以通过@SupperssWarings注解来消除相应的警告。被修饰的类或方法如果存在编译警告,将被编译器忽略。 all,忽略所有
代码演示如下:
public class SuppressWarningsDemo implements Serializable{ /* * SuppressWarnings 表示抑制警告 * all 表示忽略所有的警告 */ @SuppressWarnings("all") public void show() { //创建集合对象 List list=new ArrayList(); //向集合中添加数据 list.add("锁哥"); String s=null; s.length(); } }
-
框架阶段做配置
@Test junit的单元测试
3.3 自定义注解
3.3.1 定义—基本语法
上面我们学习了一些官方提供的注解。那么,同样我们开发者可以根据自己的需求来实现自定义注解。
注解和类,接口是属于同一级别。所以创建注解我们需要使用@interface关键字。
自定义MyAnnotation1注解,步骤如下:
说明:
1)
A:定义类: class
B:定义接口:interface
C:定义注解使用关键字: @interface
可以自定义没有属性的注解,这时候我们已经可以使用该注解,可以在我们的方法或者变量上写上@MyAnnotation1。注解的使用格式:@注解名。
只不过此时,当前这个注解只是定义了,没有具体的意义。想要让他有意义。我们需要通过后面的案例来给大家介绍。
新建一个Demo1类来使用上述自定义的注解。
上面我们定义的注解里面没有任何东西,相当于我们定义了一个class,里面没有任何东西。接下来我们学习一下定义带有属性的注解。
3)定义带有属性的注解。
属性声明的格式: 属性类型 属性名() default 属性的默认值;
举例:String value() default “黑旋风”;
int value2() default 19;
- 注解中的属性类型可以是:基本类型、字符串String、Class、枚举、注解,以及以上类型的一维数组。不能是其他的数据类型,比如自定义类型Person。
- 属性名自定义,属于标识符。但是同一注解中属性名也不能相同。
- default 属性的默认值 : 也是可以写,可以不写。
代码如下:
/*
* 定义含有属性的注解
*/
public @interface MyAnnotation1 {
//添加属性
String value() default "呵呵";
}
3.3.2 使用自定义注解
刚刚在上面我们自定义了注解,那么下面我们来使用这些注解。直接创建一个类,在类上写上注解,注解使用的格式如下。
代码如下:我们这里定义的2个注解可以写在类上,可以写在方法上,可以写在变量上。
使用格式:@注解类名( 属性名= 值 , 属性名 = 值 , …)
public class Demo2 {
@MyAnnotation1(x = "哈哈")
@MyAnnotation2(age = 10, names = {"黑旋风","柳岩"}, value = "锁哥")
public void method()
{
System.out.println("method.....");
}
}
注解使用的注意事项:
1.注解可以没有属性,如果有属性需要使用小括号括住。例如:@MyAnnotation1或@MyAnnotation2()
2.属性格式:属性名=属性值,多个属性使用逗号分隔。例如:@MyAnnotation2(age = 10, names = {“黑旋风”,“柳岩”}, value = “锁哥”)
3.如果属性类型为数组,设置内容格式为:{ 1,2,3 }。例如:arrs = {“itcast”,“itheima”}
4.如果属性类型为数组,值只有一个{} 可以省略的。例如:arrs = “itcast”
5.一个方法上,相同注解只能使用一次,不能重复使用。
6.如果属性名为value,且当前只有一个属性,value可以省略。
7.如果使用多个属性时,并且所有的属性都没有默认值。k的名称为value不能省略.
8.如果使用多个属性时,并且所有的属性都有默认值。k的名称为value可以省略.
代码演示:
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String name() default "abc";
String value() default "";
int age() default 0;
}
public class Test05 {
//@MyAnnotation(18)
//给属性value赋值
@MyAnnotation("锁哥")
public void show() {
}
}
注意:上面我们定义了自定义注解,也将注解使用到我们的类或者方法或者变量上面了。但是,可以发现,我们写了这样的注解,并不会产生任何的效果。所以,想要让我们书写的注解产生我们开发者所需要的效果,需要我们开发者使用java代码,对我们的注解进行解析,让这个注解产生我们所期望的功能。也就是说解析就是在解析注解中的属性的值,解析属性的值根据具体的业务功能来使用。所以接下来我们讲解一下注解的解析。
3.3.3元注解介绍
元注解:用于修饰注解的注解。作用:限制定义的注解的特性。
例如:
我们要学习的元注解有两种。
1、@Target 用于确定被修饰的注解使用的位置。
注解属性:
1)ElementType.TYPE 修饰类、接口;
2)ElementType.CONSTRUCTOR 修饰构造方法;
3)ElementType.METHOD 修饰方法;
4)ElementType.FIELD 修饰字段;
对于注解@Override是不能修饰类的,只能修饰方法,而抑制警告注解@SuppressWarnings是可以修饰类、方法等,那么具体哪个注解修饰方法或者类,是由什么决定的呢?其实主要就是由元注解决定的。
先看注解@Override的底层代码:
说明:对于@Override注解只能修饰方法。
再看注解@SuppressWarnings的底层代码:
说明:对于@SuppressWarnings注解可以修饰所有的内容。
总结:所以对于元注解@Target表示修饰的注解作用范围,能够使用在哪里。
案例:需求一:在自定义注解MyAnnotation2上添加一个元注解,要求自定义注解MyAnnotation2只能使用在方法上。
由于自定义注解要求只能使用在方法上,这里使用在类上就会报异常。
案例:需求二:在自定义注解MyAnnotation2上添加一个元注解,要求自定义注解MyAnnotation2只能使用在方法和类上。
2、@Retention 用于确定被修饰的自定义注解生命周期。就是使用这个元注解修饰的注解可以存活到什么时候。
在元注解@Retention中可以使用如下几个属性:
RetentionPolicy.SOURCE 被修饰的注解只能存在源码中,即.java文件中。字节码.class文件中没有。用途:提供给编译器使用。
RetentionPolicy.CLASS 被修饰的注解只能存在源码和字节码中,运行时内存中没有。用途:JVM, java虚拟机使用。
RetentionPolicy.RUNTIME 被修饰的注解存在源码、字节码、内存(运行时)。用途:取代xml配置。(xml属于配置文件的一种,我们后面会学习)注意:因为我们要解析注解,而解析注解需要使用反射技术,要想使用反射技术,那么要求注解必须存在内存中,即在元注解@Retention中需要设置RetentionPolicy.RUNTIME。
注意:上面三个属性表示当使用在元注解中,元注解所修饰的注解能够使用到什么时候。
举例:
上述案例中使用元注解@Retention修饰了注解MyAnnotation2并且设置属性RetentionPolicy.RUNTIME,那么也就是说对于Demo2类中的方法
method上面的注解MyAnnotation2会一直存在包括在内存中,如果把属性换为RetentionPolicy.SOURCE ,那么method方法上的注解只会在.java文件中出现。
3.3.4 解析自定义注解
如果给类、方法等添加注解,如果需要获得注解上设置的数据,那么我们就必须对注解进行解析。
接下来我们就开始解析注解中的属性值:
1)首先定义一个注解,并设置属性值,记住,在开发中这个注解不需要我们自己书写。
说明:自定义的注解要求可以使用在方法和类上。
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation2 {
//自定义属性 定义一个String类型的属性,没有默认值
String value();
//定义int类型的属性,没有默认值
int age();
//定义String类型的数组,没有默认值
String[] names();
}
2)在定义一个Demo2类,然后在这个类中定义方法method,在这个方法上使用注解MyAnnotation2 并给属性赋值。记住,在开发中,这个类是我们自己写的。
public class Demo2 {
@MyAnnotation2(age = 10, names = {"黑旋风","柳岩"}, value = "锁哥")
public void method()
{
System.out.println("method.....");
}
}
3)在定义解析类,用来解析注解上的属性值,注意,在开发中这个解析类是框架底层定义的,不需要我们自己书写。
/*
* 解析注解的类
*/
public class MyAnnotationParser {
//当框架执行时,执行main方法内部的代码
public static void main(String[] args) throws NoSuchMethodException, SecurityException, NoSuchFieldException {
//使用反射技术解析使用了注解的类或者方法
//1.获得类的字节码文件对象
Class clazz=Demo2.class;
//在Class类中存在getAnnotation方法,可以根据注解的字节码获取注解
// MyAnnotation2 ann = (MyAnnotation2) clazz.getAnnotation(MyAnnotation2.class);
// System.out.println(ann.age());
//2.获得使用注解的方法
Method m = clazz.getDeclaredMethod("method");
// //3.获得该方法上的注解对象,与字节码相关的对象上都存在一个getAnnotation方法
MyAnnotation2 ann = m.getAnnotation(MyAnnotation2.class);
//4.获得注解的属性值names={"柳岩","锁哥"}
System.out.println(ann.names());
String[] names = ann.names();
for (String name : names) {
System.out.println(name);
}
//age=18
int age = ann.age();
System.out.println(age);
//value="哈哈"
String value = ann.value();
System.out.println(value);
//5.获得属性值后根据业务需求做一些功能
}
}
说明:在Class类和Method类中都存在方法:T getAnnotation(Class annotationClass) 表示获得当前对象上指定的注解。
3.4案例:自定义@Test
案例分析
步骤:
1.模拟Junit测试,首先需要编写自定义注解@MyTest,并添加元注解,保证自定义注解只能修饰方法,且在运行时可以获得。
2.其次编写目标类(测试类)MyTestDemo,然后给目标方法(测试方法)使用@MyTest注解。
3.最后编写解析类TestParser,使用main方法模拟Junit的右键运行。
案例实现
步骤1:编写自定义注解类@MyTest ,需要使用2个元注解来注解一下。自定义注解代码如下。
@Target(ElementType.METHOD)//只能使用在方法上
@Retention(RetentionPolicy.RUNTIME)//运行时可以获得
public @interface MyTest {
}
步骤2:编写目标类MyTestDemo
自己编写目标类,然后定义3个方法,在其中2个方法上添加我们自定义的注解,代码如下:
public class MyTestDemo {
//定义方法使用MyTest注解
@MyTest
public void test1()
{
System.out.println("test1...");
}
@MyTest
public void test2()
{
System.out.println("test2...");
}
public void test3()
{
System.out.println("test3...");
}
}
步骤3:编写测试方法
思路:首先获得我们MyTestDemo 类中的所有方法,然后循环遍历每个方法,如果方法上有我们自定义的注解的话,我们就执行这个方法,如果没有我们自定义的注解的话,我们就不执行。获取类中的所有方法和执行方法需要用到反射机制中的内容。
补充:获取方法Method类的对象后使用Method类的父类AccessibleObject类中的方法:boolean isAnnotationPresent(Class annotationClass)表示当前对象是否有注解。
/*
* 解析注解的类
*/
public class TestParser {
public static void main(String[] args) throws Exception {
//1.获取测试类的字节码文件对象
Class clazz=MyTestDemo.class;
//2.获取MyTestDemo类中的所有方法
Method[] methods = clazz.getMethods();
//3.遍历所有的方法
for (Method method : methods) {
//判断每个方法上是否含有注解
boolean boo = method.isAnnotationPresent(MyTest.class);
if(boo)
{
//存在注解MyTest,执行该方法
method.invoke(clazz.newInstance());
}
}
}
}
输出结果: