Java 程式設計思想筆記:Learn 10

weixin_33978044發表於2018-05-13

第 14 章 型別資訊

執行時型別資訊使得你可以在程式執行時發現和使用型別資訊

Java 在執行時識別物件和類有兩種方式:

  • 傳統的 RTTI(Run-Time Type Identification),它假設我們在編譯時已經知道了所有的型別;
  • 反射機制,它允許我們在執行時發現和使用類資訊

14.1 為什麼需要 RTTI

3097305-52cec2d6d98ecedc.png
javaThought.png

物件導向程式設計中的基本目的是:讓程式碼只操縱對基類(這裡是 Shape)的引用。這樣,如果要新增一個新類(從 Shape 派生的rhomboid 來擴充套件程式),不會影響到原來的程式碼。

在這個例子中 Shape 介面中動態繫結了 draw() 方法,目的就是讓客戶端程式設計師使用泛化的 Shape 引用來呼叫 draw(). draw() 在所有的派生類裡面都會被覆蓋,並且它是被動態繫結的,所以即便通過泛化的 Shape 引用來呼叫,也能產生正確的行為。這就是多型。

因此,通常會建立一個具體物件(Circle, Square, Triagnle), 把它向上轉型成 Shape(忽略物件的具體型別),並在後面的程式使用匿名(即不知道具體型別)的 Shape 引用。

public class Shapes {
    public static void main(String[] args){
        List<Shape> shapeList = Arrays.asList(new Circle(), new Square(), new Triangle());

        for(Shape shape : shapeList){
            shape.draw();
        }
    }
}

abstract class Shape{
    void draw(){
        System.out.println(this + ".draw()");
    }
    abstract public String toString();
}

class Square extends Shape{

    @Override
    public String toString() {
        return "Square{}";
    }
}

class Triangle extends Shape{

    @Override
    public String toString() {
        return "Triangle{}";
    }
}

class Circle extends Shape{

    @Override
    public String toString() {
        return "Circle{}";
    }
}

基類中包含 draw() 方法,它通過傳遞 this 引數給 System.out.println, 間接地使用 toStirng() 列印標識類符(注意,toString() 被宣告為 abstract, 以此強制繼承者複寫該方法,並可以防止對無格式的 shape 的例項化)。

如果某個物件出現在字串表示式中(涉及 “+” 和字串物件的表示式),toString() 方法會被自動呼叫,以生成表示該物件的 String。每個派生類都要覆蓋(從Object繼承來的)toString() 方法,這樣 draw() 在不同情況下就列印出不同的訊息。——這也就是多型。

在這個例子中,當把 Shape 物件放入List<Shape>的陣列時會向上轉型。但是向上轉型為 Shape 的時候也丟失了 Shape 物件的具體型別。對於陣列而言,它們只是 Shape 類的物件。

當從陣列中取出元素時,這種容器——實際上它將所有的事物都當作 Object 持有 —— 會自動將結果轉型回 Shape。這是 RTTI 最基本的使用形式,因為在 Java 中,所有型別轉換都是在執行時進行正確性檢查。這也是RTTI(Run-Time Type Interfaec)的含義:在執行時,識別一個物件的型別。

在這個例子中,RTTI 型別轉化並不徹底:Object被轉型為Shape,而不是轉型為 Circle / Square / Triangle。這是因為目前我們只知道這個List<Shape>儲存的是 Shape。在編譯時,將由容器和 Java 的泛型系統來強制確保這一點;而在執行時,由型別轉化操作來確定這一點。

接下來就是多型機制的事情了,Shape 物件實際執行了什麼的程式碼,是由引用所指向的具體物件 Circle / Square / Triangle 而決定的。通常,也是這樣要求的,你希望大部分程式碼儘可能地少了解物件的具體型別,而是隻與物件家族中的一個通用表示打交道(這個例子中是Shape)。這樣的程式碼會更容易寫容易讀容易維護。所以,“多型”是物件導向程式設計的基本目標。

但是如果想知道某個泛化引用的確切型別,可以使用 RTTI,查詢某個 Shape 引用所指向的物件的確切型別。

14.2 Class 物件

要理解 RTTI 在 Java 中的工作原理,首先必須知道型別資訊在執行時是如何表示的。這項工作是由稱為 Class 物件的特殊物件完成的,它包含了與類有關的資訊。事實上,Class 物件就是來建立類的所有的 “常規”物件的。Java 使用 Class 物件來執行其 RTTI,即使你正在執行的是類似轉換這樣的操作。Class 類還擁有大量的使用 RTTI 的其他方式。

