反射
反射的作用
反射
是由Smith在1982年首次提出的,主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。 这一概念的提出很快引发了计算机科学领域关于应用反射性的研究。 它首先被程序语言的设计领域所采用,并在Lisp和 面向对象 方面取得了成绩。
反射机制允许程序在运行时取得任何一个已知名称的class的内部信息,包括包括其modifiers(修饰符),fields(属性),methods(方法)等,并可于运行时改变fields内容或调用methods。那么我们便可以更灵活的编写代码,代码可以在运行时装配,无需在组件之间进行源代码链接,降低代码的耦合度;还有动态代理的实现等等;但是需要注意的是反射使用不当会造成很高的资源消耗!
反射的操作步骤
获取Class对象
Class对象是编译文件时,创建的对象,每个类只有一个Class对象,与Class对象相对立的就是实例对象,无论从哪种途径获得Class对象,指向的都是同一个Class对象,因为类只有一个。
一个类文件从编写完成到初始化时,会经历三个阶段,Class对象就是在第一个阶段获得的。
- 加载,这是由类加载器(ClassLoader)执行的。通过一个类的全限定名来获取其定义的二进制字节流(Class字节码),将这个字节流所代表的静态存储结构转化为方法去的运行时数据接口,根据字节码在java堆中生成一个代表这个类的java.lang.Class对象。
- 链接。在链接阶段将验证Class文件中的字节流包含的信息是否符合当前虚拟机的要求,为静态域分配存储空间并设置类变量的初始值(默认的零值),并且如果必需的话,将常量池中的符号引用转化为直接引用。
- 初始化。到了此阶段,才真正开始执行类中定义的java程序代码。用于执行该类的静态初始器和静态初始块,如果该类有父类的话,则优先对其父类进行初始化。
**所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。**当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new创建类对象的时候也会被当作对类的静态成员的引用。因此java程序程序在它开始运行之前并非被完全加载,其各个类都是在必需时才加载的。
获取Class对象的三种方式:
-
通过类名.class获得Class对象:
Class clazz1 = Student.class;
-
通过实例对象中**getclass()**获得Class对象:
Student stu = new Student(); Class clazz2 = stu.getClass();
-
通过**Class.forName()**方法传入字符串(全限定类名)获得Class对象:
Class clazz3 = Class.forName("Examination.Student");
我们比较三个Class对象是否相同:
System.out.println(clazz1==clazz2);
System.out.println(clazz1==clazz3);
System.out.println(clazz2==clazz3);
这也印证了一个类只会生成一个Class文件。
获取Class对象中的实例对象(Object)
-
我们在平时调用类中的实例方法时,需要先获得Class类的对象,而在反射中,我们调用方法也需要先获得实例对象。
Object o = clazz1.newInstance();
Class类中的newInstance()方法可以返回一个Object实例对象;
public T newInstance() throws InstantiationException, IllegalAccessException
-
Constructor构造器:
Constructor
提供关于类的单个构造方法的信息以及对它的访问权限,通过调用newInstance()方法同样能够生成实例化对象。Constructor cons = clazz.getConstructor(int.class,String.class); Object o = cons.newInstance(age,name);
通过Class类中的getConstructor()方法可以返回一个Constructor对象,其中的参数为可变参数,表示其中的字段属性。
public Constructor<T> getConstructor(Class<?>... parameterTypes) throws NoSuchMethodException,SecurityException
获取Class对象中的方法(Method)
-
Method类:
public class MethodDescriptor extends FeatureDescriptor
通过Method类可以访问Bean类中的方法,我们可以通过Class类中的getMethod()获得Method对象。除了getMethod()方法以外还有getMethods()方法获得Method[]数组;
public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
getMethod方法中有两个参数,name与Class可变参数,String name代表Class类中的方法名,而可变参数对应的则是该方法中的参数。
Method setid = clazz1.getMethod("setName", String.class);
-
调用方法:类中的方法可以分为静态方法和非静态方法,这两者我们在调用时需要使用不同的方式。
-
静态方法:静态方法是属于该类,只需要通过类名就可以进行调用,不需要创建实例对象。
Method run = clazz1.getMethod("run"); run.invoke(null,null); ...... public static void run(){ System.out.println("这是一个静态方法"); }
-
非静态方法:非静态方法需要先获得该类的实例对象来进行调用。
Object o = clazz1.newInstance(); Method setid = clazz1.getMethod("setName", String.class); setid.invoke(o,"jack");
-
我们都知道方法与变量都拥有权限修饰符来进行类之间访问的约束,我们在编写Bean类时往往会将变量或者方法设为私有的,当要进行访问时,因为权限的原因,不能够访问使用private修饰的方法与变量。在反射中我们可以通过方法修改权限进行访问。
-
AccessibleObject类:
AccessibleObject 类是 Field、Method 和 Constructor 对象的基类。它提供了将反射的对象标记为在使用时取消默认 Java 语言访问控制检查的能力。对于公共成员、默认(打包)访问成员、受保护成员和私有成员,在分别使用 Field、Method 或 Constructor 对象来设置或获取字段、调用方法,或者创建和初始化类的新实例的时候,会执行访问检查。
我们可以通过其中的setAccessible()方法,来暴力修改访问权限。
public void setAccessible(boolean flag)
将此对象的 accessible 标志设置为指示的布尔值。值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。值为 false 则指示反射的对象应该实施 Java 语言访问检查。
Object o = clazz1.newInstance(); Method demo = clazz1.getDeclaredMethod("demos"); demo.invoke(o,null);
暴力修改权限后:
Object o = clazz1.newInstance();
Method demo = clazz1.getDeclaredMethod("demos");
demo.setAccessible(true);
demo.invoke(o);
....
private void demos(){
System.out.println("这是一个私有方法");
}
注意:在获得方法时我们使用的是getDeclaredMethod()方法而不是getMethod()方法,这两者有很大的区别,getDeclaredMethod()方法可以获得全部的方法,getmethod()只能获得共有的,对于私有的无法获得。
- getDeclaredMethod() 获取的是类自身声明的所有方法,包含public、protected和private方法。
- getMethod () 获取的是类的所有共有方法,这就包括自身的所有public方法,和从基类继承的、从接口实现的所有public方法。
获取Class对象中的属性(Field )
Field
提供有关类或接口的单个字段的信息,以及对它的动态访问权限。
- 与Method类似,Method对应的是类中的方法,而Field对应的是类是类中的字段(属性),都需要通过调用Class中的方法来获得对象。
- getField():返回一个
Field
对象,它反映此Class
对象所表示的类或接口的指定公共成员字段。 - getDeclaredField():返回一个
Field
对象,该对象反映此Class
对象所表示的类或接口的指定已声明字段。name 参数是一个 String,它指定所需字段的简称。注意,此方法不反映数组类的 length 字段。
- getField():返回一个
Field name = clazz1.getDeclaredField("name");
name.setAccessible(true);
name.set(o,"jack");
Field age = clazz1.getDeclaredField("age");
age.setAccessible(true);
age.set(o,18);
System.out.println(o);
通过Class对象获得字段,使用setAccessible修改字段的访问权限,最后通过set方法来向对象传值,打印对象的个个属性值。
Properties类的使用
Properties类
public class Properties extends Hashtable<Object,Object>
Properties
继承了Hashtable
类,而Hashtable是Map
集合中的子类,都是以键值对的方式对数据进行存储。Properties
类表示了一个持久的属性集。Properties
可保存在流中或从流中加载。与其他Map集合中的子类不同的是,Properties
集合没有约束泛型的说法,他的泛型类别已经被写死,无法进行修改,属性列表中每个键及其对应值都是一个字符串。
-
load方法实现了两种流的重载:
public void load(InputStream inStream)throws IOException
public void load(Reader reader)throws IOException
- 第一种方法面对输入流,能够从输入流中读取属性列表(键和元素对)。
- 第二种方法面对字符流,按简单的面向行的格式从输入字符流中读取属性列表(键和元素对)。
-
store方法也实现了两种流的重载:
public void store(OutputStream out,String comments)throws IOException
public void store(Writer writer,String comments)throws IOException
- 第一种方法面对输出流,以适合使用
load(InputStream)
方法加载到Properties
表中的格式,将此Properties
表中的属性列表(键和元素对)写入输出流。 - 第二种方法面对字符流,以适合使用
load(Reader)
方法的格式,将此Properties
表中的属性列表(键和元素对)写入输出字符。
- 第一种方法面对输出流,以适合使用
-
.properties文件:properties文件是一种属性文件,文件以<key,value>的方式进行存储,在文件中键与值之间使用"=“表示映射关系,当使用load与store方法读出与写入时,会自动识别与写入,”=“在文件中拥有特殊性,所以要存储含有”=“的字符串时,要加上”\"转义字符。properties文件采用ISO 8859-1 字符编码,编码采用8位二进制存储,存储容量比ASCii码大一倍,但是不能存储汉字,当写入汉字或者读出汉字都会造成文件乱码。
当我们采用load方法读取时,控制台输出的信息会对中文显示"??"
@Test
public void runs() throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Properties properties = new Properties();
Reader reader = new FileReader("E:\\JavaProjects\\Java_1_shixi\\untitled\\className.properties");
properties.load(reader);
String classname = properties.getProperty("classname");
System.out.println(classname);
Class clazz = Class.forName(classname);
Constructor cons = clazz.getConstructor(int.class,String.class);
Properties p2 = new Properties();
Reader reader1 = new FileReader("E:\\JavaProjects\\Java_1_shixi\\untitled\\data.properties");
p2.load(reader1);
Set set = p2.keySet();
String name = null;
int age = -1;
for (Object os:set
) {
if (os.equals("name"))
name = (String) p2.get(os);
else if (os.equals("age")) {
age = Integer.parseInt((String) p2.get(os));
}
}
Object o = cons.newInstance(age,name);
System.out.println(o);
}
如上述代码所示,我们获取文件地址时,采用的时绝对路径,因为我们的方法编写在Junit下,在使用相对路径寻找文件时,寻找的路径与在其他类中不一样:
- Junit方法:获取的路径是当前模块module下。
- main方法:获取的路径是当前工程project下。
main方法:获取的路径是当前工程project下。
由此我们就可以通过反射,来实现动态地访问一个类,实现了代码的灵活性。