1. RTTI(Runtime Type Identification)運行階段類型識別
# 為了確定基底類別指標實際指向的子類別的特定類型。 ——《C Primer Plus》
透過類型轉換運算子回答「是否可以安全地將物件的位址賦給特定類型的指標」這樣的問題。 ——《C Primer Plus》
在Java中,所有的型別轉換都是在執行時進行正確性檢查的。這也是RTTI的含義:在運行時,識別一個物件的類型。
#多態中表現的型別轉換是RTTI最基本的使用形式,但這種轉換並不徹底。如數組容器實際上將所有元素當作Object持有,取用時再自動將結果轉換回宣告類型。而陣列在填入(持有)物件時,具體類型可能是宣告類型的子類別#,這樣放到陣列裡就會向上轉型為宣告類型,持有的物件就遺失了具體類型。而取用時將由Object只轉型回宣告類型,並不是特定的子類別類型,所以這種轉型並不徹底。
多態中表現了具體類型的行為,但那隻是「多態機制」的事情,是由引用所指向的具體#物件而決定的,並不等價於在運行時識別#具體類型。
以上揭示了一個問題就是具體類型資訊的遺失!#有了問題,就要解決問題,這就是RTTI的需要 ,即在執行階段確定物件的具體類型。
以下範例證實了上述所描述的問題(具體類型資訊的遺失):
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(),证实了具体类型信息的丢失!! } }
要能夠在運行時識別具體類型,說明必然有東西在運行時保存了具體類型信息,這個東西就是Class物件,一種特殊物件。即Class物件表示了執行時間的類型訊息,它包含了與類別相關的資訊。
事實上Class物件就是用來建立類別的所有的「常規」物件的。
每個類別都有一個Class物件。換言之,每當編寫並且編譯了一個新類,就會產生一個Class物件(更恰當地說,是被保存在一個同名的.class檔案中)。
也就是說,Class物件在.java檔案編譯成.class檔案時就生成了,並且就儲存#在這個.class檔案中。
運行程式的JVM使用所謂的「類別載入器」的子系統(class loader subsystem)透過載入Class物件(或說.class檔案)來產生一個類別的物件。
所有的類別都是在對其第一次使用時,動態載入到JVM中的。當程式第一次使用類別的靜態成員時,就會載入這個類,這表示建構器也是靜態方法,即使建構器前面沒加static關鍵字。
因此,Java程式在它開始運行之前並非完全加載,其各個部分是在必須時才加載的。 (C 這種靜態載入語言是很難做到的。)
首先檢查一個類別的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对象的引用。
通过Class.forName()
,就是一个便捷途径,这种方式不需要为了获得Class对象引用而持有该类型的对象。(即没有创建过或没有这个类型的对象的时候就可以获得Class对象引用。)
如果已经有一个类型的对象,那就可以通过调用这个对象的getClass()
方法来获取它的Class对象引用了。这个方法属于Object,返回表示该对象的实际类型的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()); } }
.class
是获取Class对象引用的另一种方法。如 FancyToy.class
。建议使用这种方法。编译时就会受到检查(因此不需要放到try语句块中),所以既简单又安全。根除了对forName()
的调用,所以也更高效。
类字面常量.class
不仅适用于普通的类,也适用于接口、数组和基本类型。
基本类型的包装器类有一个标准字段TYPE
,它是一个引用,指向对应的基本数据类型的Class引用,即有boolean.class
等价于 Boolean.TYPE
,int.class
等价于 Integer.TYPE
…
注意,使用.class
来创建Class对象的引用时,不会自动地初始化该Class对象。
加载,这是由类加载器执行的。该步骤将查找字节码(通常在CLASSPATH所指定的路径中查找),并从这些字节码中创建一个Class对象。
链接。在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用。
初始化。如果该类具有超类,则对其初始化,执行静态初始化器和静态初始块。
初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常数静态域进行首次引用时才执行,即初始化有效地实现了尽可能 的“惰性”。
以下程序证实了上述观点。注意,将一个域设置为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); // 已初始化过 } }
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对象 } }
通配符?
是Java泛型的一部分,?
表示“任何事物”。以下程序中Class6b3d0130bba23ae47fe2b8e8cddf0195 intClass = int.class;
与 Class intClass = int.class;
是等价的,但使用Class6b3d0130bba23ae47fe2b8e8cddf0195
优于使用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; } }
将通配符与extends关键字相结合如Classa2b037db85f4e1df0e812b9647ac55a8
,就创建了一个范围,使得这个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
对象
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(); } }
RTTI形式包括:
传统类型转换,如(Shape)
代表对象的类型的Class对象
每三种形式,就是关键字 instanceof
。它返回一个布尔值,告诉我们对象是不是某个特定类型或其子类。如if(x instanceof Dog)
语句会检查对象x
是否从属于Dog
类。
还一种形式是动态的instanceof:Class.isInstance()
方法提供了一种动态地测试对象的途径。Class.isInstance()
方法使我们不再需要instanceof
表达式。
Class.isAssignableFrom()
:调用类型可以被参数类型赋值,即判断传递进来的参数是否属于调用类型继承结构(是调用类型或调用类型的子类)。
instanceof
和 isInstance()
保持了类型的概念,它指的是“你是这个类吗,或者你是这个类的派生类吗?”
==
和 equals()
没有考虑继承——它要么是这个确切的类型,要么不是。
Class
类与 java.lang.reflect
类库一起对反射的概念进行了支持。
RTTI与反射的真正区别在于:
对于RTTI来说,是编译时打开和检查.class文件。(换句话说,我们可以用“普通”方式调用对象的所有方法。)
对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件。
Java的动态代理比代理的思想更向前迈进了一步,因为它可以动态地创建代理并动态地处理对所代理方法的调用。
在动态代理上所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的对策。
通过调用静态方法Proxy.newProxyInstance()
可以创建动态代理,需要三个参数:
ClassLoader loader
一个类加载器,通常可以从已经被加载的对象中获取其类加载器
Class6b3d0130bba23ae47fe2b8e8cddf0195[] interfaces
一个希望代理要实现的接口列表(不是类或抽象类)
InvocationHandler h
一个调用处理器接口的实现
动态代理可以将所有调用重定向到调用处理器,因此通常会向调用处理器传递一个“实际”对象(即被代理的对象)的引用,从而使得调用处理器在执行其中介任务时,可以将请求转发(即去调用实际对象)。
优点:动态代理与静态代理相较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法(InvocationHandler.invoke
)中处理。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
美中不足:它始终无法摆脱仅支持interface
代理的桎梏,因为它的设计注定了这个遗憾。
极限编程(XP)的原则之一,YAGNI(You Aren’t Going to Need It,你永不需要它),即“做可以工作的最简单的事情”。
空对象的逻辑变体是模拟对象和桩。
通过使用反射,可以到达并调用一个类的所有方法,包括私有方法!如果知道方法名,就可以在其
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()"); } }
相关文章:
以上是Java程式設計思想學習課程(二)第14章-類型信息的詳細內容。更多資訊請關注PHP中文網其他相關文章!