Java中Class對象詳解


https://blog.csdn.net/mcryeasy/article/details/52344729

待優化整理 總結

Class類簡介

  在java世界里,一切皆對象。從某種意義上來說,java有兩種對象:實例對象和Class對象。每個類的運行時的類型信息就是用Class對象表示的。它包含了與類有關的信息。其實我們的實例對象就通過Class對象來創建的。Java使用Class對象執行其RTTI(運行時類型識別,Run-Time Type Identification),多態是基於RTTI實現的。

  每一個類都有一個Class對象,每當編譯一個新類就產生一個Class對象,基本類型 (boolean, byte, char, short, int, long, float, and double)有Class對象,數組有Class對象,就連關鍵字void也有Class對象(void.class)。Class對象對應着java.lang.Class類,如果說類是對象抽象和集合的話,那么Class類就是對類的抽象和集合。

  Class類沒有公共的構造方法,Class對象是在類加載的時候由Java虛擬機以及通過調用類加載器中的 defineClass 方法自動構造的,因此不能顯式地聲明一個Class對象。一個類被加載到內存並供我們使用需要經歷如下三個階段:

  1. 加載,這是由類加載器(ClassLoader)執行的。通過一個類的全限定名來獲取其定義的二進制字節流(Class字節碼),將這個字節流所代表的靜態存儲結構轉化為方法去的運行時數據接口,根據字節碼在java堆中生成一個代表這個類的java.lang.Class對象。

  2. 鏈接。在鏈接階段將驗證Class文件中的字節流包含的信息是否符合當前虛擬機的要求,為靜態域分配存儲空間並設置類變量的初始值(默認的零值),並且如果必需的話,將常量池中的符號引用轉化為直接引用。

  3. 初始化。到了此階段,才真正開始執行類中定義的java程序代碼。用於執行該類的靜態初始器和靜態初始塊,如果該類有父類的話,則優先對其父類進行初始化。

  
  所有的類都是在對其第一次使用時,動態加載到JVM中的(懶加載)。當程序創建第一個對類的靜態成員的引用時,就會加載這個類。使用new創建類對象的時候也會被當作對類的靜態成員的引用。因此java程序程序在它開始運行之前並非被完全加載,其各個類都是在必需時才加載的。這一點與許多傳統語言都不同。動態加載使能的行為,在諸如C++這樣的靜態加載語言中是很難或者根本不可能復制的。

  在類加載階段,類加載器首先檢查這個類的Class對象是否已經被加載。如果尚未加載,默認的類加載器就會根據類的全限定名查找.class文件。在這個類的字節碼被加載時,它們會接受驗證,以確保其沒有被破壞,並且不包含不良java代碼。一旦某個類的Class對象被載入內存,我們就可以它來創建這個類的所有對象。


如何獲得Class對象

有三種獲得Class對象的方式:

  1. Class.forName(“類的全限定名”)
  2. 實例對象.getClass()
  3. 類名.class (類字面常量)

Class.forName 和getClass()

