java 反射

java 反射

一、反射概念:

1.1定义

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制

静态编译和动态编译概念:
静态编译:在编译时确定类型,绑定对象;
动态编译:运行时确定类型,绑定对象

1.2 用途

与普通的类编译静态生成不一样的是,Java-Reflection能够动态的生成类,所以Java-Reflection是Java框架开发的重要的一部分,很多优秀的开源框架都是通过反射完成的。

Java-Reflection可以在在日常的第三方应用开发过程中,会遇到某个类的某个成员变量、方法或是属性是私有的或是只对系统应用开放,这时候就可以利用Java的反射机制通过反射来获取所需的私有成员或是方法。

二、反射初识

一句话概括:反射是可以绕过jvm编译阶段。可以动态加入代码。比如一个对象没有确定,可以在运行过程中动态去确定。一个对象的方法未完全实现,也可以调用该方法(部分)。

有反射,那就有正射,正射就是我们提前在代码中new 对象()

假如我需要实例化一个学生对象,代码就会是这样子。

Students user = new Students();

某一天忽然来了一个这样的场景需求,就是需要实例化老师和学校这两个对象,在代码编译阶段是不确定创建对象,那么就需要动态创建了,代码即是这样的:

 
//动态创建实体
public <T> T getClass(String param) {
    Object T = null;
    if (param.equals("student")) {
        T = new Students();
    } else if (param.equals("teacher")) {
        T = new Teacher();
    } else if (param.equals("school")) {
        T = new School();
    }
    return (T) T;
}

通过传入参数param决定使用哪一种实体,可以在项目运行时,通过动态传入参数决定使用哪一个实体,看到这里是不是有点像设计模式当中的工厂模式,是的,这个也算是应用场景之一。

那么通过这个例子简单总结下

简单总结
在代码运行之前,我们不确定将来会使用哪一种数据结构,只有在程序运行时才决定使用哪一个数据类,而反射可以在程序运行过程中动态获取类信息和调用类方法。通过反射构造类实例,代码最终会演变成下面这样。

public <T> T getPoJo(String className) throws Exception {
    Class clazz = Class.forName(className);
    return (T) clazz.newInstance();
}
  • 反射的思想:在程序运行过程中确定和解析数据类的类型。
  • 反射的作用:对于在编译期无法确定使用哪个数据类的场景,通过反射可以在程序运行时构造出不同的数据类实例。

三、Class对象特点

下图是Class类的api
请添加图片描述
从图中可以得出以下几点:

  1. Class 类的实例对象表示正在运行的 Java 应用程序中的类和接口。也就是jvm中有很多的实例,每个类都有唯一的Class对象。
  2. Class 类没有公共构造方法。Class 对象是在加载类时由 Java 虚拟机自动构造的。也就是说我们不需要创建,JVM已经帮我们创建了。
  3. Class 对象用于提供类本身的信息,比如有几种构造方法, 有多少属性,有哪些普通方法

四、实现反射机制的类

含义
java.lang.Class代表整个字节码。代表一个类型,代表整个类。
java.lang.reflect.Method代表字节码中的方法字节码。代表类中的方法。
java.lang.reflect.Constructor代表字节码中的构造方法字节码。代表类中的构造方法。
java.lang.reflect.Field代表字节码中的属性字节码。代表类中的成员变量(静态变量+实例变量)。

Java中主要由以下的类来实现Java反射机制(这些类都位于java.lang.reflect包中)
Java 反射的主要组成部分有4个:

  • Class:任何运行在内存中的所有类都是该 Class 类的实例对象,每个 Class 类对象内部都包含了本来的所有信息。记着一句话,通过反射干任何事,先找 Class 准没错!
  • Field:描述一个类的属性,内部包含了该属性的所有信息,例如数据类型,属性名,访问修饰符
  • Constructor:描述一个类的构造方法,内部包含了构造方法的所有信息,例如参数类型,参数名字,访问修饰符
  • Method:描述一个类的所有方法(包括抽象方法),内部包含了该方法的所有信息,与Constructor类似,不同之处是 Method 拥有返回值类型信息,因为构造方法是没有返回值的。

注:必须先获得Class才能获取Method、Constructor、Field。
请添加图片描述

4.1获取类对象

获取类对象有3种方式

  1. Class.forName()(常用)
  2. Hero.class
  3. new Hero().getClass()

