反射
在java,很多时我们需要通过外部配置文件,在不修改源码的情况下,控制程序(这符合设计模式的开闭原则(ocp原则),即不修改源码,扩容功能)
于是,为了方便这种需求,java提供了反射(reflection)机制
反射
我们知道,在加载完某个类之后,堆中会产生一个相应的并且唯一的class对象,这个class对象具有这个类的完整结构信息,我们可以通过这个对象获取类的结构,所以这个对象就像是类的一面镜子,于是称这种机制为反射。
反射机制允许在程序运行时通过ReflectionAPI来获取类的全部信息(类的属性,方法,构造器等等),并能操作对象的属性和方法。反射机制在底层框架和设计模式都会用到。
现在举个例子,我们现在想通过A.properties读取信息来控制程序
public static void main(String[] args) throws Exception { Properties properties = new Properties(); properties.load(new FileReader("D:\\src\\src\\com\\kzh\\test\\A.properties")); //读取类名 String className = properties.getProperty("classFullPath"); //要调用的方法 String methodName = properties.getProperty("method"); //获取该类的class对象 Class cls = Class.forName(className); //获取该对象的一个实例 Object o = cls.newInstance(); //方法对象 Method method = cls.getMethod(methodName); //确定好方法是哪个对象的,然后开始调用 method.invoke(o); } } class A { public void a() { System.out.println("调用a方法"); } }
运行结果
然后我现在想把要调用的方法改成b(),那么直接在配置文件改成b就行
public static void main(String[] args) throws Exception { Properties properties = new Properties(); properties.load(new FileReader("D:\\src\\src\\com\\kzh\\test\\A.properties")); //读取类名 String className = properties.getProperty("classFullPath"); //要调用的方法 String methodName = properties.getProperty("method"); //获取该类的class对象 Class cls = Class.forName(className); //获取该对象的一个实例 Object o = cls.newInstance(); //方法对象 Method method = cls.getMethod(methodName); //确定好方法是哪个对象的,然后开始调用 method.invoke(o); } } class A { public void a() { System.out.println("调用a方法"); } public void b(){ System.out.println("调用b方法"); } }
运行结果
这里便体现出反射的好处:不需要动源码,只需要改配置文件就能改变程序的运行结果
注意这里我们看到,在反射机制中,方法也是被当成对象(在反射中,万物皆对象),并且调用时是方法.对象名的形式,而不是传统的对象.方法名
除了上述作用,反射还可以完成下面这些
1.在运行时判断任意一个对象的类
2.在运行时构造任意一个类的对象
3.在运行时得到任意一个类的所有成员变量和方法
4.在运行时调用任意一个对象的成员变量和方法
5.生成动态代理
反射机制原理
反射优缺点
优点:可以动态地创建和使用对象(也是底层框架的核心),使用灵活
缺点:反射基本都是解释执行,执行效率不高
class对象
1.class对象时反射需要用到的对象,这个类直接继承于Object
2.class对象不是new出来的,是由系统创建的
3.对于某个类的class对象,只创建一次,在内存只占一份,所以一个类的信息只会加载一次
4.class对象是存放在堆中的
5.类的字节码二进制数据,是存放在方法区的,有的地方称为类的元数据(包括方法代码,变量名,方法名,访问权限等等)
获取clas对象的几种方法
1.当已知一个类的全类名时,且该类在类路径下,可通过class的静态方法forname获取,此方法可能抛出classNotFound异常
例子: Class cls = Class.forName("全类名")
应用:多用于配置文件,读取类的全路径,加载类
2.若已知具体的类,通过类名.class来直接获取,来方法最安全可靠,效率最高
例子:Class cls = 相应类名.class
应用:多用于参数传递,比如通过反射获取构造器对象
3.与2相似,若已有相应的类对象,可以对象。getClass()来获取
例子:Class cls = cat1.getClass()
4.通过类加载器来获取class对象
例子:ClassLoader clsld = car1.getClass().getClassLoader();
Class cls = clsld.loadClass("全类名")
5.基本数据类型和包装类
例子: Class cls = int.class;
Class cls = Integer.TYPE
不是所有类型都有class对象
下面列出有class对象的类型
1.外部类,成员内部类,静态内部类,局部内部类,匿名内部类
2.接口
3.数组
4.枚举
5.annotation(注解)
6.基本数据类型(以及相应包装类,因为基本数据类型和包装类的class对象是同一个)
7.void
类加载
java中类加载分为静态加载和动态加载
静态加载:编译时就开始加载需要的类,如果没有则报错,依赖性太强,即在编译时编译器必须找到需要加载的类,否则报错
动态加载:在运行到加载类的代码时,才开始加载该类,然后才会判断需不需要报错,降低依赖性
类加载时机
1.new对象
2.当该类的子类被加载
3.调用该类的静态成员时
4.反射(动态加载)
加载阶段
JVM在该阶段的主要目的是将字节码从数据源(可能是class文件,jar包,甚至是网络)转化为二进制字节流然后加载到内存中(加载到方法区),并生成一个代表该类的java.lang.class对象
连接阶段
①验证:目的是确保class文件中的字节流包含的信息符合当前虚拟机的要求,并且不损害虚拟机自身的安全,包括文件格式验证(是否以魔数0xcafebabe开头),元数据验证,字节码验证,符号引用验证。可以考虑用-Xverify:none参数来关闭大部分类验证措施,缩短虚拟机类加载时间。
②准备:JVM会在该阶段对静态变量分配内存并初始化(先赋予默认初始值,在初始化阶段才会赋予代码里写的值,但常量是一次性分配的,因为常量的原则就是不可更改),JDK7之前是在方法区分配,JDK7及之后分配在堆中
③解析:虚拟机将常量池内的符号引用替换为直接引用的过程(即分配地址给类的class对象)
初始化阶段
到了初始化阶段,开始真正执行java程序的代码,此阶段是执行<client>()方法的过程。
<client>()方法会让编译器按照语句在源文件出现的顺序,自动地依次收集类中的所有静态变量的赋值操作和静态代码块中的语句
虚拟机会保证一个类的<client>()方法在多线程环境下被正确地加锁、同步,如果有多个线程同时初始化一个类,那么只有一个线程能执行这个<client>()方法,其他线程阻塞,等待活动线程完毕,并且这个方法在一开始就会验证该类是否被加载,锁加验证,保证了类只能加载一次。