【Java程式設計思想】14.型別資訊
執行時型別資訊使得你可以在程式執行時發現和使用型別資訊。
Java 中在執行時識別物件和類的資訊的方式有兩種:
- “傳統的” RTTI,它假定在編譯時我們就已經知道所有型別。
- 反射機制,允許我們在執行時發現和使用類的資訊。
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
物件。這期間的準備工作實際包含以下三步:
- 載入,這是由類載入器執行的。該步驟將查詢位元組碼(通常在 classpath 所指定的路徑中查詢,但是並非必需),並從這些位元組碼中建立一個
Class
物件。 - 連結,在連結階段將驗證類中的位元組碼,為靜態域分配記憶體空間,並且如果必需的 haul,將解析這個類建立的對其他類的所有引用。
- 初始化,如果該類具有超類,則對其初始化,執行靜態初始化器和靜態初始化塊。
實際執行中,初始化這一步會被延遲到對靜態方法(要記住構造器也是隱式靜態的)或者非常數靜態域進行首次引用的時候才會執行。初始化會盡可能的“惰性”/“懶漢”。
僅使用.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 形式包括:
- 傳統的型別轉換,如 “(Shape)”,由 RTTI 確保型別轉換的正確性。這裡執行錯誤的型別轉換,會丟擲一個
ClassCastException
異常。 - 代表物件的型別的 Class 物件。通過查詢 Class 物件可以獲取執行時所需的資訊。
instanceOf
關鍵字,根據物件是不是某個特定型別的例項,返回一個布林值,基本以提問的方式使用(就是 if)。
針對 instanceOf
關鍵字的使用有比較嚴格的限制:只可將其與命名型別進行比較,而不能與 Class 物件作比較。
在呼叫
newInstance()
時,可能會產生兩種異常,分別是InstantiationException
和IllegalAccessException
,分別代表例項化過程中的異常,以及違反 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
類庫一起對反射概念提供了支援,該類庫包含 Field
,Method
以及 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