Java 运行时类型识别 与 反射

欢迎Follow我的GitHub, 关注我的简书.

在开发Java(包含Android)相关程序时, 经常涉及RTTI与反射. RTTI, 即Run-Time Type Identification, 运行时类型识别; 反射, 即Reflect. 两者都是在程序运行时发现和使用类型信息, RTTI在编译时已经获知类型; 反射在运行时才会发现类型.

结合我的编程经验, 分享一些关于RTTI反射的相关知识, 还有一些在使用时的小技巧和风险点.

RTTI

RTTI

RTTI在编译时获知对象的类型, 在运行时识别对象的类型, 应用于多态机制. RTTI支持查询引用所指向对象的确切类型, 即instanceof方法. Java使用Class对象实现RTTI的功能, Class对象被用于创建一般对象. 当编写并编译类时, 就会产生Class对象, 保存在同名的.class文件中. 系统使用类加载器(即Java虚拟机, JVM)加载Class对象, 创建实例. 当程序创建第一个类的静态成员引用时, 就会加载这个类. 构造器本质也是类的静态方法, 使用new创建新对象时, 也会导致加载. Class对象仅仅操作类型, 并不操作实例.

创建类的实例, 和获取Class对象(即调用Class#forName), 都会加载类.

/**
 * 模拟类的加载效果
 * 
 * @author wangchenlong
 */
public class ClassLoader {

    /**
     * 输出:
     * 
     * Taeyeon is ready!
     * Jessica is ready!
     */
    public static void main(String[] args) {
        // 调用构造器触发类的加载.
        new Taeyeon();
        
        try {
            // 调用forName获取Class对象的引用, 类如果未被加载, 则加载.
            Class.forName("typeinfo.Jessica"); // 使用forName时, 注意添加包名.
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            Utils.print("Jessica is not here!");
        }
        
        new Taeyeon(); // 不会重复执行static段落.
    }

}

class Taeyeon {
    // static段落, 在类第一次被加载时执行, 只执行一次.
    static { Utils.print("Taeyeon is ready!"); }
}

class Jessica {
    static { Utils.print("Jessica is ready!"); }
}

Class类中含有大量获取类型信息的静态方法. Class类的newInstance方法实现虚拟构造器, 根据类型创建对象, 则类需要含有默认构造器(无参构造器), 否则抛出异常.

/**
 * 列举Class的静态方法,
 * Class的: forName, getInterfaces, getSuperclass, newInstance;
 * getName, isInterface, getSimpleName, getCanonicalName.
 * Object的getClass.
 * 
 * @author wangchenlong
 */
public class ClassMethods {

    public static void showClassInfo(Class<?> clazz) {
        if (clazz != null) {
            Utils.print("--");
            // 输出类型的名称, 和判断是否为接口
            Utils.print("ClassName: " + clazz.getName() + 
                    ", is interface: " + (clazz.isInterface() ? "yes" : "no"));
            // 输出简单类名和标准类名, Canonical即标准的
            Utils.print("SimpleName: " + clazz.getSimpleName() + 
                    ", CanonicalName: " + clazz.getCanonicalName());
        }
    }

    /**
     * --
     * ClassName: typeinfo.Tiffany, is interface: no
     * SimpleName: Tiffany, CanonicalName: typeinfo.Tiffany
     * --
     * ClassName: typeinfo.Dance, is interface: yes
     * SimpleName: Dance, CanonicalName: typeinfo.Dance
     * --
     * ClassName: typeinfo.GG, is interface: no
     * SimpleName: GG, CanonicalName: typeinfo.GG
     */
    public static void main(String[] args) {
        // 获取类的类型信息
        Class<?> clazz = null;
        try {
            clazz = Class.forName("typeinfo.Tiffany");
        } catch (Exception e) {
            Utils.print("Taeyeon is gone!");
            return;
        }
        showClassInfo(clazz); // 输出Class的信息
        
        // 获取接口的类型信息
        Class<?>[] clazzs = clazz.getInterfaces(); // 获取类型的接口
        if (!Utils.isListEmpty(clazzs)) {
            showClassInfo(clazzs[0]); // 输出接口类型信息
        }

        // 获取父类的类型信息, 通过实例方式
        Class<?> upClass = clazz.getSuperclass(); // 获取父类
        Object object = null;
        try {
            object = upClass.newInstance(); // 父类创建对象
        } catch (Exception e) {
            Utils.print("Super class has no instance.");
            return;
        }
        showClassInfo(object.getClass()); // 获取实例的类型信息
    }

}

class Tiffany extends GG implements Dance, Sing {
    public Tiffany() {
        super("Tiffany");
    }
}

class GG {
    // Class#newInstance方法, 需要使用默认构造器, 否则无法创建.
    public GG() {}
    public GG(String name) {}
}

interface Dance {}

interface Sing {}

使用类名的.class形式, 也可以创建Class对象的引用, 比forName模式更加明确类型信息, 在编译期检查类型, 但是不会自动初始化Class对象, 延迟到首次引用非常量静态域时进行初始化.

类的创建需要三个步骤: 加载, 链接, 初始化. 查找字节码, 创建Class对象; 验证字节码, 为静态域分配存储空间; 执行父类初始化, 与静态模块初始化.

/**
 * 使用.class形式创建Class对象, 延迟初始化Class对象.
 * 静态常量也不会触发初始化, 其他静态量和forName会触发初始化
 * @author wangchenlong
 */
public class NameClass {

    public static Random sRandom = new Random(530);
    /**
     * 1990 | I'm Yoona First! | 341
     * --
     * I'm Yoona Second! (Not final) | 1990
     * --
     * I'm Yoona Third!
     */
    public static void main(String[] args) {
        Class<Yoona> clazz = Yoona.class; // .class不会触发类的初始化
        Utils.print(Yoona.staticFinalInt); // 静态常量不会触发初始化
        // 静态非常量(随机量, 非编译期常量, 运行时才获知具体值)会触发初始化
        Utils.print(Yoona.staticFinalRand);     Utils.printDivider();
        
        Utils.print(Yoona2.staticInt); // 静态非常量会触发初始化
        Utils.printDivider();
        
        try {
            // forName会触发类的初始化
            Class<?> clazz3 = Class.forName("typeinfo.Yoona3");
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            Utils.print("I don't know where Yoona is.");
        }
    }

}

class Yoona {
    static final int staticFinalInt = 1990; // static并final, 不触发初始化
    // static并final, 但不是常量, 数据可变, 非编译期常量, 运行时才获知具体值
    static final int staticFinalRand = NameClass.sRandom.nextInt(530);
    static { Utils.print("I'm Yoona First!"); }
}

class Yoona2 {
    static int staticInt = 1990; // static非final, 触发初始化
    static { Utils.print("I'm Yoona Second! (Not final)"); }
}

class Yoona3 {
    static { Utils.print("I'm Yoona Third!"); }
}

Class引用指向Class对象, 表示类的类型, 也支持创建类的实例, 包含实例的方法与静态数据. Class支持使用泛型<?>限定具体类型, 在编译期检查类型, 通配符"?"表示任何类型. 如Class限定某类数据族, 无法默认转换父类, 需使用extends, 即<? extends X>, 表示某类继承于X类; 同时也支持表示某类是X类的父类, 使用super, 即<? super X>.

RTTI(Run-Time Type Identification, 运行时类型识别)包含两种形式:

  1. 类型转换, 即"(Class)", 由RTTI保证类型转换正确, 失败则抛出ClassCastException.
  2. 对象的类型对象, 即Class对象, 获取运行时信息, 通过Class#forName直接获取, 或Object#getClass间接获取.
  3. 使用instanceof关键字, 判断对象是否是类型的实例, 用于向下转型 检查.

类型转换即向下转型, 由父类转换为子类, 在编译期无法得知父类指向的对象是否为子类, 只有在运行期才能得知, 由RTTI确保转换的正确性, 需要显式标记向下转型的类型. 而向上转型不需要显式标记, 因为子类是父类的超集.

统计多个对象的类型数, 调用Class#isInstance方法, 判断对象是否属于类型.

public void count(Member member) {
    for (Map.Entry<Class<? extends Member>, Integer> pair : entrySet()) {
        if (pair.getKey().isInstance(member)) {
            put(pair.getKey(), pair.getValue() + 1);
        }
    }
}

或者调用Class#isAssignableFrom方法, 判断对象是否属于当前类型的子类.

/**
 * 递归调用, 判断是否属于当前类型, 一直向上查找至父类型
 * 
 * @param type
 */
private void countClass(Class<?> type) {
    Integer times = get(type);
    put(type, times == null ? 1 : times + 1);
    Class<?> superClass = type.getSuperclass();
    if (superClass != null && mBaseType.isAssignableFrom(superClass)) {
        countClass(superClass); // 根据父类递归调用
    }
}

Class#isInstanceinstanceof相同, 判断某个实例是否属于某个类型; ==equal相同, 判断某个类型与当前类型是否相同.

public class ClassEqual {
    
    // instanceof 与 isInstance 相同; == 与 equal 相同.
    public static void main(String[] args) {
        ParkChoAh parkChoAh = new ParkChoAh();
        Utils.print("parkChoAh: " + parkChoAh.getClass());
        Utils.print("parkChoAh instanceof ParkChoAh: " + (parkChoAh instanceof ParkChoAh));
        Utils.print("parkChoAh instanceof AoaMember: " + (parkChoAh instanceof AoaMember));
        Utils.print("ParkChoAh.class.isInstance(parkChoAh): " + (ParkChoAh.class.isInstance(parkChoAh)));
        Utils.print("AoaMember.class.isInstance(parkChoAh): " + (AoaMember.class.isInstance(parkChoAh)));
        Utils.print("parkChoAh.getClass() == ParkChoAh.class: " + (parkChoAh.getClass() == ParkChoAh.class));
        // 不能使用==比较, 类型不同无法直接比较
//      Utils.print("parkChoAh.getClass() == AoaMember.class: " + (parkChoAh.getClass() == AoaMember.class));
        Utils.print("parkChoAh.getClass().equals(ParkChoAh.class)): " + (parkChoAh.getClass().equals(ParkChoAh.class)));
        Utils.print("parkChoAh.getClass().equals(AoaMember.class)): " + (parkChoAh.getClass().equals(AoaMember.class)));
    }

}

// AOA成员
class AoaMember {}

// 朴草娥, 属于AOA成员
class ParkChoAh extends AoaMember {}

反射

在编译时, 编译器需要获知通过RTTI处理的类实例, 但是在远程调用等其他时候, 编程时无法确定类类实例, 则需要使用反射方式. RTTI在编译时, 确认.class文件信息; 反射在运行时, 确认.class文件信息.

使用java.lang.reflect包内的类与Class类配合, 获取类型的信息. Class#getMethods获取类型的方法列表, Class#getConstructors获取类型的构造信息.

public class ShowMethods {
    private static Pattern pattern = Pattern.compile("\\w+\\."); // 表示字母与点的组合
    
    public static void showMethods(Class<?> clazz) {
        if (clazz == null) return;
        Method[] methods = clazz.getMethods(); // 获取方法列表
        Constructor<?>[] constructors = clazz.getConstructors(); // 获取构造器列表
        for (int i=0; i<methods.length; ++i) {
            // 删除所有字母+.的组合, 就是比方法前面的包名全部去掉, 用空代替.
            Utils.print(pattern.matcher(methods[i].toString()).replaceAll(""));
        }
        Utils.printDivider();
        for (int i=0; i<constructors.length; ++i) {
            Utils.print(pattern.matcher(constructors[i].toString()).replaceAll(""));
        }
    }
    
    public static void main(String[] args) {
        showMethods(MemberCount.class); // 显示方法与构造器信息
    }
}

反射在代理模式中有着重要的应用. 代理模式, 通过代理对象调用原对象的方法, 并在方法中添加若干操作, 接口保持相同. 在传统的代理模式中, 代理对象需要与原对象实现相同的接口, 保持接口一致性, 当接口较多时, 代理对象含有较多无用方法. 而通过反射提供动态代理模式, 避免代理对象的方法过多. 通过Method#getName方法, 筛选被代理方法, 添加额外信息.

/**
 * 通过动态代理Handler, 创建代理对象, 实现动态代理
 * 
 * @author wangchenlong
 */
public class SimpleDynamicProxy {
    public static void consumer(SomethingInterface iface) {
        iface.doSomething();
        iface.doSomethingElse();
    }

    public static void main(String[] args) {
        RealObject realObject = new RealObject();
        consumer(realObject);
        Utils.printDivider();
        // 通过动态代理, 创建代理对象
        SomethingInterface proxy = (SomethingInterface) Proxy.newProxyInstance(
                SomethingInterface.class.getClassLoader(), new Class[] { SomethingInterface.class },
                new DynamicProxyHandler(realObject));
        consumer(proxy);
    }
}

// 动态代理模式
class DynamicProxyHandler implements InvocationHandler {
    private Object mProxied; // 代理对象

    public DynamicProxyHandler(Object proxied) {
        mProxied = proxied;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 筛选方法, 当方法名称为doSomethingElse时, 添加额外信息
        if (method.getName().equals("doSomethingElse"))
            Utils.print("Proxy: something -> else");
        return method.invoke(mProxied, args); // 动态执行方法
    }
}

// 代理接口
interface SomethingInterface {
    void doSomething();
    void doSomethingElse(); // 需要代理对象额外操作的接口
}

// 原对象
class RealObject implements SomethingInterface {
    @Override
    public void doSomething() {
        Utils.print("I want to do something!");
    }

    @Override
    public void doSomethingElse() {
        Utils.print("You should do something else!");
    }
}

当对象未实例化时, 默认设置为null, 当null执行方法时, 产生NullPointerException, 即空指针异常. 如果引入空对象替换null, 即可避免异常问题, 也易于统一管理. 空对象是类中的属性均相同, 且为空. 动态代理模式支持创建含有相同接口的空对象, 简化创建逻辑.

public class NullMember {
    // ClassLoader任意选择, 代理接口, 空对象Handler
    public static AoaInterface newNullMember(Class<? extends AoaInterface> type) {
        return (AoaInterface) Proxy.newProxyInstance(AoaInterface.class.getClassLoader(),
                new Class[] { Null.class, AoaInterface.class }, new NullAoaProxyHandler(type));
    }

    public static void main(String[] args) {
        // 真实对象和空对象做对比
        AoaInterface[] members = { new ParkChoAhAoa(), newNullMember(ParkChoAhAoa.class) };
        for (AoaInterface member : members) {
            AoaInterface.Test.test(member);
            Utils.printDivider();
        }
    }

}

// 朴草娥
class ParkChoAhAoa implements AoaInterface {
    @Override
    public String name() {
        return "朴草娥";
    }

    @Override
    public List<Skill> skills() {
        return Arrays.asList(new Skill() {
            @Override
            public String description() {
                return "Dancing, Dancing";
            }
        }, new Skill() {
            @Override
            public String description() {
                return "Singing, Singing";
            }
        });
    }
}

/**
 * 空对象的代理Handler, 用于创建实现AoaMember接口的空对象
 */
class NullAoaProxyHandler implements InvocationHandler {
    private String mNullName;

    private AoaInterface mNProxied = new NAoaInterface(); // 代理对象始终设置为空对象

    public NullAoaProxyHandler(Class<? extends AoaInterface> clazz) {
        mNullName = clazz.getSimpleName() + " Null AoaMember"; // 空对象名称
    }

    private class NAoaInterface implements Null, AoaInterface {

        @Override
        public String name() {
            return mNullName;
        }

        // 列表的空对象, 容器的默认空对象
        @Override
        public List<Skill> skills() {
            return Collections.emptyList();
        }

    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(mNProxied, args); // 使用空的代理对象调用方法
    }
}

interface Null {
}

interface Skill {
    String description();
}

interface AoaInterface {
    String name();

    List<Skill> skills();
    
    // 接口内部的测试类
    class Test {
        public static void test(AoaInterface member) {
            if (member instanceof Null)
                Utils.print("Null Member");
            Utils.print("Name: " + member.name());
            for (Skill skill : member.skills())
                Utils.print("Skill" + skill.description());
        }
    }
}

OK, that's all! Enjoy it!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • 第一章 对象导论 对象具有状态、行为和标识。这意味着每一个对象都可以拥有内部数据和方法,并且每一个对象都可以唯一地...
    niaoge2016阅读 774评论 0 0
  • 1. Java中的多态性理解(注意与C++区分) Java中除了static方法和final方法(private方...
    小敏纸阅读 1,410评论 0 19
  • ​这是奇小娜每天一篇原创文章的第82天 没事多看书,有事多交流。这是最近半个月来的体会。 朋友推荐了一些好书,书中...
    奇小娜阅读 662评论 0 51
  • 如果失去是苦,你还怕不怕付出 如果坠落是苦,你还要不要幸福 如果迷乱是苦,该开始还是结束 如果追求是苦,这是坚强还...
    月季舒阅读 196评论 0 0