【Java SE基础 七】Java反射机制

JAVA反射机制是在运行状态中,获取任意一个类的结构 、创建对象 、得到方法、执行方法 、属性,这种在运行状态动态获取信息以及动态调用对象方法的功能被称为java语言的反射机制。

反射机制

我们知道正确的从Java源文件编译为class文件,到class文件被加载到JVM成为机器码文件。编译的过程就是java文件变为class文件,而反射机制就是一种反编译,通过class文件对应的类动态生成一个Java对象,图片来源

总而言之,可以理解为运行时从class类对象又反向获取了一个实例。正常的class文件加载机制是通过下边提到的类加载器来实现的,详细可以看我的JVM系列。

类加载器

Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中。java默认有三种类加载器:BootstrapClassLoader、ExtensionClassLoader、App ClassLoader。

  • BootstrapClassLoader(引导启动类加载器):嵌在JVM内核中的加载器,该加载器是用C++语言(原生语言)写的,主要负载加载JAVA_HOME/lib下的类库,引导启动类加载器无法被应用程序直接使用
  • ExtensionClassLoader(扩展类加载器):JAVA编写,父类加载器为BootstrapClassLoader。由sun.misc.Launcher$ExtClassLoader实现的,主要加载JAVA_HOME/lib/ext目录中的类库。
  • AppClassLoader(应用类加载器):应用程序类加载器,负责加载应用程序classpath目录下的所有jar和class文件。它的父加载器为ExtensionClassLoader

类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与文件系统。

委派加载

双亲委派模型:如果一个类加载器收到了一个类加载请求,它不会自己去尝试加载这个类,而是把这个请求转交给父类加载器去完成。每一个层次的类加载器都是如此。因此所有的类加载请求都应该传递到最顶层的启动类加载器中,只有到父类加载器反馈自己无法完成这个加载请求(在它的搜索范围没有找到这个类)时,子类加载器才会尝试自己去加载。委派的好处就是避免有些类被重复加载。

反射实现

反射机制是要对字节码文件进行剖析,那么就必须有字节码文件对象。而字节码文件是以.class为扩展名的,因此,Java提供的反射类中就有java.lang.Class;反射机制是对Java对象的属性,方法,构造方法等进行访问。 那么就有相应的反射类,

  • 对成员变量的访问java.lang.reflect.Filed,返回类的成员变量
  • 对方法的访问java.lang.reflect.Method,返回类的方法
  • 对构造器的访问java.lang.reflect.Constructor,返回类的构造器
  • 对修饰符的获取java.lang.reflect.Modifier,返回一个类或者其成员的访问修饰符的int类型常量

这些是常用的,更多的可以看Java的相关API文档,接下来我们就几种具体的实现来演示一下。待反射类如下:

     String name;
        private int age;
        public int money;
        public static int sex;
        public Person() {
            System.out.println("无参构造器");
        }

        public Person(String name, int age) {
            System.out.println("有参构造器");
            this.name = name;
            this.age = age;
        }

        public static void StaticFunc(int a){
            System.out.println("调用了:公有的,StaticFunc(): a = " + a);
        }

        @Override
        public String toString() {
            System.out.println("调用了:公有的无参的,toString(): " + "Person{" +  "name='" + name + '\'' +  ", age=" + age +  '}');
            return "Person{" +  "name='" + name + '\'' +  ", age=" + age +  '}';
        }

获取类对象并使用

要想了解一个类,必须先要获取到该类的字节码文件对象。在Java中,每一个字节码文件,被加载到内存后,都存在一个对应的Class类型的对象,封装了描述方法的Method,描述字段的Filed,描述构造器的Constructor等属性:

  • 对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象, 一个 Class 对象包含了特定某个类的有关信息。
  • Class 对象只能由系统建立对象
  • 一个类在 JVM 中只会有一个Class实例

在以上的加载器中加载的就是这个类对象。获取一个类对象操作如下,共有以下三种方式:

  public static void main(String[] args) throws ClassNotFoundException {
	    	    Class clazz = null;  		      
		        //1 直接通过类名.Class的方式得到  
		        clazz = Person.class;  
		        System.out.println("通过类名: " + clazz);  
		      
		        //2 通过对象的getClass()方法获取,这个使用的少(一般是传的是Object,不知道是什么类型的时候才用)  
		        Object obj = new Person();  
		        clazz = obj.getClass();  
		        System.out.println("通过getClass(): " + clazz);  
		      
		        //3 通过全类名获取,用的比较多,但可能抛出ClassNotFoundException异常  
		        clazz = Class.forName("packageB.Person");  
		        System.out.println("通过全类名获取: " + clazz);
		}

测试结果:

通过类名: class packageB.Person
无参构造器
通过getClass(): class packageB.Person
通过全类名获取: class packageB.Person

获取构造方法并使用

通过反射获取构造方法有如下两种方式:

1、批量获取的方法:

所有"公有的"构造方法
public Constructor[] getConstructors():
获取所有的构造方法(包括私有、受保护、默认、公有)
public Constructor[] getDeclaredConstructors()

2、单个获取的方法,并调用:

获取单个的"公有的"构造方法:
public Constructor getConstructor(Class... parameterTypes):
获取"某个构造方法"可以是私有的,或受保护、默认、公有;
public Constructor getDeclaredConstructor(Class... parameterTypes):

3,调用构造方法来创建类对象实例,我们可以在获取到类对象后,利用newInstance方法,创建一个该类对象的实例,如下所示,构造含有两个参数的初始化。

public static void main(String[] args) throws ClassNotFoundException, 
InstantiationException, IllegalAccessException, NoSuchMethodException, 
SecurityException, IllegalArgumentException, InvocationTargetException {
	    	 Class clazz = Class.forName("packageB.Person");  
	    	 //使用Class类的newInstance()方法创建类的一个对象  
	    	 //实际调用的类的哪个无参数的构造器(这就是为什么写的类的时候,要写一个无参数的构造器,就是给反射用的)  
	    	 //一般的,一个类若声明了带参数的构造器,也要声明一个无参数的构造器  
	    	 Constructor cs = clazz.getConstructor(String.class,int.class);
	    	 Object obj = cs.newInstance("tml",12);  
	    	 System.out.println(obj);  
		}

测试结果如下

有参构造器
Person{name='tml', age=12}

获取成员变量并使用

按照如上的定义我们来获取成员变量并且来使用成员变量,同样的字段获取也有几种方式:

1、批量的获取所有字段

1).Field[] getFields();获取所有的"公有字段"
2).Field[] getDeclaredFields();获取所有字段,包括:私有、受保护、默认、公有

2、获取单个的指定字段:

1).public Field getField(String fieldName);获取某个"公有的"字段;
2).public Field getDeclaredField(String fieldName);获取某个字段(可以是私有的)

以下为使用实例,看成员变量被输出什么内容:

public static void main(String[] args) throws Exception {
	    	  Class clazz=Class.forName("packageB.Person");
	          //访问本类中所有字段
	          Field[] fs=clazz.getDeclaredFields();
	          for (int i = 0; i < fs.length; i++) {
	              System.out.println("访问本类中所有字段---fs-->"+fs[i]);
	          }
	          //访问本类中的公有字段
	          Field[] fields=clazz.getFields();
	          for (int i = 0; i < fields.length; i++) {
	              System.out.println("访问本类中的公有字段---fields-->"+fields[i]);
	          }
	          System.out.println("==================================================================");
	          //getField访问某个字段,该字段必须公有
	          Field field=clazz.getField("money");
	           System.out.println("访问指定字段,该字段必须公有---field-->"+field);
	          //getDeclaredField访问本类中某个字段,可私有
	          Field field2=clazz.getDeclaredField("name");
	          System.out.println("访问指定字段,该字段可私有---declaredField-->"+field2);
	          //getField访问字段为私有,运行时异常:NoSuchFieldException
	         // Field field1=clazz.getField("id");
		}

运行结果

访问本类中所有字段---fs-->java.lang.String packageB.Person.name
访问本类中所有字段---fs-->private int packageB.Person.age
访问本类中所有字段---fs-->public int packageB.Person.money
访问本类中所有字段---fs-->public static int packageB.Person.sex
访问本类中的公有字段---fields-->public int packageB.Person.money
访问本类中的公有字段---fields-->public static int packageB.Person.sex
==================================================================
访问指定字段,该字段必须公有---field-->public int packageB.Person.money
访问指定字段,该字段可私有---declaredField-->java.lang.String packageB.Person.name

获取成员方法并使用

同获取字段类似,获取方法有getMethod和getDeclaredMethod,其中前者是获取的都是公有的,后者获取的都是本类的,包括私有

1、批量的获取所有成员方法

获取所有"公有方法";(包含了父类的方法也包含Object类)
public Method[] getMethods();
获取所有的成员方法,包括私有的(不包括继承的)
public Method[] getDeclaredMethods();

调用示例如下

 public static void main(String[] args) throws Exception {
	    	 Class clazz=Class.forName("packageB.Person");
	         Method[] methods=clazz.getMethods();   //所有公有方法
	         for (int i = 0; i < methods.length; i++) {
	             System.out.println(methods[i]);
	         }
	         System.out.println("=======================================================");
	         Method[] methods2=clazz.getDeclaredMethods();//本类的全部方法
	         for (int i = 0; i < methods2.length; i++) {
	             System.out.println(methods2[i]);
	         }
		}

运行结果

public static void packageB.Person.main(java.lang.String[]) throws java.lang.Exception
public java.lang.String packageB.Person.toString()
public static void packageB.Person.StaticFunc()
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
=======================================================
public static void packageB.Person.main(java.lang.String[]) throws java.lang.Exception
public java.lang.String packageB.Person.toString()
public static void packageB.Person.StaticFunc()

