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這個執行結果。