1. 程式人生 > >【Java程式設計思想】14.型別資訊

【Java程式設計思想】14.型別資訊

執行時型別資訊使得你可以在程式執行時發現和使用型別資訊。
Java 中在執行時識別物件和類的資訊的方式有兩種:

  1. “傳統的” RTTI,它假定在編譯時我們就已經知道所有型別。
  2. 反射機制,允許我們在執行時發現和使用類的資訊。

14.1 為什麼需要 RTTI

常見的類層次結構

如圖,對於常見的類層次結構,基類位於頂部,派生類向下拓展。
面向物件程式設計的基本思想就是:讓程式碼只操縱基類的引用,這樣在新增新的派生類時不會影響原來的程式碼。當在基類中定義了一個方法之後,在呼叫時都是在使用泛化的基類引用來呼叫該方法。該方法在基類的所有派生類中都會被覆蓋,並且由於它是動態繫結的,所以即使用過泛化的基類引用來進行呼叫,也能產生正確的行為。這就是多型

這裡需要舉個例子:

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

class Circle extends Shape {
    public String toString() {
        return "Circle";
    }
}

class Square extends Shape {
    public String toString() {
return "Square"; } } class Triangle extends Shape { public String toString() { return "Triangle"; } } 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(); } }

這個例子中,當把 Shape 物件放入 List<Shape> 的陣列時會向上轉型,但是在向上轉型的同時也丟失了 Shape 物件的具體型別。這樣對陣列而言,儲存於其中的只是 Shape 類的物件。
當從陣列中取出元素時,容器-實際上容器也將所有的事物都當做 Object 持有-會自動將結果轉型回 Shape

這是 RTTI 的最基本的使用形式,在 Java 中,所有的型別轉換都是在執行時進行正確性檢查。RTTI 的含義也就是如此–在執行時,識別一個物件的型別。

接下來就是多型機制的工作:Shape 物件實際執行什麼樣的程式碼,是由引用所指向的具體物件(派生類的物件)而決定的。使用 RTTI 可以查詢到基類引用所指向的物件的確切型別,可以明確我們應該如何執行下一步程式碼。

使用 abstract 關鍵字宣告方法,可以強制繼承者覆寫該方法,並且可以防止對無格式的基類的例項化。
另外,如果某個物件出現在字串表示式中(涉及”+“和字串物件的表示式),toString() 方法就會被自動呼叫。

14.2 Class 物件(重點理論知識)

型別資訊在執行時的表示,是由被稱作“Class 物件”的特殊物件完成的,它包含了與類有關的資訊。
實際上,Class 物件就是用來建立類的所有的“常規”物件的。
Java 使用 Class 物件來執行其 RTTI,即使你正在執行的是類似轉型這樣的操作。

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

類載入器子系統實際上可以包含一條類載入器鏈,但是隻有一個原生類載入器,它是 JVM 實現的一部分。原生類載入器載入的是所謂的可信類,包括 Java API 類,他們通常是從本地磁碟載入的。在這條鏈中,通常不需要新增額外的類載入器,但是如果有特殊需求的情況(例如以某種特殊方式載入類,以支援 Web 伺服器應用、或者在網路中下載類),那麼有一種方式可以掛接額外的類載入器。

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

這種動態載入的機制是區別於其他語言的。類載入器會首先檢查這個類的 Class 物件是否已經載入,如果尚未載入,預設的類載入器就會根據類名查詢.class 檔案(例如,某個附加類載入器可能會在資料庫中查詢位元組碼)。在這個類的位元組碼被載入時,這些位元組碼會接受驗證,以確保其沒有被破壞,並且不包含不良的 Java 程式碼。一旦這個類的 Class 物件被載入記憶體,他就會被用來建立這個類的所有物件。

關於獲取 Class 的引用:

Class.forName("class name");
上面這個方法時 Class 類(所有 Class 物件都屬於這個類)的一個 static 成員。forName() 是取得 Class 物件的引用的一種方法,它使用一個包含目標類的文字名的 String 作為入參,返回一個 Class 物件的引用,
如果 forName() 找不到要載入的類,會丟擲一個 ClassNotFoundException

