1. RTTI (Runtime Type Identification) 実行時型識別
基底クラスポインタが実際に指すサブクラスの特定の型を決定する。 ——「C++ Primer Plus」
型変換演算子を使用して、「オブジェクトのアドレスを特定の型のポインターに代入しても安全ですか?」という質問に答えます。 ——『C++入門Plus』
Javaでは、すべての型変換が実行時に正しいかどうかチェックされます。これは、実行時にオブジェクトのタイプを識別するという RTTI の意味でもあります。
1.3.1 特定の型情報が失われる問題
完全ではありません。たとえば、配列コンテナーは実際にはすべての要素を Object として保持し、アクセスされると結果を自動的に 宣言された型 に戻します。配列がオブジェクトを埋める (保持する) 場合、特定の型は宣言された型の サブクラス である可能性があり、配列内に配置されると、宣言された型に アップキャストされ、保持されているオブジェクトは失われます。具体性 を入力します。アクセスすると、Object は特定のサブクラス型ではなく、宣言された型に変換されるだけなので、この変換は完了していません。 ポリモーフィズムは特定の種類の動作を表現しますが、それは単に「ポリモーフィズムメカニズム」の問題であり、参照によって指定される特定の
によって決定され、ランタイムと同等ではありません具体的な種類の識別。 上記により、特定の型情報の損失である問題が明らかになりました。 問題が発生した場合、その問題を解決する必要があります。これは、
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(),证实了具体类型信息的丢失!! } }
実際、Class
オブジェクトは、クラスのすべての「通常の」オブジェクトをすべてのクラスには Class
オブジェクトがあります。つまり、新しいクラスが作成されされるたびに、Classオブジェクトが生成されます(より適切には、同じ名前の.classファイルに保存します)。 言い換えると、Class オブジェクト
は、.java ファイルが .class ファイルにコンパイルされるときに生成され、この .に保存されます。 2.2 クラスオブジェクトは、オブジェクト(通常のオブジェクト、非クラスオブジェクト)を生成するために使用されます プログラムを実行するJVMは、いわゆる「
(または .class ファイル) を使用してクラス オブジェクトを生成します。 すべてのクラスは、初めて使用されるときに JVM に動的にロードされます。プログラムがクラスの静的メンバーを初めて使用するとき、クラスはロードされます。これは、コンストラクターの前に static キーワードが追加されていなくても、コンストラクターも静的メソッドであることを意味します。
必要な場合にのみロードされます。 (C++のような言語を静的にロードするのは難しいです。)
まずクラスの
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泛型的一部分,?
表示“任何事物”。以下程序中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; } }
将通配符与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
对象
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
一个类加载器,通常可以从已经被加载的对象中获取其类加载器
Class<?>[] 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プログラミング思考学習クラス(3) 第15章 - ジェネリック
以上がJavaプログラミング思考学習教室(2) 第14章-種類情報の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。