什麼是反射?
答:能夠分析類能力的程序稱爲反射(reflective)。
反射機制可以用來幹嘛?
答:1.在運行時分析類的能力 2.在運行時查看對象,例如toString方法。 3.實現通用的數組操作代碼。4.利用Method對象。
在程序運行期間,Java運行時系統始終爲所有的對象維護一個稱爲運行時的類型標識。這個信息跟蹤着每個對象所屬的類。虛擬機利用運行時類型信息選擇相應的方法執行。我們可以通過專門的Java類去訪問這些信息,保存這些信息的類稱爲Class。
獲取Class對象有三種方法:
1.通過對象.getClass()方法獲得。
Class c1 = new String("getClass方法").getClass();
2.通過靜態方法forName(String className)獲得className類名對應的Class對象。注意,只有在className是類名或接口名時才能使用這個方法,並且無論何時使用這個方法,都必須提供一個異常處理器。
String className = "java.util.Random";
try {
Class c1 = Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
3.使用T.class,如果T是任意的Java類型(或void關鍵字),T.class將代表匹配的類對象。
Class c1 = Random.class;
Class c2 = int.class;
Class c3 = Double[].class;
Class c4 = void.class;
需要注意的是一個Class對象實際上表示的是一個類型,而這個類型不一定是一種類。例如,int,void不是類,但是int.class是一個Class對象。其實Class類實際上是一個泛型類。例如Random.class的類型是Class<Random>。
注意,獲取類對象的時候,會導致類屬性被初始化。無論什麼途徑獲取類對象,都會導致靜態屬性被初始化,而且只會執行一次。(除了直接使用 Class c = T.class 這種方式,這種方式不會導致靜態屬性被初始化)
public class TestReflection {
public static void main(String[] args) throws InterruptedException {
Class c = Test.class;
}
}
class Test {
static String test;
static {
System.out.println("靜態方法執行");
test = "類屬性初始化成功";
System.out.println("test: " + test);
}
}
public class TestReflection {
public static void main(String[] args) throws InterruptedException {
Class c = Test.class;
Class c1 = new Test().getClass();
}
}
class Test {
static String test;
static {
System.out.println("靜態方法執行");
test = "類屬性初始化成功";
System.out.println("test: " + test);
}
}
虛擬機爲每個類型管理一個Class對象。因此可以使用"=="運算符來比較兩個對象。例如:
public class TestReflection {
public static void main(String[] args) throws InterruptedException, ClassNotFoundException {
Class c = Test.class;
Class c1 = new Test().getClass();
if(c == c1){
System.out.println("相等");
}
}
}
class Test {
static String test;
static {
System.out.println("靜態方法執行");
test = "類屬性初始化成功";
System.out.println("test: " + test);
}
}
Class對象中有一個很有用的方法---->newInstance(),這個方法可以用來動態地創建一個類的實例。例如,test.getClass().newInstance();創建了一個與test具有相同類型的實例。newInstace方法調用默認的構造器(沒有參數的構造器)初始化新創建的對象。如果這個類沒有默認的構造器,就會拋出一個異常。通常我們將forName與newInstance配合起來使用。
注意:如果希望向按名稱創建的類的構造器提供參數,就必須使用Constructor類中的newInstance方法。
分割線---------------------------------------------------------------------------------------------------------------------------
下面簡要的介紹一下反射機制最重要的內容——檢查類的結構。
咋java.lang.reflect包中有三個類Filed、Method和Constructor分別用來描述類的域、方法和構造器。這三個類都有一個叫做getName的方法,用來返回項目的名稱,一個getModifiers的方法,返回一個整形數值,用不同的位開關描述public和static這樣的修飾符使用狀況。Filed類有一個getType方法,用來返回描述域所屬類型的Class對象。Method 和 Constructor 類有能夠報告參數類型的方法,Method 類還有一個可以報告返回類型的方法。
Class類中的getFileds、getMethods 和 getConstructors方法將返回類提供的public域、方法和構造器數組,其中包括超類的公有成員。Class類的 getDeclareFields、getDeclareMethods 和 getDeclaredConstructors方法將返回類中聲明的全部域、方法和構造器,其中包括私有和受保護成員,但不包括超類的成員。
下面程序顯示瞭如何打印一個類的全部信息的方法。輸入要打印的類名即可,注意如果前面有包名的話,也要輸入。
package 反射;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Scanner;
/**
* @ClassName: TestReflection
* @Description: 打印類的全部信息
* @author: GGBOY
* @date 2019/10/15 21:19
* @Version: 1.0
**/
public class TestReflection {
public static void main(String[] args) {
// read class name from command line args user input
String name;
Scanner in = new Scanner(System.in);
System.out.println("Enter class name (e.g java.util,Date):");
name = in.next();
try {
// print class name and superclass name (if != object)
Class cl = Class.forName(name);
Class superCl = cl.getSuperclass();
String modifiers = Modifier.toString(cl.getModifiers());
if (modifiers.length() > 0) {
// 打印修飾符
System.out.print(modifiers + " ");
}
// 打印類名
System.out.print("class " + name);
// 如果有繼承關係,則把父類名也給打印出來
if (superCl != null && superCl != Object.class) {
System.out.print(" extends " + superCl.getName());
}
System.out.print("\n{\n");
printConstructors(cl);
System.out.println();
printMethods(cl);
System.out.println();
printFields(cl);
System.out.println("}");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 打印類的所有構造器
*
* @param c1 a class
* @return void
* @author GGBOY
* @date 2019/10/15
*/
public static void printConstructors(Class c1) {
// getDeclaredConstructors方法返回這個類的所有構造器。
// getConstructors方法返回這個類的公有構造器。
Constructor[] constructors = c1.getDeclaredConstructors();
for (Constructor c :
constructors) {
String name = c.getName();
System.out.print(" ");
String modifiers = Modifier.toString(c.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print(name + "(");
// 獲取構造器中的參數類型
Class[] paramTypes = c.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) {
if (i > 0) {
System.out.print(", ");
}
// 打印構造器中的參數類型
System.out.print(paramTypes[i].getName());
}
System.out.println(");");
}
}
/**
* 打印類的所有方法,包括實現接口的全部方法,但不包括由父類繼承的方法
*
* @param cl 一個類
* @return void
* @author GGBOY
* @date 2019/10/16
*/
public static void printMethods(Class cl) {
// getDeclaredMethods 返回這個類或接口的全部方法,但不包括由超類繼承了的方法。
// getMethods會返回所有的公有方法,包括從超類繼承來的方法。
Method[] methods = cl.getDeclaredMethods();
for (Method method :
methods) {
// 獲取方法的返回類型
Class retType = method.getReturnType();
// 獲取方法名
String name = method.getName();
System.out.print(" ");
String modifiers = Modifier.toString(method.getModifiers());
if (modifiers.length() > 0) {
// 打印修飾符
System.out.print(modifiers + " ");
}
// 打印返回類型和方法名
System.out.print(retType.getName() + " " + name + "(");
// 獲取方法的參數
Class[] paramTypes = method.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) {
if (i > 0) {
System.out.print(", ");
}
System.out.print(paramTypes[i].getName());
}
System.out.println(");");
}
}
/**
* 打印類的所有屬性,包括父類的公有屬性
*
* @param cl 一個類
* @return void
* @author GGBOY
* @date 2019/10/16
*/
public static void printFields(Class cl) {
// getDeclaredFields方法返回包含Field對象的數組,這些對象記錄了類的全部域。
// getFields方法將返回一個包含Field對象的數組,這些對象記錄了這個類或其超類的公有域。
// 如果類中沒有域,或者Class對象描述的是基本類型或數組類型,這些方法將返回一個長度爲0的數組
Field[] fields = cl.getDeclaredFields();
for (Field field :
fields) {
// 獲取屬性類型
Class type = field.getType();
// 獲取屬性名
String name = field.getName();
System.out.print(" ");
String modifiers = Modifier.toString(field.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.println(type.getName() + " " + name + ";");
}
}
}
通過前面的講解,知道了如何查看任意對象的數據域名稱和類型:通過獲得對於的Class對象,再通過Class對象調用getDeclaredFieds。
那麼如何查看數據域中的內容呢?
答:查看對象域的關鍵方法是Field類中的get方法。如果f是一個Field類型的對象(例如,通過getDeclaredFields得到的對象),obj是某個包含f域的類的對象,f.get(obj)將返回一個對象,其值爲obj域的當前值。例如:
public class ReflectionTest {
public static void main(String[] args) {
Test t = new Test("xxx", 222.22);
Class cl = t.getClass();
Field f = null;
try {
// 獲得Test類的name域
f = cl.getDeclaredField("name");
// 取出name域中的值
Object v = f.get(t);
// 打印
System.out.println(v.toString());
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
class Test {
private String name;
private double salary;
public Test(String name, double salary) {
this.salary = salary;
this.name = name;
}
}
實際上,上述代碼有一個問題。由於name是一個私有域,所以get 方法會拋出一個IllegalAccessException異常。只有利用get方法才能得到可訪問域的值。除非擁有訪問權限,否則Java安全機制只允許查看任意對象有哪些域,而不允許讀取它們的值。
我們可以通過 f.setAccessible(true)方法來得到訪問權限,這樣我們就可以通過f.get(t)獲得name域的值了。注意:get方法還有一個需要解決的問題。name域是一個String類型,所以把它作爲Object返回沒有什麼問題。但是,假定我們想要查看的是salary域,它屬於double類型,而Java中數值類型不是對象。要想要解決這個問題,可以使用Field類中的getDouble方法,這樣反射機制將會自動的將這個域值打包到相應的對象包裝器中,即打包成Double.
當然,可以獲得就可以設置。通過調用f.set(obj, value)可以將obj對象的f域設置成新的value值。
分割線---------------------------------------------------------------------------------------------------------------------------
使用反射編寫泛型數組:
java.lang.reflect包中的Array類允許動態的創建數組,例如:
Test[] t = new Test[100];
...
// t 數組已經滿了
t = Arrays.copyOf(t, 2 * t.length);
copyOf方法可以用於擴展已經填滿的數組。那麼如何編寫一個方法,正好能將Test[]數組轉換爲Object[]數組呢,下面做一個嘗試。
public static Object[] badCopyOf(Object[] a, int newLength) {
Object[] newArray = new Object[newLength];
System.arraycopy(a, 0, newArray, 0,
Math.min(a.length, newLength));
return newArray;
}
這個方法咋一看好像是可以的,但是實際上存在一個問題,因爲這個方法返回的是對象數組類型,這是由於使用了 new Object[newLength]代碼創建的數組。一個對象數組不能轉換成Test數組,如果這樣做的話,在運行時會拋出ClassCastException異常。將一個Test[]臨時轉換成Object[]數組,再把它轉換回來是可以的,但是一個從一開始就是Object[]的數組卻永遠不能轉換成Test[]數組。
爲了編寫通用的代碼,需要能夠創建與原數組類型相同的新數組,此時我們就可以使用Array類中的靜態方法newInstance,它能構造新數組。在調用它時必須提供兩個參數,一個是數組的元素類型,一個是數組的長度.
Object newArray = Array.newInstance(componentType, newLength);
我們可以通過調用Array.getLength(a)獲得數組的長度。想要獲得新數組元素類型時,就需要這樣做了:1.首先獲得數組a的類對象 2.確認它是一個數組。3.使用Class類的getComponentType方法確定數組對應的類型。下面是新的拷貝方法:
public static Object goodCopyOf(Object a, int newLength) {
Class cl = a.getClass();
// 如果傳進來的參數不是數組類型
if (!cl.isArray()) {
return null;
}
Class componentType = cl.getComponentType();
int length = Array.getLength(a);
Object newArray = Array.newInstance(componentType, newLength);
System.arraycopy(a, 0, newArray, 0,
Math.min(length, newLength));
return newArray;
}
這個CopyOf方法可以用來擴展任意類型的數組,而不僅僅是對象數組。在這個方法中,將參數聲明爲Object類型是因爲,數組類型xx[]可以轉換爲Object,而不能轉換成對象數組。下面是兩個擴展數組方法的測試程序,需要注意的是badCopyOf的返回值進行類型轉換時會拋出一個異常。
package 反射;
import java.lang.reflect.Array;
import java.util.Arrays;
/**
* @ClassName: CopyOfTest
* @Description: 擴展任意類型數組方法的測試類
* @author: GGBOY
* @date 2019/10/16 14:16
* @Version: 1.0
**/
public class CopyOfTest {
public static void main(String[] args) {
int[] a = {1, 2, 3};
a = (int[]) goodCopyOf(a, 10);
System.out.println(Arrays.toString(a));
String[] b = {"小米", "紅米", "華爲"};
b = (String[]) goodCopyOf(b, 10);
System.out.println(Arrays.toString(b));
System.out.println("下面的擴展方法在進行類型轉換時會報錯");
b = (String[]) badCopyOf(b, 10);
}
/**
* 擴充數組
*
* @param a 需要擴展的數組
* @param newLength 新數組的長度
* @return java.lang.Object[]
* @author GGBOY
* @date 2019/10/16
*/
public static Object[] badCopyOf(Object[] a, int newLength) {
Object[] newArray = new Object[newLength];
System.arraycopy(a, 0, newArray, 0,
Math.min(a.length, newLength));
return newArray;
}
/**
* 通過創建相同類型的數組來擴充數組
*
* @param a 需要擴充的原數組
* @param newLength 新數組的長度
* @return java.lang.Object
* @author GGBOY
* @date 2019/10/16
*/
public static Object goodCopyOf(Object a, int newLength) {
Class cl = a.getClass();
// 如果傳進來的參數不是數組類型
if (!cl.isArray()) {
return null;
}
Class componentType = cl.getComponentType();
int length = Array.getLength(a);
// newInstance(Class componentType, int[] lengths) 返回一個具有給定類型、給定維數的新數組
// newInstance(Class componentType, int length)
Object newArray = Array.newInstance(componentType, newLength);
System.arraycopy(a, 0, newArray, 0,
Math.min(length, newLength));
return newArray;
}
}
分割線---------------------------------------------------------------------------------------------------------------------------
使用反射調用類中的任意方法
在Method類中有一個invoke方法,它允許調用包裝在當前Method對象中的方法。invoke方法的聲明是:Object invoke(Object obj, Object... args) 第一個參數是隱士參數,其餘的對象提供了顯示參數。
對於靜態方法而言,第一個參數可以被忽略,即可以將它設置爲null。例如,假設用tl 代碼Test類的getName方法,下面這條語句顯示瞭如何調用這個方法:
String n = (String) tl.invoke(t);
我們可以通過調用getDeclareMethods方法,對返回的Method對象數組進行查找,搜索我們想要的方法。當然也可以通過調用Class類中的getMethod方法得到想要的方法。getMethod方法聲明如下:
Method getMethod(String name, Class... parameterType)
如果類中有同名方法的話,使用getMethod方法時,要注意parameterType參數的匹配。例如:
public class Test {
public static void main(String[] args) throws Exception {
Hero hero = new Hero();
Class cl = hero.getClass();
Method method = cl.getMethod("method",
String.class);
method.invoke(hero, "this");
}
}
class Hero {
public void method() {
System.out.println("這是無參方法一");
}
public void method(String s) {
System.out.println("這是有參數方法,參數是: " + s);
}
}
注意:如果返回的是基本類型,invoke方法會返回其包裝器類型。invoke的參數和返回值必須是Object類型,所以必須進行多次類型轉換,並且這樣做的話,編譯器無法檢查代碼,要等到運行測試階段纔會發現這些錯誤。
反射介紹到這裏就完了,如果有什麼錯誤,請指出來,不勝感激。----菜雞程序員
參考資料:
《java核心技術 卷I》