2、获取单个指定成员方法

获取指定"公有方法";(包含了父类的方法也包含Object类)
public Method getMethod(String name,Class<?>... parameterTypes);
获取指定的成员方法,包括私有的(不包括继承的)
public Method getDeclaredMethod(String name,Class<?>... parameterTypes);

调用示例如下:

 public static void main(String[] args) throws Exception {
        Class clazz=Class.forName("packageB.Person");
        //有参的方法获取
        Method method=clazz.getMethod("StaticFunc", int.class);
        System.out.println("有参方法"+method);
        //利用缺省构造方法初始化然后来调用有参方法
        Object obj=clazz.newInstance();
        method.invoke(obj, 21);
        //无参的方法获取
        Method method1=clazz.getMethod("toString", null);
        System.out.println("无参方法"+method1);
        //利用缺省构造方法初始化然后来调用无参方法
        Object object=clazz.newInstance();
        method1.invoke(object, null);
        //利用有参构造方法初始化然后来调用无参方法
        Constructor cs=clazz.getConstructor(String.class,int.class);
        Object obj2=cs.newInstance("xiaoming",23);
        Method method2=clazz.getMethod("toString",null);  //运行前已赋值
        method2.invoke(obj2, null);
    }

运行结果如下

有参方法public static void packageB.Person.StaticFunc(int)
无参构造器
调用了:公有的,StaticFunc(): a = 21
无参方法public java.lang.String packageB.Person.toString()
无参构造器
调用了:公有的无参的,toString(): Person{name='null', age=0}
有参构造器
调用了:公有的无参的,toString(): Person{name='xiaoming', age=23}

反射的用途

理解了反射机制,那么我们什么时候需要使用反射呢?反射在日常工作中有什么作用呢?

  1. 反编译文件:.class–>.java,有了反射就可以看到源代码了
  2. 通过反射机制访问java对象的属性,方法,构造方法等并且使用
  3. 当我们在使用IDE时,当我们输入一个对象或者类,并想调用他的属性和方法是,一按点号,编译器就会自动列出他的属性或者方法,这里就是用到反射。
  4. 反射最重要的用途就是开发各种通用框架。比如很多框架(Spring)都是配置化的(比如通过XML文件配置Bean),为了保证框架的通用性,他们可能需要根据配置文件加载不同的类或者对象,调用不同的方法,这个时候就必须使用到反射了,运行时动态加载需要的加载的对象。
  5. 加载数据库驱动,用到的也是反射,Class.forName(“com.mysql.jdbc.Driver”); // 动态加载mysql驱动

框架的举例是:例如,在使用Strut2框架的开发过程中,我们一般会在struts.xml里去配置Action,

<action name="login" class="org.ScZyhSoft.test.action.SimpleLoginAction" method="execute">   
    <result>/shop/shop-index.jsp</result>           
    <result name="error">login.jsp</result>       
</action>

那么一个请求的正常步骤如下:

  1. 请求login.action时,那么StrutsPrepareAndExecuteFilter拦截请求,然后就会去解析struts.xml文件,从action中查找出name为login的Action
  2. 根据class属性创建SimpleLoginAction实例,并用Invoke方法来调用execute方法

整个过程离不开反射。配置文件与Action建立了一种映射关系,拦截请求后去动态加载类对象的实例然后去调用方法。

总结而言:以普通的方式来创建对象,调用方法,就是我们常用的new关键字:编译器将.java文件编译成.class文件之后,普通方式创建的对象就不能再变了,我只能选择是运行还是不运行这个.class文件。是不是感觉很僵硬,假如现在我有个写好的程序已经放在了服务器上,每天供人家来访问,这时候Mysql数据库宕掉了,改用Oracle,这时候该怎么怎么办呢?假如没有反射的话,我们是不是得修改代码,将Mysql驱动改为Oracle驱动,重新编译运行,再放到服务器上。是不是很麻烦,还影响用户的访问。假如我们使用反射Class.forName()来加载驱动,只需要修改配置文件就可以动态加载这个类,Class.forName()生成的结果在编译时是不可知的,只有在运行的时候才能加载这个类,换句话说,此时我们是不需要将程序停下来,只需要修改配置文件里面的信息就可以了。这样当有用户在浏览器访问这个网站时,都不会感觉到服务器程序程序发生了变化,同理对于通用型框架来说,配置性大大提高,如同Spring IOC容器,给很多配置设置参数,使得java应用程序能够顺利跑起来,大大提高了Java的灵活性和可配置性,降低模块间的耦合

尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心:

  • 性能限制,反射包括了一些动态类型,所以JVM无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被 执行的代码或对性能要求很高的程序中使用反射。
  • 安全限制,使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如Applet,那么这就是个问题了。
  • 内部暴露,由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,代码有功能上的错误,降低可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。

总之反射有优势也有缺点,具体还是看使用场景了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

存在morning

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值