一.反射概述
1.我们为什么要用反射
在我们编写程序时,如果我们要用到一个类而这个类却没有完成,如果我们直接调用这个类是会报错的,JAVA为我们提供了一个方法就是反射,利用这个反射功能我们就可以在这个类没有被完成时而对他进行调用。
这个反射机制被应用在框架的编写上,框架就好比是一座房子的最初模型,我们的任务是要建立一所房子的框架,就是毛坯房,这时候我们不知道这所毛坯房要被装修成什么风格的家,是欧式的,还是中式的,是小清新还是杀马特,但是我知道这个家里必须得有门,有窗,所以我给他们留了装门和窗的地方,至于以后是装什么风格的门和窗那不是我要考虑的问题,如果我都给按照我的风格装修好了,他如果不认账怎么办,那我不就逗比了,付出了心血却看不到银子,为了解决这个问题,我把房子做成了毛坯房。
在上面的例子中,毛坯房就是框架,而门和窗就是我们会使用到的类,当我们在调用没有完成的类时就要使用反射。
2.什么是反射
度娘告诉我们反射就是JAVA反射机制是在运行状态时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制,(我要是第一次读的时候我是读不下去的)。
按照我的理解,我们在没有接触反射这个东东的时候,我们写的程序如果能运行,必须确定我们写的java类能被JAVA虚拟机加载,而现在我们要用到的类都没有被创建出来,也就是这个类不可以直接new对象出来,我们怎么办,反射就出来拯救世界了,他告诉我们说你要用到的类没办法被虚拟机加载没有关系,我可以让他直接运行,我给你提供方法,你编译时没有被虚拟机加载,你运行时确定了你要使用的类就ok,你只要运行了,你就可以获得这个类的各种属性和方法,换句话说,虚拟机在编译时没有加载你的类,但是他加载了反射机制,通过反射机制获取到了类中的属性与方法。
张老师告诉我们“反射就是把java类中的各种成分映射成相应的java类”,这句话没有提到编译和运行时的区别,但是把反射要做的事情明确的告诉了我们,我听到这句话时,想到了万物皆对象,java类是为了描述对象而创建的吧,比如Person类是描述人的,String类是描述字符串的,那么Person和String等等这些类可不可以看做是对象呢,当然是可以的,那么描述对象的类亦是一种对象,我们就可以对这些类进行分析与描述,比如类都有字段、构造函数、方法等等,这就是反射能回馈给我们的东西。
总而言之反射就好比一面X光镜子,通过这个镜子,我们可以看到类的各种成分,类的内脏是字段,类得手是方法,类的头是包,把类拆的七零八落然后再分类。
二. 反射要用到哪些类
1.反射的基石——Class
(1)上面说到描述对象的类也可以被看做对象,那么描述类的类就是Class,注意这个Class是大写的C,而我们平常用于定义类时写的class是小写的c。Class类是各个类的共性向上抽取而来的,一个类的属性包括:Filed(字段),Method(方法),Construction(构造函数),Packeage(包)等。
(2)Class是描述类的,我们怎么才可以获得他的实例对象呢,首先我们要先明确一点就是Class类的实例对象是各个类的字节码,字节码就是各种类被我们编译后生成的class文件,这个class文件就是一堆的二进制码,也叫字节码,这个字节码就是Class的实例对象。以往我们获取对象的方式是new;例如Person p = new Person();但是Class类是不可以直接new对象出来的,也就是只有通过其他方法获取,获取Class实例对象有三种方式:
第一种:
利用对象获取所属类的字节码:对象.getClass()方法获取,例:
Persong p = new Person();
Class c1 =p.getClass();
这种方式是我们必须确定该类可以被加载且能创建对象。
第二种:
利用类的静态属性class来获取:类名.class,例:
Classc2 = Person.class;
这种方式我们必须确定该类已经被加载,也就是该类必须是已经存在的,因为class是他的属性,要不然“.class”是不可以使用的
第三种:
利用Class的静态方法获取:Class.forName(”完整类名”),例:
Classc3 = Class.farName(“java.lang.String”);
这种方式最为简单因为他不需要确定该类是否已经被加载,是否可以创建出对象,注意,该方法也是需要该类可以被加载的,但是这个方法的好处在于你的类如果被虚拟机加载了那我直接返回他的字节码,如果没有我再调用虚拟机把他加载在进来,我只需知道他的名字就可以,结合我们前面说的内容,可以看出这种方式是最靠谱的,也是使用最多的方式。
(3)9个预定义Class
预定义的9个数据类型:8个基本数据类型和关键字void,他们都被定义成了Class对象。
8个基本数据类型: byte
、short
、int
、long
、float
、double
、char
、boolean
,他们可以直接调用”.class”方法来获取字节码。
注意点:int.class是不等同于Integer.class的,因为基本数据类型是一种类型,其对应的包装类是另外一种类型,他们所对应的字节码是不同的。我们可以利用Class的方法isPrimitive()来判断是否是基本数据类型。
在基本类型的包装类中专门定义了一个常量叫TYPE,他代表着包装类中所包含的其对应的基本数据类型的字节码,这时候int.class 和Integer.TYPE就是一样的了,通过此处我觉得这也是获取Class对象的方法,只是只有基本数据包装类才可以使用。
void:void我们的理解就是没有返回值,他和数据类型的地位是一样的,你可以返回值是int,但是也可是void,JAVA也把他定义成了Class对象:void.class,所以void.class.isPrimitive()返回值是true。
(4)数组类型的Class
在java程序中只要是类型便有其对应的Class对象,int[],String[],int,void等都是类型,如果我们要判断一个类型是不是数组类型可以调用Class.isArray()方法来完成。
public class Reflect_Test1 {
public static void main(String[] args)throws Exception {
/*
* 首先我们演示一下获取Class实例对象的三种方式
* */
//第一种:对象获取
String str1 = "小宏";
Class c1 = str1.getClass();
//第二种:类的静态属性获取
Class c2 = String.class;
//第三种:Class类静态方法forName()获取,注意此处需要抛异常
Class c3 = Class.forName("java.lang.String");
//打印一下我们获取到的三个Sting类的实例对象也就是字节码是不是同一个
System.out.println(c1 == c2);//true
System.out.println(c1 == c3);//true
/*
* 演示一下基本数据类型和其包装类的Class实例对象是否相同
* isPrimitive()方法用于判断Class对象是否为基本数据类型
* TYPE表示包装类中所包含的基本数据类型的字节码
* */
//判断String是否是基本数据类型
System.out.println(c1.isPrimitive());//false
//判断double的字节码和Double包装类的字节码是否相同
System.out.println(double.class == Double.class);//false
//判断double字节码和Double.TYPE是否相同
System.out.println(double.class == Double.TYPE);//true
/*
* Class方法isArray()判断是否为数组类型
* */
System.out.println(String[].class.isArray());//true
}
}
(5)java中获取类中各个器官的方法
①获取类中的字段(Field)
1> getFileld(String name)
返回一个指定的Field对象,表示这个Class对象中所包括的公共的字段,非private的
2> getDeclaredFiled(String name)
返回一个指定Field对象,可以是私有的字段
3> getFields()
返回一个包含Filed对象的数组,包含所有的可访问的公共字段
4> getDeclaredFileds()
返回一个Field数组,包含了Class对象中所有的字段,无论是公共的还是私有的
getFields返回的是申明为public的属性,包括父类中定义
getDeclaredFields返回的是指定类定义的所有定义的属性,不包括父类的。
②获取类中的构造器(Constructor)
1> getConstructor(Class<?>...parameterTypes)
返回一个类的指定的Constructor对象,表示该类中公共的构造方法
2> getDeclaredConstructor(Class<?>…parameterTypes)
返回一个类中指定的Constructor对象,无论是私有的还是公共的
3> getConstructors()
返回一个类中公共的Constructor对象数组
4> getDeclaredConstructors()
返回一个类中所有的Constructor对象数组
③获取类中的方法(Method)
1> getMethod(Stringname,
Class
<?>
...parameterTypes
)
获取类中指定的公共方法也就是Method对象
2> getDeclaredMethod(String name,
Class
<?>
...parameterTypes
)
获取类中指定的方法,无论私有还是公共
3> getMethods()
获取类中公共的方法数组Method[]
4> getDeclaredMethods()
获取类中所有的方法数组,无论公共还是私有
2.Constructor类——构造器
(1)Constructor这个类是描述构造器的,而我们用构造器可以干什么呢,Constructor类给我们提供了很多方法,但是其中最为有用的方法叫做:
newInstance(Object..initargs)。
这个方法是可以用来是创建实例对象的,具体流程就是我们首先用Class获取某一个类的Constructor对象(这个类的构造器),再利用这个Constructor来建立该类的实例对象。
(2)在Class类的方法中我们知道获取一个类的构造器方法有4种,以获取String类中指定的构造器String(newStringBuffer()),然后利用这个构造器创建一个String对象来说明Constructor类的用法:
public class ConstructorTest1 {
public static void main(String[] args) {
/*
* 我们要新建一个String对象,其构造方法的参数是StringBuffer,以往我们的做法是
* String str1 = new String(new StringBuffer("abc"));
* 那么我们如何利用反射的方式来做呢?
*
* Constructor类是描述类的构造方法的,我们首先演示获得
* 指定构造器的方法:
*getConstructor(Class<?>... parameterTypes)
*
*如何来确定我们获得哪一个构造方法,一个类中构造方法的区别在于参数的不同
* 所以我们可以根据指定参数来获得该构造器
*
* 在java1.5版本后的新特性之一就是可变参数,也就是在getConstructor(Class<?>... parameterTypes)
* 方法中你可定义若干个Class对象来获取构造器,在1.5版本之前是通过传入Object数组来控制参数个数
* */
try {
//首先我们先拿到Stirng类的字节码
Class clsString = Class.forName("java.lang.String");
//获取String类的StringBuffer构造器
Constructor cons1 = clsString.getConstructor(StringBuffer.class);
/* 得到了构造器我们可以用该构造器的方法newInstance()方法来建立对象
*这个地方有一个注意点就是我们在编译时是不知道cons1对应的是哪个类的哪个构造方法的
*只有在运行时才可以知道,所以我们要在newInstance中定义该构造方法的参数对象,
*并且给他打一个该类的标示才可以通过编译
**/
String str1 = (String)cons1.newInstance(new StringBuffer("heima"));
System.out.println(str1);//heima
//总结就是得到构造器的时候需要参数类型,
//用该构造器生成对象的时候也需要同样类型的对象
} catch (Exception e) {
throw new RuntimeException("无法获取该类的字节码!");
}
}
}
现在我们来看一看这个看似没有几行的代码需要我们注意的地方:
① 利用String类字节码获取Constructor对象时需要参数类型
② 用Constructor对象建立String对象时需要同类型的对象
③ 要建立的对象实在运行时被创建的,为了编译通过要给他加一个该类的标识
(3)利用Class类建立我们所需类的实例对象
上面我们创建String对象的方法是是利用Class获取String类的字节码,利用其字节码获取构造器,利用构造器再去创建对象,在Class类中也有newInstance()方法,我们可以直接利用Class类来创建所需类的实例对象:
Class clasString = Class.forName("java.lang.String");
String str2 = (String)clasString.newInstance();
这种方式的局限性在于类的构造方法必须是空参的
3.Field类——字段
(1)Field类是用来描述类中的字段,也就是类中的成员变量。
获取该类对象的方法在Class类中已经提及,我们自定义一个类Person来演示如何获得Field类的应用:
package edu.reflct;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
class Persons
{
public int age;
private String name;
//public一定要写
public Persons(int age, String name) {
super();
this.age = age;
this.name = name;
}
}
class Test1 {
public static void main(String[] args) {
try {
//首先创建出一个Person对象
Class c1 = Class.forName("edu.reflect.Persons");
Constructor consPersons = c1.getConstructor(int.class,String.class);
Persons p1 = (Persons)consPersons.newInstance(new Integer(23),new String("小宏"));
//获取Person类中age字段,这个字段不代表某一个值,因为他不是对象所拥有的
Field fa = c1.getField("age");
//age这个变量在p1这个对象的值是多少呢,我们要利用Filed方法get()来取到
System.out.println(fa.get(p1));
//我们的name字段是私有的,我们要获取指定私有字段,方法是getDeclaredField()
Field fn = c1.getDeclaredField("name");
//我们想要获取一个私有字段在对象上的值,必须先设置成可以强制访问
fn.setAccessible(true);
System.out.println(fn.get(p1));
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*打印:
* 23
小宏
* */
我们来总结一下Field类的用法:
一:我们拿到所需类的字节码
二.通过getField(“变量名”)方法来获取所需类中我们指定的公共字段,注意这个字段是类所有的,他在不同的类对象身上有着不同的值
三.我们如果想看在某个所需类的对象上面变量对应的值是多少,利用Field类的方法get(对象名)来获取到。
四.如果想要获取所需类中的私有字段,通过getDeclaredField(“变量名”)来获取到,这个字段同样是属于类的,不代表某一个具体值,我们如果想看某个对象上这个私有字段的值,首先要把这个字段设置成可以访问的,也就是暴力访问:
SetAccessible(true);
(2)我们通过一个小程序来更好的理解Field的用法
需求是:将任意一个对象中所有的String类型的成员变量所对应的字符串内容中的’x’改为’y’。
//我们自定义一个类Person
class Person
{
public int age ;
private String name;
public String str1;
public String str2;
public String str3;
public Person(int age, String name, String str1, String str2, String str3) {
super();
this.age = age;
this.name = name;
this.str1 = str1;
this.str2 = str2;
this.str3 = str3;
}
@Override
public String toString() {
return "Person [age=" + age + ", name=" + name + ", str1=" + str1
+ ", str2=" + str2 + ", str3=" + str3 + "]";
}
}
public class ReflectField {
public static void main(String[] args) {
try {
//为了演示方便,就直接new对象出来了
Person p2 = new Person(23,"xiaohong","xiaolv","xiaoxiao","xxx");
//获取到Person类中所有的字段
Field[] fString = Person.class.getDeclaredFields();
//遍历字段
for(Field fstr:fString)
{
//判断该字段是否是String类型的,利用Fidld类的getType()方法获取类型
//此处用 == 判断而不用equals是因为字节码是唯一且确定的
if(fstr.getType() == String.class)
{
//拿到String类型的字段后我们首先暴力获取一下
fstr.setAccessible(true);
//然后我们拿在对象上该字段具体的值
String oldValue = (String)fstr.get(p2);
//把该值中的'x'替换成'y'
String newValue = oldValue.replace('x','y');
//把新的值重新写入到对象中
fstr.set(p2,newValue);
}
}
System.out.println(p2);
//Person [age=23, name=yiaohong, str1=yiaolv, str2=yiaoyiao, str3=yyy]
} catch (Exception e) {
e.printStackTrace();
}
}
}
在这个例子中当有我们没有了解过Field的两个方法,一个是getType()获取类型,一个是set(obj,value)给对象设置新的变量,有很多方法是我没有学过的,但是只要我学会查阅JDK文档,很多问题就能解决了,我觉得拥有快速而有效的找到解决问题思维才能成为一个好的程序猿。
4.Method类——方法
(1)我们可以通过反射来获取一个类中的方法,以前我们使用类中的方法都是利用类直接使用(静态方法)或者利用对象调用方法,而现在我们学习了反射,把方法拆解出来单独构成了一个类,那么我们使用这个方法的时候,就是利用方法来调用对象,这是一个顺序问题,以前是对象调用方法,现在是方法调用对象。
Method 方法:
invoke(Object obj,args);
这个方法便是利用方法类调用对象来实现功能。obj代表哪个对象使用,args代表这个方法的参数,用来区分重载方法。
注意点:如果obj为null的话代表这个方法是静态的,因为invoke ()方法是Method类的方法,Method是其他类通过反射抽离出来的,如果这个方法没有通过对象就可以调用,那么只有一种解释就是这个方法是静态的,无需通过对象。
class MetDemo1 {
public static void main(String[] args) {
try {
//获取Sting类的字节码
Class c1 = Class.forName("java.lang.String");
//获取String类里面的指定方法charAt(int index)
Method mCharAt = c1.getMethod("charAt", int.class);
char c = (char)mCharAt.invoke(new String("abc"), 2);
System.out.println(c);
} catch (Exception e) {
e.printStackTrace();
}
}
}
(2)利用反射的方式调用main方法
首先我们要想一下为什么要用反射方式调用一个类的main方法?
我们知道反射最大的好处就是在类可以不用加载而直接在运行的时候调用,当一个类没有完成时,我们是不可以直接调用他的main方法的,但是我们连这个类的名字都不知道怎么办?我们可以在执行我们程序的main方法时,把我们要启动的那个类名作为一个字符串名字传递进来:Eclipse中的做法是右键单击选择Run As-->RunConfigurations-->Arguments,在Program arguments中把要运行的类名添加到里面,点击Apply,就ok了。
如果这个类中有main方法,我们就可以利用main方法来调用这个类,main方法是静态的,他的参数是Sting类型的数组,可以理解为:Method.invoke(null,args[])。
我们通过一个示例来演示一下,在下面的示例中,我首先在Eclipse的Program arguments中写入运行的类名“Person”,注意要写全名,如果有包的话,包名也要写,这就相当与在运行ReflectMethod2的主函数时把类名“Person”作为参数传递。
class Person
{
public static void main(String[] args) {
for(String str:args)
{
System.out.println(str);
}
show();
}
public static void show()
{
System.out.println("黑马你好!");
}
}
public class ReflectMethod2 {
public static void main(String[] args) {
try {
//args[0]里面就装着我们要执行的那个类的名字
String strClassname = args[0];
//得到了类名我们就可以得到他的字节码
Class c2 = Class.forName(strClassname);
//我们获取到这个类的main方法,得到了他的main方法我们就可以启动这个类
Method mMain = c2.getMethod("main", String[].class);
/*
* 这里需要注意:
* 当我们传多个String到一个String数组时候,按道理说是可以的,但是编译器报错了,告诉我们
* 数组的个数不对,因为JDK1.5之前的版本不会吧String数组看做一个参数,而是分解成多个
* Object,为了兼容以前版本编译器把这个String类型的数组进行了拆包动作,
* 把三个String字符串看做是三个Object对象,这就造成了参数个数错误的异常
* 为了消除这个错误我们有两种做法
* */
//第一种把String类的数组打包成一个Object数组
//mMain.invoke(null, new Object[]{new String[]{"小宏","小绿","小蓝"}});
//第二种方法在这个数组前加一个标识Object,告诉编译器不要给我拆包,这个String类型的数组是一个参数
mMain.invoke(null, (Object)new String[]{"小宏","小绿","小蓝"});
} catch (Exception e) {
System.out.println("反射失败");
e.printStackTrace();
}
}
}
/*打印:
小宏
小绿
小蓝
黑马你好!
* */
三. 数组的反射
(1)数组是也是一种类型,当数组具有相同的元素类型且具有的相同的维度时他们所对应的Class就是同一个。
public class ReflectArray {
public static void main(String[] args) {
/*
* 首先我们了解到具有相同元素类型且相同维度的数组所对应的Class相同
* */
int[] i1 = new int[]{1,2,3};
int[] i2 = new int[]{23,22,21};
int[][] i3 = {{2,3,4},{4,3,2,1}};
String[] str1 = new String[]{"小宏","小绿"};
System.out.println(i1.getClass() == i2.getClass());//ture
//在这编译器就直接告诉我这两个Class不是相同的了
// System.out.println(i1.getClass() == str1.getClass());
/*
* 我们来看一看他们的父类都是谁
* 然后来看看一个有意思的东西
* */
System.out.println(i1.getClass().getSuperclass().getName());//Object
System.out.println(i3.getClass().getSuperclass().getName());//Object
System.out.println(str1.getClass().getSuperclass().getName());//Object
//int类型的数组是一个Object对象
Object iObj1 = i1;
/*
* i1可以代表一个Object对象,但是他不可代表一个Object数组,因为i1里面装的元素
* 是数字,数字不是Object对象,所以编译器告诉给我们报错了
*/
// Object[] iObj2 = i2;
//i3是一个二维的int数组,这个二维数组可以理解成一个int数组里面装着int数组,也就
//是一个Object对象里面装着Object对象
Object[] iObj3 = i3;
//Sting[]里面装的是String,String[]是Object对象,String也是Object对象
Object[] strObj = str1;
/*
* 知道了这个小知识点有什么卵用呢,我也不知道,但是最起码我明白这个地方的区别
* 当我们操作数组的时候我们有一个类Arrays,以其中的asList()方法为例说明一下
* asList()方法是把数组转换成集合
*/
System.out.println(Arrays.asList(i1));//[[I@1dd3812]
System.out.println(Arrays.asList(str1));//[小宏, 小绿]
/*
* 这是怎么一个情况?为什么Stirng[]中把成员打印出来了而int[]中还是一个地址值,why
* 原因是asList()方法中接收到了String[]之后把按1.5以前版本处理,把他看做了一个Object数组
* 进行了拆解,把每一个String字符串看做一个Object,而int[]是不可以看做一个Object[]的,按照
* 1.5以后得版本把整个的int[]数组看做一个 参数了
*/
}
}
总结一下需要注意地方:
int[]是Object对象,而int不是Object对象
(2)数组的反射是什么样子的,数组类型的反射,我第一次接触数组的反射的时候觉得这不是什么重点,好像没什么用的样子啊,但是直觉告诉我这个数组的反射不可以忽略我们在写程序的时候要考虑各种各样的情况发生,比如有个对象不是我定义的,我不知道他是什么,我觉得他可能是数组,但我不知道他的类型和长度,怎么办,利用反射,因为我们不知道数组的类型,我们用Array类用来操作。
public class ReflectArray2 {
public static void main(String args[]){
String[] name = new String[]{"小宏","小绿","小蓝"};
int[] i1 = {1,2,3};
String str = "对象";
printObject(name);
printObject(i1);
printObject(str);
}
private static void printObject(Object obj) {
//获取到这个对象的Class类
Class c1 = obj.getClass();
//如果他是数组我们把他的成员打印出来
if(c1.isArray()){
//获取长度
int length = Array.getLength(obj);
//打印出他的成员
for(int x = 0;x<length;x++){
//通过Array来拿到数组的成员
System.out.println(Array.get(obj, x));
}
}else
System.out.println(obj);
}
}
/*
* 打印:
* 1
2
3
对象
* */
四. 反射在框架中的应用
在文件的开头我们就说到反射机制应用最多的情况是在框架中的,在我们编写框架时,我们要用到类还没有完成,我们怎么样获得,我们可以把我们要用到类写在一个文本文档中,利用IO技术读取到这个类名,然后获得他的字节码,下面做一个小程序来演示一下,在我们的Eclipse工程中,首先建立一个File文件,把键值对写入到这个File文件中:className=java.util.ArrayList
class Person
{
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
public class Reflect6 {
public static void main(String[] args) {
try {
// 首先我们利用IO技术获取到这个File文件里面的内容
InputStream Filein = new FileInputStream("className.properties");
//Properties是和流紧密联系在一起的,是HashMap的进化
Properties p = new Properties();
//从流中获取属性列表,注意流使用后不会释放
p.load(Filein);
//关流
Filein.close();
//获取到我们这个框架所需的类名
String className = p.getProperty("className");
//新建集合对象,注意打标识
Collection coll =(Collection) Class.forName(className).newInstance();
Person p1 = new Person("小宏",23);
Person p2 = new Person("小绿",22);
Person p3 = new Person("小宏",23);
Person p4 = new Person("小黑",20);
coll.add(p1);
coll.add(p2);
coll.add(p3);
coll.add(p4);
System.out.println(coll.size());//4
System.out.println(coll);
//[Person [name=小绿, age=22], Person [name=小宏, age=23], Person [name=小黑, age=20], Person [name=小宏, age=23]]
} catch (Exception e) {
e.printStackTrace();
}
}
}