在一个JVM中,一种类,只会有一个类对象存在。所以以上三种方式取出来的类对象,都是一样。(此处准确是在ClassLoader下,只有一个类对象)

package pojo;
public class ObjectTest {
	
	public static void main(String[] args) {
        String className = "pogo.Hero";
		try {
        	//获取类对象的第一种方式
            Class pClass1 = Class.forName(className);
            //获取类对象的第二种方式
            Class pClass2 = Hero.class;
            //获取类对象的第三种方式
            Class pClass3 = new Hero().getClass();
            System.out.println(pClass1==pClass2);//输出true
            System.out.println(pClass1==pClass3);//输出true
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 }
}

三种方式中,常用第一种,第二种需要导入类的包,依赖太强,不导包就抛编译错误。第三种对象都有了还要反射干什么。一般都第一种,一个字符串可以传入也可写在配置文件中等多种方法。

4.2利用反射机制创建对象

基本步骤
与传统的通过new 来获取对象的方式不同反射机制,反射会先拿到Hero的“类对象”,然后通过类对象获取“构造器对象”再通过构造器对象创建一个对象,具体步骤:

1.获取类对象 Class class = Class.forName(“pojo.Hero”);
2.获取构造器对象 Constructor con = clazz.getConstructor(形参.class);
3 获取对象 Hero hero =con.newInstance(实参);

上面是最简单的获取方法,当Hero的构造方法不是无参构造方法时,获取构造器对象略有不同,见下面测试:

构造方法不同时,获取构造器对象的方法
示例:

  1. Hero类添加6种构造方法
//---------------构造方法-------------------
	//(默认的构造方法)
	Hero(String str){
		System.out.println("(默认)的构造方法 s = " + str);
	}
	
	//无参构造方法
	public Hero(){
		System.out.println("调用了公有、无参构造方法执行了。。。");
	}
	
	//有一个参数的构造方法
	public Hero(char name){
		System.out.println("姓名:" + name);
	}
	
	//有多个参数的构造方法
	public Hero(String name ,float hp){
		System.out.println("姓名:"+name+"血量:"+ hp);
	}
	
	//受保护的构造方法
	protected Hero(boolean n){
		System.out.println("受保护的构造方法 n = " + n);
	}
	
	//私有构造方法
	private Hero(float hp){
		System.out.println("私有的构造方法   血量:"+ hp);
	}

  1. 通过反射机制获取对象
package test;
public class ConstructorTest {
	
	 
	/*
	 * 通过Class对象可以获取某个类中的:构造方法、成员变量、成员方法;并访问成员;
	 * 
	 * 1.获取构造方法:
	 * 		1).批量的方法:
	 * 			public Constructor[] getConstructors():所有"公有的"构造方法
	            public Constructor[] getDeclaredConstructors():获取所有的构造方法(包括私有、受保护、默认、公有)
	     
	 * 		2).获取单个的方法,并调用:
	 * 			public Constructor getConstructor(Class... parameterTypes):获取单个的"公有的"构造方法:
	 * 			public Constructor getDeclaredConstructor(Class... parameterTypes):获取"某个构造方法"可以是私有的,或受保护、默认、公有;
	 * 		
	 * 2.创建对象
	 * 		Constructor对象调用newInstance(Object... initargs)
	 */

	 
	public static void main(String[] args) throws Exception {
		//1.加载Class对象
		Class clazz = Class.forName("pojo.Hero");
		
		
		//2.获取所有公有构造方法
		System.out.println("**********************所有公有构造方法*********************************");
		Constructor[] conArray = clazz.getConstructors();
		for(Constructor c : conArray){
			System.out.println(c);
		}
		
		
		System.out.println("************所有的构造方法(包括:私有、受保护、默认、公有)***************");
		conArray = clazz.getDeclaredConstructors();
		for(Constructor c : conArray){
			System.out.println(c);
		}
		
		System.out.println("*****************获取公有、无参的构造方法*******************************");
		Constructor con = clazz.getConstructor(null);
		//1>、因为是无参的构造方法所以类型是一个null,不写也可以:这里需要的是一个参数的类型,切记是类型
		//2>、返回的是描述这个无参构造函数的类对象。
		System.out.println("con = " + con);
		//调用构造方法
		Object obj = con.newInstance();
		
		
		System.out.println("******************获取私有构造方法,并调用*******************************");
		con = clazz.getDeclaredConstructor(float.class);
		System.out.println(con);
		//调用构造方法
		con.setAccessible(true);//暴力访问(忽略掉访问修饰符)
		obj = con.newInstance(100);
	}
	

}

注:将此对象的 accessible 标志设置为指示的布尔值。值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。值为 false 则指示反射的对象应该实施 Java 语言访问检查;实际上setAccessible是启用和禁用访问安全检查的开关,并不是为true就能访问为false就不能访问 ;
由于JDK的安全检查耗时较多.所以通过setAccessible(true)的方式关闭安全检查就可以达到提升反射速度的目的

总结:

1.获取构造器对象方法:
1).批量的方法:
public Constructor[] getConstructors():所有"公有的"构造方法
public Constructor[] getDeclaredConstructors():获取所有的构造方法(包括私有、受保护、默认、公有)
2).获取单个的方法:
public Constructor getConstructor(Class… parameterTypes): 获取单个的"公有的"构造方法
public Constructor getDeclaredConstructor(Class…parameterTypes):获取"某个构造方法"可以是私有的,或受保护、默认、公有;

