Java編程思想——第14章 類型資訊(一)

  • 2019 年 11 月 7 日
  • 筆記

  運行時類型資訊使得你可以在程式運行時發現和使用類型資訊。Java是如何讓我們在運行時識別對象和類的資訊得呢?

主要有兩種方式:1.傳統RTTI,他假定我們在編譯期間已經知道了所有類型;2.反射,它允許我們在運行時發現和使用類的資訊。

一、為什麼需要RTTI

我們來看一個例子:

 

  這是一個典型的類層次結構圖,基類位於頂部,派生類向下擴展。面向對象編程中的基本目的是:讓程式碼只操縱對基類(Shape)的引用。這樣,如果添加一個新類(比如從Shape派生的Rhomboid)來擴展程式就不會影響原來程式碼了。這個例子中Shape介面動態綁定了draw()方法,目的就是讓客戶端程式設計師用泛化的Shape引用來調用draw()。draw()在所有派生類里都會被覆蓋,並且由於它是被動態綁定的,所以即使是通過泛化的Shape引用來調用,也能產生正確的行為。這就是多態。

 

abstract class Shape {      void draw() {          System.out.println(this + ".draw()");      }        @Override      abstract public String toString();  }    class Circle extends Shape {      @Override      public String toString() {          return "Circle";      }  }    class Square extends Shape {      @Override      public String toString() {          return "Square";      }  }    class Triangle extends Shape {      @Override      public String toString() {          return "Triangle";      }  }    public class Shapes {      public static void main(String[] args) {          List<Shape> shapes = Arrays.asList(new Circle(), new Triangle(), new Square());          for (Shape shape : shapes) {              shape.draw();          }      }  }

結果:

Circle.draw()  Triangle.draw()  Square.draw()

分析:1.toString()被聲明為abstract,以此強制繼承者覆寫該方法,並且防止對無格式的Shape的實例化。

      2.如果某個對象出現在字元串表達式中(比如“+”,字元串對象的表達式),toString()會自動被調用,以生成該對象的String。

      3.當Shape派生類的對象放入List<Shape>的數組時會向上轉型,但是當向上轉型為Shape時也丟失了Shape對象的具體類型。對數組而言,他們都只是Shape類的對象。

      4.當從數組中取出元素時(這種容器把所有的事物都當作Object持有),將結果轉型會Shape。這是RTTI的最基本使用形式,因為在Java中所有的類型轉換都是在運行時進行正確性檢查的。這也是RTTI名字的含義:在運行時,識別一個對象的類型。

      5.但是例子中RTTI轉型並不徹底:Object被轉型為Shape而不是具體的派生類型,這是因為List<Shape>提供的資訊是保存的都是Shape類型。在編譯時,由容器和泛型保證這點,在運行時,由類型轉換操作來確保這點。

      6.Shape對象具體執行什麼,是由引用所指向的具體對象(派生類對象)而現實中大部分人正是希望儘可能少的了解對象的具體類型,而只是和家族中一個通用的類打交道,如:Shape,使得程式碼更易讀寫,設計更好實現,理解和改變,所以“多態”是面向對象編程的基本目標。

