java反射和内省

本文学习并总结java反射和内省技术,这2部分知识个人感觉可以算是java的高深内容了,反射是用来做框架的,内省是用来操作javaBean对象的,这2部分知识有相似之处,不可或缺。

java反射的基础-Class类
java程序中的各个java类,属于同一类事物,可以用一个类来描述这类事物,这个类的名字就是Class。
Class类描述了java类的名称、访问属性、类所属的包名、字段名称的列表、方法名称的列表等。
Class对象代表了某个java类的字节码文件,将硬盘中存储的类字节码文件加载到内存有3种方法:
1. 类名.class,如Class clazz1=Person.class
2. 对象.getClass(),如Class clazz2=new Person().getClass()
3. Class.forName("类名")
如Class clazz3=Class.forName("cn.itcast.Person"),必须是完整类名(加包路径),该方法会抛ClassNotFoundException异常。

Jvm只会加载一份字节码文件到内存,同一个程序中用3种不同方法获取同一个类的字节码文件得到的Class对象是同一个。

九个预定义的Class实例对象
8种基本数据类型(byte,short,int,long,char,float,double,boolean)和void也都有Class类对象,如int.class, void.class.
只要是在源程序中出现的类型,都有各自的Class实例对象,如int[]等。

public class ReflectDemo {
	public static void main(String[] args) throws Exception
	{
		String str="abc";
		//3种方式加载String类的字节码文件
		Class cls1=str.getClass();
		Class cls2=String.class;
		Class cls3=Class.forName("java.lang.String");
		//下面2句都打印true,说明内存中只有一份String类的字节码文件
		System.out.println(cls1==cls2);
		System.out.println(cls2==cls3);
		
		//Class类常用方法
		//判断Class对象是否表示一个基本类型,只有8种基本数据类型的Class对象调用该方法时返回true
		System.out.println(cls1.isPrimitive());//false
		System.out.println(int.class.isPrimitive());//true
		//int和Integer的Class对象并不是同一个
		System.out.println(int.class==Integer.class);//false
		//8种基本数据类型都有一个静态常量TYPE,表示包装类型所包含的基本类型的Class实例
		System.out.println(int.class==Integer.TYPE);//true
		//int[]数组的Class对象也不表示基本类型,而是数组类型。
		System.out.println(int[].class.isPrimitive());//false
		System.out.println(int[].class.isArray());//true
	}
}

反射(Reflect)
反射就是把java类中的各种成分映射成相应的java类。
一个java类的组成部分:成员变量、方法、构造函数、包信息等都可以用一个个java类来表示,分别是Field、Method、Contructor、Package等,都是java.lang.reflect包中的类。
一个类中的每个成员都可以用相应的反射API类的一个实例对象来表示,这些实例对象可调用Class类的方法得到,得到这些实例对象后可通过架构思想创建java类对象,使得对象中的方法。

下面通过代码演示java类中构造函数、方法和成员变量的反射方式:

import java.lang.reflect.*;
class Point{
    private int x;
    private int y;
    public int h;
    Point(int x, int y){
        this.x=x;
        this.y=y;
    }
    private Point(int x,int y, int h){
        this.x=x;
        this.y=y;
        this.h=h;
    }
    private void show(){
    	System.out.println("x...."+x+" , "+"y...."+y);
    }
}
public class ReflectDemo2{
    public static void main(String[] args) throws Exception
    {
        String str1="wus";
        //反射String类的一个带参数的构造函数
        Constructor con1=String.class.getConstructor(StringBuffer.class);
        //通过Constructor对象的newInstance()方法创建String对象,newInstance()方法中需传递对应参数,返回Object对象,使用返回值时需强制转换。
        String str2=(String)con1.newInstance(new StringBuffer("can"));
        System.out.println(str2.charAt(2));
        //暴力反射Point类中的私有构造函数
        Constructor con2=Point.class.getDeclaredConstructor(int.class,int.class,int.class);
        //上面得到的con2所代表的构造函数仍是私有的,使用前需要强制公开其权限。
        con2.setAccessible(true);
        Point p1=(Point)con2.newInstance(6,7,8);
        
        //反射String类中的charAt()方法
        Method methodCharAt=String.class.getMethod("charAt",int.class);
        //使用Method对象的invoke()函数执行str1的charAt()方法,这是jdk1.5中invoke()调用方式。
        System.out.println(methodCharAt.invoke(str1,1));
        //jdk1.4中invoke()函数调用方式
        System.out.println(methodCharAt.invoke(str1, new Object[]{2}));
        //暴力反射Point类中的无参私有方法,下面的null也可以不写
        Method methodShow=Point.class.getDeclaredMethod("show",null);
        methodShow.setAccessible(true);
        methodShow.invoke(p1);
       
        //调用Class对象的getField()方法得反射Point类中的一个公有成员变量x.
        Field fieldh=p1.getClass().getField("h");
        //调用Field对象的get()方法,传入具体对象,获取该对象中成员变量x的值。
        System.out.println(fieldh.get(p1));
        //暴力反射成员变量y
        Field fieldy=p1.getClass().getDeclaredField("y");
        fieldy.setAccessible(true);
        System.out.println(fieldy.get(p1));        
    }
}
注解:
1. 通过Class对象的getConstructor()、getMethod()等方法反射类的构造函数、成员方法时,会抛出NoSuchMethodException和SecurityException,getField()等方法会抛出NoSuchFieldException和SecurityException。
2. newInstance()、invoke()和get()方法抛出IllegalAccessException和IllegalArgumentException,setAccessible()会抛出SecurityException异常。
3. 私有权限的构造函数和成员必须使用getDeclaredXXX()方法来反射,否则会报错,反射后使用前必须公开其权限。
4. Class类中也有newInstance()方法,是先反射出类的无参构造函数,再创建实例,因此使用该方法时,类中必须有无参构造函数。
5. 静态成员的反射,得到静态成员的Method或Field对象后,再调用invoke()或get()时,不用传递具体的对象参数,用null即可。

jdk1.5和jdk1.4中Method类的invoke()方法函数形参是不一样的:
jdk1.5中是可变参数形式Object invoke(Object obj, Object...args).
jdk1.4中是参数数组形式Object invoke(Object obj, Object[] obj).
jdk1.5为了兼容jdk1.4,invoke()方法要接收到一个参数数组时,也会将数组进行拆分,当成方法的多个参数。

import java.lang.reflect.*;
public class ReflectDemo3{
    public static void main(String[] args) throws Exception
    {
    	String startingClassName=args[0];
        Class clazz=Class.forName(startingClassName);
        Method mainMethod=clazz.getMethod("main",String[].class);
        //jdk1.5会将参数数组拆成多个String对象,从而会报IllegalArgumentException异常
        //mainMethod.invoke(null,String[]{"111","222","333"});
        //可以用下面2种方法规避这个问题
        //将String[]数组封装到一个Object[]数组中,Object[]数组里只有一个元素。
        mainMethod.invoke(null,new Object[]{new String[]{"111","222","333"}});
        //将String[]数组强转成Object类型,运行时String[]数组也就不会被拆分了。
        mainMethod.invoke(null,(Object)new String[]{"111","222","333"});
        
        Method showMethod=clazz.getMethod("show", int[].class);
        //int[]等基本数据类型数组不会被自动拆分,编译通过
        showMethod.invoke(null, new int[]{6,7,8});
    }
}
int[]等基本类型数组和String[]等引用型数组在很多使用及处理方式上都有所不同,基本类型数组更多时候是作为一个整体使用,引用型对象数组则会被自动拆分,这一点从上面的代码中已经可以看出来,下面再具体说明:
import java.lang.reflect.Array;
import java.util.Arrays;
public class ReflectDemo4 {