除此之外,如果已經擁有了一個物件,那麼可以通過呼叫 getClass() 來獲取 Class 引用。該方法屬於 Object 的一部分,將返回該物件的實際型別的 Class 引用。

實際上 Class 物件和其他物件一樣,我們都可以通過類載入器獲取並操作它的引用。

Class 的引用可以有很多用法:

  • getName() 獲取全限定的類名
  • getSimpleName() 獲取不包含包名的類名
  • getCanonicalName() 獲取全限定的型別
  • isInterface() 判斷 Class 物件是否表示某個介面
  • getInterfaces() 返回 Class 物件中包含的介面
  • getSuperclass() 返回 Class 物件的基類
  • newInstance() 實現 “虛擬構造器” 的一種途徑。虛擬構造器允許宣告:我不知道你的確切型別,但是無論如何都要正確的建立你自己。使用該方法建立的類,必須帶有預設的構造器。

類字面常量

Java 還提供另一種方法來生成對 Class 物件的引用,即使用類字面常量,就像 ClassName.class。這樣做會在編譯時就會受到檢查,並且它根除了對 forName() 方法的呼叫,因此是安全且高效的。
類字面常量可以應用於普通類、介面。陣列以及基本資料型別。對於基本資料型別的包裝器類,還有一個標準欄位 TYPE。TYPE 欄位是一個引用,指向對應的基本資料型別的 Class 物件。

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

需要注意的是,使用 “.class” 建立對 Class 物件的引用時,不會自動地初始化該 Class 物件。這期間的準備工作實際包含以下三步:

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

實際執行中,初始化這一步會被延遲到對靜態方法(要記住構造器也是隱式靜態的)或者非常數靜態域進行首次引用的時候才會執行。初始化會盡可能的“惰性”/“懶漢”。
僅使用.class 語法來獲得對類的引用不會引發初始化;但是在使用 Class.forName() 的時候,為了產生 Class 物件的引用,會立即就進行初始化。

泛化的 Class 引用

Class 引用總是指向某個 Class 物件,他可以製造類的例項,幷包含可作用於這些例項的所有方法程式碼。他還包含該類的靜態成員。因此 Class 引用表示的就是他所指向的物件的確切型別,而該物件便是 Class 類的一個物件。
實際上 Java SE5 之後,允許對 Class 引用所指向的 Class 物件的型別進行限定–>利用泛型語法。

Class intClass = int.class;
Class<Integer> genericIntClass = int.class;
genericIntClass = Integer.class; // Same thing
intClass = double.class;
// genericIntClass = double.class; // Illegal

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

Class<Number> genericIntClass = int.class;

上面這種做法也是非法的,雖然 Integer 繼承自 Number,但是並不意味著 Integer Class 物件也是 Number Class 的子類。兩者並不相同。
如果一定要在使用泛化的 Class 引用時放鬆限制,可以使用萬用字元,這也是泛型語法的一種。
萬用字元就是“?”,表示“任何事物”。
使用 Class<?> 的好處在於,他可以表示並非是碰巧或疏忽而使用了一個非具體的類引用,使用者想要選擇的就是非具體的版本。

為了建立一個被限定為某種型別,或是該型別的任何子型別的 Class 引用,可以使用萬用字元結合 extends 關鍵字,建立一個範圍

Class<? extends Number> bounded = int.class;
bounded = double.class;
bounded = Number.class;
// Or anything else derived from Number.

上面這麼折騰,其實就是為了保證在編譯器型別檢查的時候,就可以發現泛化 Class 引用中不安全的操作,而不是必須等到在執行時才能發現。

Class<FancyToy> ftClass = FancyToy.class;
// Produces exact type:
FancyToy fancyToy = ftClass.newInstance();
Class<? super FancyToy> up = ftClass.getSuperclass();
// This won't compile:
// Class<Toy> up2 = ftClass.getSuperclass();
// Only produces Object:
Object obj = up.newInstance();