二、Class對象

  使用RTTI,可以查詢某個Shape引用所指向的對象的確切類型,然後選擇或者剔除某些特性。要理解RTTI在Java中的工作原理,首先應該知道類型資訊在運行時是如何表示的。Class對象承擔了這項工作,Class對象包含了與類有關的資訊。Class對象就是用來創建所有對象的,Java使用Class對象來執行其RTTI,Class類除了執行轉型操作外,還有擁有大量的使用RTTI的其他方式。

  每當編寫並編譯一個新類,就會生成一個Class對象,被存在同名的.class文件中。運行這個程式的Java 虛擬機(JVM)使用被稱為“類載入器”的子系統,生成這個類的對象。類載入器子系統實際上可以包含以挑類載入鏈,但是只有一個原生類載入器,它是JVM實現的一部分。原生類載入器載入的通常是載入本地盤的內容,這條鏈通常不需要添加額外的類載入器,但是如果有特殊需求比如:支援Web伺服器應用,那麼就要掛接額外的類載入器。

  所有的類在被第一次使用時,動態載入到JVM中。當第一個對類的靜態成員的引用被創建時,就會載入這個類。這也證明構造器是靜態方法,使用new操作符創建類的新對象也會被當作對類的靜態成員的引用。因此,Java程式在開始運行之前並未完全載入,各個部分在必須時才會載入。類載入器首先檢查這個類的Class對象是否已載入,如果尚未載入,默認的類載入器就會根據類名查找.class文件。

  一旦某個類的Class對象被載入入記憶體,它就被用來創建這個類的所有對象:

class Candy {      static {          System.out.println("Loading Candy");      }  }    class Gum {      Gum() {          System.out.println("Constructed Gum");      }        static {          System.out.println("Loading Gum");      }  }    public class SweetShop {      public static void main(String[] args) {          System.out.println("inside main");          new Candy();          try {              Class.forName("Gum");          } catch (ClassNotFoundException e) {              e.printStackTrace();          }          new Gum();          new Candy();      }  }

結果:
Inside main
Loading Candy
Loading Gum
Constructed Gum

  Class.forName()會讓類初始化:每個類都有一個static子句,該子句在類被第一次載入時執行,注意第二次載入時候就不走了。

  Class.forName(“”);這個方法是Class類的一個static成員。Class對象就和其他對象一樣 我們可以獲取並操作它的引用(這也就是類載入器的工作)。forName()是取得Class對象引用的一種方法,Class clazz = Class.forName(“xxx”); xxx必須是帶包名的全名!!! 如果你已經得到了一個類的引用xxx,還可以通過:Class clazz = xxx.getClass();方式獲取class對象。Class包含很多方法 其中比較常用的有:

public T newInstance() 得到一個實例 相當於new 
public native boolean isInstance(Object obj) 是否是參數類的一個實例
public native boolean isAssignableFrom(Class<?> cls) 判定此 Class 對象所表示的類或介面與指定的 Class 參數所表示的類或介面是否相同,或是否是其超類或超介面
public native boolean isInterface();判斷是否是介面
public native boolean isArray();判斷是否是數組
public native boolean isPrimitive();判斷是否是基本類型
public boolean isAnnotation() 判斷是否是註解
public boolean isSynthetic() 判斷是否是同步程式碼塊
public String getName() 返回帶包的全名,包含類的類型資訊(是引用類型 還是各種基本類型)

public ClassLoader getClassLoader() 獲取該類的類載入器。
public TypeVariable<Class<T>>[] getTypeParameters() 獲取泛型
public native Class<? super T> getSuperclass(); 獲取父類和父類泛型
public Package getPackage() 獲取包名
public Class<?>[] getInterfaces() 獲取該類實現的介面列表
public Type[] getGenericInterfaces() 獲取實現的介面列表及泛型
public native Class<?> getComponentType() 返回表示數組組件類型的 Class。如果此類不表示數組類,則此方法返回 null。如果此類是數組,則返回表示此類組件類型的 Class
public native int getModifiers() 返回類的修飾符
public native Object[] getSigners();// 我目前還不知道是幹什麼的如果有明確用處請評論告訴我 萬分感謝
native void setSigners(Object[] signers)
public Method getEnclosingMethod() 獲取局部或匿名內部類在定義時所在的方法
public String getSimpleName() 獲取最簡單的那個類名
public String getCanonicalName() 帶有包名的類名
public String getTypeName() 如果是數組的話[類名] 否則和getSimpleName一樣
public Field[] getFields() 獲取類中public的欄位
public Field[] getDeclaredFields() 獲取類中所有欄位
public Class<?>[] getClasses() 獲取該類以及父類所有的public的內部類
public Class<?>[] getDeclaredClasses() 獲取該類所有內部類,不包含父類的
public Method[] getMethods() 獲取該類及父類所有的public的方法
public Method[] getDeclaredMethods() 獲取該類中所有方法 ,不包含父類
public Constructor<?>[] getConstructors() 獲取所有public的構造方法
public Constructor<?>[] getDeclaredConstructors() 獲取該類所有構造方法
public Field getField(String name) 根據欄位名public獲取欄位
public Field getDeclaredField(String name) 根據欄位名所有獲取欄位
public Method getMethod(String name, Class<?>... parameterTypes) 根據方法名和參數獲取public方法
public Method getDeclaredMethod(String name, Class<?>... parameterTypes) 根據方法名和參數獲取所有方法
public Constructor<T> getConstructor(Class<?>... parameterTypes) 根據參數獲取public構造方法
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 根據參數獲取所有構造反方法、
...  如遺漏重要方法 請提醒 謝謝~