我們先看看如下的例子:


   
   
  
  
          
  1. package com.cry;
  2. class Dog {
  3. static {
  4. System. out.println( "Loading Dog");
  5. }
  6. }
  7. class Cat {
  8. static {
  9. System. out.println( "Loading Cat");
  10. }
  11. }
  12. public class Test {
  13. public static void main (String[] args){
  14. System. out.println( "inside main");
  15. new Dog();
  16. System. out.println( "after creating Dog");
  17. try {
  18. Class cat=Class.forName( "com.cry.Cat");
  19. } catch (ClassNotFoundException e) {
  20. System. out.println( "Couldn't find Cat");
  21. }
  22. System. out.println( "finish main");
  23. }
  24. }
  25. /* Output:
  26. inside main
  27. Loading Dog
  28. after creating Dog
  29. Loading Cat
  30. finish main
  31. */

  上面的Dog、Cat類中都有一個靜態語句塊,該語句塊在類第一次被加載時候被執行。這時會有相應的信息打印出來,告訴我們這個類什么時候被加載了。從輸出中可以看到,Class對象僅在需要的時候才被加載,static初始化是在類加載時進行的。

  Class.forName方法是Class類的一個靜態成員。forName在執行的過程中發現如果類Dog還沒有被加載,那么JVM就會調用類加載器去加載Dog類,並返回加載后的Class對象。Class對象和其他對象一樣,我們可以獲取並操作它的引用。在類加載的過程中,Dog類的靜態語句塊會被執行。如果Class .forName找不到你要加載的類,它會拋出ClassNotFoundException異常。

  Class.forName的好處就在於,不需要為了獲得Class引用而持有該類型的對象,只要通過全限定名就可以返回該類型的一個Class引用。如果你已經有了該類型的對象,那么我們就可以通過調用getClass()方法來獲取Class引用了,這個方法屬於根類Object的一部分,它返回的是表示該對象的實際類型的Class引用:


   
   
  
  
          
  1. package com.cry;
  2. class Dog {
  3. static {
  4. System. out.println( "Loading Dog");
  5. }
  6. }
  7. public class Test {
  8. public static void main (String[] args) {
  9. System. out.println( "inside main");
  10. Dog d = new Dog();
  11. System. out.println( "after creating Dog");
  12. Class c = d.getClass();
  13. System. out.println( "finish main");
  14. }
  15. }
  16. /* Output:
  17. inside main
  18. Loading Dog
  19. after creating Dog
  20. finish main
  21. */

利用new操作符創建對象后,類已經裝載到內存中了,所以執行getClass()方法的時候,就不會再去執行類加載的操作了,而是直接從java堆中返回該類型的Class引用。

類字面常量

  java還提供了另一種方法來生成對Class對象的引用。即使用類字面常量,就像這樣:Cat.class,這樣做不僅更簡單,而且更安全,因為它在編譯時就會受到檢查(因此不需要置於try語句塊中)。並且根除了對forName()方法的調用,所有也更高效。類字面量不僅可以應用於普通的類,也可以應用於接口、數組及基本數據類型。

注意:基本數據類型的Class對象和包裝類的Class對象是不一樣的:


   
   
  
  
          
  1. Class c1 = Integer. class;
  2. Class c2 = int. class;
  3. System.out.println(c1);
  4. System.out.println(c2);
  5. System.out.println(c1 == c2);
  6. /* Output
  7. class java.lang.Integer
  8. int
  9. false
  10. */

但是在包裝類中有個一個字段TYPE,TYPE字段是一個引用,指向對應的基本數據類型的Class對象,如下所示,左右兩邊相互等價: 
這里寫圖片描述 
  
  用.class來創建對Class對象的引用時,不會自動地初始化該Class對象(這點和Class.forName方法不同)。類對象的初始化階段被延遲到了對靜態方法或者非常數靜態域首次引用時才執行:


   
   
  
  
          
  1. package com.cry;
  2. class Dog {
  3. static final String s1 = "Dog_s1";
  4. static String s2 = "Dog_s2";
  5. static {
  6. System.out. println( "Loading Dog");
  7. }
  8. }
  9. class Cat {
  10. static String s1 = "Cat_s1";
  11. static {
  12. System.out. println( "Loading Cat");
  13. }
  14. }
  15. public class Test {
  16. public static void main( String[] args) throws ClassNotFoundException {
  17. System.out. println( "----Star Dog----");
  18. Class dog = Dog. class;
  19. System.out. println( "------");
  20. System.out. println( Dog.s1);
  21. System.out. println( "------");
  22. System.out. println( Dog.s2);
  23. System.out. println( "---start Cat---");
  24. Class cat = Class.forName( "com.cry.Cat");
  25. System.out. println( "-------");
  26. System.out. println( Cat.s1);
  27. System.out. println( "finish main");
  28. }
  29. }
  30. /* Output:
  31. ----Star Dog----
  32. ------
  33. Dog_s1
  34. ------
  35. Loading Dog
  36. Dog_s2
  37. ---start Cat---
  38. Loading Cat
  39. -------
  40. Cat_s1
  41. finish main
  42. */

  從上面我們可以看到,如果僅使用.class語法來獲得對類的Class引用是不會引發初始化的。但是如果使用Class.forName來產生引用,就會立即進行了初始化,就像Cat所看到的。

  如果一個字段被static final修飾,我們稱為”編譯時常量“,就像Dog的s1字段那樣,那么在調用這個字段的時候是不會對Dog類進行初始化的。因為被static和final修飾的字段,在編譯期就把結果放入了常量池中了。但是,如果只是將一個域設置為static 或final的,還不足以確保這種行為,就如調用Dog的s2字段后,會強制Dog進行類的初始化,因為s2字段不是一個編譯時常量。

