1. 程式人生 > >Java程式設計思想 第十四章:型別資訊

Java程式設計思想 第十四章:型別資訊

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

Java是如何讓我們在執行時識別物件和類的資訊的。主要有兩種方式:

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

1.為什麼需要使用RTTI

以使用了多型的類層次結構的例子舉例:

在這裡插入圖片描述

如上圖,泛型是基類Shape,而派生出來的具體類有Circle,Square和Triangle。

這是一個類層次結構圖,基類位於頂部,而派生類向下拓展。

程式碼值操縱對基類的引用,如果新增一個新類來拓展程式也不會影響原來的程式碼(可拓展性),如圖中Shape介面中動態繫結的draw()方法,目的就是使用泛化的Shape引用來呼叫draw()。draw()方法在所有派生類中都會被覆蓋,由於是draw()方法是被動態繫結的,所以通過泛化的Shape引用來呼叫,也能產生正確的行為,即多型。

通常,我們會建立一個具體物件(本例中,Circle,Square或Triangle),然後把它向上轉型為Shape(忽略物件的具體型別),並在後面的程式中使用匿名(即不知道具體型別)的Shape引用。

Shape shape = new Circle();

//Shape物件放入陣列時會向上轉型,轉型為Shape且丟失了Shape物件的具體型別
//對陣列而言,它們只是Shape類的物件
List<Shape> shapeList = Arrays.asList(new Circle(),new Square(),new Triangle());

//從陣列中取出元素時,這種容器(實際上將所有事物都當作Object持有)會自動將結果轉型為Shape。
for(Shape shape:shapeList){ shape.draw(); }

Java中,所有的型別轉換都是在執行時進行正確性檢查的,也就是RTTI(Runtime Type Information)名字的含義:在執行時,識別一個物件的型別。

通常,我們希望大部分程式碼儘可能地少了解物件的具體型別,而是隻與物件家族中的一個通用表示打交道。這樣,程式碼更容易寫,更容易讀,更便於維護,設計也更容易實現,理解和改變。所以,多型是面向物件程式設計的基本目標。

使用RTTI,可以查詢某個Shape引用所指向的物件的確切型別,然後選擇或者剔除特例。

2. Class物件

Class物件:包含了與類有關的資訊,Class物件是用來建立類的所有的“常規”物件的。Java使用Class物件來執行其RTTI。

編寫並編譯一個新類——>產生一個Class物件(被儲存在一個同名的.class檔案中)——>為了生成這個類的物件,執行這個程式的JVM將使用被稱為"類載入器"的子系統。

Java程式在它開始執行之前並非被完全載入,其各個部分是在必需時才載入的

Class物件和其他物件一樣,我們可以獲取並操作它的引用。

//這個方法是Class類(所有Class物件都屬於這個類)的一個靜態成員。
//是取得Class物件的引用的一種方法。用一個包含目標類的文字名的字串作為輸入引數,返回一個Class物件的引用。
public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

Class中包含很多有用的方法,如下:

//全限定類名
getName();
//判斷是否是介面
isInterface();
//獲取不含包名的類名
getSimpleName();
//獲取全限定的類名
getCanonicalName();
//獲取直接基類
getSuperClass();
//獲取包含的介面
getInterfaces();
//獲取例項
newInstance();

2.1 類字面常量

Java除了forName()獲取Class物件引用外,還可通過使用類字面常量

FancyToy.class;
//這樣使用簡單,安全,編譯時會受到檢查(不需要trycatch語句的使用)

類字面常量不僅可以應用於普通的類,更可以應用於介面,陣列以及基本資料結構。(對於基本資料型別的包裝器類,它有一個標準欄位TYPE。TYPE欄位是一個引用,指向對應的基本資料型別的Class物件)

//等價於,一般直接使用".class"的形式,以保持與普通類的一致性。
boolean.class ======Boolean.TYPE
void.class=========Void.TYPE
char.class=========Character.TYPE

使用".class"來建立對Class物件的引用時,不會自動初始化Class物件。

為了使用類而做的準備工作有:

  1. 載入(由類載入器執行,查詢位元組碼並從中建立一個Class物件)
  2. 連結 (驗證類中的位元組碼,為靜態域分配儲存空間)
  3. 初始化(若該類具有超類,則對其初始化,執行靜態初始化器和靜態初始化塊)

2.2 泛化的Class引用