類的字面常量

  Java還有另一種方式生成對Class的引用,即使用字面常量:ClassDemo.class;  這種方式 簡單 高效(因為不用初始化類),且這種方式同樣適用於介面,數組,基本類型。對於基本類型的包裝類,還有一個標準欄位TYPE:

        boolean.class = Boolean.TYPE          char.class = Character.TYPE          byte.class = Byte.TYPE          short.class = Short.TYPE          int.class = Integer.TYPE          long.class = Long.TYPE          float.class = Float.TYPE          double.class = Double.TYPE          void.class = Void.TYPE

  使用這種方式創建對象的引用時,不會自動初始化該Class對象,為了使用類而做的準備工作實際包含三個步驟:

1.載入,由類載入器進行。該步驟將查找位元組碼,並從這些位元組碼中創建一個Class對象。

2.連接,在連接階段將驗證類中的位元組碼,為靜態域分配存空間,如果有需要,將解析這個類創建的對其他類的所有引用。

3.初始化,如果該類具有父類,則對其初始化,執行靜態初始化器和靜態初始化塊。

  初始化在對靜態方法(構造器是隱式地靜態)或非常數靜態域進行首次引用時才會執行:

class InitableA {      static final int staticFinalA = 47;      static final int staticFinalB = ClassInitialization.random.nextInt(1000);      static {          System.out.println("Initializing InitableA");      }  }    class InitableB {      static int staticNoFinal = 147;      static {          System.out.println("Initializing InitableB");      }  }    public class ClassInitialization {      public static Random random = new Random(47);      public static void main(String[] args) {          System.out.println(InitableA.staticFinalA);          System.out.println(InitableA.staticFinalB);          System.out.println(InitableB.staticNoFinal);      }  }

結果:
1 Initializing InitableA 258 Initializing InitableB 147

  如果一個static final 是”編譯期常量” ,就像 staticFinalA 那麼需要對它的類進行初始化就可以讀取,但是 staticFinalB 卻會使類初始化,因為它不是編譯期常量;只有static修飾符的  staticNoFinal 在對它讀取之前,要先進行鏈接,為這個域分配存儲空間並且初始化該存儲空間。

泛型化的Class引用

  通過使用泛型語法,可以讓編譯器強制執行額外的類型檢查:Class<Integer> intClass= int.class; 如果想放鬆限制,可以使用通配符”?”:Class<?> anyClass = AnyClass.class;如果想限定某類型的子類可以使用extends 關鍵字:Class<? extends Number> numCLass = int.class; numClass = double.class;

  當使用泛型語法Class<Toy>用於創建Class對象時,newInstance()將返回該對象得確切資訊而不只是Object。

  如果你手頭是Toy的父類ToyFather,toyClass.getSuperclass()方法卻只能返回 Class<? super Toy> 而不是確切的ToyFather ,並且在newInstance()時返回Object。