Java學習_反射

桐君過客發表於2020-12-27
  • 什麼是反射?
    • 反射就是Reflection,Java的反射是指程式在執行期可以拿到一個物件的所有資訊。
    • 反射是為了解決在執行期,對某個例項一無所知的情況下,如何呼叫其方法。
    • JAVA反射機制是在執行狀態中,對於任何一個類,都能夠知道這個類的所有屬性和方法;對於任何一個物件,都能夠呼叫它的任意方法和屬性;這種動態獲取資訊以及動態呼叫物件方法的功能稱為java語言的反射機制。
  • Class類
    • class是由JVM在執行過程中動態載入的。JVM在第一次讀取到一種class型別時,將其載入進記憶體。每載入一種class,JVM就為其建立一個Class型別的例項,並關聯起來。注意:這裡的Class型別是一個名叫Classclass
      public final class Class {
          private Class() {}
      }

      String類為例,當JVM載入String類時,它首先讀取String.class檔案到記憶體,然後,為String類建立一個Class例項並關聯起來。

      Class cls = new Class(String);

      檢視JDK原始碼,可以發現Class類的構造方法是private,只有JVM能建立Class例項,我們自己的Java程式是無法建立Class例項的。JVM持有的每個Class例項都指向一個資料型別(classinterface)。一個Class例項包含了該class的所有完整資訊。

      ┌───────────────────────────┐
      │      Class Instance       │──────> String
      ├───────────────────────────┤
      │name = "java.lang.String"  │
      └───────────────────────────┘
      ┌───────────────────────────┐
      │      Class Instance       │──────> Random
      ├───────────────────────────┤
      │name = "java.util.Random"  │
      └───────────────────────────┘
      ┌───────────────────────────┐
      │      Class Instance       │──────> Runnable
      ├───────────────────────────┤
      │name = "java.lang.Runnable"│
      └───────────────────────────┘
      ┌───────────────────────────┐
      │      Class Instance       │──────> String
      ├───────────────────────────┤
      │name = "java.lang.String"  │
      ├───────────────────────────┤
      │package = "java.lang"      │
      ├───────────────────────────┤
      │super = "java.lang.Object" │
      ├───────────────────────────┤
      │interface = CharSequence...│
      ├───────────────────────────┤
      │field = value[],hash,...   │
      ├───────────────────────────┤
      │method = indexOf()...      │
      └───────────────────────────┘
    • JVM為每個載入的class建立了對應的Class例項,並在例項中儲存了該class的所有資訊,包括類名、包名、父類、實現的介面、所有方法、欄位等,因此,如果獲取了某個Class例項,我們就可以通過這個Class例項獲取到該例項對應的class的所有資訊。這種通過Class例項獲取class資訊的方法稱為反射(Reflection)。

    • 獲取一個classClass例項,有三個方法。
      • 方法一:直接通過一個class的靜態變數class獲取。
        Class cls = String.class;
      • 如果有一個例項變數,可以通過該例項變數提供的getClass()方法獲取。

        String s = "Hello";
        Class cls = s.getClass();
      • 如果知道一個class的完整類名,可以通過靜態方法Class.forName()獲取。

        Class cls = Class.forName("java.lang.String");
    • Class例項在JVM中是唯一的,所以,上述方法獲取的Class例項是同一個例項。可以用==比較兩個Class例項。
      Integer n = new Integer(123);
      
      boolean b1 = n instanceof Integer; // true,因為n是Integer型別
      boolean b2 = n instanceof Number; // true,因為n是Number型別的子類
      
      boolean b3 = n.getClass() == Integer.class; // true,因為n.getClass()返回Integer.class
      boolean b4 = n.getClass() == Number.class; // false,因為Integer.class!=Number.class

      instanceof不但匹配指定型別,還匹配指定型別的子類。而用==判斷class例項可以精確地判斷資料型別,但不能作子型別比較。通常情況下,我們應該用instanceof判斷資料型別,因為面向抽象程式設計的時候,我們不關心具體的子型別。只有在需要精確判斷一個型別是不是某個class的時候,我們才使用==判斷class例項。

    • Class例項獲取獲取的基本資訊。
       1 public class Main {
       2     public static void main(String[] args) {
       3         printClassInfo("".getClass());
       4         printClassInfo(Runnable.class);
       5         printClassInfo(java.time.Month.class);
       6         printClassInfo(String[].class);
       7         printClassInfo(int.class);
       8     }
       9 
      10     static void printClassInfo(Class cls) {
      11         System.out.println("Class name: " + cls.getName());
      12         System.out.println("Simple name: " + cls.getSimpleName());
      13         if (cls.getPackage() != null) {
      14             System.out.println("Package name: " + cls.getPackage().getName());
      15         }
      16         System.out.println("is interface: " + cls.isInterface());
      17         System.out.println("is enum: " + cls.isEnum());
      18         System.out.println("is array: " + cls.isArray());
      19         System.out.println("is primitive: " + cls.isPrimitive());
      20     }
      21 }
    • 獲取到了一個Class例項,可以通過該Class例項來建立對應型別的例項。
      // 獲取String的Class例項:
      Class cls = String.class;
      // 建立一個String例項:
      String s = (String) cls.newInstance();

      上述程式碼相當於new String()。通過Class.newInstance()可以建立類例項,它的侷限是:只能呼叫public的無引數構造方法。帶引數的構造方法,或者非public的構造方法都無法通過Class.newInstance()被呼叫。

    • 動態載入

      • JVM在執行Java程式的時候,並不是一次性把所有用到的class全部載入到記憶體,而是第一次需要用到class時才載入。 

  • 訪問欄位
    • Class類提供了以下幾個方法來獲取欄位:

      • Field getField(name):根據欄位名獲取某個public的field(包括父類)
      • Field getDeclaredField(name):根據欄位名獲取當前類的某個field(不包括父類)
      • Field[] getFields():獲取所有public的field(包括父類)
      • Field[] getDeclaredFields():獲取當前類的所有field(不包括父類)
         1 public class Main {
         2     public static void main(String[] args) throws Exception {
         3         Class stdClass = Student.class;
         4         // 獲取public欄位"score":
         5         System.out.println(stdClass.getField("score"));
         6         // 獲取繼承的public欄位"name":
         7         System.out.println(stdClass.getField("name"));
         8         // 獲取private欄位"grade":
         9         System.out.println(stdClass.getDeclaredField("grade"));
        10     }
        11 }
        12 
        13 class Student extends Person {
        14     public int score;
        15     private int grade;
        16 }
        17 
        18 class Person {
        19     public String name;
        20 }
    • 一個Field物件包含了一個欄位的所有資訊:

      • getName():返回欄位名稱,例如,"name"
      • getType():返回欄位型別,也是一個Class例項,例如,String.class
      • getModifiers():返回欄位的修飾符,它是一個int,不同的bit表示不同的含義。
      • String類的value欄位為例,它的定義是:

        public final class String {
            private final byte[] value;
        }

        用反射獲取該欄位的資訊:

        Field f = String.class.getDeclaredField("value");
        f.getName(); // "value"
        f.getType(); // class [B 表示byte[]型別
        int m = f.getModifiers();
        Modifier.isFinal(m); // true
        Modifier.isPublic(m); // false
        Modifier.isProtected(m); // false
        Modifier.isPrivate(m); // true
        Modifier.isStatic(m); // false
    • 獲取欄位值

      • 利用反射拿到欄位的一個Field例項只是第一步,還可以拿到一個例項對應的該欄位的值。例如,對於一個Person例項,可以先拿到name欄位對應的Field,再獲取這個例項的name欄位的值。

         1 import java.lang.reflect.Field;
         2 
         3 public class Main {
         4 
         5     public static void main(String[] args) throws Exception {
         6         Object p = new Person("Xiao Ming");
         7         Class c = p.getClass();
         8         Field f = c.getDeclaredField("name");
        f.setAccessible(true);
        9 Object value = f.get(p); 10 System.out.println(value); // "Xiao Ming" 11 } 12 } 13 14 class Person { 15 private String name; 16 17 public Person(String name) { 18 this.name = name; 19 } 20 }
        Object get​(Object obj)
        返回指定物件上此 欄位表示的欄位的值。

         

    • 設定欄位值

      • 設定欄位值是通過Field.set(Object, Object)實現的,其中第一個Object引數是指定的例項,第二個Object引數是待修改的值。
         1 import java.lang.reflect.Field;
         2 
         3 public class Main {
         4 
         5     public static void main(String[] args) throws Exception {
         6         Person p = new Person("Xiao Ming");
         7         System.out.println(p.getName()); // "Xiao Ming"
         8         Class c = p.getClass();
         9         Field f = c.getDeclaredField("name");
        10         f.setAccessible(true);
        11         f.set(p, "Xiao Hong");
        12         System.out.println(p.getName()); // "Xiao Hong"
        13     }
        14 }
        15 
        16 class Person {
        17     private String name;
        18 
        19     public Person(String name) {
        20         this.name = name;
        21     }
        22 
        23     public String getName() {
        24         return this.name;
        25     }
        26 }
    • Java的反射API提供的Field類封裝了欄位的所有資訊:
      • 通過Class例項的方法可以獲取Field例項:getField()getFields()getDeclaredField()getDeclaredFields()
      • 通過Field例項可以獲取欄位資訊:getName()getType()getModifiers()
      • 通過Field例項可以讀取或設定某個物件的欄位,如果存在訪問限制,要首先呼叫setAccessible(true)來訪問非public欄位。
      • 通過反射讀寫欄位是一種非常規方法,它會破壞物件的封裝。
  • 呼叫方法
    • Class類提供了以下幾個方法來獲取Method

      • Method getMethod(name, Class...):獲取某個publicMethod(包括父類)
      • Method getDeclaredMethod(name, Class...):獲取當前類的某個Method(不包括父類)
      • Method[] getMethods():獲取所有publicMethod(包括父類)
      • Method[] getDeclaredMethods():獲取當前類的所有Method(不包括父類)
         1 public class Main {
         2     public static void main(String[] args) throws Exception {
         3         Class stdClass = Student.class;
         4         // 獲取public方法getScore,引數為String:
         5         System.out.println(stdClass.getMethod("getScore", String.class));
         6         // 獲取繼承的public方法getName,無引數:
         7         System.out.println(stdClass.getMethod("getName"));
         8         // 獲取private方法getGrade,引數為int:
         9         System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
        10     }
        11 }
        12 
        13 class Student extends Person {
        14     public int getScore(String type) {
        15         return 99;
        16     }
        17     private int getGrade(int year) {
        18         return 1;
        19     }
        20 }
        21 
        22 class Person {
        23     public String getName() {
        24         return "Person";
        25     }
        26 }
      • 一個Method物件包含一個方法的所有資訊:

        • getName():返回方法名稱,例如:"getScore"
        • getReturnType():返回方法返回值型別,也是一個Class例項,例如:String.class
        • getParameterTypes():返回方法的引數型別,是一個Class陣列,例如:{String.class, int.class}
        • getModifiers():返回方法的修飾符,它是一個int,不同的bit表示不同的含義。
      • 如果用反射來呼叫substring方法。
         1 import java.lang.reflect.Method;
         2 
         3 public class Main {
         4     public static void main(String[] args) throws Exception {
         5         // String物件:
         6         String s = "Hello world";
         7         // 獲取String substring(int)方法,引數為int:
         8         Method m = String.class.getMethod("substring", int.class);
         9         // 在s物件上呼叫該方法並獲取結果:
        10         String r = (String) m.invoke(s, 6);
        11         // 列印呼叫結果:
        12         System.out.println(r);
        13     }
        14 }
    • 呼叫靜態方法

      • 如果獲取到的Method表示一個靜態方法,呼叫靜態方法時,由於無需指定例項物件,所以invoke方法傳入的第一個引數永遠為null。以Integer.parseInt(String)為例。

         1 import java.lang.reflect.Method;
         2 
         3 public class Main {
         4     public static void main(String[] args) throws Exception {
         5         // 獲取Integer.parseInt(String)方法,引數為String:
         6         Method m = Integer.class.getMethod("parseInt", String.class);
         7         // 呼叫該靜態方法並獲取結果:
         8         Integer n = (Integer) m.invoke(null, "12345");
         9         // 列印呼叫結果:
        10         System.out.println(n);
        11     }
        12 }
    • 呼叫非public方法

      • 和Field類似,對於非public方法,雖然可以通過Class.getDeclaredMethod()獲取該方法例項,但直接對其呼叫將得到一個IllegalAccessException。為了呼叫非public方法,通過Method.setAccessible(true)允許其呼叫。

        import java.lang.reflect.Method;
        
        public class Main {
            public static void main(String[] args) throws Exception {
                Person p = new Person();
                Method m = p.getClass().getDeclaredMethod("setName", String.class);
                m.setAccessible(true);
                m.invoke(p, "Bob");
                System.out.println(p.name);
            }
        }
        
        class Person {
            String name;
            private void setName(String name) {
                this.name = name;
            }
        }
    • 多型

      • 一個Person類定義了hello()方法,並且它的子類Student也覆寫了hello()方法,那麼,從Person.class獲取的Method,作用於Student例項時(如下)。結果可知使用反射呼叫方法時,仍然遵循多型原則:即總是呼叫實際型別的覆寫方法(如果存在)。 

         1 import java.lang.reflect.Method;
         2 
         3 public class Main {
         4     public static void main(String[] args) throws Exception {
         5         // 獲取Person的hello方法:
         6         Method h = Person.class.getMethod("hello");
         7         // 對Student例項呼叫hello方法:
         8         h.invoke(new Student());
         9     }
        10 }
        11 
        12 class Person {
        13     public void hello() {
        14         System.out.println("Person:hello");
        15     }
        16 }
        17 
        18 class Student extends Person {
        19     public void hello() {
        20         System.out.println("Student:hello");
        21     }
        22 }

        通過設定setAccessible(true)來訪問非public方法;

  • 呼叫構造方法

    • 通過反射來建立新的例項,可以呼叫Class提供的newInstance()方法:

      Person p = Person.class.newInstance();

      呼叫Class.newInstance()的侷限是,它只能呼叫該類的public無引數構造方法。如果構造方法帶有引數,或者不是public,就無法直接通過Class.newInstance()來呼叫。

    • 為了呼叫任意的構造方法,Java的反射API提供了Constructor物件,它包含一個構造方法的所有資訊,可以建立一個例項。Constructor物件和Method非常類似,不同之處僅在於它是一個構造方法,並且,呼叫結果總是返回例項。

       1 import java.lang.reflect.Constructor;
       2 
       3 public class Main {
       4     public static void main(String[] args) throws Exception {
       5         // 獲取構造方法Integer(int):
       6         Constructor cons1 = Integer.class.getConstructor(int.class);
       7         // 呼叫構造方法:
       8         Integer n1 = (Integer) cons1.newInstance(123);
       9         System.out.println(n1);
      10 
      11         // 獲取構造方法Integer(String)
      12         Constructor cons2 = Integer.class.getConstructor(String.class);
      13         Integer n2 = (Integer) cons2.newInstance("456");
      14         System.out.println(n2);
      15     }
      16 }
    • 通過Class例項獲取Constructor的方法

      • getConstructor(Class...):獲取某個publicConstructor
      • getDeclaredConstructor(Class...):獲取某個Constructor
      • getConstructors():獲取所有publicConstructor
      • getDeclaredConstructors():獲取所有Constructor

      注意Constructor總是當前類定義的構造方法,和父類無關,因此不存在多型的問題。

      呼叫非publicConstructor時,必須首先通過setAccessible(true)設定允許訪問。setAccessible(true)可能會失敗。

  • 獲取繼承關係

    • 有了Class例項,還可以獲取它的父類的Class。
      public class Main {
          public static void main(String[] args) throws Exception {
              Class i = Integer.class;
              Class n = i.getSuperclass();
              System.out.println(n);
              Class o = n.getSuperclass();
              System.out.println(o);
              System.out.println(o.getSuperclass());
          }
      }
    • 由於一個類可能實現一個或多個介面,通過Class可以查詢到實現的介面型別。

      import java.lang.reflect.Method;
      
      public class Main {
          public static void main(String[] args) throws Exception {
              Class s = Integer.class;
              Class[] is = s.getInterfaces();
              for (Class i : is) {
                  System.out.println(i);
              }
          }
      }

      getInterfaces()只返回當前類直接實現的介面型別,並不包括其父類實現的介面型別。對所有interfaceClass呼叫getSuperclass()返回的是null,獲取介面的父介面要用getInterfaces()。如果一個類沒有實現任何interface,那麼getInterfaces()返回空陣列。

    • 判斷一個例項是否是某個型別時,正常情況下,使用instanceof操作符,如果是兩個Class例項,要判斷一個向上轉型是否成立,可以呼叫isAssignableFrom()。
      // Integer i = ?
      Integer.class.isAssignableFrom(Integer.class); // true,因為Integer可以賦值給Integer
      // Number n = ?
      Number.class.isAssignableFrom(Integer.class); // true,因為Integer可以賦值給Number
      // Object o = ?
      Object.class.isAssignableFrom(Integer.class); // true,因為Integer可以賦值給Object
      // Integer i = ?
      Integer.class.isAssignableFrom(Number.class); // false,因為Number不能賦值給Integer

相關文章