通過javap -c -v對Dog的字節碼進行反匯編:


   
   
  
  
          
  1. {
  2. static final java.lang.String s1;
  3. flags: ACC_STATIC, ACC_FINAL
  4. ConstantValue: String Dog_s1
  5. static java.lang.String s2;
  6. flags: ACC_STATIC
  7. com.cry.Dog();
  8. flags:
  9. Code:
  10. stack= 1, locals= 1, args_size= 1
  11. 0: aload_0
  12. 1: invokespecial # 1 // Method java/lang/Object."<init>":()V
  13. 4 : return
  14. LineNumberTable:
  15. line 3 : 0
  16. LocalVariableTable:
  17. Start Length Slot Name Signature
  18. 0 5 0 this Lcom/cry/Dog;
  19. static {};
  20. flags: ACC_STATIC
  21. Code:
  22. stack = 2 , locals= 0 , args_size= 0
  23. 0 : ldc # 2 // String Dog_s2
  24. 2 : putstatic # 3 // Field s2:Ljava/lang/String;
  25. 5 : getstatic # 4 // Field java/lang/System.out:Ljava/io/PrintStream;
  26. 8 : ldc # 5 // String Loading Dog
  27. 10 : invokevirtual # 6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  28. 13 : return
  29. LineNumberTable:
  30. line 6 : 0
  31. line 9 : 5
  32. line 10 : 13
  33. }

  從上面可以看出s1在編譯后被ConstantValue屬性修飾 ConstantValue: String Dog_s1,表示即同時被final和static修飾。而s2並沒有被ConstantValue修飾,因為它不是一個編譯時常量。在static{}中表示類的初始化操作,在操作中我們看到只有s2字段進行了賦值,而卻沒有s1的蹤影,因此調用s1字段是不會觸發類的初始化的。

小結

  一旦類被加載了到了內存中,那么不論通過哪種方式獲得該類的Class對象,它們返回的都是指向同一個java堆地址上的Class引用。jvm不會創建兩個相同類型的Class對象:


   
   
  
  
          
  1. package com.cry;
  2. class Cat {
  3. static {
  4. System. out.println( "Loading Cat");
  5. }
  6. }
  7. public class Test {
  8. public static void main (String[] args) throws ClassNotFoundException {
  9. System. out.println( "inside main");
  10. Class c1 = Cat.class;
  11. Class c2= Class.forName( "com.cry.Cat");
  12. Class c3= new Cat().getClass();
  13. Class c4 = new Cat().getClass();
  14. System. out.println(c1==c2);
  15. System. out.println(c2==c3);
  16. System. out.println( "finish main");
  17. }
  18. }
  19. /* Output:
  20. inside main
  21. -------
  22. Loading Cat
  23. true
  24. true
  25. finish main
  26. */