	public static void main(String[] args) {
		int[] a1=new int[]{1,2,3};
	    int[] a2=new int[4];
	    int[][] a3=new int[2][3];
	    String[] s1=new String[]{"a","b","c"};
	    System.out.println(a1.getClass()==a2.getClass());//true
	    /*
	              下面2句编译失败,根本无法比较:
	     Incompatible operand types Class<capture#3-of ? extends int[]> and Class<capture#4-of ? extends String[]>
		 Incompatible operand types Class<capture#5-of ? extends int[]> and Class<capture#6-of ? extends int[][]>                   
	    */
	    //System.out.println(a1.getClass()==s1.getClass());
	    //System.out.println(a1.getClass()==a3.getClass());
	    //打印[I, [代表数组,I代表int类型
	    System.out.println(a1.getClass().getName());
	    //int[]和String[]的Class对象的父类都是Object
	    System.out.println(a1.getClass().getSuperclass().getName());
	    System.out.println(s1.getClass().getSuperclass().getName());
	    
	    Object obj1=a1;
	    Object obj2=s1;
	    Object obj3=a3;
	    //int[]一维数组不能赋给Object[]数组
	    //Object[] obj4=a1;
	    //int[][]二维数组和String[]都可以赋给Object[]数组
	    Object[] obj5=a3;
	    Object[] obj6=s1;
	    System.out.println(obj1);
	    System.out.println(obj2);
	    System.out.println(obj3);
	    System.out.println(obj5);
	    System.out.println(obj6);    
	    /*
	     jdk1.4和jdk1.5中,Arrays类中asList()方法形参形式并不同
	     jdk1.4中是asList(Object[] objs)
	     jdk1.5中是asList(T... a),可变参数形式
	    */
	    //int[]数组不能转换成Object[]数组,所以只能匹配jdk1.5中的asList()可变形参方式,从而将整 个int[]数组作为一个元素放在List集合 
	    System.out.println(Arrays.asList(a1));
	    //String[]数组可以转换成Object[]数组,jdk1.5兼容jdk1.4,接收到String[]数组时会自动进行拆分,匹配Object[]数组
	    System.out.println(Arrays.asList(s1));
	    
	    //打印Object对象,可以传入数组或单个对象
	    printObject(a1);
	    //printObject(a3);
	    printObject("xyz");
	    
	}
	//数组的反射
	public static void printObject(Object obj){
		Class clazz=obj.getClass();
		if(clazz.isArray()){
			int len=Array.getLength(obj);
			for(int i=0;i<len;i++)
				System.out.println(Array.get(obj, i));
		}else{
			System.out.println(obj);
		}
	}
}
/*
 运行结果:
true
[I
java.lang.Object
java.lang.Object
[I@54fe0ce1
[Ljava.lang.String;@72ffb35e
[[I@71591b4d
[[I@71591b4d
[Ljava.lang.String;@72ffb35e
[[I@54fe0ce1]
[a, b, c]
1
2
3
xyz
*/
反射的作用-实现框架功能
软件开发时,一般先做框架,这样可以提高开发效率时,而做框架时,具体使用哪些类及类的具体功能可能都还未实现,不能直接用new创建某些对象,只能先从配置文件读取类名或传入类名字符串等进行反射,等运行时,再填补具体的对象。
怎么在程序中加载一个配置文件以供读取呢?下面的代码中给出了3种方式:
package cn.itcast.day03;
import java.io.*;
import java.util.Properties;
public class ReflectDemo5 {

	public static void main(String[] args) throws Exception
	{
		//这里相对路径针对的是当前java project的工作空间根目录,如当前java project是Blog3,Eclipse工作空间是F:\eclipsework\workspace,那么这里的相对路径根目录就是F:\eclipsework\workspace\Blog3
		FileInputStream fis=new FileInputStream("config.properties");
		//eclipse在保存文件时,会将源程序src文件夹中的java文件进行自动编译,并将生成的class文件自动放到bin目录下,将非java文件原封不动地移到bin目录下。
		//下面这种方式的相对路径就是当前java project的class文件根目录,不含包名,在eclipse中编程时就是bin目录,完整路径是F:\eclipsework\workspace\Blog3\bin
		//可以直接将config.properties文件写在java project中src目录下,这样该文件会自动被放到bin目录下
		//使用这种方式获取配置文件时,路径名不能以"/"开头,否则会找不到文件
		InputStream is=ReflectDemo5.class.getClassLoader().getResourceAsStream("cn/itcast/day03/config.properties");
		//下面这种方式的相对路径是调用getResourceAsStream()方法的Class对象对应的class文件所在目录,含包名,即F:\eclipsework\workspace\Blog3\bin\cn\itcast\day03
		//这种方式也可以使用绝对路径,加上包名,即"/cn/itcast/day03/config.properties
		InputStream iss=ReflectDemo5.class.getResourceAsStream("Resource/config.properties");
		Properties prop=new Properties();
		prop.load(iss);
		String className=prop.getProperty("className");
		System.out.println(className);		
	}

}
注解:
1. 程序中写路径名是一定要用完整路径,但完整的路径不是硬编码,而是运算出来的。
2. 第一种方式除了可以读配置文件,还可以用FileWriter写配置文件。
3. 第二种和第三种方式,其实config.properties文件放置的路径目录是一样的,只不过程序中代码写法有点不同,第三种方式更简洁。
内省(Introspector)
内省是专门用来操作JavaBean的。
JavaBean是一种特殊的Java类,这种类中获取和设置属性的方法名必须是get+属性名和set+属性名,符合这个规则的Java类就叫JavaBean,JavaBean中有对应get和set方法的属性才叫JavaBean的属性。任何一个java类中都有getClass()方法,从而都有class属性。
      get和set方法名中除get和set以外剩下的名称是属性名称,但使用属性名时要注意的是,如果属性名第二个字母为小写,则第一个字母也自动小写,如getAge()方法中的属性名应该是age。get和set方法一般是供外部访问成员变量的,多为public权限。
