[Java基礎]Class物件

Duancf發表於2024-07-19

Class 類

[class物件通常存放在方法區]
在程式執行期間,Java執行時系統始終為所有物件維護一個執行時型別標識。這個資訊會跟蹤每個物件所屬的類。虛擬機器利用執行時型別資訊選擇要執行的正確的方法。不過,可以使用一個特殊的Java類訪問這些資訊。儲存這些資訊的類名為Class,這個名字有些讓人困惑。

Object類中的getclass()方法將會返回一個class型別的例項。

Employee e;
Class cl=e.getClass();

就像 Employee物件描述一個特定員工的屬性一樣,(lass物件會描述一個特定類的屬性。可能最常用的Class方法就是getName。這個方法將返回類的名字。例如,下面這條語句:System.out.println(e.getClass().getName()+""+ e.getName());如果e是一個員工,則會輸出:Employee Harry Hacker如果e是經理,則會輸出:Manager Harry Hacker如果類在一個包裡,包的名字也作為類名的一部分:var generator = new Random();Class cl= generator.getClass();String name =cl.getName();//name is set to "iava.util.Random'還可以使用靜態方法forName獲得類名對應的class 物件String className ="java.util.Random";Class cl=Class.forName(className);如果類名儲存在一個字串中,這個字串會在執行時變化,就可以使用這個方法。如果cassName是一個類名或介面名,這個方法可以正常執行。否則,forName方法將丟擲一個檢查型異常(checked exception)。無論何時使用這個方法,都應該提供一個異常處理器exception handler)。關於如何提供異常處理器,請參看下一節。

有三種方法可以獲得class物件的例項

  • Class.forName(""),傳入類名的全限定符
  • 每一個物件都有getClass()方法,例如a.getClass()
  • 直接使用類名.class,例如MyClass.Class

提示:在啟動時,包含main方法的類被載入。它會載入所有需要的類。這些被載入的類又要載入它們需要的類,以此類推。對於一個大型的應用程式來說,這將會花費很長時間,使用者會因此感到不耐煩。可以使用下面這個技巧給使用者一種啟動速度比較快的假象。不過,要確保包含 main方法的類沒有顯式地引用其他的類。首先,顯示一個啟動畫面;然後,透過呼叫Class.forName手工地強制載入其他類。獲得class類物件的第三種方法是一個很方便的快捷方式。如果T是任意的Java型別200Java核心技術卷基礎知識或 void 關鍵字),T.class 將代表匹配的類物件。例如:Class cl1=Random.class;// if you import java.util.*;Class cl2=int.class:Class cl3 = Double[].class;請注意,一個lass物件實際上表示的是一個型別,這可能是類,也可能不是類。例如int不是類,但int.class 是一個(lass 型別的物件。註釋:Class類實際上是一個泛型類。例如,Employee.class的型別是Class。我們沒有深究這個問題的原因是:它將已經很抽象的概念變得更加複雜。在大多數實際問題中,可以忽略型別引數,而使用原始的(lass類。有關這個問題更詳細的介紹請參看第8章。警告:鑑於歷史原因,getName方法在應用於陣列型別的時候會返回有些奇怪的名字:Double[].class.getName()返回"[Ljava.lang.Double;".int[].class.getName()返回"[I".虛擬機器為每個型別管理一個唯一的Class物件。因此,可以利用=運算子實現兩個類物件的比較。例如,if(e.getClass()==Employee.class)...如果e是一個Employee例項,這個測試將透過。與條件einstanceofEmployee不同,如果e是某個子類(如Manager)的例項,這個測試將失敗。如果有一個class型別的物件,可以用它構造類的例項。呼叫getConstructor方法將得到-個Constructor型別的物件,然後使用newInstance方法來構造一個例項。例如:var className ="java.util.Random";//or any other name of a class with//a no-arg constructorClass cl=Class.forName(className);Object obj=cl.getConstructor().newInstance();如果這個類沒有無引數的構造器,getconstructor方法會丟擲一個異常。可以參見5.7.7節瞭解如何呼叫其他構造器。