類是程式的一部分,每個類都有一個 Class 物件。換言之,每當編寫並編譯了一個新類,就會產生一個 Class 物件(更恰當地說,是被儲存在同名的 .class 檔案中)。為了生成這個類的物件,執行這個程式的 Java 虛擬機器將使用 “類載入器” 的子系統。

類載入器子系統實際上可以包含了哦一條類載入器鏈,但是隻有一個是原生類載入器,它是 JVM 實現的一部分。原生類載入器載入是所謂的可信類,包括 Java API 類,通常是從本地盤載入的。

所有類都是在對其第一次使用時,動態載入到 JVM 中的。當程式建立第一個對類的靜態成員時,就會載入這個類。這個證明構造器也是類的靜態方法,即使在構造器之間並沒有使用 static 關鍵字。因此,使用 new 操作符建立類的新物件也會被當作對類的靜態成員的引用。

因此,Java 程式在它開始執行之前並非被完全載入,其各個部分是在必需時才載入的。類載入器首先檢視這個類的 Class 物件是否已經載入。如果尚未載入,預設的類載入器就會根據類名查詢 .class檔案。在這個類的位元組碼被載入時,它們會接受驗證,以確保沒有被破壞。

一旦某個類的 Class 物件被載入記憶體,它就被用來建立這個類的所有物件。

public class SweetShop {

    public static void main(String[] args){
        System.out.println("inside main");
        new Candy();
        System.out.println("after creating candy");
        try{
            Class.forName("Gum");
        }catch (ClassNotFoundException e){
            System.out.println("Could not find Gum");
        }
        System.out.println("After class for name Gum");
        new Cookie();
        System.out.println("After creating Cookie");
    }
}

class Candy{
    static {System.out.println("loading candy");}
}

class Cookie{
    static {System.out.println("loading Cookie");}
}

從輸出中可以看出,Class 物件僅在需要的時候才被載入,static 初始化是在類載入時進行的。

Class.forName("Gum") 是 Class 類(所有Class 物件都屬於這個類)的一個 static 成員。 Class 物件就和其他物件一樣,我們可以獲取並操作它的引用。(這也是類載入器的工作)。forName 是取得 Class 物件引用的一種方法。它是用一個包含目標類的文字名(注意區分大小寫和拼寫)的 String 作為引數,返回的是一個 Class 物件的引用,上面的程式碼忽略了返回值。對 forName() 的呼叫是為了它產生的副作用。如果類 Gum 還沒被載入就載入它。在載入的過程中,Gum 的static 子句被執行。

在前面的例子裡,如果Class.forName() 找不到你要載入的類,它會丟擲異常 ClassNotFoundException.

無論何時,只要你想執行時使用型別資訊,就必須首先獲得對恰當 Class 物件的引用。Class.forName() 就是實現此功能的便捷途徑,因為你不需要為了獲得 Class 引用而持有該型別的物件。但是,如果你已經擁有了一個感興趣的型別物件,那就可以通過呼叫 getClass() 方法來獲取 Class 引用了,這個方法屬於根類 Object的一部分,它將返回該物件的實際型別的Class 引用。Class 物件包含了很多有用的方法,下面是其中一部分:

public class ToyTest {
    static void printInfo(Class cc){
        System.out.println("Class Name: " + cc.getName() + "is interface [" + cc.isInterface() + "]");
        System.out.println("Simple name: " + cc.getSimpleName());
        System.out.println("Canonical name: " + cc.getCanonicalName());
    }

