Thinking in Java -- 类型信息

RTTI与Java反射技术详解
本文深入探讨了运行时类型信息(RTTI)及其在Java中的应用,通过实例展示了如何利用Class对象获取类的元信息,理解静态代码块的执行时机,以及泛型Class引用的使用。进一步阐述了反射概念,解释了RTTI与反射的区别,通过实例演示了如何通过反射动态获取和调用类的方法,包括访问私有方法和成员。同时,对比了C++的RTTI支持,并通过具体代码示例说明了Java反射的强大功能。

运行时类型信息(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.forName()

从上图得知我们可以通过类的 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");
    }
}

private内部类

上图我们可以看出通过反射可以访问私有内部类的方法和成员。

如果是匿名类呢?

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,返回静态类型信息。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏天的技术博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值