如果手頭有的是超類(子類),那麼編譯器將只允許宣告超類引用是“某個類,他是 FancyToy 超類的基類”,就像 Class<? super FancyToy> 中所看到的,他不會接受 Class<Toy> 這樣的宣告。正也是因為這種含糊性,up.newInstance() 返回值不是精準型別。

新的轉型語法

cast() 用於 Class 引用的轉型語法。該方法接收引數物件,並將其轉型為 Class 引用的型別。在編寫泛型程式碼時,如果儲存了 Class 引用,並希望以後通過這個引用來執行轉型,那麼這個方法就非常有用。
asSubClass() 用於將一個類的物件轉型為更加具體的型別。

14.3 型別轉換前先做檢查

Java 中型別轉換的時候會執行型別檢查,這通常被稱為“型別安全的向下轉型”。這是根據類層次結構圖的排列確定的。相對於向下轉型,編譯器允許自由地向上轉型(顯而易見的)。
如果不適用顯式的型別轉換,編譯器就不允許執行向下轉型賦值,以告知編譯器使用者擁有的額外資訊,這些資訊明確說明了該型別是哪一種特定型別。
編譯器將檢查向下轉型是否合理,因此它不允許向下轉型到實際上不是待轉型類的子類的型別上。

區別於型別轉換,基本資料型別與包裝器型別的互相轉換通常稱之為 “裝箱 / 拆箱”。
裝箱就是自動將基本資料型別轉換為包裝器型別;拆箱就是自動將包裝器型別轉換為基本資料型別。

至此我們總結 Java 中的 RTTI 形式包括:

  1. 傳統的型別轉換,如 “(Shape)”,由 RTTI 確保型別轉換的正確性。這裡執行錯誤的型別轉換,會丟擲一個 ClassCastException 異常。
  2. 代表物件的型別的 Class 物件。通過查詢 Class 物件可以獲取執行時所需的資訊。
  3. instanceOf 關鍵字,根據物件是不是某個特定型別的例項,返回一個布林值,基本以提問的方式使用(就是 if)。

針對 instanceOf 關鍵字的使用有比較嚴格的限制:只可將其與命名型別進行比較,而不能與 Class 物件作比較。

在呼叫 newInstance() 時,可能會產生兩種異常,分別是 InstantiationExceptionIllegalAccessException,分別代表例項化過程中的異常,以及違反 Java 安全機制(例如預設構造器為 private 的情況)。

@SuppressWarnings 註解不能直接置於靜態初始化子句之上。

幾種用於 Java RTTI 類檢測的方法比較:

  • instanceOf 關鍵字,只被用於物件引用變數,檢查左邊物件是不是右邊類或介面的例項化。如果被測物件是 null 值,則測試結果總是 false。
  • Class<?>.isInstance(Object obj) 方法提供了一種動態的測試物件的途徑,可以避免寫出不斷去使用 instanceOf 關鍵字進行單調判斷的程式碼。如果 obj 是呼叫這個方法的 class 或介面的例項,則返回 true。如果被檢測的物件是 null 或者基本型別,那麼返回值是 false。
  • Class<?>.isAssignableFrom(Class cls),如果呼叫這個方法的 class 或介面與引數 cls 表示的類或介面相同,或者是引數 cls 表示的類或介面的父類,則返回 true。
  • Class<?>.isMemberClass(Class cls),如果呼叫這個方法的 class 或介面是引數 cls 表示的類中的內部類,或者是引數 cls 表示的類或介面的父類,則返回 true。
  • Class<?>.isAnonymousClass(),判斷該類是不是匿名類。

本章節以一個例項不斷被深入的抽象、優化的過程,展示了泛型以及型別轉換在方法抽象中的應用,需要仔細體會其中程式碼。下面展示的是最終完成的,通用的類計數工具。

public class TypeCounter extends HashMap<Class<?>, Integer> {
    private Class<?> baseType;

    public TypeCounter(Class<?> baseType) {
        this.baseType = baseType;
    }

    public void count(Object obj) {
        Class<?> type = obj.getClass();
        if (!baseType.isAssignableFrom(type))
            throw new RuntimeException(obj + " incorrect type: "
                    + type + ", should be type or subtype of " + baseType);
        countClass(type);
    }