從上面我們可以看出執行不同獲取Class引用的方法,返回的其實都是同一個Class對象。

  其實對於任意一個Class對象,都需要由它的類加載器和這個類本身一同確定其在就Java虛擬機中的唯一性,也就是說,即使兩個Class對象來源於同一個Class文件,只要加載它們的類加載器不同,那這兩個Class對象就必定不相等。這里的“相等”包括了代表類的Class對象的equals()、isAssignableFrom()、isInstance()等方法的返回結果,也包括了使用instanceof關鍵字對對象所屬關系的判定結果。所以在java虛擬機中使用雙親委派模型來組織類加載器之間的關系,來保證Class對象的唯一性。


泛型Class引用

  Class引用表示的就是它所指向的對象的確切類型,而該對象便是Class類的一個對象。在JavaSE5中,允許你對Class引用所指向的Class對象的類型進行限定,也就是說你可以對Class對象使用泛型語法。通過泛型語法,可以讓編譯器強制指向額外的類型檢查:


   
   
  
  
          
  1. public final class Class<T> implements java.io.Serializable,
  2. GenericDeclaration ,
  3. Type ,
  4. AnnotatedElement {

   
   
  
  
          
  1. Class<Integer> c1 = int. class;
  2. c1=Integer. class;
  3. //c1=Double.class; 編譯報錯

雖然int.class和Integer.class指向的不是同一個Class對象引用,但是它們基本類型和包裝類的關系,int可以自動包裝為Integer,所以編譯器可以編譯通過。

泛型中的類型可以持有其子類的引用嗎?不行:

Class<Number> c1 = Integer.class;  //編譯報錯
  
  
 
 
         

雖然Integer繼承自Number,但是編譯器無法編譯通過。

為了使用泛化的Class引用放松限制,我們還可以使用通配符,它是Java泛型的一部分。通配符的符合是”?“,表示“任何事物“:


   
   
  
  
          
  1. Class<?> c1 = int. class;
  2. c1= double. class;

Class


   
   
  
  
          
  1. Class<? extends Number> c1 = Integer. class;
  2. c1 = Number. class;
  3. c1 = Double. class;
  4. // c1=String.class; 報錯,不屬於Number類和其子類

通配符?不僅可以與extend結合,而且還可以與super關鍵字相結合,表示被限定為某種類型,或該類型的任何父類型:


   
   
  
  
          
  1. Class<? super Integer> c1 = Integer. class;
  2. c1 = Number. class;
  3. c1 = Object. class;
  4. c1= Integer. class.getSuperclass();

向Class引用添加泛型語法的原因僅僅是為了提供編譯期類型檢查。


Class類的方法

方法名 說明
forName() (1)獲取Class對象的一個引用,但引用的類還沒有加載(該類的第一個對象沒有生成)就加載了這個類。
(2)為了產生Class引用,forName()立即就進行了初始化。
Object-getClass() 獲取Class對象的一個引用,返回表示該對象的實際類型的Class引用。
getName() 取全限定的類名(包括包名),即類的完整名字。
getSimpleName() 獲取類名(不包括包名)
getCanonicalName() 獲取全限定的類名(包括包名)
isInterface() 判斷Class對象是否是表示一個接口
getInterfaces() 返回Class對象數組,表示Class對象所引用的類所實現的所有接口。
getSupercalss() 返回Class對象,表示Class對象所引用的類所繼承的直接基類。應用該方法可在運行時發現一個對象完整的繼承結構。
newInstance() 返回一個Oject對象,是實現“虛擬構造器”的一種途徑。使用該方法創建的類,必須帶有無參的構造器
getFields() 獲得某個類的所有的公共(public)的字段,包括繼承自父類的所有公共字段。 類似的還有getMethods和getConstructors。
getDeclaredFields 獲得某個類的自己聲明的字段,即包括public、private和proteced,默認但是不包括父類聲明的任何字段。類似的還有getDeclaredMethods和getDeclaredConstructors。


   
   
  
  
          
  1. package com.cry;
  2. import java.lang.reflect.Field;
  3. interface I1 {
  4. }
  5. interface I2 {
  6. }
  7. class Cell{
  8. public int mCellPublic;
  9. }
  10. class Animal extends Cell{
  11. private int mAnimalPrivate;
  12. protected int mAnimalProtected;
  13. int mAnimalDefault;
  14. public int mAnimalPublic;
  15. private static int sAnimalPrivate;
  16. protected static int sAnimalProtected;
  17. static int sAnimalDefault;
  18. public static int sAnimalPublic;
  19. }
  20. class Dog extends Animal implements I1, I2 {
  21. private int mDogPrivate;
  22. public int mDogPublic;
  23. protected int mDogProtected;
  24. private int mDogDefault;
  25. private static int sDogPrivate;
  26. protected static int sDogProtected;
  27. static int sDogDefault;
  28. public static int sDogPublic;
  29. }
  30. public class Test {
  31. public static void main (String[] args) throws IllegalAccessException, InstantiationException {
  32. Class<Dog> dog = Dog.class;
  33. //類名打印
  34. System.out.println(dog.getName()); //com.cry.Dog
  35. System.out.println(dog.getSimpleName()); //Dog
  36. System.out.println(dog.getCanonicalName()); //com.cry.Dog
  37. //接口
  38. System.out.println(dog.isInterface()); //false
  39. for (Class iI : dog.getInterfaces()) {
  40. System.out.println(iI);
  41. }
  42. /*
  43. interface com.cry.I1
  44. interface com.cry.I2
  45. */
  46. //父類
  47. System.out.println(dog.getSuperclass()); //class com.cry.Animal
  48. //創建對象
  49. Dog d = dog.newInstance();
  50. //字段
  51. for (Field f : dog.getFields()) {
  52. System.out.println(f.getName());
  53. }
  54. /*
  55. mDogPublic
  56. sDogPublic
  57. mAnimalPublic
  58. sAnimalPublic
  59. mCellPublic //父類的父類的公共字段也打印出來了
  60. */
  61. System.out.println( "---------" );
  62. for (Field f : dog.getDeclaredFields()) {
  63. System.out.println(f.getName());
  64. }
  65. /** 只有自己類聲明的字段
  66. mDogPrivate
  67. mDogPublic
  68. mDogProtected
  69. mDogDefault
  70. sDogPrivate
  71. sDogProtected
  72. sDogDefault
  73. sDogPublic
  74. */
  75. }
  76. }

getName、getCanonicalName與getSimpleName的區別:

getSimpleName:只獲取類名 
getName:類的全限定名,jvm中Class的表示,可以用於動態加載Class對象,例如Class.forName。 
getCanonicalName:返回更容易理解的表示,主要用於輸出(toString)或log打印,大多數情況下和getName一樣,但是在內部類、數組等類型的表示形式就不同了。


   
   
  
  
          
  1. package com.cry;
  2. public class Test {
  3. private class inner{
  4. }
  5. public static void main( String[] args) throws ClassNotFoundException {
  6. //普通類
  7. System .out. println ( Test . class .getSimpleName()); //Test
  8. System .out. println ( Test . class .getName()); //com.cry.Test
  9. System .out. println ( Test . class .getCanonicalName()); //com.cry.Test
  10. //內部類
  11. System .out. println (inner. class .getSimpleName()); //inner
  12. System .out. println (inner. class .getName()); //com.cry.Test$inner
  13. System .out. println (inner. class .getCanonicalName()); //com.cry.Test.inner
  14. //數組
  15. System .out. println (args.getClass().getSimpleName()); //String[]
  16. System .out. println (args.getClass().getName()); //[Ljava.lang.String;
  17. System .out. println (args.getClass().getCanonicalName()); //java.lang.String[]
  18. //我們不能用getCanonicalName去加載類對象,必須用getName
  19. //Class.forName(inner.class.getCanonicalName()); 報錯
  20. Class .forName(inner. class .getName());
  21. }
  22. }

參考: 
java中的Class對象和new關鍵字解析 
Thinking in Java


原文地址:https://blog.csdn.net/dufufd/article/details/80537638


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM