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")));	
	}
}




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