JavaBean可以当作普通类进行使用,也可以当作JavaBean使用,作为JavaBean使用的好处是
1. JavaEE开发中,经常要用到JavaBean,很多环境要求按照JavaBean方式进行操作。
2. JDK中提供了对JavaBean进行操作的一些API, 这套API就称为内省,用内省来操作JavaBean比使用普通类的方式更方便。
内省的一个基本演示:
/*
 定义Person类
 */
package cn.itcast.day03;
import java.util.Date;
public class Person{
	String name;
	int age;
	Date birthday;	
	public Person(){
		
	}
	public Person(String name, int age, Date birthday) {
		super();
		this.name = name;
		this.age = age;
		this.birthday = birthday;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	public Date getBirthday() {
		return birthday;
	}
	public void setBirthday(Date birthday) {
		this.birthday = birthday;
	}
	public String toString(){
		return this.name+"..."+this.age+"..."+this.birthday;
	}
}
package cn.itcast.day03;
import java.beans.*;
import java.lang.reflect.*;
/*
给定对象和属性名,只知道类中有该属性的get和set方法,用内省来获取和设置对象的属性值。
 */
public class JavaBeanDemo1 {

	public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, IntrospectionException 
	{
		Person p=new Person("lisi",20,"1987-09-09");
		System.out.println(getProperty(p,"age"));
		setProperty(p,"age",33);
		System.out.println(getProperty(p,"age"));
	}
	public static void setProperty(Object obj,String propertyName,Object value) throws IntrospectionException,IllegalAccessException,InvocationTargetException
	{
		//获取属性描述器
		PropertyDescriptor pd=new PropertyDescriptor(propertyName,obj.getClass());
		//获取属性的get方法
		Method writeMethod=pd.getWriteMethod();
		writeMethod.invoke(obj, value);
		//获取要操作的属性所属类型
		System.out.println("属性类型:"+pd.getPropertyType());
		
	}
	public static Object getProperty(Object obj, String propertyName) throws IntrospectionException,IllegalAccessException,InvocationTargetException
	{
		PropertyDescriptor pd=new PropertyDescriptor(propertyName,obj.getClass());
		//获取属性的set方法。
		Method readMethod=pd.getReadMethod();
		Object retVal=readMethod.invoke(obj);
		return retVal;
	
	}
	//另一种方式获取属性值的方式,略显复杂,类似地的方式也可以重写setProperty方法。
	public static Object getProperty2(Object obj, String propertyName) throws IntrospectionException,IllegalAccessException,InvocationTargetException
	{	
		//BeanInfo对象封装了把这个类当作JavaBean看的结果信息,里面有javaBean的所有属性,包括从所有父类继承过来的属性。
		BeanInfo beanInfo=Introspector.getBeanInfo(obj.getClass());
		//下面的方式可以去除从Object类中继承的属性,只获取javabean自己的属性。
		//BeanInfo beanInfo=Introspector.getBeanInfo(obj.getClass(), Object.class);
		PropertyDescriptor[] pds=beanInfo.getPropertyDescriptors();
		Object retVal=null;
		for(PropertyDescriptor pd:pds){
			if(pd.getName().equals(propertyName)){
				Method readMethod=pd.getReadMethod();
				retVal=readMethod.invoke(obj);
				break;
			}
		}
		return retVal;	
	}
}
/*
 运行结果:
20
属性类型:int
33
 */
BeanUtil工具包,可专门用来操作javabean属性,具体包是commons-beanutils.jar(包名中带版本号,此jar包相当于commons-beanutils-bean-collections.jar和commons-beanutils-core.jar两个jar包的总合),因该jar包还使用了log打印包(commons-logging-1.1.jar),所以使用时需要导入这2个包。
下面演示BeanUtils的基本用法:
package cn.itcast.day03;
import java.lang.reflect.*;
import java.util.*;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
public class beanUtilsDemo {
	public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException 
	{
		Person p1=new Person("lisi",20,"1987-09-09");
		Person p2=new Person();
		System.out.println(BeanUtils.getProperty(p1,"age"));
		//BeanUtils中getProperty()得到的属性值都是String类型的,setProperty()方法参数形式是setProperty(Object obj,String name,Object value),如果value值为String字符串,BeanUtils也会自动该字符串转成实际属性的类型,但只限于8种基本数据类型,不能自动转成对象类型
		System.out.println(BeanUtils.getProperty(p1,"age").getClass().getName());
		BeanUtils.setProperty(p1, "age", "30");
		//PropertyUtils中,getPropety()方法得到的属性值就是实际javabean中的属性类型,同样setProperty()方法中value值必须与javabean中属性实际类型一致,不能写成字符串形式,否则运行时会报IllegalArgumentException异常。
		PropertyUtils.setProperty(p1, "age", 50);
		System.out.println(PropertyUtils.getProperty(p1, "age").getClass().getName());
		System.out.println(BeanUtils.getProperty(p1,"age"));
		//将一个javabean对象的属性cope到另一个对象
		BeanUtils.copyProperties(p2, p1);
		System.out.println(BeanUtils.getProperty(p2,"name"));
		
	    //javabean与map之间的转换
		//javabean转换成Map集合
		Map map=BeanUtils.describe(p1);
        System.out.println(map);
        //BeanUtils还可直接操作Map集合
        BeanUtils.setProperty(map, "age", 40);
        System.out.println(map);
        //Map集合封装成javabean
        Person p3=new Person();
        BeanUtils.populate(p3, map);
        System.out.println(p3);
        //BeanUtils中字符串和8种基本数据类型会按需要自动相互转换。
        Map map2=new HashMap();
        map2.put("name", "zhaoliu");
        map2.put("age", "37");//写成37也可以
        map2.put("birthday", "1987-02-02");
        Person p4=new Person();
        BeanUtils.populate(p4, map2);
        System.out.println(p4);
	}
}
/*
运行结果:
20
java.lang.String
java.lang.Integer
50
lisi
{birthday=1987-09-09, name=lisi, age=50, class=class cn.itcast.day03.Person}
{birthday=1987-09-09, name=lisi, age=40, class=class cn.itcast.day03.Person}
lisi...40...1987-09-09
zhaoliu...37...1987-02-02
*/

上面的演示中已经指出,BeanUtils中字符串和8种基本数据类型可以根据需要相互自动转换,那如果javabean中有一个属性不是基本数据类型,也不是字符串,怎么用BeanUtils设置该属性值呢?
这时候就需要在BeanUtils中注册一个转换器
注册转换器是使用ConverUtils工具类中的register(Converter con, Class clazz)方法,其中的Converter是一个接口,该接口中只有一个方法Object convert(Class type, Object value),使用时只需复写该方法,可以使用匿名内部类实现。

/*注册Date日期转换器,方便用BeanUtils设置javabean的Date属性*/                                        import java.util.Date;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import org.apache.commons.beanutils.*;
import org.apache.commons.beanutils.locale.converters.DateLocaleConverter;
import cn.itcast.day03.Person11;
public class ConvertUtilsDemo {
	public static void main(String[] args) throws Exception
	{
		setDate();
	}
	public static void setDate() throws Exception
	{
		Person p=new Person("tianqi",10,new Date(System.currentTimeMillis()));
		String birthday="2002-10-09";
		SimpleDateFormat df=new SimpleDateFormat("yyyy-MM-dd");
		System.out.println(df.format(PropertyUtils.getProperty(p, "birthday")));
		//注册日期转换器
		ConvertUtils.register(new Converter()
		{
			public Object convert(Class type,Object value){
				if(value==null)
					return null;
				if(!(value instanceof String))
					throw new ConversionException("只支持String类型的转换");
				String str=(String)value;
				if(str.trim().equals(""))
					return null;
				SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
				try{
					return sdf.parse(str);
					
				}
				catch(ParseException e){
					throw new RuntimeException(e);
				}
			}
		}, Date.class);		
		BeanUtils.setProperty(p,"birthday",birthday);
		//另一种方式 ,DateLocaleConverter是Converter接口的一个实现子类,但该类内部在bug,不太健壮,在value值为空时,仍可转换,不会报错。
		ConvertUtils.register(new DateLocaleConverter(), Date.class);
		System.out.println(df.format(PropertyUtils.getProperty(p, "birthday")));	
	}
}




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章