    private void countClass(Class<?> type) {
        Integer quantity = get(type);
        put(type, quantity == null ? 1 : quantity + 1);
        Class<?> superClass = type.getSuperclass();
        if (superClass != null &&
                baseType.isAssignableFrom(superClass))
            countClass(superClass);
    }

    public String toString() {
        StringBuilder result = new StringBuilder("{");
        for (Map.Entry<Class<?>, Integer> pair : entrySet()) {
            result.append(pair.getKey().getSimpleName());
            result.append("=");
            result.append(pair.getValue());
            result.append(", ");
        }
        result.delete(result.length() - 2, result.length());
        result.append("}");
        return result.toString();
    }
}

14.4 註冊工廠

使用工廠方法設計模式,將物件的建立工作交給類自己去完成,該工廠方法可以被多型地呼叫,從而建立恰當型別的物件,這種方式可以稱之為註冊工廠
實際上,這就是將物件的建立與物件的使用進行了解耦,註冊工廠同意了物件的建立方法,使用者不需要知道物件是如何被建立的,只要呼叫恰當的型別,使用特定的物件就可以了。

// 定義“建立”功能的工廠
public interface Factory<T> {
    T create();
}
class Part {
    public String toString() {
        return getClass().getSimpleName();
    }
    // 存放所有的工廠
    static List<Factory<? extends Part>> partFactories = new ArrayList<>();
    // 在靜態程式碼塊中新增物件工廠
    static {
        // Collections.addAll() gives an "unchecked generic
        // array creation ... for varargs parameter" warning.
        partFactories.add(new FuelFilter.Factory());
        partFactories.add(new AirFilter.Factory());
        partFactories.add(new CabinAirFilter.Factory());
        partFactories.add(new OilFilter.Factory());
        partFactories.add(new FanBelt.Factory());
        partFactories.add(new PowerSteeringBelt.Factory());
        partFactories.add(new GeneratorBelt.Factory());
    }
    private static Random rand = new Random(47);
    public static Part createRandom() {
        int n = rand.nextInt(partFactories.size());
        return partFactories.get(n).create();
    }
}

class Filter extends Part {}
class FuelFilter extends Filter {
    // Create a Class Factory for each specific type:其實就是讓物件繼承對應的工廠
    public static class Factory implements typeinfo.factory.Factory<FuelFilter> {
        public FuelFilter create() {
            return new FuelFilter();
        }
    }
}
class AirFilter extends Filter {
    public static class Factory typeinfo.factory.Factory<AirFilter> {
        public AirFilter create() {
            return new AirFilter();
        }
    }
}
class CabinAirFilter extends Filter {
    public static class Factory implements typeinfo.factory.Factory<CabinAirFilter> {
        public CabinAirFilter create() {
            return new CabinAirFilter();
        }
    }
}
class OilFilter extends Filter {
    public static class Factory
            implements typeinfo.factory.Factory<OilFilter> {
        public OilFilter create() {
            return new OilFilter();
        }
    }
}

class Belt extends Part {}
class FanBelt extends Belt {
    public static class Factory implements typeinfo.factory.Factory<FanBelt> {
        public FanBelt create() {
            return new FanBelt();
        }
    }
}
class GeneratorBelt extends Belt {
    public static class Factory
            implements typeinfo.factory.Factory<GeneratorBelt> {
        public GeneratorBelt create() {
            return new GeneratorBelt();
        }
    }
}
class PowerSteeringBelt extends Belt {
    public static class Factory implements typeinfo.factory.Factory<PowerSteeringBelt> {
        public PowerSteeringBelt create() {
            return new PowerSteeringBelt();
        }
    }
}

public class RegisteredFactories {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++)
            System.out.println(Part.createRandom());
    }
}

14.5 instanceOf 與 Class 的等價性

在查詢型別資訊時,以 instanceOf 關鍵字或 isInstance() 方法的形式,與直接比較 Class 物件有一個很重要的差別:instanceOf 這類比較保持了型別的概念(指的是“你是否是這個類或者這個類的派生類);而使用==或者 equals() 方法比較的是實際的物件,不會去考慮繼承關係。

