Java編程思想——第14章 類型資訊(一)
- 2019 年 11 月 7 日
- 筆記
運行時類型資訊使得你可以在程式運行時發現和使用類型資訊。Java是如何讓我們在運行時識別對象和類的資訊得呢?
主要有兩種方式:1.傳統RTTI,他假定我們在編譯期間已經知道了所有類型;2.反射,它允許我們在運行時發現和使用類的資訊。
一、為什麼需要RTTI
我們來看一個例子:
這是一個典型的類層次結構圖,基類位於頂部,派生類向下擴展。面向對象編程中的基本目的是:讓程式碼只操縱對基類(Shape)的引用。這樣,如果添加一個新類(比如從Shape派生的Rhomboid)來擴展程式就不會影響原來程式碼了。這個例子中Shape介面動態綁定了draw()方法,目的就是讓客戶端程式設計師用泛化的Shape引用來調用draw()。draw()在所有派生類里都會被覆蓋,並且由於它是被動態綁定的,所以即使是通過泛化的Shape引用來調用,也能產生正確的行為。這就是多態。
abstract class Shape { void draw() { System.out.println(this + ".draw()"); } @Override abstract public String toString(); } class Circle extends Shape { @Override public String toString() { return "Circle"; } } 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> shapes = Arrays.asList(new Circle(), new Triangle(), new Square()); for (Shape shape : shapes) { shape.draw(); } } }
結果:
Circle.draw() Triangle.draw() Square.draw()
分析:1.toString()被聲明為abstract,以此強制繼承者覆寫該方法,並且防止對無格式的Shape的實例化。
2.如果某個對象出現在字元串表達式中(比如“+”,字元串對象的表達式),toString()會自動被調用,以生成該對象的String。
3.當Shape派生類的對象放入List<Shape>的數組時會向上轉型,但是當向上轉型為Shape時也丟失了Shape對象的具體類型。對數組而言,他們都只是Shape類的對象。
4.當從數組中取出元素時(這種容器把所有的事物都當作Object持有),將結果轉型會Shape。這是RTTI的最基本使用形式,因為在Java中所有的類型轉換都是在運行時進行正確性檢查的。這也是RTTI名字的含義:在運行時,識別一個對象的類型。
5.但是例子中RTTI轉型並不徹底:Object被轉型為Shape而不是具體的派生類型,這是因為List<Shape>提供的資訊是保存的都是Shape類型。在編譯時,由容器和泛型保證這點,在運行時,由類型轉換操作來確保這點。
6.Shape對象具體執行什麼,是由引用所指向的具體對象(派生類對象)而現實中大部分人正是希望儘可能少的了解對象的具體類型,而只是和家族中一個通用的類打交道,如:Shape,使得程式碼更易讀寫,設計更好實現,理解和改變,所以“多態”是面向對象編程的基本目標。
二、Class對象
使用RTTI,可以查詢某個Shape引用所指向的對象的確切類型,然後選擇或者剔除某些特性。要理解RTTI在Java中的工作原理,首先應該知道類型資訊在運行時是如何表示的。Class對象承擔了這項工作,Class對象包含了與類有關的資訊。Class對象就是用來創建所有對象的,Java使用Class對象來執行其RTTI,Class類除了執行轉型操作外,還有擁有大量的使用RTTI的其他方式。
每當編寫並編譯一個新類,就會生成一個Class對象,被存在同名的.class文件中。運行這個程式的Java 虛擬機(JVM)使用被稱為“類載入器”的子系統,生成這個類的對象。類載入器子系統實際上可以包含以挑類載入鏈,但是只有一個原生類載入器,它是JVM實現的一部分。原生類載入器載入的通常是載入本地盤的內容,這條鏈通常不需要添加額外的類載入器,但是如果有特殊需求比如:支援Web伺服器應用,那麼就要掛接額外的類載入器。
所有的類在被第一次使用時,動態載入到JVM中。當第一個對類的靜態成員的引用被創建時,就會載入這個類。這也證明構造器是靜態方法,使用new操作符創建類的新對象也會被當作對類的靜態成員的引用。因此,Java程式在開始運行之前並未完全載入,各個部分在必須時才會載入。類載入器首先檢查這個類的Class對象是否已載入,如果尚未載入,默認的類載入器就會根據類名查找.class文件。
一旦某個類的Class對象被載入入記憶體,它就被用來創建這個類的所有對象:
class Candy { static { System.out.println("Loading Candy"); } } class Gum { Gum() { System.out.println("Constructed Gum"); } static { System.out.println("Loading Gum"); } } public class SweetShop { public static void main(String[] args) { System.out.println("inside main"); new Candy(); try { Class.forName("Gum"); } catch (ClassNotFoundException e) { e.printStackTrace(); } new Gum(); new Candy(); } }
結果:
Inside main
Loading Candy
Loading Gum
Constructed Gum
Class.forName()會讓類初始化:每個類都有一個static子句,該子句在類被第一次載入時執行,注意第二次載入時候就不走了。
Class.forName(“”);這個方法是Class類的一個static成員。Class對象就和其他對象一樣 我們可以獲取並操作它的引用(這也就是類載入器的工作)。forName()是取得Class對象引用的一種方法,Class clazz = Class.forName(“xxx”); xxx必須是帶包名的全名!!! 如果你已經得到了一個類的引用xxx,還可以通過:Class clazz = xxx.getClass();方式獲取class對象。Class包含很多方法 其中比較常用的有:
public T newInstance() 得到一個實例 相當於new
public native boolean isInstance(Object obj) 是否是參數類的一個實例
public native boolean isAssignableFrom(Class<?> cls) 判定此Class
對象所表示的類或介面與指定的Class
參數所表示的類或介面是否相同,或是否是其超類或超介面
public native boolean isInterface();判斷是否是介面
public native boolean isArray();判斷是否是數組
public native boolean isPrimitive();判斷是否是基本類型
public boolean isAnnotation() 判斷是否是註解
public boolean isSynthetic() 判斷是否是同步程式碼塊
public String getName() 返回帶包的全名,包含類的類型資訊(是引用類型 還是各種基本類型)
public ClassLoader getClassLoader() 獲取該類的類載入器。
public TypeVariable<Class<T>>[] getTypeParameters() 獲取泛型
public native Class<? super T> getSuperclass(); 獲取父類和父類泛型
public Package getPackage() 獲取包名
public Class<?>[] getInterfaces() 獲取該類實現的介面列表
public Type[] getGenericInterfaces() 獲取實現的介面列表及泛型
public native Class<?> getComponentType() 返回表示數組組件類型的 Class。如果此類不表示數組類,則此方法返回 null。如果此類是數組,則返回表示此類組件類型的 Class
public native int getModifiers() 返回類的修飾符
public native Object[] getSigners();// 我目前還不知道是幹什麼的如果有明確用處請評論告訴我 萬分感謝
native void setSigners(Object[] signers)
public Method getEnclosingMethod() 獲取局部或匿名內部類在定義時所在的方法
public String getSimpleName() 獲取最簡單的那個類名
public String getCanonicalName() 帶有包名的類名
public String getTypeName() 如果是數組的話[類名] 否則和getSimpleName一樣
public Field[] getFields() 獲取類中public的欄位
public Field[] getDeclaredFields() 獲取類中所有欄位
public Class<?>[] getClasses() 獲取該類以及父類所有的public的內部類
public Class<?>[] getDeclaredClasses() 獲取該類所有內部類,不包含父類的
public Method[] getMethods() 獲取該類及父類所有的public的方法
public Method[] getDeclaredMethods() 獲取該類中所有方法 ,不包含父類
public Constructor<?>[] getConstructors() 獲取所有public的構造方法
public Constructor<?>[] getDeclaredConstructors() 獲取該類所有構造方法
public Field getField(String name) 根據欄位名public獲取欄位
public Field getDeclaredField(String name) 根據欄位名所有獲取欄位
public Method getMethod(String name, Class<?>... parameterTypes) 根據方法名和參數獲取public方法
public Method getDeclaredMethod(String name, Class<?>... parameterTypes) 根據方法名和參數獲取所有方法
public Constructor<T> getConstructor(Class<?>... parameterTypes) 根據參數獲取public構造方法
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 根據參數獲取所有構造反方法、
... 如遺漏重要方法 請提醒 謝謝~
類的字面常量
Java還有另一種方式生成對Class的引用,即使用字面常量:ClassDemo.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 = Double.TYPE void.class = Void.TYPE
使用這種方式創建對象的引用時,不會自動初始化該Class對象,為了使用類而做的準備工作實際包含三個步驟:
1.載入,由類載入器進行。該步驟將查找位元組碼,並從這些位元組碼中創建一個Class對象。
2.連接,在連接階段將驗證類中的位元組碼,為靜態域分配存空間,如果有需要,將解析這個類創建的對其他類的所有引用。
3.初始化,如果該類具有父類,則對其初始化,執行靜態初始化器和靜態初始化塊。
初始化在對靜態方法(構造器是隱式地靜態)或非常數靜態域進行首次引用時才會執行:
class InitableA { static final int staticFinalA = 47; static final int staticFinalB = ClassInitialization.random.nextInt(1000); static { System.out.println("Initializing InitableA"); } } class InitableB { static int staticNoFinal = 147; static { System.out.println("Initializing InitableB"); } } public class ClassInitialization { public static Random random = new Random(47); public static void main(String[] args) { System.out.println(InitableA.staticFinalA); System.out.println(InitableA.staticFinalB); System.out.println(InitableB.staticNoFinal); } }
結果:
1 Initializing InitableA 258 Initializing InitableB 147
如果一個static final 是”編譯期常量” ,就像 staticFinalA 那麼需要對它的類進行初始化就可以讀取,但是 staticFinalB 卻會使類初始化,因為它不是編譯期常量;只有static修飾符的 staticNoFinal 在對它讀取之前,要先進行鏈接,為這個域分配存儲空間並且初始化該存儲空間。
泛型化的Class引用
通過使用泛型語法,可以讓編譯器強制執行額外的類型檢查:Class<Integer> intClass= int.class; 如果想放鬆限制,可以使用通配符”?”:Class<?> anyClass = AnyClass.class;如果想限定某類型的子類可以使用extends 關鍵字:Class<? extends Number> numCLass = int.class; numClass = double.class;
當使用泛型語法Class<Toy>用於創建Class對象時,newInstance()將返回該對象得確切資訊而不只是Object。
如果你手頭是Toy的父類ToyFather,toyClass.getSuperclass()方法卻只能返回 Class<? super Toy> 而不是確切的ToyFather ,並且在newInstance()時返回Object。