Java程式設計思想(二)第14章-型別資訊
目錄:
1. RTTI(Runtime Type Identification)執行階段型別識別
1.1 用途:
為了確定基類指標實際指向的子類的具體型別。——《C++ Primer Plus》
1.2 工作原理:
通過型別轉換運算子回答“是否可以安全地將物件的地址賦給特定型別的指標”這樣的問題。——《C++ Primer Plus》
1.3 Java中
在Java中,所有的型別轉換都是在執行時進行正確性檢查的。這也是RTTI的含義:在執行時,識別一個物件的型別。
1.3.1 丟失具體型別資訊的問題
- 多型中表現的型別轉換是RTTI最基本的使用形式,但這種轉換並不徹底
- 多型中表現了具體型別的行為,但那只是“多型機制”的事情,是由引用所指向的具體物件而決定的,並不等價於在執行時識別具體型別。 以上揭示了一個問題就是具體型別資訊的丟失!有了問題,就要解決問題,這就是RTTI的需要,即在執行時確定物件的具體型別。
1.3.2 證實具體型別資訊的丟失
以下示例證實了上面描述的問題(具體型別資訊的丟失):
package net.mrliuli.rtti; import java.util.Arrays; import java.util.List; /** * Created by leon on 2017/12/3. */ abstract class Shape{ void draw(){ System.out.println(this + ".draw()"); } abstract public String toString(); //要求子類需要實現 toString() } class Circle extends Shape{ @Override public String toString() { return "Circle"; } public void drawCircle(){} } 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> shapeList = Arrays.asList( new Circle(), new Square(), new Triangle() // 向上轉型為 Shape,此處會丟失原來的具體型別資訊!!對於陣列而言,它們只是Shape類物件! ); for(Shape shape : shapeList){ shape.draw(); // 陣列實際上將所有事物都當作Object持有,在取用時會自動將結果轉型回聲明型別即Shape。 } //shapeList.get(0).drawCircle(); //這裡會編譯錯誤:在Shape類中找不到符號drawCircle(),證實了具體型別資訊的丟失!! } }
2 Class物件
2.1 RTTI在Java中的工作原理
要能夠在執行時識別具體型別,說明必然有東西在執行時儲存了具體型別資訊,這個東西就是Class物件,一種特殊物件。即Class物件表示了執行時的型別資訊,它包含了與類有關的資訊。
- 事實上Class物件就是用來建立類的所有的“常規”物件的。
- 每個類都有一個Class物件。換言之,每當編寫並且編譯了一個新類,就會產生一個Class物件(更恰當地說,是被儲存在一個同名的.class檔案中)。
- 也就是說,Class物件在.java檔案編譯成.class檔案時就生成了,且就儲存在這個.class檔案中。
2.2 Class物件用來生成物件(常規物件,非Class物件)
執行程式的JVM使用所謂的“類載入器”的子系統(class loader subsystem)通過載入Class物件(或者說.class檔案)來生成一個類的物件。
- 所有的類都是在對其第一次使用時,動態載入到JVM中的。當程式第一次使用類的靜態成員時,就會載入這個類,這說明構造器也是靜態方法,即使構造器前面沒加static關鍵字。
- 因此,Java程式在它開始執行之前並非被完全載入,其各個部分是在必須時才被載入的。(C++這種靜態載入語言是很難做到的。)
2.3 類載入器的工作(過程)
- 首先檢查一個類的Class物件(或理解.class檔案)是否已被載入;
- 如果尚未載入,預設的類載入器就會根據類名查詢.class檔案;
- 一旦Class物件(.class檔案)被載入了(載入記憶體),它就被用來建立這個類的所有物件。
以下程式證實上一點。
package net.mrliuli.rtti;
/**
* Created by leon on 2017/12/3.
*/
class Candy{
static { System.out.println("Loading Candy"); }
}
class Gum{
static { System.out.println("Loading Gum"); }
}
class Cookie{
static { System.out.println("Loading Cookie"); }
}
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("net.mrliuli.rtti.Gum");
}catch (ClassNotFoundException e){
System.out.println("Couldn't find Gum");
}
System.out.println("After Class.forName(\"Gum\")");
new Cookie();
System.out.println("After creating Cookie");
}
}
- 以上程式每個類都有一個static子句,static子句在類第一次被載入時執行。
- 從輸出中可以看出,
- Class物件僅在需要時才被載入,
- static初始化是在類載入時進行的。
Class.forName(net.mrliuli.rtti.Gum)
是Class類的一個靜態成員,用來返回一個Class物件的引用(Class物件和其他物件一樣,我們可以獲取並操作它的引用(這也就是類載入器的工作))。使用這個方法時,如果net.mrliuli.rtti.Gum
還沒有被載入就載入它。在載入過程中,Gum的static子句被執行。
總之,無論何時,只要你想在執行時使用型別資訊,就必須首先獲得對恰當的Class物件的引用。
2.4 獲得Class物件引用的方法
- 通過
Class.forName()
,就是一個便捷途徑,這種方式不需要為了獲得Class物件引用而持有該型別的物件。(即沒有建立過或沒有這個型別的物件的時候就可以獲得Class物件引用。) - 如果已經有一個型別的物件,那就可以通過呼叫這個物件的
getClass()
方法來獲取它的Class物件引用了。這個方法屬於Object,返回表示該物件的實際型別的Class物件引用。
2.5 Class包含的有用的方法
以下程式展示Class包含的很多有用的方法:
getName()
獲取類的全限定名稱getSimpleName()
獲取不含包名的類名getCanonicalName()
獲取全限定的類名isInterface()
判斷某個Class物件是否是介面getInterfaces()
返回Class物件實現的介面陣列getSuperClass()
返回Class物件的直接基類newInstance()
建立一個這個Class物件所代表的類的一個例項物件。- Class引用在編譯期不具備任何更進一步的型別資訊,所以它返回的只是一個Object引用,但是這個Object引用指向的是這個Class引用所代表的具體型別。即需要轉型到具體型別才能給它傳送Object以外的訊息
newInstance()
這個方法依賴於Class物件所代表的類必須具有可訪問的預設的建構函式(Nullary constructor,即無參的構造器),否則會丟擲InstantiationException
或IllegalAccessException
異常
package net.mrliuli.rtti;
/**
* Created by li.liu on 2017/12/4.
*/
interface HasBatteries{}
interface Waterproof{}
interface Shoots{}
class Toy{
Toy(){}
Toy(int i){}
}
class FancyToy extends Toy implements HasBatteries, Waterproof, Shoots{
FancyToy(){ super(1); }
}
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("net.mrliuli.rtti.FancyToy");
}catch (ClassNotFoundException e){
System.out.println("Can't find FancyToy");
System.exit(1);
}
printInfo(c);
System.out.println("=============================");
for(Class face : c.getInterfaces()){
printInfo(face);
}
System.out.println("=============================");
Class up = c.getSuperclass();
Object obj = null;
try{
// Requires default constructor:
obj = up.newInstance();
}catch (InstantiationException e){
System.out.println("Cannot instantiate");
System.exit(1);
}catch (IllegalAccessException e){
System.out.println("Cannot access");
System.exit(1);
}
printInfo(obj.getClass());
}
}
2.6 類字面常量
2.6.1 使用類字面常量.class
是獲取Class物件引用的另一種方法。如 FancyToy.class
。建議使用這種方法。
- 編譯時就會受到檢查(因此不需要放到try語句塊中),所以既簡單又安全。根除了對
forName()
的呼叫,所以也更高效。 - 類字面常量
.class
不僅適用於普通的類,也適用於介面、陣列和基本型別。 - 基本型別的包裝器類有一個標準欄位
TYPE
,它是一個引用,指向對應的基本資料型別的Class引用,即有boolean.class
等價於Boolean.TYPE
,int.class
等價於Integer.TYPE
… - 注意,使用
.class
來建立Class物件的引用時,不會自動地初始化該Class物件。
2.6.2 為了使用類而做的準備工作實際包含三個步驟:
- 載入,這是由類載入器執行的。該步驟將查詢位元組碼(通常在CLASSPATH所指定的路徑中查詢),並從這些位元組碼中建立一個Class物件。
- 連結。在連結階段將驗證類中的位元組碼,為靜態域分配儲存空間,並且如果必需的話,將解析這個類建立的對其他類的所有引用。
- 初始化。如果該類具有超類,則對其初始化,執行靜態初始化器和靜態初始塊。
2.6.3 初始化惰性
初始化被延遲到了對靜態方法(構造器隱式地是靜態的)或者非常數靜態域進行首次引用時才執行,即初始化有效地實現了儘可能 的“惰性”。
以下程式證實了上述觀點。注意,將一個域設定為static
和 final
的,不足以成為“編譯期常量”或“常數靜態域”,如 static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);
就不是編譯期常量,對它的引用將強制進行類的初始化。
package net.mrliuli.rtti;
import java.util.Random;
class Initable{
static final int staticFinal = 47; // 常數靜態域
static final int staticFinal2 = 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 initalbe = Initable.class; // 使用類字面常量.class獲取Class物件引用,不會初始化
System.out.println("After creating Initable ref");
System.out.println(Initable.staticFinal); // 常數靜態域首次引用,不會初始化
System.out.println(Initable.staticFinal2); // 非常數靜態域首次引用,會初始化
System.out.println(Initable2.staticNonFinal); // 非常數靜態域首次引用,會初始化
Class initable3 = Class.forName("net.mrliuli.rtti.Initable3"); // 使用Class.forName()獲取Class物件引用,會初始化
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal); // 已初始化過
}
}
2.7 泛化的Class引用
2.7.1 Class物件型別限制
Class引用總是指向某個Class物件,此時,這個Class物件可以是各種型別的,當使用泛型語法對Class引用所指向的Class物件的型別進行限定時,這就使得Class物件的型別變得具體,這樣編譯器編譯時也會做一些額外的型別檢查工作。如
package net.mrliuli.rtti;
public class GenericClassReferences {
public static void main(String[] args){
Class intClass = int.class;
Class<Integer> genericIntClass = int.class;
genericIntClass = Integer.class; // Same thing
intClass = double.class;
// genericIntClass = double.class; // Illegal, genericIntClass 限制為Integer 的Class物件
}
}
2.7.2 使用萬用字元?放鬆對Class物件型別的限制
萬用字元?
是Java泛型的一部分,?
表示“任何事物”。以下程式中Class<?> intClass = int.class;
與 Class intClass = int.class;
是等價的,但使用Class<?>
優於使用Class
,因為它說明了你是明確要使用一個非具體的類引用,才選擇了一個非具體的版本,而不是由於你的疏忽。
package net.mrliuli.rtti;
/**
* Created by li.liu on 2017/12/4.
*/
public class WildcardClassReferences {
public static void main(String[] args){
Class<?> intClass = int.class;
intClass = double.class;
}
}
2.7.3 類型範圍
將萬用字元與extends關鍵字相結合如Class<? extends Number>
,就建立了一個範圍,使得這個Class引用被限定為Number
型別或其子型別
。
package net.mrliuli.rtti;
/**
* Created by li.liu on 2017/12/4.
*/
public class BoundedClassReferences {
public static void main(String[] args){
Class<? extends Number> bounded = int.class;
bounded = double.class;
bounded = Number.class;
// Or anything derived from Number
}
}
泛型類語法示例:
package net.mrliuli.rtti;
import java.util.ArrayList;
import java.util.List;
/**
* Created by li.liu on 2017/12/4.
*/
class CountedInteger{
private static long counter;
private final long id = counter++;
public String toString(){
return Long.toString(id);
}
}
public class FilledList<T> {
private Class<T> type;
public FilledList(Class<T> type){
this.type = type;
}
public List<T> create(int nElements){
List<T> result = new ArrayList<T>();
try{
for(int i = 0; i < nElements; i++){
result.add(type.newInstance());
}
}catch(Exception e){
throw new RuntimeException(e);
}
return result;
}
public static void main(String[] args){
FilledList<CountedInteger> fl = new FilledList<CountedInteger>(CountedInteger.class); // 儲存一個類引用
System.out.println(fl.create(15)); // 產生一個list
}
}
總結,使用泛型類後
- 使得編譯期進行型別檢查
.newInstance()
將返回確切型別的物件,而不是Object
物件
2.7.4 Class
package net.mrliuli.rtti;
public class GenericToyTest {
public static void main(String[] args) throws Exception{
Class<FancyToy> ftClass = FancyToy.class;
// Produces exact type:
FancyToy fancyToy = ftClass.newInstance();
Class<? super FancyToy> up = ftClass.getSuperclass(); //
// This won't compile:
// Toy toy = up.newInstance();
// Class<Toy> up2 = up.getSuperclass(); // 這裡 getSuperclass() 已經知道結果是Toy.class了,卻不能賦給 Class<Toy>,這就是所謂的含糊性(vagueness)
// Only produces Object: (because of the vagueness)
Object obj = up.newInstance();
}
}
2.7.5 型別轉換前先做檢查
RTTI形式包括:
- 傳統型別轉換,如
(Shape)
- 代表物件的型別的Class物件
- 每三種形式,就是關鍵字
instanceof
。它返回一個布林值,告訴我們物件是不是某個特定型別或其子類。如if(x instanceof Dog)
語句會檢查物件x
是否從屬於Dog
類。 - 還一種形式是動態的instanceof:
Class.isInstance()
方法提供了一種動態地測試物件的途徑。Class.isInstance()
方法使我們不再需要instanceof
表示式。
2.7.6 isAssignableFrom()
Class.isAssignableFrom()
:呼叫型別可以被引數型別賦值,即判斷傳遞進來的引數是否屬於呼叫型別繼承結構(是呼叫型別或呼叫型別的子類)。
3 註冊工廠
4 instanceof 與 Class 的等價性
instanceof
和isInstance()
保持了型別的概念,它指的是“你是這個類嗎,或者你是這個類的派生類嗎?”==
和equals()
沒有考慮繼承——它要麼是這個確切的型別,要麼不是。
5 反射:執行時的類資訊(Reflection: runtime class information)
Class
類與 java.lang.reflect
類庫一起對反射的概念進行了支援。
RTTI與反射的真正區別在於:
- 對於RTTI來說,是編譯時開啟和檢查.class檔案。(換句話說,我們可以用“普通”方式呼叫物件的所有方法。)
- 對於反射機制來說,.class檔案在編譯時是不可獲取的,所以是在執行時開啟和檢查.class檔案。
6 動態代理
- Java的動態代理比代理的思想更向前邁進了一步,因為它可以動態地建立代理並動態地處理對所代理方法的呼叫。
- 在動態代理上所做的所有呼叫都會被重定向到單一的呼叫處理器上,它的工作是揭示呼叫的型別並確定相應的對策。
- 通過呼叫靜態方法
Proxy.newProxyInstance()
可以建立動態代理,需要三個引數:ClassLoader loader
一個類載入器,通常可以從已經被載入的物件中獲取其類載入器Class<?>[] interfaces
一個希望代理要實現的介面列表(不是類或抽象類)InvocationHandler h
一個呼叫處理器介面的實現
- 動態代理可以將所有呼叫重定向到呼叫處理器,因此通常會向呼叫處理器傳遞一個“實際”物件(即被代理的物件)的引用,從而使得呼叫處理器在執行其中介任務時,可以將請求轉發(即去呼叫實際物件)。
6.1 動態代理的優點及美中不足
- 優點:動態代理與靜態代理相較,最大的好處是介面中宣告的所有方法都被轉移到呼叫處理器一個集中的方法(
InvocationHandler.invoke
)中處理。這樣,在介面方法數量比較多的時候,我們可以進行靈活處理,而不需要像靜態代理那樣每一個方法進行中轉。 - 美中不足:它始終無法擺脫僅支援
interface
代理的桎梏,因為它的設計註定了這個遺憾。
7. 空物件
7.1 YAGNI
極限程式設計(XP)的原則之一,YAGNI(You Aren’t Going to Need It,你永不需要它),即“做可以工作的最簡單的事情”。
7.2 模擬物件與樁(Mock Objects & Stubs)
空物件的邏輯變體是模擬物件和樁。
8. 介面與型別資訊
通過使用反射,可以到達並呼叫一個類的所有方法,包括私有方法!如果知道方法名,就可以在其
Method
物件上呼叫setAccessible(true)
,然後訪問私有方法。
以下命令顯示類的所有成員,包括私有成員。-private
標誌表示所有成員都顯示。
javap -private 類名
因此任何人都可以獲取你最私有的方法的名字和簽名,即使這個類是私有內部類或是匿名內部類。
package net.mrliuli.rtti;
/**
* Created by li.liu on 2017/12/6.
*/
import java.lang.reflect.Method;
/**
* 通過反射呼叫所有方法(包括私有的)
*/
public class HiddenImplementation {
static void callHiddenMethod(Object obj, String methodName, Object[] args) throws Exception{
Method method = obj.getClass().getDeclaredMethod(methodName);
method.setAccessible(true);
method.invoke(obj, args);
}
public static void main(String[] args) throws Exception{
callHiddenMethod(new B(), "g", null);
}
}
interface A {
void f();
}
class B implements A{
@Override
public void f(){}
private void g(){
System.out.println("B.g()");
}
}