    public static void main(String[] args){
        Class c = null;
        try{
            c = Class.forName("com.zzjack.rdsapi_demo.javathought.FancyToy");
        } catch (ClassNotFoundException e){
            System.out.println("can not find Fancy");
        }
        printInfo(c);
        for(Class face : c.getInterfaces()){
            printInfo(face);
        }
        Class up = c.getSuperclass();
        Object obj = null;
        try{
            obj = up.newInstance();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        System.out.println(".....................");
        printInfo(obj.getClass());
    }
}

interface HasBatterirs{}

interface Waterproof{}

interface Shoots{}

class FancyToy extends Toy implements HasBatterirs, Waterproof, Shoots{
    FancyToy(){}
}

class Toy{
    Toy(){}
    Toy(int i){}
}

這個例子中體現的 Class 的方法如下:

Class.getName()  是獲取的 物件 的完整鏈路名,
Class.isInterface() 是判斷是否為介面
Class.getSimpleName() 僅僅獲取呼叫物件的名稱,
Class.getCanonicalName() 獲取物件的全鏈路名
c = Class.forName() 獲取物件的引用
c.getInterfaces() 獲取物件的所有介面
c.getSuperClass() 獲取父類
obj = c.newInstance() 把類例項化
obj.getClass() 獲取這個例項的類物件

14.2.1 類字面常量

Java 還提供了另外一種方法來生成對 Class 物件的引用,即類字面常量。即 FancyToy.Class

這樣不僅簡單,而且更安全。因為在編譯時就會受到檢查,所以不用放到 try 語句塊中,並且它根除了對 forName() 方法的呼叫,所以也更高效。

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 Float.TYPE
double.class Double.TYPE
void.class Void.TYPE

類字面常量不僅可以用於普通的類,還可以用於介面、陣列和基本資料型別。另外,對於基本資料型別的包裝類,還有一個標準欄位 TYPE。TYPE 是一個引用,指向對應的基本資料型別的 Class 物件,如下圖所示:

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 Float.TYPE
double.class Double.TYPE
void.class Void.TYPE

我建議使用 ".class" 的形式,以保持與普通類的一致性。
注意,有一點很有趣,當使用 “.class” 來建立對 Class 物件的引用時,不會自動地初始化該 Class 物件。為了使用類而做的準備工作實際包含3個步驟:

  1. 載入。這是由類載入器執行的。該步驟將查詢位元組碼(通常在 classpath 所指定的路徑中查詢,但這並非是必要的),並從這些位元組碼中建立一個 Class 物件。
  2. 連結。在連結階段將驗證類中的位元組碼,為靜態域分配儲存空間,並且如果必需的話,將解析這個類建立的對其他類的所有引用。
  3. 初始化。如果該類具有超類,則對其初始化,執行靜態初始化器和靜態初始化塊。

初始化被延遲到了對靜態方法(構造器隱式地是靜態的)或者非常數靜態域進行首次引用時才執行:

class Initable{
    static final int staticFinal = 47;
    static final int getStaticFinal2 =
            ClassInitialization.rand.nextInt(1000);
    static {
        System.out.println("Initializing Initable");
    }
}

class Initable2{
    static int staticNonFinal = 147;
    static {
        System.out.println("Initializing Initable2");
    }
}

class Initable3{
    static int staticNonFinal = 74;
    static {
        System.out.println("Initializing Initable3 ---->");
    }
}

public class ClassInitialization {
    public static Random rand = new Random(47);
    public static void main(String[] args) throws Exception{
        Class initable = Initable.class;
        System.out.println("After creating Initable ref");
        System.out.println(Initable.staticFinal);
        System.out.println(Initable2.staticNonFinal);
        System.out.println(Initable3.staticNonFinal);
        Class initable3 = Class.forName("Initable3");
        System.out.println("After creating Initable3 ref");
//        System.out.println(initable3.staticNonFinal);
    }
}

初始化有效地實現了儘可能的 “惰性”。從對 initable 引用的建立中可以看到,僅使用.class 語法來獲得對類的引用不會引發初始化。但是,為了產生 Class 引用,Class.forName() 立即就進行了初始化,就像在對 initable3 引用的建立中所看到的。

如果一個 static final 值是 “編譯期常量”,就像 Initable.staticFinal 那樣,那麼這個值不需要對 Initable 類進行初始化就可以被讀取。但是,如果只是將一個域設定為 static 和 final 的,還不足以確保這種行為,例如,對 Initable.staticFinal2 的訪問將強制進行類的初始化,因為它不是一個編譯期常量。

如果一個 static 域不是 final的,那麼對它訪問時,總是要求在它被讀取之前,要先進行連結(為這個域分配儲存空間)和初始化(初始化該儲存空間),就像在對 Initable2.staticNonFinal 的訪問中所看到的那樣。

14.2.2 泛化的 Class 引用

Class 引用總是指向某個 Class 物件,它可以製造類的例項,幷包含可作用於這些例項的所有方法程式碼。它還包含該類的靜態成員,因此,Class 引用表示的就是它所指向的物件的確切型別,而該物件便是 Class 類的一個物件。

但是在 java5 中,也可以通過泛型,使得它的型別更加具體,以下兩種寫法是相等的:

public class GenericClassReference{
  public static void main(String[] args){
      Class intClass = int.class;
      Class<Integer> integerClass = int.class;
      intClass = integerClass;
  }
}

普通類引用不會產生警告資訊,儘管泛型類引用只能賦值為指向其宣告的型別,但是普通的類引用可以被重新賦值為指向任何其他的 Class 物件。通過使用泛型語法,可以讓編譯器強制執行額外的型別檢查。

Class<Number> genericNumberClass = int.class;

這看起來似乎是起作用的,因為 Integer 繼承自 Number。但是它無法工作,因為 Integer Class 物件不是 Number Class 物件的子類。

為了在使用泛化的 Class 飲用時放鬆限制,可以使用萬用字元“?”,表示任何事物。使用萬用字元 “?” 來改寫上面的例子:

public class WildcardClassReference{
  public static void main(String[] args){
    Class<?> intClass = int.class;
    intClass = double.class;
  }
}

在 Java5 中,Class<?> 優於平凡的 Class,即便它們是等價的。Class<?> 的好處在於它表示你並非疏忽,而是使用了一個非具體的類引用。

為了建立一個 Class 引用,它被限定為某種型別,或該型別的任何子型別,你需要將萬用字元與 extends 關鍵字相結合,建立一個範圍。因此,與僅僅宣告Class<Number>不同,現在做如下:

public class BoundedClassReference{
  public static void main(String[] args){
      Class<? extends Number> bounded = int.class;
      bounded = double.class;
      bounded = Number.class;
  }
}

向 Class 引用新增泛型語法的原因僅僅是為了提供編譯期型別檢查,如果你操作有誤,稍後立即就會發現這一點。

下面的例子使用了泛型類語法。它儲存了一個類引用,稍後又產生了一個 List,填充這個 List 對是使用了 newInstance() 方法,通過該引用生成的:

14.2.3 新的轉型語法

public class ClassCasts {
    public static void main(String[] args){
        Building b = new House();
        Class<House> houseClass = House.class;
        House h = houseClass.cast(b);
        h = (House) b;
    }
}

class Building {}

class House extends Building{}

cast() 方法接受引數物件,並將其轉型為Class引用的型別。

14.3 型別轉化錢先做的檢查

RTTI形式:

  1. 傳統的型別轉化,如 "(Shape)", 由於RTTI確保型別轉化的正確性,如果執行了一個錯誤的型別轉換,就會丟擲一個 ClassCastException 異常。

  2. 代表物件的型別的 Class 物件。也就是向下轉型,因為這個操作是安全的,可以由編譯器自動完成

  3. 使用關鍵詞 instanceof。返回1個布林值,告訴我們物件是不是某個特定型別的例項。

if (x instanceof Dog){
  ((Dog)x)bark()
}

在將x轉型成一個Dog之前,上面的if語句會檢查物件x是否屬於Dog類。

14.3.1 使用類字面常量

pass

14.6 反射:執行時的類資訊

Class 類與 java.lang.reflect 類庫一起對反射的概念進行了支援,該類庫包含了 Field / Method / Constructor 類(每個類都實現了Member介面。)這些型別的物件是由 JVM 在執行時建立的,泳衣表示未知類裡的對應的成員。這樣就可以使用 Constructor 建立新的物件,用 get 和 set 方法讀取和修改與Field物件關聯的欄位,用 invoke 方法呼叫與Method 物件關聯的方法。另外,還可以呼叫 getFields()、getMethods() 和 getConstructors() 等很便利的方法,以返回表示欄位、方法以及構造器的物件的陣列。這樣,匿名物件的類資訊就能在執行時被完全確定下來。

重要的是,要認識到反射機制與RTTI的真正區別。對RTTI來說,編譯器在編譯時開啟和檢查 .class 檔案。(換句話說,我們可以用“普通”方式呼叫物件的所有方法)。而對於反射機制來說,.class 檔案在編譯時不可獲取的,所以在執行時開啟和檢查 .class 檔案。

14.6.1 類方法提取器

public class ShowMethods {

    private static String usage = "useage:" +
            "Show Method qualified class.name\n" +
            "to show all methods in class or";

    private static Pattern p = Pattern.compile("\\w+ \\.");

