1. 程式人生 > >Java反射的常見用法

Java反射的常見用法

    反射的常見用法有三類,第一類是“檢視”,比如輸入某個類的屬性方法等資訊,第二類是“裝載“,比如裝載指定的類到記憶體裡,第三類是“呼叫”,比如通過傳入引數,呼叫指定的方法。

1 檢視屬性的修飾符、型別和名字

    通過反射機制,我們能從.class檔案裡看到指定類的屬性,比如屬性的修飾符,屬性和型別和屬性的變數名。通過下面的ReflectionReadVar.java,我們看演示下具體的做法。    

1	import java.lang.reflect.Field;
2	import java.lang.reflect.Modifier;
3	class MyValClass{
4		private int val1;
5		public String val2;
6		final protected String val3 = "Java";

    我們在第3行定義了一個MyValCalss的類,並在第4到第6行裡,定義了三個屬性變數。    

8	public class ReflectionReadVar {
9		public static void main(String[] args) {
10			Class<MyValClass> clazz = MyValClass.class;
11			//獲取這個類的所有屬性
12	        Field[] fields = clazz.getDeclaredFields();
13		    for(Field field : fields) {
14		    	   //輸出修飾符	    	   System.out.print(Modifier.toString(field.getModifiers()) + "\t");
15		    	   //輸出屬性的型別
16		       System.out.print(field.getGenericType().toString() + "\t");
17		    	   //輸出屬性的名字
18		    	   System.out.println(field.getName());
19		      }
20		}
21	}

    在main函式的第10行裡,通過MyValClass.class,得到了Class<MyValClass>型別的變數clazz,在這個變數中,儲存了MyValClass這個類的一些資訊。

    在第12行裡,通過了clazz.getDeclaredFields()方法得到了MyValClass類裡的所有屬性的資訊,並把這些屬性的資訊存入到Field陣列型別的fields變數裡。

    通過了第13行的for迴圈依次輸出了這些屬性資訊。具體來講,通過第14行的程式碼輸出了該屬性的修飾符,通過第16行的程式碼輸出了該屬性的型別,通過第18行的程式碼輸出了該屬性的變數名。這段程式碼的輸出如下,從中我們能看到各屬性的資訊。

          1      private    int val1

          2      public class java.lang.String   val2

          3      protected final   class java.lang.String   val3   

2 檢視方法的返回型別,引數和名字

    通過ReflectionReadFunc.java,我們能通過反射機制看到指定類的方法。    

1	import java.lang.reflect.Constructor;
2	import java.lang.reflect.Method;
3	class MyFuncClass{
4		public MyFuncClass(){}
5		public MyFuncClass(int i){}
6		private void f1(){}
7		protected int f2(int i){return 0;}
8		public String f2(String s) {return "Java";}

    在第3行定義的MyFuncClass這個類裡,我們定義了2個建構函式和3個方法。 

10	public class ReflectionReadFunc {
11		public static void main(String[] args) {
12			Class<MyFuncClass> clazz = MyFuncClass.class;
13	        Method[] methods = clazz.getDeclaredMethods();
14	        for (Method method : methods) 
15	        { System.out.println(method); }
16	        //得到所有的建構函式
17	        Constructor[] c1 = clazz.getDeclaredConstructors();
18	        //輸出所有的建構函式
19	        for(Constructor ct : c1)
20	        { System.out.println(ct);  }
21		}
22	}

    在main函式的第12行,我們同樣是通過了類名.class的方式(也就是MyFuncClass.class的方式)得到了Class<MyFuncClass>型別的clazz物件。

    在第13行裡,是通過了getDeclaredMethods方法得到了MyFuncClass類的所有方法,並在第14行的for迴圈裡輸出了各方法。在第17行裡,是通過了getDeclaredConstructors方法得到了所有的建構函式,並通過第19行的迴圈輸出。

    本程式碼的輸出結果如下所示,其中第1到第3行輸出的是類的方法,第4和第5行輸出的是類的建構函式。    

1	private void MyFuncClass.f1()
2	protected int MyFuncClass.f2(int)
3	public java.lang.String MyFuncClass.f2(java.lang.String)
4	public MyFuncClass()
5	public MyFuncClass(int)

    不過在實際的專案裡,我們一般不會僅僅“檢視”類的屬性和方法,在更多的情況裡,我們是通過反射裝載和呼叫類裡的方法。

3 通過forName和newInstance方法載入類

    在前文JDBC操作資料庫的程式碼裡,我們看到在建立資料庫連線物件(Connection)之前,需要通過Class.forName("com.mysql.jdbc.Driver");的程式碼來裝載資料庫(這裡是MySQL)的驅動。

    可以說,Class類的forName方法最常見的用法就是裝載資料庫的驅動,以至於不少人會錯誤地認為這個方法的作用是“裝載類”。

    其實forName方法的作用僅僅是返回一個Class型別的物件,它一般會和newInstance方法配套使用,而newInstance方法的作用才是載入類。

    通過下面的ForClassDemo.java這段程式碼,我們來看下綜合使用forName和newInstance這兩個方法載入物件的方式。    

1	class MyClass{
2		public void print()
3		{	System.out.println("Java");	}
4	}
5	public class ForClassDemo {
6		public static void main(String[] args)	{
7	        //通過new建立類和使用類的方式
8			MyClass myClassObj = new MyClass();
9			myClassObj.print();//輸出是Java
10			//通過forName和newInstance載入類的方式	
11			try {
12				Class<?> clazz = Class.forName("MyClass");
13				MyClass myClass = (MyClass)clazz.newInstance();
14				myClass.print();//輸出是Java
15			} catch (ClassNotFoundException e) {
16				e.printStackTrace();
17			} catch (InstantiationException e) {
18				e.printStackTrace();
19			} catch (IllegalAccessException e) {
20				e.printStackTrace();
21			}		
22		}
23	}

    在第1行定義的MyClass這個類裡,我們在其中的第2行定義了一個print方法。

    Main函式的第8和第9行裡,我們演示了通過常規new的方式建立和使用類的方式,通過第9行,我們能輸出“Java”這個字串。

    在第12行,我們通過Class.forName("MyClass")方法返回了一個Class型別的物件,請注意,forName方法的作用不是“載入MyClass類”,而是返回一個包含MyClass資訊的Class型別的物件。這裡我們是通過第13行的newInstance方法,載入了一個MyClass型別的物件,並在第14行呼叫了其中的print方法。

    既然forName方法的作用僅僅是“返回Class型別的物件”,那麼在JDBC部分的程式碼裡,為什麼我們能通過Class.forName("com.mysql.jdbc.Driver");程式碼來裝載MySQL的驅動呢?在MySQL的com.mysql.jdbc.Driver驅動類中有如下的一段靜態初始化程式碼。   

1	static {
2	try {
3	   java.sql.DriverManager.registerDriver(new Driver());
4	} catch (SQLException e) {
5	throw new RuntimeException(“Can’t register driver!”);
6	}
7	}

    也就是說,當我們呼叫Class.forName方法後,會通過執行這段程式碼會新建一個Driver的物件,並呼叫第3行的DriverManager.registerDriver把剛建立的Driver物件註冊到DriverManager裡。

    在上述的程式碼裡,我們看到了除了new之外,我們還能通過newInstance來建立物件。

    其實這裡說“建立”並不準確,雖然說通過new和newInstance我們都能得到一個可用的物件,但newInstance的作用其實是通過Java虛擬機器的類載入機制把指定的類載入到記憶體裡。

    我們在工廠模式中,經常會通過newInstance方法來載入類,但這個方法只能是通過呼叫類的無參建構函式來載入類,如果我們在建立物件時需要傳入引數,那麼就得使用new來呼叫對應的帶參的構造函數了。

4 通過反射機制呼叫類的方法

    如果我們通過反射機制來呼叫類的方式,那麼就得解決三個問題,第一,通過什麼方式來調?第二,如何傳入引數,第三,如何得到返回結果?

    通過下面的CallFuncDemo.java程式碼,我們將通過反射來呼叫類裡的方法,在其中我們能看下上述三個問題的解決方法。    

1	import java.lang.reflect.Constructor;
2	import java.lang.reflect.InvocationTargetException;
3	import java.lang.reflect.Method;
4	class Person {
5		private String name;
6		public Person(String name) 
7	    {this.name = name;}
8		public void saySkill(String skill) {
9		  System.out.println("Name is:"+name+",skill is:" + skill);
10		}
11		public int addSalary(int current) 
12	    {	return current + 100;}
13	}

    在第4行裡,我們定義了一個Person類,在其中的第6行裡,我們定義了一個帶參的建構函式,在第8行裡,我們定義了一個帶參但無返回值得saySkill方法,在第11行裡,我們定義了一個帶參而且返回int型別的addSalary方法。    

14	public class CallFuncDemo {
15		public static void main(String[] args) {
16			Class c1azz = null;
17			Constructor c = null;
18			try {
19				c1azz = Class.forName("Person");
20				c = c1azz.getDeclaredConstructor(String.class);
21				Person p = (Person)c.newInstance("Peter");
22				//output: Name is:Peter, skill is:java
23				p.saySkill("Java");
24				// 呼叫方法,必須傳遞物件例項,同時傳遞引數值
25				Method method1 = c1azz.getMethod("saySkill", String.class);
26				//因為沒返回值,所以能直接調
27				//輸出結果是Name is:Peter, skill is:C#
28	            method1.invoke(p, "C#");             
29	            Method method2 = c1azz.getMethod("addSalary", int.class);
30	            Object invoke = method2.invoke(p, 100);
31	            //輸出200
32				System.out.println(invoke);
33			} catch (ClassNotFoundException e) {
34				e.printStackTrace();
35			} catch (NoSuchMethodException e1) {
36				e1.printStackTrace();
37			} catch (InstantiationException e) {
38				e.printStackTrace();
39			} catch (IllegalAccessException e) {
40				e.printStackTrace();
41			} catch (InvocationTargetException e) {
42				e.printStackTrace();
43			}
44		}
45	}

    在第19行裡,我們通過Class.forName得到了一個Class型別的物件,其中包含了Person類的資訊。在第20行裡,通過傳入String.class引數,得到了Person類的帶參的建構函式,並通過了第21行的newInstance方法,通過這個帶參的建構函式建立了一個Person型別的物件。隨後在第23行裡呼叫了saySkill方法。這裡我們演示通過反射呼叫類的建構函式來建立物件的方式。

    在第25行裡,我們通過了getMethod方法,得到了帶參的saySkill方法的Method型別的物件,隨後通過第28行的invoke方法呼叫了這個saySkill方法,這裡第一個引數是由哪個物件來呼叫,通過第二個引數,我們傳入了saySkill方法的String型別的引數。

    用同樣的方式,我們在第29和30行通過反射呼叫了Person類的addSalary方法,由於這個方法有返回值,所以我們在30行用了一個Object型別的invoke物件來接收返回值,通過第32行的列印語句,我們能看到200這個執行結果。