4.3获取成员变量并使用

基本步骤

1.获取HeroPlus类的对象 h
2. 获取属性 Field f1 = h.getDeclaredField(“属性名”)
3. 修改属性 f1.set(h,实参),注意这里的h是对象,不是类对象

示例:
1.新增HeroPlus类

package pojo;
public class HeroPlus {
	public String name;
    public float hp;
    public int damage;
    public int id;
     
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public HeroPlus(){
         
    }
    public HeroPlus(String string) {
        name =string;
    }
 
    @Override
    public String toString() {
        return "Hero [name=" + name + "]";
    }
    public boolean isDead() {
        // TODO Auto-generated method stub
        return false;
    }
    public void attackHero(HeroPlus h2) {
        System.out.println(this.name+ " 正在攻击 " + h2.getName());
    }
}

2.获取属性并修改

package test;
public class ParaTest {
	 public static void main(String[] args) {
         HeroPlus h =new HeroPlus();
         //使用传统方式修改name的值为garen
         h.name = "garen";
        
         try {
             //获取类HeroPlus的名字叫做name的字段
             Field f1= h.getClass().getDeclaredField("name");
             //修改这个字段的值
             f1.set(h, "teemo");
             //打印被修改后的值
             System.out.println(h.name);
              
         } catch (Exception e) {
             // TODO Auto-generated catch block
             e.printStackTrace();
         }
 }
}

补充:
getField和getDeclaredField的区别

getField 只能获取public的,包括从父类继承来的字段。
getDeclaredField 可以获取本类所有的字段,包括private的,但是 不能获取继承来的字段。 (注: 这里只能获取到private的字段,但并不能访问该private字段的值,除非加上setAccessible(true))

4.4获取成员方法并使用

1.获取HeroPlus类的对象 h
2.获取成员方法:
public Method getMethod(String name ,Class<?>… parameterTypes):获取"公有方法";(包含了父类的方法也包含Object类) public Method getDeclaredMethods(String name ,Class<?>… parameterTypes) :获取成员方法,包括私有的(不包括继承的)
参数解释:
name : 方法名;
Class … : 形参的Class类型对象
3.调用方法
Method --> public Object invoke(Object obj,Object… args):
参数说明:
obj : 要调用方法的对象;
args:调用方式时所传递的实参;

示例:

package test;
public class MethodTest {
	public static void main(String[] args) {
	
	 HeroPlus h = new HeroPlus();
	 
     try {
         // 获取这个名字叫做setName,参数类型是String的方法
         Method m = h.getClass().getMethod("setName", String.class);
         // 对h对象,调用这个方法
         m.invoke(h, "盖伦");
         // 使用传统的方式,调用getName方法
         System.out.println(h.getName());

     } catch (Exception e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
     }

 }
}


4.5获取main方法并使用

示例:
1.HeroPlus 新增main方法

public static void main(String[] args) {
		System.out.println("执行main方法");
	}

2.通过下面步骤获取main方法