    public static void main(String[] args){
        if(args.length < 1){
            System.out.println(usage);
            System.exit(0);
        }
        int lines = 0;
        try{
            Class<?> c = Class.forName(args[0]);
            Method[] methods = c.getMethods();
            Constructor[] ctors = c.getConstructors();
            if(args.length == 1){
                for(Method method : methods){
                    System.out.println(
                            p.matcher(method.toString()).replaceAll("")
                    );
                }
                for(Constructor constructor : ctors){
                    System.out.println(
                            p.matcher(constructor.toString()).replaceAll("")
                    );
                }
            } else{
                for(Method method : methods){
                    if(method.toString().indexOf(args[1]) != -1){
                        System.out.println(
                                p.matcher(method.toString()).replaceAll("")
                        );
                        lines++;
                    }
                }
            }
        }catch (ClassNotFoundException ex){
            System.out.println("No such class: " + ex);
        }
    }
}

Class 的 getMethods() 和 getConstructors() 方法分別返回 Method 物件的陣列和Construcotr 物件的陣列。這兩個類都提供了深層方法,用以解析其物件所代表的方法,並獲取其名字、輸入引數以及返回值。

14.7 動態代理

代理是基本的設計模式之一,它是你為了提供額外的或不同的操作,而插入的用來替代 “實際” 物件的物件。

public class SimpleProxyDemo {
    public static void consumer(Interface iface){
        iface.doSomething();
        iface.somethingElse("bonobo");
    }

    public static void main(String[] args){
        consumer(new RealObject());
        consumer(new SimpleProxy(new RealObject()));
    }
}

interface Interface{
    void doSomething();
    void somethingElse(String ars);
}

class SimpleProxy implements Interface{
    private Interface proxied;

    public SimpleProxy(Interface proxied){
        this.proxied = proxied;
    }

    public void somethingElse(String arg){
        System.out.println("SimpleProxy somethingElse " + arg);
        proxied.somethingElse(arg);
    }

    public void doSomething(){
        System.out.println("SimpleProxy doSomething");
        proxied.doSomething();
    }
}

class RealObject implements Interface{
    public void doSomething(){
        System.out.println("do something");
    }

    public void somethingElse(String arg){
        System.out.println("somethingElse " + arg);
    }
}

簡單的代理,就是 consumer 接受的 interface,所以它能夠接受任何實現了 Interface 的類。SimpleProxied 也是一個實現了 Interface 的類,它接受一個實現了 Interface 的類作為建構函式的引數。說白了,consumer 和 SimpleProxied 一共把 Interface 包了兩層。

class DynamicProxyHandler implements InvocationHandler {
  private Object proxied;
  
  public DynamicProxyHandler(Object proxied){
    this.proxied = proxied;
  }

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
      System.out.println("*** proxy: " + proxy.getClass() + " .method: " + method + ", args: " + args);
      if(args != null){
          for(Object arg : args){
              System.out.println(" " + args);
          }
      }
      return method.invoke(proxied, args);
  }
}

class SimpleDynamicProxy{
  public static void consumer(Interface iface){
      iface.doSomething();
      iface.somethingElse("bonobo");
  }

  public static void main(String[] args){
      RealObject real = new RealObejct();
      consumer(real);
      // Insert a proxy and call again
      Interface proxy = (Interface)Proxy.newProxyInstance(
         Interace.class.getClassLoader(),
         new Class[] { Inteface.class},
         new DynamicProxyHandler(real));
      consumer(proxy);
  }
}

通過呼叫靜態方法 Proxy.newProxyInstance() 可以建立動態代理,這個方法需要得到:

  • 一個類載入器(你通常可以從已經被載入的物件中獲取其類載入器,然後傳遞給它),也就是 Interface.class.getClassLoader()
  • 一個你希望該代理實現的介面列表(不是類或抽象類),即 new Class[] {Interface.class}
  • 以及 InvocationHandler 介面的一個實現,即 new DynamicProxyHandler(real)

動態代理可以將所有呼叫重定向到呼叫處理器,因此通常會向呼叫處理器的構造器傳遞一個 “實際”物件,從而使得呼叫著處理器在執行其中介任務時,可以將請求轉發。

14.9 介面與型別資訊

Interface 關鍵字的一種重要目標是允許程式設計師隔離構件,進而降低耦合性。但是通過型別資訊,這種耦合性還是會傳播出去——介面並非是對解耦的一種無懈可擊的保證。

具體沒看懂,大致就是說還是可以通過反射拿到私有屬性。

相關文章