运行时类型信息(RTTI:Run-Time Type Identification)使得你可以在程序运行时发现和使用类型信息
RTTI
为什么需要 RTTI
通常,我们希望大部分代码尽可能的少了解对象的具体类型,仅仅与对象家族中的一个通用表示打交道。这样的代码会更容易写,更容易读,且更容易维护;设计也更容易实现、理解和改变。所以“多态”是面向对象编程的基本目标。
来看书上的一个例子:
package typeinfo;
import java.util.ArrayList;
import java.util.List;
/**
* Created by wwh on 16-3-21.
*/
abstract class Shape {
void draw() {
System.out.println(this + ".draw()");
}
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> shapeList = new ArrayList<Shape>();
shapeList.add(new Circle());
shapeList.add(new Triangle());
shapeList.add(new Square());
for(Shape shape : shapeList) {
shape.draw();
}
}
}
很简单的一个例子,我们可以通过基类(抽象类)的引用控制派生类,这样基类(抽象类)就是此类的一个通用的方法,不同的需求放在派生类中来实现。
实际运用可参考这篇文章 重构:运用Java反射加多态 “干掉” switch
Class 对象
类型信息在运行时是通过 Class 对象来完成的,Class 对象保存着同名类的元信息,它用来创建类的实例对象。
元信息包含:类的所有方法代码,类的静态成员等。
每个类都有个同名的 Class 对象,在起初用命令行时,javac xx.java 编译后就会生成 .class 文件(保存着对应类的 Class 数据)。在实际使用中,JVM 通过类加载系统来生成此类对象。
所有的类都是在第一次使用时,动态加载到 JVM 中(惰性加载)。所谓第一次使用指的是:当程序创建第一个对类的静态成员的引用时。
package typeinfo;
/**
* Created by wwh on 16-3-21.
*/
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) throws ClassNotFoundException {
/* 类的构造方法也是静态方法,第一次加载调用 static 代码块 */
new Candy();
try{
/* forName 是显示加载类 */
Class.forName("typeinfo.Gum");
}catch (ClassNotFoundException e) {
System.out.println("Not found Gum");
}
new Cookie();
/* 第二次加载没有调用 static 代码块*/
new Candy();
}
}
类的静态代码块只有第一次使用该类时才会调用,从上面例子我们可以得知在第一次实例化类或者通过 Class.forName() 都可以加载类的 Class,一旦某个类的 Class 被载入内存,它就被用来创建这个类的所有对象。
所有 Class 对象都属于 Class 类,我们可以通过 Class 对象的 forName ()方法来获取不同类的 Class。
从上图得知我们可以通过类的 Class 对象在运行时获取很多相关信息。
类字面常量
Java 还提供了另一种方法来生成 Class 对象的引用,即类字面常量。如直接使用 Gum.class 而不用通过 forName() 方法。比较高效,而且更简单,安全。
注意:当使用 .class 创建对 Class 对象的引用时,不会自动初始化该 Class 对象。forName() 方法会立即初始化。
为了使用类而做的准备工作包含三个步骤:
1.加载:由类加载器执行嗯。查找字节码,并从字节码中创建一个 Class 对象。
2.链接:验证类中字节码,为静态域分配存储空间。
3.初始化:如果该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化块。
如果类中的一个值是编译期常量,那么不需要加载就可以读取。如 static final 修饰的变量。如果仅仅是 static 修饰的变量,对他访问时,要先进行链接和初始化。
泛化的 Class 引用
如果我们直接定义一个 Class 引用,这样会缺少类型检查,不会产生编译器警告。如下
public class WildcardClassReferences {
public static void main(String []args){
Class intClass = int.class;
intClass = double.class;
}
}
此时我们可以使用泛化的 Class
public class WildcardClassReferences {
public static void main(String []args){
Class<?> intclass = int.class;
intclass = double.class;
}
}
Class <?> 可以表明我们本身就是要选择一个非具体的类引用。除此之外,我们还可以为 Class 引用限定为某种类型,或该类型的任何子类,将通配符与 extends 关键字配合创建一个范围(这样可以提供编译器类型检查,不会到运行时才发现错误)。
Class<? extends Number> bounded = int.class
bounded = double.class;
bounded = Number.class
类型转换前先做检查
在 Java 中,编译器允许自由地做向上转型的赋值操作,而不需要任何显式的转型操作。但如果不使用显式的类型转换,编译器不允许你执行向下转型赋值。除非告知编译器额外的信息,以确定你是某种特定类型。我们通过 instanceof 来实现告知编译器相关信息。
/* 判断 triangle 是否是 shape 的一个实例,如果不使用 instanceof,则会抛出 ClassCastException 异常 */
if(triangle instanceof Shape) {
triangle.draw();
}
注意:只能将 instanceof 与命名类型进行比较,不能将它与 Class 对象作比较。
instanceof 和 Class 的等价性
instanceof 表明“你是这个类吗,你是这个类的基类吗?”,而用 == 比较 Class 则不会考虑继承,仅考虑是这个类型或者不是。
class Base {}
class Derived extends Base{}
public class InstanceofAndClass {
public static void main(String[] args) {
Base b = new Base();
Derived d = new Derived();
System.out.println((b instanceof Base));
System.out.println((d instanceof Base));
System.out.println((b.getClass() == Base.class));
/* 编译错误 Java:不可比较的类型 */
//System.out.println((d.getClass() == Base.class));
}
}
反射:运行时类信息
如果我们不确定某个对象的类型,RTTI 能够告诉我们。但有一个限制:该对象的类型必须编译时已知,这样才能用 RTTI 来识别它。但假设程序运行中我们从磁盘中或者网络上获取到一个类名字,需要创建该类的对象或者获取该类的相关信息该怎么做呢?在 Java 中,我们通过反射来实现。
PS:反射是什么
我的理解反射是程序在运行时可以动态识别并控制自己的一种机制。
Class 类与 java.lang.reflect 类库一起对 Java 反射的概念进行了支持,类库包含 Field、Method 和 Constructor 类。这些类型的对象由 JVM 在运行时创建,表示未知的类里对应的字段、方法和构造器。我们可以通过 get() 和 set() 方法可以读取和修改与 Field 对象关联的字段,用 invoke() 方法调用与 Method 对象关联的方法。使用 Constructor 创建新的对象。
RTTI 和 反射的区别:
RTTI:编译器在编译时打开和检查 .class 文件。
反射: .class 文件在编译时是不可获取的,所以在运行是打开和检查 .class 文件。
类方法提取器
来看一个例子,运行时通过命令行参数来获得未知对象的所有类方法,并调用第一个 HelloWorld 方法。
package typeinfo;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.regex.Pattern;
/**
* Created by wwh on 16-3-21.
*/
public class ShowMethods {
private static String usage =
"usage:\n" +
"ShowMethods qualified.class.name\n" +
"To Show all methods in class or:\n" +
"ShowMethods qualified.class.name word\n" +
"To search for methods involving 'word'";
private static Pattern p = Pattern.compile("\\w+\\.");
public static void HelloWorld() {
System.out.println("-------------------");
System.out.println("hello world");
System.out.println("-------------------");
}
public static void main(String [] args){
if(args.length < 1) {
System.out.println(usage);
System.exit(1);
}
int lines = 0;
try{
Class<?> c = Class.forName(args[0]);
Method[] methods = c.getMethods();
/* 调用 hello world 方法*/
Method methodHello = methods[0];
methodHello.invoke(null);
Constructor[] ctors = c.getConstructors();
for(Method method : methods) {
System.out.println(p.matcher(method.toString()).replaceAll(""));
}
for(Constructor ctor : ctors) {
System.out.println(p.matcher(ctor.toString()).replaceAll(""));
}
lines = methods.length + ctors.length;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
命令行参数:> java ShowMethods typeinfo.ShowMethods
反射的威力
反射能做到许多平时我们做不到的事,来看看
假设我们现在有一个接口 A,B 来实现接口 A。通过 RTTI 发现 a 是被当作 B 来实现的。通过转型为 B,我们可以调用不在 A 中的方法。
public interface A {
void f();
}
package typeinfo;
import typeinfo.interfacea.A;
/**
* Created by wwh on 16-3-21.
*/
class B implements A {
public void f() {}
public void g() {}
}
public class InterfaceViolation {
public static void main(String[] args) {
A a = new B();
a.f();
if(a instanceof B) {
B b = (B)a;
b.g();
}
}
}
这样虽然可以访问 B 类的方法,但如果 A 和 B 是我们提供给别人使用(如上面的 IterfaceViolation)代码之间的耦合性就比较高了。
我们可以通过访问权限来限定其他人使用。
package typeinfo.packageaccess;
import typeinfo.interfacea.A;
/**
* Created by wwh on 16-3-21.
*/
class C implements A {
public void f() {
System.out.println("public C.f()");
}
public void g() {
System.out.println("public C.g()");
}
/* 包访问权限 */
void u() {
System.out.println("package C.u()");
}
/* protected 访问权限 */
protected void v() {
System.out.println("protected C.v()");
}
/* private 权限 */
private void w() {
System.out.println("private C.w()");
}
}
public class HiddenC {
public static A makeA() {
return new C();
}
}
使用:
package typeinfo;
import typeinfo.interfacea.A;
import typeinfo.packageaccess.HiddenC;
/**
* Created by wwh on 16-3-21.
*/
public class HiddenImlementation {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
A a = HiddenC.makeA();
a.f();
System.out.println(a.getClass().getName());
//error:cannot find symbol 'C'
//if(a instanceof C) {
// C c = (C)a;
// c.g();
//}
}
通过反射来访问
package typeinfo;
import typeinfo.interfacea.A;
import typeinfo.packageaccess.HiddenC;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Created by wwh on 16-3-21.
*/
public class HiddenImlementation {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
A a = HiddenC.makeA();
a.f();
System.out.println(a.getClass().getName());
callHiddenMethod(a, "g");
callHiddenMethod(a, "u");
callHiddenMethod(a, "v");
callHiddenMethod(a, "w");
}
static void callHiddenMethod(Object a, String methodName) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method g = a.getClass().getDeclaredMethod(methodName);
/* 取消访问控制
* 值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。
* 值为 false 则指示反射的对象应该实施 Java 语言访问检查。 */
g.setAccessible(true);
g.invoke(a);
}
}
从上图我们可以看出通过反射可以调用所有方法,包括 private 方法!
如果是接口是私有内部类呢?
package typeinfo;
import typeinfo.interfacea.A;
import java.lang.reflect.InvocationTargetException;
/**
* Created by wwh on 16-3-21.
*/
class InnnerA {
/* private 内部类 */
private static class C implements A {
public void f() {
System.out.println("public C.f()");
}
public void g() {
System.out.println("public C.g()");
}
void u(){
System.out.println("package C.u()");
}
protected void v() {
System.out.println("protected C.v()");
}
private void w() {
System.out.println("private C.w()");
}
}
public static A makeA() { return new C(); }
}
public class InnerImplementation {
public static void main(String [] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
A a = InnnerA.makeA();
a.f();
System.out.println(a.getClass().getName());
HiddenImlementation.callHiddenMethod(a, "g");
HiddenImlementation.callHiddenMethod(a, "u");
HiddenImlementation.callHiddenMethod(a, "v");
HiddenImlementation.callHiddenMethod(a, "w");
}
}
上图我们可以看出通过反射可以访问私有内部类的方法和成员。
如果是匿名类呢?
package typeinfo;
import typeinfo.interfacea.A;
import java.lang.reflect.InvocationTargetException;
/**
* Created by wwh on 16-3-21.
*/
class AnonymousA {
public static A makeA() {
/* 匿名内部类 */
return new A() {
public void f() {
System.out.println("public C.f()");
}
public void g() {
System.out.println("public C.g()");
}
void u() {
System.out.println("void C.u()");
}
protected void v() {
System.out.println("protected C.v()");
}
private void w() {
System.out.println("private C.w()");
}
};
}
}
public class AnonymousImplementation {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
A a = AnonymousA.makeA();
a.f();
System.out.println(a.getClass().getName());
HiddenImlementation.callHiddenMethod(a, "g");
HiddenImlementation.callHiddenMethod(a, "u");
HiddenImlementation.callHiddenMethod(a, "v");
HiddenImlementation.callHiddenMethod(a, "w");
}
}
从上面的例子可以看出,无论是普通的类,还是 private 内部类,又或是匿名内部类,反射都可以访问其类的内部成员,包括 private 成员。
C++ RTTI
相对与 Java,C++对动态支持就弱多了,但也不一定是坏处,静态检查代码是值得的。
C++ 通过两种方式来支持 RTTI,如下
1.typeid:返回指针或引用所指对象的实际类型
2.dynamic_cast:将基类类型的指针或引用安全的转换为派生类的指针或引用。
对于带虚函数的类,在运行时执行RTTI操作符,返回动态类型信息;对于其他类型,在编译时执行RTTI,返回静态类型信息。