Thinking in Java -- 类型信息

来源:互联网 发布:淘宝注册店铺单能做吗 编辑:程序博客网 时间:2024/06/01 10:10

运行时类型信息(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.classbounded = 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,返回静态类型信息。

0 0
原创粉丝点击