JAVA反射详解
文章目录
JAVA反射
什么是反射
反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
反射是java程序开发语言的特性之一,它允许运行中的java程序获取自身的信息,并且可以操作类或者对象内部的属性.
反射的核心是JVM在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。
为什么需要反射
优点
- 代码更灵活
RTTI和反射之间真正的区别只在于,对RTTI来说,编译器在在编译时打开和检查.class文件.(换句话说,我们可以用"普通"方式调用对象的所有方法).对于反射机制来说,.class文件在编译时是不可获取的,所以在运行时打开和检查.class文件。 –《编程思想》
首先RTTI的意思就是以普通的方式来创建对象,调用方法,就是我们常用的new
关键字。这段话的意思简化版就是:编译器将.java文件编译成.class文件之后,普通方式创建的对象就不能再变了,我只能选择是运行还是不运行这个.class文件。是不是感觉很僵硬,假如现在我有个写好的程序已经放在了服务器上,每天供人家来访问,这时候Mysql数据库宕掉了,改用Oracle,这时候该怎么怎么办呢?假如没有反射的话,我们是不是得修改代码,将Mysql驱动改为Oracle驱动,重新编译运行,再放到服务器上。是不是很麻烦,还影响用户的访问。
假如我们使用反射Class.forName()来加载驱动,只需要修改配置文件就可以动态加载这个类Class.forName()生成的结果在编译时是不可知的,只有在运行的时候才能加载这个类,换句话说,此时我们是不需要将程序停下来,只需要修改配置文件里面的信息就可以了。这样当有用户在浏览器访问这个网站时,都不会感觉到服务器程序程序发生了变化。
反射是字符串式编程,没有调用具体的实体,所以
不需要知道类实体,只提供类的字符串和类的方法名称,通过反射就可以实现方法的调用
-
便于维护
所以的调用都是通过字符串来实现的,所以修改类中方法的名称只需要修改调用方法的对应的字符串,不需要在每个调用到该方法的地方去修改方法的名字
缺点
-
性能问题
1.使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此Java反射机制主要应用在对灵活性和扩展性要求很高的系统框架上,普通程序不建议使用。
2.反射包括了一些动态类型,所以JVM无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被 执行的代码或对性能要求很高的程序中使用反射。
-
使用反射会模糊程序内部逻辑
程序人员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术,因而会带来维护问题。反射代码比相应的直接代码更复杂。
-
安全限制
使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如Applet,那么这就是个问题了
-
内部暴露
由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用--代码有功能上的错误,降低可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
-
Java反射可以访问和修改私有成员变量,那封装成private还有意义么?
既然小偷可以访问和搬走私有成员家具,那封装成防盗门还有意义么?这是一样的道理,并且Java从应用层给我们提供了安全管理机制——安全管理器,每个Java应用都可以拥有自己的安全管理器,它会在运行阶段检查需要保护的资源的访问权限及其它规定的操作权限,保护系统免受恶意操作攻击,以达到系统的安全策略。所以其实反射在使用时,内部有安全控制,如果安全设置禁止了这些,那么反射机制就无法访问私有成员。 -
反射是否真的会让你的程序性能降低?
1.反射大概比直接调用慢50~100倍,但是需要你在执行100万遍的时候才会有所感觉2.判断一个函数的性能,你需要把这个函数执行100万遍甚至1000万遍
3.如果你只是偶尔调用一下反射,请忘记反射带来的性能影响
4.如果你需要大量调用反射,请考虑缓存。
5.你的编程的思想才是限制你程序性能的最主要的因素
一. Class类
Class类的实例对象,用于记录类描述信息。
class是Class的实例对象
Class类没有公共的构造方法,无法通过new运算符实例化;只能通过对象的getClass方法,或是通过Class.forName(…)来获得实例。
获取Class类的三种方法
1. 知道变量名 通过<变量名>.getClass();
public class Main { public static void main(String args[]) { String tmp = "hello"; // 方法1: 通过 <变量名>.getClass() Class<?> _class = tmp.getClass(); System.out.println(_class.getName()); } }
输出结果为
java.lang.String
这种方法需要知道类的实例,通过调用实例的getClass()方法来实现。
下面两种方法则只需要知道类的名字就可以获取Class
2.知道类名通过<类名>.class
public class Main { public static void main(String args[]) { // 方法2: 通过 <类名>.class Class<?> _class = String.class; System.out.println(_class.getName()); } }
这种方法只需要得到类名,直接访问类的class属性。 最好是用
包名.类名
的形式,因为不同的包里面可以分别包含一个同名的类
3. 知道完整的类名通过Class.forNamse(“类名字”)
这种方法会抛出一个必须要捕捉的异常ClassNotFoundException
,源码部分
public static Class<?> forName(String className) throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); }
这里传入的参数是类的全称字符串。
如果Class.forName(String) 是过不了编译的。
public class Main { public static void main(String args[]) { try { Class<?> _class = Class.forName("java.lang.String"); System.out.println(_class.getName()); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
Class.forName(“pkgName.className”)的方法不需要得到具体的类的实例,参数中必须包含包名,这保证了返回的Class对象的确定的。
Class类的常用方法
-
getName()
一个Class对象描述了一个特定类的属性,Class类中最常用的方法getName以 String 的形式返回此 Class 对象所表示的实体(类、接口、数组类、基本类型或 void)名称。 -
getSimpleName()
获取类的简单名称,如
String
全称为java.long.String
,这个方法只返回String
-
newInstance()
Class还有一个有用的方法可以为类创建一个实例,这个方法叫做newInstance()。例如:x.getClass.newInstance(),创建了一个同x一样类型的新实例。newInstance()方法调用默认构造器(无参数构造器)初始化新建对象。使用方法:
Class<?> c = null; String className = "TestReflect"; //这个类和和测试类在同一个包 c = Class.forName(className); test = (TestReflect) c.newInstance(); 反射会额外的消耗一定的系统资源,如果不需要动态的创建一个对象,那么就不需要用反射
-
getConstructor()
这个函数获取类的构造方法,返回类型为Constructor类的对象,可以通过Constructor对象调用newInstance()函数来实现有参数的构造方法
Constructor 对象名字 = 获取的Class对象名.getConstructor(参数1.class,参数2.class....)
值得注意的是这里参数是Class对象,对于基本数据类型int就需要Integer.class
例子(使用时会抛出各种异常,必须要进行处理)
//类的参数构造方法 public TestReflect(String name, Integer age) { this.name = name; this.age = age; } //Constructor的使用 Class<?> c = null; Constructor constructor = null; String className = "TestReflect"; c = Class.forName(className); constructor = c.getConstructor(String.class, Integer.class); test = (TestReflect) constructor.newInstance("Tom",18); -
getClassLoader()
返回该类的类加载器。 -
getComponentType()
返回表示数组组件类型的 Class。 -
getSuperclass()
返回表示此 Class 所表示的实体(类、接口、基本类型或 void)的超类的 Class。 -
isArray()
判定此 Class 对象是否表示一个数组类。 -
getInterfaces()
获取该类实现的所有接口,返回类型是Class<?>数组
study s = new study();//study为类名 Class<?> c = s.getClass(); Class<?> inter[] = c.getInterfaces();
二. 用反射创建一个类的实例
1. 用newInstance()函数(调用的是无参数的构造方法)
实例代码
class TestReflect { private String name; private int age; public TestReflect(){ } public TestReflect(String name, int age) { this.name = name; this.age = age; } public void printMsg(){ System.out.println("name = " + this.name + "\nage = " + this.age); } } public class Main { public static void main(String args[]) { //获取Class Class<?> c = null; try{ String className = "TestReflect"; c = Class.forName(className); } catch ( ClassNotFoundException e) { e.printStackTrace(); } TestReflect test = null; try{ test = (TestReflect) c.newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e){ e.printStackTrace(); } test.printMsg(); } }
代码中出现的异常是必须要进行捕捉的,不然就会报错,还有在调用newInstance()时,要对返回的对象类型进行转换,(对象的类名) <Class对象>.newInstance();
这里对象的类名如果是父类或者接口
(父类)Class.forName("子类名字").newInstance();
这就是一个简单的工厂模式。不需要大量的判断来确定是哪个子类。改变forName的参数就可以获取不同的子类的对象
2. Constructor对象调用有参数的构造方法
先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。这种方法可以用指定的构造器构造类的实例。
Constructor类是获取类的构造方法。可以通过Constructot的对象创建需要的类的对象。
(强制转换的类)<Constructor对象>.newInstance(参数);
调用之前要先引入对应的类import java.lang.reflect.Constructor;
- 实现步骤:
-
获取class文件对象 (Class.forName)
-
使用class文件对象中的方法,解剖class文件获取构造方法Constructor
-
使用构造方法Constructor中的方法newInstance执行构造方法创建对象
import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; class TestReflect { private String name; private int age; public TestReflect(){ } public TestReflect(String name, Integer age) { this.name = name; this.age = age; } public void printMsg(){ System.out.println("name = " + this.name + "\nage = " + this.age); } } public class Main { public static void main(String args[]) { //获取Class Class<?> c = null; Constructor constructor = null; try{ String className = "TestReflect"; c = Class.forName(className); constructor = c.getConstructor(String.class, Integer.class); } catch ( ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } TestReflect test = null; try{ test = (TestReflect) constructor.newInstance("Tom",18); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e){ e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } test.printMsg(); } }
三. 使用Method对象调用类的方法
1. 获取Method类对象4个方法
- 一共有4种方法,全部都在Class类中:
Method[] getMethods():
获得类的public类型的方法,包括继承自父类的,重写接口的方法Method getMethod(String name, Class[] params)
: 获得类的特定方法,name参数指定方法的名字,params参数指定方法的参数类型Method[] getDeclaredMethods()
: 获取类中所有的方法(public、protected、default、private),但不包括继承的方法,private
字段不支持访问和调用Method getDeclaredMethod(String name, Class[] params)
: 获得类的特定方法,name参数指定方法的名字,params参数指定方法的参数类型,不能访问private
方法,如果要访问要调用method.setAccessible(true);
改变访问属性
-
使用方法(getMethod为例子)
Method name = <Class名>.getMethod("方法名字", 参数数组)
参数数组必须使用class类的, 如
String.class
// TestReflect的printMsg方法 public void printMsg(String name,Integer age){ System.out.println("name = " + name + "\nage = " + age); } //获取Method对象并且调用Method对象的invoke方法执行类的方法 String className = "TestReflect"; TestReflect reflect = new TestReflect("Jack",19); c = Class.forName(className); //---------获取---------- Method method = c.getMethod("printMsg", Class[]{String.class, Integer.class}); //---------------------- System.out.println(method.getName()); //方法的名字 System.out.println(method.getReturnType());//方法的返回类型,返回Class<?> Class<?>[] param = method.getParameterTypes();//方法的参数列表,返回Class <?>[] for (Class<?> p : param) { System.out.println(p.getName()); } //----------执行---------- method.invoke(reflect,new Object[]{new String("canshujihe"),new Integer(10)}); //----------------------- //执行reflect对象的printMsg方法,参数为"canshujihe",10 /*打印结果为 printMsg void java.lang.String java.lang.Integer name = canshujihe age = 10 */
Method中有很多方法,可以获得类中的方法信息,修改方法的权限
比较特殊getModifiers()
返回修饰符,返回类型是int型。但是修饰符输出应该是字符串。这里就需要用Modifier.toString()来进行转换。
Modifier.toString(m.getModifiers())
2. invoke(Object obj, Object args[])函数调用对象obj的方法
Method类的invoke(Object obj,Object args[])方法接收的参数必须为对象,如果参数为基本类型数据,必须转换为相应的包装类型的对象(int包装为Integer)。invoke()方法的返回值总是对象,如果实际被调用的方法的返回类型是基本类型数据,那么invoke()方法会把它转换为相应的包装类型的对象,再将其返回.
注意
如果调用的方法是静态方法,则第一个参数obj可以不是具体的对象,而是null.
例子参看上面获取Method类对象
的代码
四. 字段反射,Field对象获得或者更改对象的属性
上面已经介绍了通过getMethod()的方法来获得类的方法,现在介绍获取类属性的方法getField
1. 获得Field类的四个方法
一共有4种方法,全部都在Class类中:
-
Field[] getFields()
方法返回所有可访问的公共字段在类中声明或继承自超类。
-
Field[] getDeclaredFields()
方法返回所有字段只出现在类的声明中(不是从继承的字段)
-
Field getField(String name)
通过字段名获取
Field
对象 -
Field getDeclaredField(String name)
通过字段名获取
Field
对象
2. 使用Field对象获取类的属性
通过getXxxx()
方法可以得到想要的数据,注意方法的返回值,可以参看函数的源码来查看函数返回值
获取字段的值是用get(Object obj)函数
//类的属性 class TestReflect { public String name; public int age;} //获取类属性的方法(只能是公有(public)属性) String className = "TestReflect"; TestReflect reflect = new TestReflect("Jack",19); c = Class.forName(className); Field[] fields = c.getFields(); for(Field f: fields) { System.out.println(String.format("%s %s %s",Modifier.toString(f.getModifiers()),f.getType(),f.getName())); }
3. 使用Field对象设置某个实例的属性
要设置字段的值,请调用相应的setXxx()
方法。
设置字段的值直接调用set(Object obj, value)
; 一个Field对象确定了一个字段,所有指定要修改的对象和修改后的值就可以修改对象的该字段的值
//类的属性 class TestReflect { public String name; public int age;} String className = "TestReflect"; TestReflect reflect = new TestReflect("Jack",19); c = Class.forName(className); Field fields = c.getField("name"); System.out.println("Old name = " + fields.get(reflect)); //----------------- fields.set(reflect,"Tom"); //----------------- System.out.println("New name = "+ fields.get(reflect)); /*结果为 Old name = Jack New name = Tom /*
五. 通过setAccessible(boolean flag)获取私有方法和字段
上面访问方法的函数中有的不能访问私有方法,字段中直接不能访问私有字段。这时就可以通过setAccessible(boolean flag)
方法,传入true
改变访问属性。对private
的方法进行访问还是调用都会抛出异常
//类的属性 class TestReflect { private String name; public int age;} String className = "TestReflect"; TestReflect reflect = new TestReflect("Jack",19); c = Class.forName(className); Field fields = c.getDeclaredField("name"); //注意这里使用getField()就会抛出异常 //------------ fields.setAccessible(true); //------------- System.out.println("Old name = " + fields.get(reflect)); fields.set(reflect,"Tom"); System.out.println("New name = "+ fields.get(reflect)); /*结果为 Old name = Jack New name = Tom /*
参考博客:
本文来自博客园,作者:墨镜一戴谁也不爱,转载请注明原文链接:https://www.cnblogs.com/hnuzmh/p/16196533.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律