註釋:有一個已經廢棄的(lass.toInstance方法,它也可以用無引數構造器構造一個例項。不過,如果構造器丟擲一個檢查型異常,這個異常將不做任何檢查重新丟擲。這違反了編譯時異常檢查的原則。與之不同,(onstructor.newInstance會把所有構造器異常包裝到一個InvocationTargetException中。C++註釋:newInstance方法相當於C++中的虛擬構造器概念。不過,C++中的虛擬構造器不是一個語言特性,而是需要一個專業庫支援的習慣用法。(lass 類類似於C++中的type info類,getClass方法則等價於typeid運算子。不過,Java的Class比type info功能更全面。C++的 type info只能給出表示型別名的一個字串,而不能建立那個型別的新物件。第5章繼死201srl java.lang.class 10static Class forName(String className)返回一個 Class 物件,表示名為 className 的類。Constructor getConstructor(Class...parameterTypes)1.1生成一個物件,描述有指定引數型別的構造器。參見5.7.7節更多地瞭解如何提供引數型別。java.lang,reflect .Constructor 110bject newInstance(0bject... params)將params傳遞到構造器,來構造這個構造器宣告類的一個新例項。參見5.7.7節更多地瞭解如何提供引數。java.lang.Throwable 1.0void printStackTrace()將 Throwable 物件和堆疊軌跡列印到標準錯誤流。

利用反射分析類的能力

下面簡要介紹反射機制最重要的內容--檢查類的結構。在java.lang.reflect包中有三個類Field、Method和Constructor分別用於描述類的欄位、方法和構造器。這三個類都有一個叫做 getName的方法,用來返回欄位、方法或構造器的名稱.Java核心技術 卷204基礎知識Field類有一個getType方法,用來返回描述欄位型別的一個物件,這個物件的型別同樣是Class。Method和Constructor類有報告引數型別的方法,Method類還有一個報告返回型別的方法。這三個類都有一個名為getModifiers的方法,它將返回一個整數,用不同的 0/1位描述所使用的修飾符,如public和static。另外,還可以利用java.lang.reflect包中Modifier類的靜態方法分析 getModifiers返回的這個整數。例如,可以使用Modifier類中的isPublic、isPrivate 或isFinal 判斷方法或構造器是public、private還是final。我們需要做的就是在 getModifiers 返回的整數上呼叫Modifier類中適當的方法,另外,還可以利用Modifier.tostring方法將修飾符列印出來。Class類中的getFields、getMethods和getConstructors方法將分別返回這個類支援的公共欄位、方法和構造器的陣列,其中包括超類的公共成員。class類的getDeclareFieldsgetDeclareMethods和getDeclaredConstructors方法將分別返回類中宣告的全部欄位、方法和構造器的陣列,其中包括私有成員、包成員和受保護成員,但不包括超類的成員。

使用反射在執行時分析物件

從前面一節中,我們已經知道如何檢視任意物件資料欄位欄位的名字和型別:獲得對應的 Class 物件。在這個Class物件上呼叫getDeclaredFields。本節將進一步檢視欄位的具體內容。當然,在編寫程式時,如果知道想要檢視的欄位名和型別,檢視物件中指定欄位的內容是一件很容易的事情。而利用反射機制可以檢視在編譯如isAbstract方法就是檢查modifiers值中對應修飾符abstract的二進位制位。--譯者注日第5章 繼死209時還不知道的物件欄位。要做到這一點,關鍵方法是Field類中的get方法。如果f是一個Field型別的物件(例如,透過 getDeclaredFields 得到的物件),obj是某個包含f欄位的類的物件,f.get(obj)將返回一個物件,其值為oj的當前欄位值。這樣說起來顯得有點抽象,下面來看一個程式。var harry=new Employee("Harry Hacker",50000,10,1,1989);Class cl=harry.getClass();//the class obiect representing EmployeeField f= cl.getDeclaredField("name");// the name field of the Employee classObject v=f.get(harry);// the value of the name field of the harry object, i.e.//the String object "Harry Hacker'當然,不僅可以獲得值,也可以設定值。呼叫f.set(obj,value)將把物件obj的f表示的欄位設定為新值。實際上,這段程式碼存在一個問題。由於name是一個私有欄位,所以get和set方法會丟擲一個IllegalAccessException。只能對可以訪問的欄位使用get和set方法。Java安全機制允許檢視一個物件有哪些欄位,但是除非擁有訪問許可權,否則不允許讀寫那些欄位的值。反射機制的預設行為受限於Java的訪問控制。不過,可以呼叫Field、Method或Constructor物件的setAccessible方法覆蓋Java的訪問控制。例如f.setAccessible(true);//now OK to call f.get(harry)setAccessible方法是Accessible0bject類中的一個方法,它是Field、Method和Constructor類的公共超類。這個特性是為除錯、持久儲存和類似機制提供的。本節稍後將利用它編寫一個通用的 toString 方法。如果不允許訪問,setAccessible呼叫會丟擲一個異常。訪問可以被模組系統(見卷I的第9章)或安全管理器(卷Ⅱ的第10章)拒絕。安全管理器並不常用。不過,在Java9中,由於Java API是模組化的,每個程式都包含模組。

使用反射編寫泛型陣列程式碼

呼叫任意方法和構造器

在C和C++中,可以透過一個函式指標執行任意函式。從表面上看,Java 沒有提供方法指標,也就是說,Java沒有提供途徑將一個方法的儲存地址傳給另外一個方法,以便第二個方法以後呼叫。事實上,Java的設計者曾說過:方法指標是很危險的,而且很容易出錯。他們認為 Java 的介面(interface)和lambda表示式(將在下一章討論)是一種更好的解決方案。不過,反射機制允許你呼叫任意的方法。回想一下,可以用Field類的 get方法檢視一個物件的欄位。與之類似,Method類有一個invoke 方法,允許你呼叫包裝在當前Method物件中的方法。invoke方法的簽名是:0bject invoke(0bject obj, 0bject... args)第一個引數是隱式引數,其餘的物件提供了顯式引數。對於靜態方法,第一個引數可以忽略,即可以將它設定為null。例如,假設用m表示Employee類的getName方法,下面這條語句顯示瞭如何呼叫這個方法:String n=(String)ml.invoke(harry);如果返回型別是基本型別,invoke方法會返回其包裝器型別。例如,假設m2表示Employee類的 getsalary方法,那麼返回的物件實際上是一個Double,必須相應地完成強制型別轉換。可以使用自動拆箱將它轉換為一個double:double s=(Double)m2.invoke(harry);如何得到 Method物件呢?當然,可以呼叫 getDeclareMethods方法,然後搜尋返回的 Method物件陣列,直到發現想要的方法為止。也可以呼叫Class類的etMethod方法得到想要的方法

它與 getField方法類似。getfield方法根據表示欄位名的字串,返回一個Field 物件。不過有可能存在若干個同名的方法,因此要準確地得到想要的那個方法必須格外小心。有鑑於還必須提供想要的方法的引數型別。getMethod的簽名是:此,Method getMethod(String name, Class... parameterTypes)例如,下面說明了如何獲得 Employee類的getName方法和raisesalary方法的方法指標Method m1=Employee.class.getMethod("getName");Method m2=Employee.class.getMethod("raiseSalary",double.class);可以使用類似的方法呼叫任意的構造器。將構造器的引數型別提供給assgetConstructor方法,並把引數值提供給Constructor.newInstance方法:Class cl=Random.class;//or any other class with a constructor that//accepts a long parameterConstructor cons=cl.getConstructor(long.class);0bject obj= cons.newInstance(42L);到此為止,我們已經瞭解了使用 Method物件的規則。下面來看如何具體使用。程式清單5-18中的程式會列印一個數學函式(如 Math.sqrt或Math.sin)的值的表格。列印的結果如下所示:

public static native double java.lang.Math.sqrt(double)1.00001.00002.00001.41421.73213.00002.00004.00002.23615.00002.44956.00007.00002.64582.82848.00003.00009.000010.00003.1623

當然,列印表格的程式碼與具體要列印表格的數學函式無關。

double dx=(to-from)/(n-1);for(double x=from;x<=to;x+= dx)double y=(Double)f.invoke(null,x);System.out.printf("%10.4f%10.4f%n",x,y);

在這裡,f是一個Method型別的物件。由於正在呼叫的方法是一個靜態方法,所以invoke的第一個引數是 null。要列印 Math.sqrt 函式的值表格,需要將f設定為:Math.class.getMethod("sqrt",double.class)這是Math類的一個方法,名為sqrt,有一個double型別的引數。

這個例子清楚地表明,利用method物件可以實現C語言中函式指標(或C#中的委託)所能完成的所有操作。同C中一樣,這種程式設計風格不是很簡便,而且總是很容易出錯。如果在呼叫方法的時候提供了錯誤的引數會發生什麼?invoke方法將會丟擲一個異常。另外,invoke的引數和返回值必須是0biect型別。這就意味著必須來回進行多次強制型別轉換。這樣一來,編譯器會喪失檢查程式碼的機會,以至於等到測試階段才會發現錯誤,而這個時候查詢和修正錯誤會麻煩得多。不僅如此,使用反射獲得方法指標的程式碼要比直接呼叫方法的程式碼慢得多。
有鑑於此,建議僅在絕對必要的時候才在你自己的程式中使用Method物件。通常更好的做法是使用介面以及Java8引人的lambda表示式(第6章中介紹)。特別要強調:我們建議Java開發者不要使用回撥函式的Method物件。可以使用回撥的介面,這樣不僅程式碼的執行速度更快,也更易於維護。

相關文章