目录
反射的基本使用
1.前言
- 反射是java体系中非常重要的部分,也被称为框架设计的灵魂,框架就是半成品的软件,不能独立运行,开发人员会在框架的基础上进一步软件开发,可以简化代码。(比如不用框架需要1万行代码,用框架做同一款软件可能只需要1千行,其中9千行框架已经写好了,)。框架的设计灵魂就是反射,如果我们将来后期在使用框架时,不会反射也没有关系,因为框架已经写好了,但如果自己要开发一套框架,让别人使用自己的框架编程,就需要深入理解和掌握反射。
- 当然学习反射后,在学习使用框架就比较容易,因为掌握反射后,框架内部原理也就容易掌握,
2.Java代码在计算机中经历三个阶段
第一个阶段:Source源代码阶段
- 写一个类文件Person.java,在类中定义成员变量,成员方法和构造器,这个java文件就写完了,
- 这个文件不能独自运行,需要javac命令对文件进行编译,然后在硬盘上生成字节码文件Person.class,
- 字节码文件中主要是成员变量、构造器和成员方法等,
- 这就是java代码在计算机中经历的第一阶段称Source源代码阶段,
- 这个阶段java代码没有进内存,还是在硬盘上存储着。
第二个阶段:Class类对象阶段
- 将字节码文件通过类加载器ClassLoader加载到内存,在内存中通过Class类对象来描述字节码文件中内容,
- Class类是java中的一个类,它功能是描述所有字节码文件(物理文件)的共同特征和行为,
- 只要是字节码文件就有共同的属性,主要包括成员变量、成员方法和构造方法三个部分等,这三个部分都有各自特征,
- 成员变量可以获取值设置值,构造方法可以用来创建对象,成员方法可以运行执行,
- 所以把类中这三个部分分别封装成对象,把成员变量部分封装为Field对象,把构造方法部分封装为Constructor对象,把成员方法封装为Method对象
- 在字节码文件中可能不止一个成员变量,所以在Class类对象中使用Field对象数组来描述所有的成员变量,数组类型就是Field对象,数组中元素都是Field对象。
- 同理构造方法Constructor[] cons和成员方法Method[] methods,
- 这就是Class类对象组成成分,将来就可以通过这个Class类对象的一些行为来创建对象,这就是java代码在计算机中经历的第二阶段。
- 这也就是反射机制,将类的各个组成部分封装为其它对象。
第三个阶段:Runtime运行时阶段
- 该阶段主要是创建对象new Person
2.反射机制及好处
- 反射:框架设计的灵魂。
- 框架:就是个半成品软件。可以在框架的基础上进行软件开发,从而简化编码。
反射:
- 就是将类中各个组成部分封装为其它对象,可以管理未来未知的对象和类,这就是反射机制。
- JAVA反射机制是在运行状态中,获取任意一个类的结构 , 创建对象 , 得到方法,执行方法 , 属性 !; 这种在运行状态动态获取信息以及动态调用对象方法的功能被称为java语言的反射机制。
反射好处:
- 1.可以在程序运行过程中,(在内存中)通过class对象操作这些对象。
- 比如,在软件(应用程序/IDE)中定义String str="abc"字符串对象str,我们可以使用str对象加点运算符即"str."来调用String类中的方法,那么这些方法从哪里来的,怎么知道字符串中有这些方法,其实内部就是用了反射机制,在定义字符串String str="abc"时,就会把String类的字节码文件加载到内存,在内存中的一个Class类对象已经把字节码文件中所有方法抽取出来封装为Method对象,存入Method类型数组中,当调用数组方法时,就把这些方法展示到列表中。IDE程序一直在运行过程,我们却可以知道str中有哪些方法。
- 2.可以解耦(将一个整体分成多个部分),提高程序的可扩展性。
- 3.打破封装,获取私有内容。
3.类加载器
3.1.功能
- Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分, 负责动态加载Java类到Java虚拟机的内存空间中。
3.2.Java默认三种类加载器
- BootstrapClassLoader(引导启动类加载器)
- 嵌在JVM内核中的加载器,该加载器是用C++语言写的,主要负载加载JAVA_HOME/lib下的类库,引 导启动类加载器无法被应用程序直接使用。
- ExtensionClassLoader(扩展类加载器)
- ExtensionClassLoader是用JAVA编写,且它的父类加载器是Bootstrap。 是由sun.misc.Launcher$ExtClassLoader实现的,主要加载JAVA_HOME/lib/ext目录中的类 库。 它的父加载器是BootstrapClassLoader
- App ClassLoader(应用类加载器):
- App ClassLoader是应用程序类加载器,负责加载应用程序classpath目录下的所有jar和class文 件。它的父加载器为Ext ClassLoader
- 类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与 文件系统。
3.3.双亲委派模型
面试题:多个类加载器如何避免类被重复加载?
- 如果一个类加载器收到了一个类加载请求,它不会自己去尝试加载这个类,而是把这个请求 转交给父类加载器去完成。
- 每一个层次的类加载器都是如此。因此所有的类加载请求都应该传递到最顶层的 启动类加载器中,只有到父类加载器反馈自己无法完成这个加载请求(在它的搜索范围没有找到这个类) 时,子类加载器才会尝试自己去加载。
- A将加载请求转交给E,E再转交给B,若B能加载就加载,不能则B转交给E来加载,E再不能加载就A来。
- 委派的好处就是避免有些类被重复加载。
3.获取Class类对象三种方法
class类对象-字节码文件对象-管理字节码文件-将字节码文件内容分别封装成不同对象
我们知道反射机制就是将一个类中各个组成部分封装为其它对象,要获取使用这些对象,关键要获取Class类对象,
下面就聊聊获取内存中字节码Class对象的三种方法:
1.Class.forName("全类名"):
- 将字节码文件加载进内存,返回Class对象。
- 该Class类中静态方法forName只适用于java程序第一阶段Source源代码阶段,
- 即程序还未进内存,只是字节码文件。另外全类名只包名加类名(即src下开始)
2.类名.class:
- 通过类名的class属性获取
- 该方法只适用于Java程序第二阶段Class类对象阶段,此时字节码文件已经加载到内存,
3.对象.getClass():
- 该方法在Object类中定义的,所有对象都可以调用。
结论:
- 同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,无论通过哪一种方式获取的Class对象都是同一个。(在各个阶段获取的都是同一个)
- 上述的三种方式, 在调用时, 如果类在内存中不存在, 则会加载到内存 ! 如果类已经在内存中存在, 不 会重复加载, 而是重复利用 !
首先创建domain包,在包中创建实体类Person,然后在反射包reflect中写测试类
package reflect;
import domain.Person;
import domain.Student;
public class Demo01Reflect {
public static void main(String[] args) throws Exception {
//注意:当在domain包中定义完Person类保存后就会自动编译为Person.class字节码文件
//1. Class.forName("全类名"):将字节码文件Person加载进内存,返回Class对象
// 注意该方法经常抛异常ClassNotFoundException,即全类名有问题,
// 建议全类名通过复制包名和类名组成,不要自己手写。
Class aClass = Class.forName("domain.Person");
System.out.println(aClass);//输出class domain.Person
//2. 类名.class:通过类名的属性class获取Class对象
Class aClass1 = Person.class;
System.out.println(aClass1);//输出class domain.Person
//3.对象.getClass():通过Object类中的定义的getClass方法(任何对象都可以使用)
Person p = new Person();
Class aClass2 = p.getClass();
System.out.println(aClass2);//输出class domain.Person
/*注意上述三个方法获取Class对象,都是Person.class字节码文件在内存中生成
的Class对象,输出对象的字符串形式是一样的,也是同一个Class对象,
下面来验证
*/
//使用==来比较,比较的是对象地址
System.out.println(aClass==aClass1);//true
System.out.println(aClass==aClass2);//true
/*结论:同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,
无论通过哪一种方式获取的Class对象都是同一个。
*/
//每一个字节码文件对应的Class对象都不相同,下面是Person和student两个
//字节码文件对应的Class对象比较
Class aClass3 = Student.class;
System.out.println(aClass==aClass3);//false
}
}
4.Class类对象使用
Class对象功能: 该对象中的常用方法的使用
1.获取成员变量们
Field[] getFields():获取所有public修饰的成员变量到Field对象数组中,不需指定对象
Field getField(String name):获取指定的public变量(Field类型),不需指定对象
Field[] getDeclaredFields():获取所有的成员变量,不考虑修饰符--反射技术打破封装,可以获取私有
Field getDeclaredField(String name):获取指定的成员变量,不考虑修饰符
获取Class对象中的Field后,可进行设置值和获取值
Field对象操作:
1.设置值:void set(Object object,Object value)指定给哪个对象设置什么样变量值p.set(obj,value)
2.获取值:get(Object obj)需要指定是获取哪个对象的变量值p.get(obj)
3.忽略访问权限修饰符的安全检查(暴力反射)setAccessible(true)
2.获取构造方法们
Constructor<?>[] getConstructors()获取所有public类型的无参数构造方法存入Constructor对象数组中
Constructor<T> getConstructor(类<?>...parameterTypes)获取指定参数类型的构造方法
Constructor<?>[] getDeclaredConstructor()获取所有的构造方法存入对象数组中,不限修饰符
Constructor<T> getDeclaredConstructor(类<?>...parameterTypes)获取指定参数类型构造方法,不限制修饰符
在访问私有类型构造器会抛出异常,需要忽略访问权限修饰符的安全检查(暴力反射)
setAccessible(true)
Class对象获取构造器后,创建对象
获取到构造方法(即构造器对象)后就是使用构造方法创建对象了
构造器对象中的创建对象方法是: newInstance(Object...initargs)
构造方法的对象调用该方法并传递参数来创建对象,
但若是空参构造方法创建对象时可以简化上述创建构造器然后调用构造器newInstance(Object...initargs)
方法创建对象
在Class对象中也有newInstance()方法,不过只能创建无参数构造方法的对象
Object person=personClass.newInstance();
3.获取成员方法们
Method[] getMethods()获取所有public类型的成员方法存入Method对象数组中
Method getMethod(String name,类<?>...parameterTypes)
获取指定的方法名name和参数的成员方法 (方法名+参数=确定方法)
Method[] getDeclaredMethods()获取所有的成员方法存入对象数组,不考虑修饰符
Method getDeclaredMethod(String name,类<?>...parameterTypes)
获取指定方法名和参数的成员方法,不考虑修饰符
setAccessible(true)
获取到Method对象(即方法对象)后,调用invoke(obj,args)方法来执行这个方法,
Object invoke(Object obj,Object...args):传递指定对象和参数来执行方法
调用getName()方法来获取方法对象的方法名
String getName();
获取类名的方法:获取的是全类名(包名+类名)
String className=personClass.getName();
4.获取类名
String getName()
5.Demo
以下统一使用的Person类:
package domain;
//domain实体类
public class Person {
private String name;
private int age;
public String a1;
public String a2;
protected String b;
String c; //默认私有
private String d;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person() { }
public String getA1() { return a1; }
public void setA1(String a1) { this.a1 = a1; }
public String getA2() { return a2; }
public void setA2(String a2) { this.a2 = a2; }
public String getB() { return b; }
public void setB(String b) { this.b = b; }
public String getC() { return c; }
public void setC(String c) { this.c = c; }
public String getD() { return d; }
public void setD(String d) { this.d = d; }
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", a1='" + a1 + '\'' +
", a2='" + a2 + '\'' +
", b='" + b + '\'' +
", c='" + c + '\'' +
", d='" + d + '\'' +
'}';
}
public void eat(){
System.out.println("eat....");
}
public void eat(String food){
System.out.println("eat...."+food);
}
}
Class对象获取Field变量对象以及Field对象自身方法使用
package reflect;
import com.sun.org.apache.bcel.internal.classfile.ClassParser;
import domain.Person;
import java.lang.reflect.Field;
public class Demo02Reflect {
public static void main(String[] args) throws Exception {
//0.获取Person类的Class对象(字节码文件对象)
Class personClass = Person.class;
//1.Field[] getFields():获取所有public修饰的成员变量到Field对象数组中
Field[] fields = personClass.getFields();
for (Field f:fields){
System.out.println(f);
}
/*public java.lang.String domain.Person.a1
public java.lang.String domain.Person.a2
*/
System.out.println("1.................");
//2.Field getField(String name):获取指定的public成员变量a1
Field a1 = personClass.getField("a1");
System.out.println(a1);//public java.lang.String domain.Person.a1
System.out.println("2.................");
/**3.
* 获取(指定对象obj的)成员变量的值: *get(Object obj)
* 获取Person类中成员变量a1的'值',初始化变量是null,需要指定哪个对象
*/
//对变量操作时(获取/设置值)要指定是哪个对象的变量,所以要创建对象
Person p = new Person();
Object value = a1.get(p);//指定获取p对象中的a1变量的值
System.out.println("初始化时成员变量a1的值:"+value);//null
System.out.println("3.................");
/**4.
* 设置(指定对象的)成员变量的值:*void set(Object object,Object value)
* 设置p对象中成员变量a1的值为:韩非
*/
a1.set(p,"韩非"); //给p对象中a1变量设置值
Object value1=a1.get(p);//获取p对象中a1变量的值
System.out.println(value1);//韩非
System.out.println(p);
/*此时的p对象中成员变量如下
Person{a1='韩非', a2='null', b='null', c='null', d='null'}
*/
System.out.println("4.................");
/**5.
*Field[] getDeclaredFields():获取所有的成员变量,不考虑修饰符
*Field getDeclaredField(String name):获取指定的成员变量,不考虑修饰符
*这些方法都是Class对象personClass中的方法
*/
//Class对象调用getDeclaredFields()方法获取所有成员变量存入Field对象数组中
Field[] declaredFields = personClass.getDeclaredFields();
for (Field f:declaredFields){ //遍历数组输出所有变量
System.out.println(f);
}
/*以下为Person类中所有成员变量(不考虑修饰符)
public java.lang.String domain.Person.a1
public java.lang.String domain.Person.a2
protected java.lang.String domain.Person.b
java.lang.String domain.Person.c
private java.lang.String domain.Person.d
*/
//Class对象调用Field getDeclaredField(String name)方法获取指定变量d
//并访问(获取)这个私有变量d,注意私有变量是无法在类外被访问的,这里需要
//使用d.setAccessible(true)方法暴力反射,忽略访问权限修饰符的安全检查
//否则抛出IllegalAccessException异常
Field d = personClass.getDeclaredField("d");
//忽略访问权限修饰符的安全检查
d.setAccessible(true);//暴力反射
Object value2 = d.get(p);//获取对象p的私有变量d的值
System.out.println("对象p的私有变量d的值:"+value2);//null
}
}
Class对象获取Constructor构造器对象以及该对象自身方法使用
package reflect;
import domain.Person;
import java.lang.reflect.Constructor;
public class Demo03Reflect_Cons {
public static void main(String[] args) throws Exception {
//0.获取Person类的Class类对象
Class personClass = Person.class;
/**1.Constructor<T> getConstructor(类<?>...parameterTypes)
* java 反射机制 之 getConstructor获取有参数构造函数(是个对象)
*/
Constructor constructor = personClass.getConstructor(String.class,int.class);
System.out.println(constructor);//public domain.Person(java.lang.String,int)
//构造方法的对象constructor调用newInstance方法并传递参数来创建对象
Object person = constructor.newInstance("韩非", 22);
System.out.println(person);
System.out.println("..................");
//输出person对象,其中name,age成员变量有值
//Person{name='韩非', age=22, a1='null', a2='null', b='null', c='null', d='null'}
/*2.Constructor<T> getConstructor()
java 反射机制 之 getConstructor获取无参数构造函数(是个对象)*/
Constructor constructor1 = personClass.getConstructor();
Object person1 = constructor1.newInstance();
System.out.println(person1);
System.out.println("..................");
//无参数构造方法创建的对象
//Person1{name='null', age=0, a1='null', a2='null', b='null', c='null', d='null'}
//使用Class对象中newInstance方法获取无参数构造器的对象
Object person2 = personClass.newInstance();
System.out.println(person2);
//无参数构造方法创建的对象
//Person{name='null', age=0, a1='null', a2='null', b='null', c='null', d='null'}
}
}
Class对象获取Method方法对象以及该对象自身方法使用
package reflect;
import domain.Person;
import java.lang.reflect.Method;
public class Demo04Reflect_Method {
public static void main(String[] args) throws Exception {
//0.获取Person类的Class对象
Class personClass = Person.class;
/**1.
* Method getMethod(String name,类<?>...parameterTypes)
* 获取指定的方法名name和参数的成员方法 (方法名+参数=确定方法)
* Object invoke(Object obj,Object...args):传递指定对象和参数来执行方法
*/
//使用CLass对象中getMethod方法获取无参eat()成员方法
Method eat = personClass.getMethod("eat");
//使用Method对象中invoke方法传递指定对象来执行这个Method
Person person = new Person();
eat.invoke(person);//输出eat....
//使用CLass对象中getMethod方法获取含参eat(String food)成员方法
Method eat1 = personClass.getMethod("eat", String.class);
//使用Method对象中invoke方法传递指定对象和参数来执行这个Method
eat1.invoke(person,"苹果");//输出eat....苹果
System.out.println("....................");
/**2.
* Method[] getMethods()
* 获取所有public类型的成员方法存入Method对象数组中
* getMethods方法获取的不仅是Person类中所有public方法,
* 还有Object类中的一些方法
*/
Method[] methods = personClass.getMethods();
for (Method m:methods){
System.out.println(m); //输出所有的方法对象Method(每一个都是一个方法的封装)
System.out.println("方法名:"+m.getName());//输出方法对象中的方法名
}
/**3.
* 获取类名的方法,获取的是全类名(包名+类名)
* String className=personClass.getName();
*/
String name = personClass.getName();
System.out.println("类名为:"+name);//类名为:domain.Person
}
}
5.反射案例
需求:
- 写一个"框架",在不改变该类的任何代码的前提下,可以帮我们创建任意类的对象,并且执行其中的任意方法
分析:
- 像之前创建对象调用方法Person p=new Person();p.eat()这种操作经常做,这种操作的弊端是:如果将来把该类当做框架,框架是半成品软件,是提前写好,不允许改变的,框架的前提是不能改变该类的任何代码,但可以创建任意类的对象,可以执行类中任意方法。若是按上述,则每创建一个类对象就要修改一次程序,就不是框架了。
- 下面使用配置文件来定义全类名和要执行的方法名,若方法有参数也可以定义,然后使用反射来获取Class对象中的方法对象Method,从而获取字节码文件中要执行的方法
现Person类中的eat()方法和Student类中sleep()方法,要求只修改配置文件中的className全类名和methodName方法名来创建对象调用方法。
实现所需技术:
- 1.加载配置文件
- 2.反射
实现步骤:
- 1.将需要创建的对象的全类名和需要执行的方法定义在配置文件pro.properties
className=domain.Person
methodName=eat
- 2.在程序中加载读取配置文件pro.properties
- 3.使用反射技术来加载类文件进内存,获取Class对象
- 4.Class对象获取构造方法创建对象
- 5.执行方法
改java代码和改配置文件的区别:
- 如果将来写的系统非常庞大的话,如果代码被改动,就需要重新测试,
- 如果修改配置文件就不需要修改代码,也可以提高程序扩展性。
- 将来很多地方都会使用配置文件,遇到文件中设置了全类名,就需要知道用了反射。
package reflect;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Properties;
public class Demo05Reflect_Test {
public static void main(String[] args) throws Exception{
//1.加载(读取)配置文件
//1.1.创建Properties对象
Properties pro = new Properties();
//1.2加载配置文件,转换为一个集合(双列Map集合)
//1.2.1.获取class(类)目录下的配置文件(即获取文件路径)
//首先通过类的class属性获取字节码文件,然后获取类加载器
ClassLoader cl = Demo05Reflect_Test.class.getClassLoader();
//类加载器可以找到类(class)目录下的class(字节码)文件,也可以找到配置文件
//类加载器中有个方法getResource(String name)获取资源的路径
//还有一个方法getResourceAsStream(String name)直接获取这个资源对应的字节流
//pro对象调用load()方法加载配置文件时就需要传递字节流。
//properties文件放置在src文件下
InputStream is = cl.getResourceAsStream("pro.properties");
pro.load(is);
//2.获取配置文件中定义的数据
String className = pro.getProperty("className");//获取全类名
String methodName = pro.getProperty("methodName");//获取方法名
//3.加载该类进内存,获取Class对象
Class cls = Class.forName(className);
//4.创建对象(空参构造方法的对象)
Object obj = cls.newInstance();
//5.获取方法对象(空参方法)
Method method = cls.getMethod(methodName);
//6.执行方法
method.invoke(obj);
}
}