第一部分:反射概述
一、反射基本理解
1、Java反射机制是在运行状态中,对于任意一个类(class文件),都能够知道这个类的所有属性和方法。
对于任意一个对象,都能够调用它的任意一个方法和属性。
2、动态获取的信息以及动态调用对象的方法的功能称为Java的语言的反射机制。
动态获取一个类中的信息,就是Java的反射机制。可以理解为对类的解剖。
3、反射大大提高了程序的扩展性。
以前通过多态来提高程序的扩展性,但是需要传入子类对象;现在可以通过反射更强的提高程序扩展性,即将后期出现的类存放到配置文件中即可,不用再new对象。
4、使用反射时一般有一个接口与配置文件。
1)通过接口可以对外暴露规则,完成功能扩展。
2)配置文件中配置的是变化的参数信息,并通过流的形式将配置文件加载到内存并使用指定的参数。
二、反射的特点图解1:
图解2:
1、反射就是把Java类中的各个成分映射成为相应的Java类
2、反射技术提高了程序的扩展性
3、应用起来很简单,与用户之间的桥梁变成了配置文件。用户不用再面对源代码。(开发用接口加配置文件)注意:学习框架时,按以下步骤学习:
1)知道框架的作用
2)知道框架的配置文件怎么用。(配置文件中要写很多信息)
3)学框架中涉及的常用对象的用法
4)框架的底层原理,基本实现方式
第二部分:反射的基石:Class类
|---- Class<T>
一、Class类图解
注意:
1、反射就是通过Class类完成的。
2、想要对一个类文件完成解剖,只要获取到该类对应的字节码文件对象即可。
二、获取字节码文件对象的三种方式
1、Object类中的getClass方法(对象.getClass())
想要用这种方式,必须要明确具体的类并创建对象。该方法比较麻烦。
2、任何数据类型都具备一个静态的属性:class(类名.class)
通过.class属性来获取其对应的Class对象。相对简单,但是还是要明确用到类中的静态成员。该方法还是不够扩展。
3、只要通过给定的类的字符串名称就可以获取该类(Class.forName(String className))
通过Class类中的方法完成。方法如下:
static Class<?> forName(String className)
返回与带有给定字符串名的类或接口相关联的 Class 对象
注意:
1)className必须是包名.类名,与导包没关系。导包导入类,而这里是操作的字符串。
2)可以将类名写到配置文件中,程序只要加载配置文件中的数据就可以获取到该类。
4、得到字节码的两种情况
1)该类的字节码已经加载到了内存中,不再需要加载,直接返回。
2)该类的字节码尚未加载过,调用类加载器去加载,并把加载的字节码缓存到Jvm中,以后就不用再加载了。
5、示例
基本类Person:
package day.day16; /* * 演示反射技术的类:Person */ public class Person { private int age; private String name; public Person(String name,int age) { super(); this.age = age; this.name = name; System.out.println("Person param run..."+this.name+":"+this.age); } public Person() { super(); System.out.println("person run"); } public void show(){ System.out.println(name+"...show run..."+age); } private void privateMethod(){ System.out.println(" method run "); } public void paramMethod(String str,int num){ System.out.println("paramMethod run....."+str+":"+num); } public static void staticMethod(){ System.out.println(" static method run......"); } }
示例代码:
package day.day16; /* * 演示获取字节码文件对象的三中方式。 * * 第三种方法更利于程序的扩展。 * 可以将类的全名写到配置文件中,程序通过加载配置文件获取该类的字节码文件对象 */ public class ClassDemo { public static void main(String[] args) throws ClassNotFoundException{ method_3(); } //方法3、通过Class类中的静态方法forName获取(Class.forName(String name)) //类名必须是全名,因为操作的是字符串,与倒不倒包没有关系 public static void method_3() throws ClassNotFoundException{ Class clazz = Class.forName("day.day16.Person"); System.out.println(clazz); } //方法2、通过类中的静态成员class获取(类名.class) public static void method_2(){ Class clazz = Person.class; System.out.println(clazz); } //方法1、对象.getClass(Object类中的方法) public static void method_1(){ Person p1 = new Person(); Class clazz1 = p1.getClass(); Person p2 = new Person(); Class clazz2 = p2.getClass(); System.out.println(clazz1==clazz2);//true,同一份字节码文件 } }
注意:同一类在内存中只有一个字节码文件对象(.class文件)
三、基本的Class对象
1、预定义的Class对象
包括8个基本数据类型与void,如int.class,void.class。
2、基本数据类型与基本数据类型包装类的Class对象
Integer与int的Class文件不是同一个。
Intger.TYPE == int.class,即基本数据类型包装类的TYPE字段返回的是基本数据类型的字节码文件对象。
数组也有自己对应的字节码文件。
3、Class类中的相关判断方法
boolean isArray() 判定此 Class 对象是否表示一个数组类
boolean isPrimitive() 判定指定的 Class 对象是否表示一个基本类型
第三部分:反射的实现(java.lang.reflect)
|---- Constructor<T>
|---- Field
|---- Method
|---- Array
一、构造方法的反射
1、基础方法
Class类中:
2、示例T newInstance() 创建此 Class 对象所表示的类的一个空参数的新实例(一般被反射的类中都有空参构造函数)
Constructor<T> getConstructor(Class<?>... parameterTypes)
返回一个 Constructor 对象,它反映此 Class 对象所表示的类的指定公共构造方法
Constructor<?>[] getConstructors()
返回一个包含某些 Constructor 对象的数组,这些对象反映此 Class 对象所表示的类的所有公共构造方法
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
返回一个 Constructor 对象,该对象反映此 Class 对象所表示的类或接口的指定构造方法
Constructor<?>[] getDeclaredConstructors()
返回 Constructor 对象的一个数组,这些对象反映此 Class 对象表示的类声明的所有构造方法String getName() 以字符串形式返回此字节码的名称
Class<?>[] getInterfaces() 返回该类所实现接口的数组
Constructor类中:
T newInstance(Object... initargs)
使用此 Constructor 对象表示的构造方法来创建该构造方法的声明类的新实例,并用指定的初始化参数初始化该实例
Class<?>[] getParameterTypes()
按照声明顺序返回 Class 对象的数组,这些对象描述了此 Class对象所表示的方法的形参类型
String getName() 以字符串形式返回此Class对象所表示实体的名称
package day.day16; import java.lang.reflect.Constructor; /* * 需求:利用反射技术创建对象 */ public class ReflectNewInstanceDemo { public static void main(String[] args) throws Exception { creatNewObject_2(); } //指定参数构造函数初始化对象 public static void creatNewObject_2() throws Exception { day.day16.Person p = new day.day16.Person("小强",24); /* * 当获取指定名称对应类中所体现的对象时 * 而该对象初始化不适用空参数构造方法该怎么办呢? * 既然是通过指定的构造函数进行对象的初始化 * 所以应该先获取到该构造函数。通过字节码文件对象即可完成 * 该方法是:Constructor<T> getConstructor(Class<?>... parameterTypes) */ String className = "day.day16.Person"; //找寻该名称的类文件,并加载进内存,产生Class对象 Class clazz = Class.forName(className); //获取到了指定的构造函数对象 Constructor constructor = clazz.getConstructor(String.class,int.class); //通过该构造器对象的newInstance方法进行对象的初始化(指定参数) Object obj = constructor.newInstance("旺财",20); } //空参数构造函数初始化对象 public static void creatNewObject_1() throws ClassNotFoundException, InstantiationException, IllegalAccessException{ /* * 早期:new的时候,先根据被new的类的名称去寻找该类的字节码文件,并加载进内存 * 创建该字节码文件对象,并接着创建该字节码文件的对应的Person对象 */ day.day16.Person p = new day.day16.Person(); //现在 String className = "day.day16.Person"; //找寻该名称类文件,并加载进内存,产生Class对象 Class clazz = Class.forName(className); //如何产生该类的对象呢?使用Class类中的newInstance()方法 Object obj = clazz.newInstance();//(空参数的构造函数) } }
二、字段的反射
1、基础方法
Class类中:
Field getDeclaredField(String name)
返回一个 Field 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明字段
Field[] getDeclaredFields()
返回 Field 对象的一个数组,这些对象反映此 Class 对象所表示的类或接口所声明的所有字段
Field getField(String name)
返回一个 Field 对象,它反映此 Class 对象所表示的类或接口的指定公共成员字段
Field[] getFields()
返回一个包含某些 Field 对象的数组,这些对象反映此 Class 对象所表示的类或接口的所有可访问公共字段
Field类中:
void set(Object obj, Object value)
将指定对象变量上此 Field 对象表示的字段设置为指定的新值
Object get(Object obj)
返回指定对象上此 Field 表示的字段的值
int getInt(Object obj) (可以操作其他基本数据类型)
获取 int 类型或另一个通过扩展转换可以转换为 int 类型的基本类型的静态或实例字段的值
void setInt(Object obj, int i)
将字段的值设置为指定对象上的一个 int 值Class<?> getType()
返回一个 Class 对象,它标识了此 Field 对象所表示字段的声明类型
String getName() 以字符串形式返回此字段的名称
2、AccessibleObject类
|---- AccessibleObject
|---- Constructor<T>|---- Field|---- Method
AccessibleObject 类是 Field、Method 和 Constructor 对象的基类。它提供了将反射的对象标记为在使用时取消默认 Java 语言访问控制检查的能力。
常用方法:
void setAccessible(boolean flag)
将此对象的 accessible 标志设置为指示的布尔值(true则取消访问检查)
注意:
1)通过设置,取消访问检查访问私有成员的方式称为暴力反射。
2)一般不建议使用暴力访问,既然私有了就说明不想要外界访问。
3、示例
package day.day16; import java.lang.reflect.Field; /* * 获取字节码文件的字段 */ public class ReflectDemo2 { public static void main(String[] args) throws Exception{ getFieldDemo(); } public static void getFieldDemo() throws Exception{ Class clazz = Class.forName("day.day16.Person"); Field field = null;//clazz.getField("age")获取公有字段 field = clazz.getDeclaredField("age");//只获取本类,但包括私有的字段 //对私有字段的访问取消权限检查。暴力访问(反射) field.setAccessible(true); //反射的方式创建对象obj Object obj = clazz.newInstance(); //设置指定对象obj上该字段的值 field.set(obj, 30); //获取指定对象obj上该字段的值o Object o = field.get(obj); System.out.println(o); //一般设置对象值的步骤 // day.day16.Person p = new day.day16.Person(); // p.age = 30; } }
三、一般方法的反射
1、基础方法
Class类中:
Method getDeclaredMethod(String name, Class<?>... parameterTypes)
返回一个 Method 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法
Method[] getDeclaredMethods()
返回 Method 对象的一个数组,这些对象反映此 Class 对象表示的类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法
Method getMethod(String name, Class<?>... parameterTypes)
返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法
Method[] getMethods()
返回一个包含某些 Method 对象的数组,这些对象反映此 Class 对象所表示的类或接口(包括那些由该类或接口声明的以及从超类和超接口继承的那些的类或接口)的公共 member 方法
Method类中:
Object invoke(Object obj, Object... args) 对带有指定参数的指定对象调用由此 Method 对象表示的底层方法
Class<?>[] getParameterTypes()
按照声明顺序返回 Class 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型
String getName() 以字符串形式返回此方法的名称
注意:
1)invoke(null,args...)表示访问静态方法,不需要对象
2)invoke(obj,null)或者invoke(obj)(可变参数可以直接不传)表示访问空参数的方法,不需要参数
2、示例
package day.day16; import java.lang.reflect.Constructor; import java.lang.reflect.Method; /* * 获取Class中的方法 */ public class ReflectDemo3 { public static void main(String[] args) throws Exception{ getMethodDemo_3(); } //获取指定名称与参数的方法 public static void getMethodDemo_3() throws Exception { Class clazz = Class.forName("day.day16.Person"); Method method = clazz.getMethod("paramMethod", String.class,int.class);//获取指定名称与参数的方法 Object obj = clazz.newInstance(); method.invoke(obj, "丢丢",21); } //获取指定名称的无参的构造方法 public static void getMethodDemo_2() throws Exception{ Class clazz = Class.forName("day.day16.Person"); Method method = clazz.getMethod("show",null);//获取空参数的show方法 // Object obj = clazz.newInstance();//创建对象(未对name与age初始化) Constructor constructor = clazz.getConstructor(String.class,int.class); Object obj = constructor.newInstance("点点",24); method.invoke(obj, null);//调用指定对象的该方法 /* 一般调用方法 * Person p = new Person(); * p.show(); */ } //获取指定Class中的所有公共方法 public static void getMethodDemo() throws Exception{ Class clazz = Class.forName("day.day16.Person"); Method[] methods = clazz.getMethods();//获取本类及父类中的公有方法 methods = clazz.getDeclaredMethods();//只获取本类中的所有方法,包括private for(Method m:methods){ System.out.println(m); } } }
四、数组的反射
1、概述
1)具有相同维数和元素类型的数组属于同一个类型,即具有相同的Class实例对象。
2)代表数组的Class实例对象的getSuperClass()方法返回的是父类Object类对应的Class。
3)基本数据类型的一维数组,可以被当做Object类型来使用,不能当做Object[]类型使用(二维数组可以);非基本数据类型的一维数组,即可以当做Object类型使用,又可以当做Object[]类型使用。
2、使用invoke方法的注意事项
由于Method类中的invoke(Object obj,Object...args)方法中有可变参数。当方法的参数列表只有非基本数据类型的数组时,传入的参数数组会被拆包而不再作为一个整体。
在JDK1.4版本中没有可变参数,因此参数是接收一个数组:Object[] args。基本数据类型的数组作为一个Object对象可以直接传递给args参数,未被拆包;而非基本数组则被分解为多个Object对象传递给可变参数。
如,要调用某一个类的主函数main(String[] args),则需要将非基本数据类型的数组再封包,即new Object[]{new String[]{"aa","bb"}};或者将整个数组申明为Object类型,即(Object)new String[]{"aa",“bb”}
3、数组的反射(Array工具类)
Array工具类用于完成对数组的反射操作。
static Object get(Object array, int index) 返回指定数组对象中索引组件的值
static void set(Object array, int index, Object value) 将指定数组对象中索引组件的值设置为指定的新值
static int getLength(Object array) 以 int 形式返回指定数组对象的长度
4、示例
package com.itheima; import java.lang.reflect.*; /* * 由于 数组 也是Object的子类(注意:基本数据类型不是,基本数据类型的数组才是) * 打印数组还可以用 Arrays.asList(T...args)来做 * 打印集合是打印集合中元素的字符串表示形式 */ public class ArrayReflect { public static void main(String[] args) { int[] arr = new int[]{1,2,3,4}; Object obj = new Object(); printObj(arr); printObj(obj); } //判断Obj是不是数组,是就按数组的方式打印,不是就直接打印 public static void printObj(Object obj){ //里面只能全都用反射的方法,因为Obj是一个对象 if(obj.getClass().isArray()){ int len = Array.getLength(obj); for(int x=0;x<len;x++){ System.out.println(Array.get(obj,x)); } }else{ System.out.println(obj); } } }
五、配置文件的加载
1、配置文件的存放位置
在使用框架时,如果配置文件放在一个硬盘的绝对路径中,那么在定义流加载配置文件时,目录就不好确定。因此,配置文件一般与类文件放在同一个包中,即同一个目录下。
2、加载配置文件的方式
1)传统的流加载
该方式不方便确定配置文件的路径。
2)类加载器加载(一)
(a)通过 类名.class 获取当前类的.class文件对象
(b)通过.class文件对象的 ClassLoader getClassLoader() 方法获取到当前类的类加载器
(c)通过类加载器的 InputStream getResourceAsStream(String name) 方法加载配置文件
如:InputStream in = Computer.class.getClassLoader().getResourceAsStream("day/day16/pci.properties");
注意:
(a)配置文件的开头没有任何符号,直接是包名(完整的包名)
(b)与传统的流的分隔符不同。传统的是\\(反斜线),该示例是/(正斜线)
3)类加载器加载(二)
(a)通过 类名.class 获取当前类的.class文件对象
(b)通过.class文件对象的 InputStream getResourceAsStream(String name) 方法加载配置文件
如:InputStream in = Computer.class.getResourceAsStream("pci.properties");
注意:
(a)配置文件路径简单化,不用再写前面的包的路径(配置文件的路径必须与类文件路径相同)
(b)若配置文件路径与类文件路径不同,则name="/day/day16/resource/pci.properties",(完整的包名)目录的第一位有/
第四部分:综合示例(框架)
一、主程序(电脑)
package day.day16; /* * 创建一个功能:定义个Computer,Computer中有MainCard。当Computer运行时,MainCard运行,符合PCI规则的程序自动运行 * * 将代码封装,对外暴露配置文件,将需要运行的配置的符合PCI接口的类名存放到该配置文件中。当Computer运行时就会自动运行 */ import java.io.InputStream; import java.util.Properties; public class Computer { public static void main(String[] args) throws Exception { //创建主板对象并运行 MainCard mc = new MainCard(); mc.run(); //读取配置文件并加载进加载到流中(通过类加载器[内部]) Properties prop = new Properties(); InputStream in = null;//Computer.class.getClassLoader().getResourceAsStream("day/day16/pci.properties") in = Computer.class.getResourceAsStream("pci.properties"); prop.load(in); for(int x=0;x<prop.size();x++){ //获取配置文件中的信息(类名) String pciName = prop.getProperty("pci"+(x+1)); //通过反射的方式创建对象(反射完成扩展) Class clazz = Class.forName(pciName); PCI p = (PCI)clazz.newInstance();//空参数的构造函数 mc.usePCI(p); } in.close(); } }
二、主板程序
package day.day16; /* * 主板+使用PCI接口中的各类卡 */ public class MainCard { public void run(){ System.out.println("maincard run"); } public void usePCI(PCI p){//多态完成扩展 p.open(); p.close(); } }
三、PCI接口
package day.day16; public interface PCI { public void open(); public void close(); }
四、符合PCI规则的声卡、网卡
package day.day16; public class VoiceCard implements PCI { public void open(){ System.out.println("voicecard open"); } public void close() { System.out.println("voicecard close"); } }
package day.day16; public class NetCard implements PCI { public void open() { System.out.println("NetCard run"); } public void close() { System.out.println("NetCard close"); } }
五、配置文件
注意:
只要将符合PCI接口的类文件名放在该配置文件中,电脑就可以自动运行,而不用再修改源代码,只要创建类实现PCI接口即可完成功能扩展。
六、运行结果
第五部分:泛型的反射
一、指定泛型的集合中存入其他类型的元素
泛型是提供给javac编译器的,可以限定集合中的输入类型,让编译器挡住程序中的非法输入,编译器编译带类型说明的集合时会去除掉 泛型的类型 信息,使程序运行效率不受影响。
对于参数化的泛型类型,getClass方法返回值与泛型类型一样。由于编译生成的字节码会去掉泛型的类型信息,只要跳过编译器,就可以往某个泛型集合中加入其它类型的数据。
二、获取指定泛型的集合中元素的泛型
由于集合对象的getClass方法返回的是集合的泛型,因此必须采用其他方法。
Method类中:
Type[] getGenericParameterTypes()
按照声明顺序返回 Type 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型的
Type接口:
ParameterizedTypeType 是 Java 编程语言中所有类型的公共高级接口。它们包括原始类型、参数化类型、数组类型、类型变量和基本类型。
Type接口子接口。表示参数化类型,如 Collection<String>
Type[] getActualTypeArguments()
返回表示此类型实际类型参数的 Type 对象的数组
Type getRawType()
返回 Type 对象,表示声明此类型的类或接口(获取对象的类型)
三、示例代码
package day.day20; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Vector; public class GenericTest { public static void main(String[] args) throws Exception{ getGenericClass(); } /* * 获取Vector<String> 上的泛型 */ public static void getGenericClass() throws Exception{ Vector<String> v = new Vector<>(); //1.通过反射获取getTheClass方法的Method对象 Method me = GenericTest.class.getMethod("getTheClass",Vector.class); //2.通过方法中的getGenericParameterTypes方法获取方法参数的类型 Type[] types = me.getGenericParameterTypes(); //3.由与getTheClass方法只有一个参数。获取数组中的0角标位元素并强转为子类ParameterizedType ParameterizedType paramType = (ParameterizedType)types[0]; //4.获取getTheClass方法参数的实际类型。返回的同样是个数组,获取第一个元素 System.out.println(paramType.getActualTypeArguments()[0]); } public static void getTheClass(Vector<String> v){ //解决依赖方法 } /* * 跳过ArrayList<Integer> 往里面存一个String类型的元素 */ public static void show() throws Exception{ ArrayList<Integer> al = new ArrayList<Integer>(); //获取ArrayList的字节码 Class clazz= al.getClass(); System.out.println(clazz.getName());//获取到的是java.util.ArrayList(因为泛型只存在于编译时期) //获取add方法 Method methodAdd = clazz.getMethod("add", Object.class); //调用add方法 methodAdd.invoke(al, "Java"); System.out.println(al); } }