運行時類型信息使得你能夠在程序運行時發現和使用類型信息。Java是如何讓咱們在運行時識別對象和類的信息得呢?程序員
主要有兩種方式:1.傳統RTTI,他假定咱們在編譯期間已經知道了全部類型;2.反射,它容許咱們在運行時發現和使用類的信息。編程
咱們來看一個例子:數組
這是一個典型的類層次結構圖,基類位於頂部,派生類向下擴展。面向對象編程中的基本目的是:讓代碼只操縱對基類(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(); } } }
結果:dom
Circle.draw()
Triangle.draw()
Square.draw()
分析:1.toString()被聲明爲abstract,以此強制繼承者覆寫該方法,而且防止對無格式的Shape的實例化。ide
2.若是某個對象出如今字符串表達式中(好比「+」,字符串對象的表達式),toString()會自動被調用,以生成該對象的String。this
3.當Shape派生類的對象放入List<Shape>的數組時會向上轉型,可是當向上轉型爲Shape時也丟失了Shape對象的具體類型。對數組而言,他們都只是Shape類的對象。spa
4.當從數組中取出元素時(這種容器把全部的事物都看成Object持有),將結果轉型會Shape。這是RTTI的最基本使用形式,由於在Java中全部的類型轉換都是在運行時進行正確性檢查的。這也是RTTI名字的含義:在運行時,識別一個對象的類型。設計
5.可是例子中RTTI轉型並不完全:Object被轉型爲Shape而不是具體的派生類型,這是由於List<Shape>提供的信息是保存的都是Shape類型。在編譯時,由容器和泛型保證這點,在運行時,由類型轉換操做來確保這點。code
6.Shape對象具體執行什麼,是由引用所指向的具體對象(派生類對象)而現實中大部分人正是但願儘量少的瞭解對象的具體類型,而只是和家族中一個通用的類打交道,如:Shape,使得代碼更易讀寫,設計更好實現,理解和改變,因此「多態」是面向對象編程的基本目標。
使用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) 斷定此 對象所表示的類或接口與指定的 參數所表示的類或接口是否相同,或是不是其超類或超接口ClassClass
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<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。