package test;
public class MainTest {
	public static void main(String[] args) {
		try {
			//1、获取HeroPlus对象的字节码
			Class clazz = Class.forName("pojo.HeroPlus");
			
			//2、获取main方法,第一个参数:方法名称,第二个参数:方法形参的类型,
			 Method methodMain = clazz.getMethod("main", String[].class);
			//3、调用main方法
			// methodMain.invoke(null, new String[]{"a","b","c"});
			//第一个参数,对象类型,因为方法是static静态的,所以为null可以,第二个参数是String数组,这里要注意在jdk1.4时是数组,jdk1.5之后是可变参数
			//这里拆的时候将  new String[]{"a","b","c"} 拆成3个对象。所以需要将它强转。
			 methodMain.invoke(null, (Object)new String[]{"a","b","c"});//方式一
			// methodMain.invoke(null, new Object[]{new String[]{"a","b","c"}});//方式二
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		
	}
}

注意:启动Java程序的main方法的参数是一个字符串数组,即public static void main(String[] args),通过反射方式来调用这个main方法时,如何为invoke方法设置参数呢?

按jdk 1.5的语法,由于使用的是可变参数(Object类型),设置的数组参数会被作为一个参数进行传递,而按jdk 1.4的语法,此处应设置一个Object数组,数组中的每个元素对应所调用方法的一个参数。

当把一个字符串数组作为参数传递给invoke方式时,编译器会兼容jdk 1.4的语法,即按照1.4的语法进行处理,即把字符串数组打散成为若干个单独的参数,这样就会产生参数个数不匹配的异常。

解决方法:采用上述强制向上转型后,可以是编译器按照正确的方法进行参数处理,即将整个字符串参数作为整体传递给目标main方法。

注:使用new Object[]{new String[]{“a”, “b”, “c”}}作为invoke方法的第二个参数也可以得到正确的结果。比如在目标main方法中便利字符串数组元素。

五、关于反射的用法举例

反射非常强大,但是从上面的记录来看,反而觉得还不如直接调用方法来的直接和方便。
通常来说,需要在学习了Spring 的依赖注入,反转控制之后,才会对反射有更好的理解,所以先这里举两个例子,来演示一下反射的一种实际运用。

5.1 通过反射运行配置文件内容

student类:

public class Student {
	public void show(){
		System.out.println("is show()");
	}
}

配置文件以txt文件为例子(pro.txt):

className = cn.fanshe.Student
methodName = show

测试类:

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Properties;
 
/*
 * 我们利用反射和配置文件,可以使:应用程序更新时,对源码无需进行任何修改
 * 我们只需要将新类发送给客户端,并修改配置文件即可
 */
public class Demo {
	public static void main(String[] args) throws Exception {
		//通过反射获取Class对象
		Class stuClass = Class.forName(getValue("className"));//"cn.fanshe.Student"
		//2获取show()方法
		Method m = stuClass.getMethod(getValue("methodName"));//show
		//3.调用show()方法
		m.invoke(stuClass.getConstructor().newInstance());
		
	}
	
	//此方法接收一个key,在配置文件中获取相应的value
	public static String getValue(String key) throws IOException{
		Properties pro = new Properties();//获取配置文件的对象
		FileReader in = new FileReader("pro.txt");//获取输入流
		pro.load(in);//将流加载到配置文件对象中
		in.close();
		return pro.getProperty(key);//返回根据key获取的value值
	}
}
控制台输出:
is show()

需求:
当我们升级这个系统时,不要Student类,而需要新写一个Student2的类时,这时只需要更改pro.txt的文件内容就可以了。代码就一点不用改动

要替换的student2类:

public class Student2 {
	public void show2(){
		System.out.println("is show2()");
	}
}

配置文件更改为:

className = cn.fanshe.Student2
methodName = show2
控制台输出:
is show2();
5.2通过反射越过泛型检查

泛型是在编译期间起作用的。在编译后的.class文件中是没有泛型的。所有比如T或者E类型啊,本质都是通过Object处理的。所以可以通过使用反射来越过泛型。

示例:

import java.lang.reflect.Method;
import java.util.ArrayList;
 
/*
 * 通过反射越过泛型检查
 * 
 * 例如:有一个String泛型的集合,怎样能向这个集合中添加一个Integer类型的值?
 */
public class Demo {
	public static void main(String[] args) throws Exception{
		ArrayList<String> strList = new ArrayList<>();
		strList.add("aaa");
		strList.add("bbb");
		
	//	strList.add(100);
		//获取ArrayList的Class对象,反向的调用add()方法,添加数据
		Class listClass = strList.getClass(); //得到 strList 对象的字节码 对象
		//获取add()方法
		Method m = listClass.getMethod("add", Object.class);
		//调用add()方法
		m.invoke(strList, 100);
		
		//遍历集合
		for(Object obj : strList){
			System.out.println(obj);
		}
	}
}
控制台输出:
aaa
bbb
100
  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值