14.6 反射:執行時的類資訊

使用 RTTI 機制有一個限制:這個型別在編譯的時候必須是已知的,這樣才能使用 RTTI 識別它,並利用這些資訊做一些相應的事。
因此,在使用整合程開發環境(IDE)或者遠端方法呼叫(RMI)時,這個限制就有問題了,因此 Class 類與 java.lang.reflect 類庫一起對反射概念提供了支援,該類庫包含 FieldMethod 以及 Constructor 類(每個類都實現了 Member介面)。這些型別的物件是由 JVM 在執行時建立的,用以表示未知類裡對應的成員。

  • 這樣就可以使用 Constructor 建立新的物件;
  • get()set() 方法讀取和修改與 Field 物件關聯的欄位;
  • invoke() 方法呼叫與 Method 物件關聯的方法;
  • 另外還可以呼叫 getFields()getMethods()getConstructors() 等方法,以返回表示欄位、方法以及構造器的物件的陣列。

經歷了以上的過程,匿名物件的類資訊就能在執行時被完全確定下來,在編譯時即使不知道任何事,無法使用 RTTI 機制,也可以利用反射達成相同的效果。
由此看來,存在於 RTTI 與反射之間正正的區別只有:

  • 對 RTTI 來說,編譯器在編譯時開啟和檢查. class 檔案(就是使用 “普通” 的方式呼叫物件的所有方法)。
  • 對反射機制來說,.class 無法在編譯時獲取,那麼就只能在執行時使用 JVM 開啟和檢查. class 檔案。

一般情況下都不需要使用反射工具,在 Java 中反射是用來支援物件序列化以及 JavaBean 等特性的,但是如果能動態地提取某個類的資訊還是很有用的。反射機制提供了足夠的支援,使得能夠建立一個在編譯期完全未知的物件,並呼叫此方法

14.7 動態代理

代理是基本的設計模式之一,它提供額外的或不同的操作,而插入的用來代替“實際”物件(被代理者)的物件。這些操作通常涉及到“實際”物件的通訊,因此代理通常充當著中間人的角色。下面是代理模式的案例:

public interface ISubject {
      void action();
}

public class ConcreteSubject implements ISubject {
      private static final Logger LOG = LoggerFactory.getLogger(ConcreteSubject.class);
      @Override
      public void action() {
            LOG.info("ConcreteSubject action()");
      }
}

public class ProxySubject implements ISubject {
      private static final Logger LOG = LoggerFactory.getLogger(ProxySubject.class);
      private ISubject subject;
      public ProxySubject() {
            subject = new ConcreteSubject();
      }
      @Override
      public void action() {
            preAction();
            if((new Random()).nextBoolean()){
                  subject.action();
            } else {
                  LOG.info("Permission denied");
            }
            postAction();
      }
      private void preAction() {
            LOG.info("ProxySubject.preAction()");
      }
      private void postAction() {
            LOG.info("ProxySubject.postAction()");
      }
}
// 呼叫方
public class StaticProxyClient {
      public static void main(String[] args) {
            ISubject subject = new ProxySubject();
            subject.action();
      }
}

可以看出,ProxySubject 已經被插入在客戶端和 ConcreteSubject 之間,因此實際執行操作的是 ProxySubject,然後代理者再呼叫 ConcreteSubject 內的方法。
在任何時刻,只要你想要將額外的操作從“實際”物件中分離到不同的地方,特別是在希望可以很容易作出修改,從沒有使用額外操作轉而使用代理者的操作,或是反過來,那麼代理模式就十分有用。

設計模式的關鍵就是封裝修改,因此需要修改事務以證明這種模式的正確性。

動態代理模式要更進一步,它可以動態的建立代理並動態的處理所有代理方法的呼叫。在動態代理上所做的所有呼叫,都會被重定向到單一的呼叫處理器上,它的工作是揭示呼叫的型別,並確定相應的策略。下面是例子:

public interface ISubject {
      void action();
}

public class ConcreteSubject implements ISubject