Class引用指向某個Class物件,可製造類的例項,幷包含可作用於這些例項的所有方法程式碼,還包含該類的靜態成員。

Java SE5允許對Class引用所指向的Class物件的型別進行限定(通過泛型語法)

Class intClass = int.class;
//通過泛型語法,可讓編譯器強制執行額外的型別檢查
Class<Integer> genericIntClass = int.class;
//使用了萬用字元?(表示任何事物)
Class<?> intClass = int.class;

向Class引用新增泛型語法的原因僅僅是為了提供編譯期型別檢查。

2.3 新的轉換語法

Java SE5 中還添加了用於Class引用的轉型語法,即cast語法

class Building {}
class House extends Building {}

public class ClassCasts {
  public static void main(String[] args) {
    Building b = new House();
    Class<House> houseType = House.class;
    House h = houseType.cast(b);
    h = (House)b; // ... or just do this.
  }
} 

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

3. 型別轉換前先做檢查

3.1 RTTI的幾種表現形式

  1. 傳統的型別轉換,如"(Shape)"。由RTTI確保型別轉換的正確性,若執行了一個錯誤的型別轉換,丟擲ClassCastException異常。(Java中,此操作要執行型別檢查,有向上轉型和向下轉型兩種)
  2. 代表物件的型別的Class物件。通過查詢Class物件可以獲取執行時所需的資訊。
  3. 關鍵字instanceof,它返回一個布林值,告訴我們物件是不是某個特定型別的例項。
if(x instanceof Dog)
  ((Dog)x).bark();

3.2 動態的instanceof

Class.isInstance()方法提供了一種動態測試物件的途徑。

4. 註冊工廠

使用註冊工廠的目的是將物件的建立工作交給類去完成,即建立一個工廠方法,然後進行多型的呼叫,從而為你建立恰當型別的物件。在如下的簡單的版本中,工廠方法就是Factory介面中的Create方法。

public interface Factory<T>{T create();}

所謂工廠方法,就是意味著我只提供一個建立例項的工廠方法,而無需每建立一個繼承類就編寫一個新的方法。關於工廠方法更多細節後邊在設計模式模組專門再進行學習。

5. instanceof與Class的等價性

Instanceof保持了型別的概念,指的是“你是這個類嗎,或者你是這個類的派生類嗎”
"=="則是比較的實際的Class物件,沒有考慮繼承(要麼是這個確切的型別,要麼不是)

6. 反射:執行時的類資訊

反射概念是程式在編譯的時候並不知道具體的型別資訊,直到程式執行時通過反射才獲取到了準確的類資訊。這裡提供了幾個方法支援獲取準確的類資訊,以便建立動態的程式碼。使用Class類的getMethods()方法可以獲取這個類所包含的所有方法,使用getConstructors()方法可以獲取這個類的所有建構函式。前文也提到過,使用Class.forName()可以用來動態的載入類。它的生成結果在編譯的時候是不可知的,因此所有的方法特徵資訊和簽名都是在執行時被提取出來的。

6.1 類方法提取器

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

Clsss.forName()生成的結果在編譯期間是不可知的,因此所有的方法特徵簽名信息都是在執行時被提取出來的。

7. 動態代理

代理是常用的設計模式之一,它的作用是在基本的物件操作之外,增加一些其它額外的操作。比如我想使用一個物件,同事想了解這個物件的執行過程,那麼這個執行過程的監控顯然就不能放在基本的物件程式碼中。有點類似前面文章中提到的組合類的思想,新建一個代理類,代理類引用要使用的物件,然後增加一些新的功能。剛好前幾天跟公司一個同事面了一個新人,同事問道代理模式之後自己做了一些闡述,可以把代理模式類比成是中介,中介的目的是賣東西的基礎上賺錢,賣東西就是基本操作,賺錢就是中介也就是代理做的額外的操作。下面一個示例展示簡單的代理模式:

  1. 先定義一個介面:
public interface Interface {
	void doSomeThing();
	void somethingElse(String arg);
}
  1. 然後是這個介面的實現類,也就是前邊所說的真正要操作的執行的物件:

public class RealObject implements Interface {
 
	@Override
	public void doSomeThing() {
		// TODO Auto-generated method stub
		System.out.println("RealObject DoSomeThing");
	}
 
	@Override
	public void somethingElse(String arg) {
		// TODO Auto-generated method stub
		System.out.println("RealObject somethingElse");
	}
	
}
  1. 然後定義一個代理類,代理類實現了Interface介面,同時通過傳參的形式傳入了前邊的實現類,在完成實現類功能的基礎上,做了自己的操作:
public class SimpleProxy implements Interface{
	
	private Interface proxied;
	
	public SimpleProxy(Interface proxied) {
		// TODO Auto-generated constructor stub
		this.proxied = proxied;
	}
	@Override
	public void doSomeThing() {
		// TODO Auto-generated method stub
		System.out.println("Proxy DoSomething");
		proxied.doSomeThing();
		
	}
	@Override
	public void somethingElse(String arg) {
		// TODO Auto-generated method stub
		System.out.println("Proxy somethingElse");
		proxied.somethingElse(arg);
	}
	
}
  1. 最後是Main方法,因為consumer方法傳參是Interface介面,所以任何實現了它介面的實體類都可以當做引數。這裡演示了使用基本的實體類和使用代理的區別,代理在完成普通實體類的功能基礎上列印了自己的操作內容:

public class SimpleProxyDemo {
	public static void consumer(Interface iface){
		iface.doSomeThing();
		iface.somethingElse("bobo");
	}
	public static void main(String[] args) {
		consumer(new RealObject());
		System.out.println("==============");
		consumer(new SimpleProxy(new RealObject()));
	}
}
執行結果:
doSomething
somethingElse bonobo
SimpleProxy doSomething
doSomething
SimpleProxy somethingElse bonobo
somethingElse bonobo

8. Java動態代理

Java的動態代理比代理的思想更向前邁進了一步,因為它可以動態的建立代理,並且動態的處理對所代理方法的呼叫。動態代理所做的所有操作都會被重定向到單一的呼叫處理器上,它的工作是揭示呼叫的型別,並確定相應的對策。下面用動態代理重寫上邊的示例:

Java中要實現動態代理類,必須要繼承InvocationHandle這個類,這個類內部嵌入的物件是要被實現的真正的物件,同樣使用構造方法傳入,這個類唯一的一個方法invoke,它有三個引數,第一個引數是生成的動態代理類。這裡我個人理解,既然動態代理是動態的建立代理,那麼這個引數固然是所建立的動態代理,第二個引數是傳入的物件執行的方法,第三個引數是傳入的引數。最後將請求通過Method.invoke()方法,傳入必要的引數,執行代理物件的方法。

動態代理Handler類:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
 
public class DynamicProxyHandler implements InvocationHandler{
	
	private Object proxied;
	
	public DynamicProxyHandler(Object proxied) {
		// TODO Auto-generated constructor stub
		this.proxied = proxied;
	}
	@Override
	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("  " + arg);
			}
		}
		// TODO Auto-generated method stub
		return method.invoke(proxied, args);
	}
}

Main函式:

import java.lang.reflect.Proxy;
 
public class SimpleDynamicProxyDemo {
	public static void consumer(Interface iface){
		iface.doSomeThing();
		iface.somethingElse("DIDI");
	}
	public static void main(String[] args) {
		RealObject real = new RealObject();
		consumer(real);
		System.out.println("=========");
		Interface proxy = (Interface)Proxy.newProxyInstance(Interface.class.getClassLoader(),new Class[]{Interface.class},new DynamicProxyHandler(real));
		consumer(proxy);
	}
}

執行結果:
doSomething
somethingElse bonobo
**** proxy: class com.basic.java.$Proxy0, method: public abstract void com.basic.java.Interface.doSomething(), args: null
doSomething
**** proxy: class com.basic.java.$Proxy0, method: public abstract void com.basic.java.Interface.somethingElse(java.lang.String), args: [Ljava.lang.Object;@4b67cf4d
  bonobo
somethingElse bonobo

最近專案中用到了AOP做日誌記錄的功能,Spring的AOP核心思想就是動態代理,現在更加理解了一些,簡單表述一下就是在切入點處執行一寫其它操作,如我這裡是記錄日誌,然後執行正常的業務方法。記錄日誌就是脫離在實際業務之外的一些操作。

8. 空物件

這裡感覺不是很常用,大概理解了一下,就是當一個物件為null的時候,任何對這個物件的操作都會引發異常。為了避免這種情況,當一個物件為null的時候,我們定義一個空物件賦值給它。何謂空物件呢?就是一個不存在實際意義但是不會引發異常的物件。文中具體程式碼就不寫了。