Effective-Java 第三版中文版 接口优于反射

65. 接口优于反射

核心反射机制 java.lang.reflect 提供对任意类的编程访问。给定一个 Class 对象,你可以获得 Constructor、Method 和 Field 实例,分别代表了该 Class 实例所表示的类的构造器、方法和字段。这些对象提供对类的成员名、字段类型、方法签名等的编程访问。

此外,Constructor、Method 和 Field 实例允许你反射性地操作它们的底层对应项:你可以通过调用 Constructor、Method 和 Field 实例上的方法,可以构造底层类的实例、调用底层类的方法,并访问底层类中的字段。例如,Method.invoke 允许你在任何类的任何对象上调用任何方法(受默认的安全约束)。反射允许一个类使用另一个类,即使在编译前者时后者并不存在。然而,这种能力是有代价的:

  • 你失去了编译时类型检查的所有好处, 包括异常检查。如果一个程序试图反射性地调用一个不存在的或不可访问的方法,它将在运行时失败,除非你采取了特殊的预防措施。
  • 执行反射访问所需的代码既笨拙又冗长。 写起来很乏味,读起来也很困难。
  • 性能降低。 反射方法调用比普通方法调用慢得多。到底慢了多少还很难说,因为有很多因素在起作用。在我的机器上,调用一个没有输入参数和返回 int 类型的方法时,用反射执行要慢 11 倍。

有一些复杂的应用程序需要反射。包括代码分析工具和依赖注入框架。即使是这样的工具,随着它的缺点变得越来越明显,人们也在逐渐远离并反思这种用法。如果你对应用程序是否需要反射有任何疑问,那么它可能不需要。

通过非常有限的形式使用反射,你可以获得反射的许多好处,同时花费的代价很少。 对于许多程序,它们必须用到在编译时无法获取的类,在编译时存在一个适当的接口或超类来引用该类(详见第 64 条)。如果是这种情况,可以用反射方式创建实例,并通过它们的接口或超类正常地访问它们。

例如,这是一个创建 Set<String> 实例的程序,类由第一个命令行参数指定。程序将剩余的命令行参数插入到集合中并打印出来。不管第一个参数是什么,程序都会打印剩余的参数,并去掉重复项。然而,打印这些参数的顺序取决于第一个参数中指定的类。如果你指定 java.util.HashSet,它们显然是随机排列的;如果你指定 java.util.TreeSet,它们是按字母顺序打印的,因为 TreeSet 中的元素是有序的:

// Reflective instantiation with interface access
public static void main(String[] args) {

    // Translate the class name into a Class object
    Class<? extends Set<String>> cl = null;
    try {
        cl = (Class<? extends Set<String>>) // Unchecked cast!
        Class.forName(args[0]);
    } catch (ClassNotFoundException e) {
        fatalError("Class not found.");
    }

    // Get the constructor
    Constructor<? extends Set<String>> cons = null;
    try {
        cons = cl.getDeclaredConstructor();
    } catch (NoSuchMethodException e) {
        fatalError("No parameterless constructor");
    }

    // Instantiate the set
    Set<String> s = null;
    try {
        s = cons.newInstance();
    } catch (IllegalAccessException e) {
        fatalError("Constructor not accessible");
    } catch (InstantiationException e) {
        fatalError("Class not instantiable.");
    } catch (InvocationTargetException e) {
        fatalError("Constructor threw " + e.getCause());
    } catch (ClassCastException e) {
        fatalError("Class doesn't implement Set");
    }

    // Exercise the set
    s.addAll(Arrays.asList(args).subList(1, args.length));
    System.out.println(s);
}

private static void fatalError(String msg) {
    System.err.println(msg);
    System.exit(1);
}

虽然这个程序只是一个小把戏,但它演示的技术非常强大。这个程序可以很容易地转换成一个通用的集合测试器,通过积极地操作一个或多个实例并检查它们是否遵守 Set 接口约定来验证指定的 Set 实现。类似地,它可以变成一个通用的集合性能分析工具。事实上,该技术足够强大,可以实现一个成熟的服务提供者框架(详见第 1 条)。

这个例子也说明了反射的两个缺点。首先,该示例可以在运行时生成六个不同的异常,如果没有使用反射实例化,所有这些异常都将是编译时错误。(有趣的是,你可以通过传入适当的命令行参数,使程序生成六个异常中的每一个。)第二个缺点是,根据类的名称生成类的实例需要 25 行冗长的代码,而构造函数调用只需要一行。通过捕获 ReflectiveOperationException(Java 7 中引入的各种反射异常的超类),可以减少程序的长度。这两个缺点都只限于实例化对象的程序部分。实例化后,与任何其他 Set 实例将难以区分。在实际的程序中,通过这种限定使用反射的方法,大部分代码可以免受影响。

如果编译此程序,将得到 unchecked 的强制转换警告。这个警告是合法的,即使指定的类不是 Set 实现,Class<? extends Set<String>> 也会成功,在这种情况下,程序在实例化类时抛出 ClassCastException。要了解如何抑制警告,请阅读条目 27。

反射的合法用途(很少)是管理类对运行时可能不存在的其他类、方法或字段的依赖关系。如果你正在编写一个包,并且必须针对其他包的多个版本运行,此时反射将非常有用。该技术是根据支持包所需的最小环境(通常是最老的版本)编译包,并反射性地访问任何较新的类或方法。如果你试图访问的新类或方法在运行时不存在,要使此工作正常进行,则必须采取适当的操作。适当的操作可能包括使用一些替代方法来完成相同的目标,或者使用简化的功能进行操作。

总之,反射是一种功能强大的工具,对于某些复杂的系统编程任务是必需的,但是它有很多缺点。如果编写的程序必须在编译时处理未知的类,则应该尽可能只使用反射实例化对象,并使用在编译时已知的接口或超类访问对象。

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
译者序 序 前言 致谢 第1章 引言 第2章 创建和销毁对象 第1条:考虑用静态工厂方法代替构造器 第2条:遇到多个构造器参数时要考虑用构建器 第3条:用私有构造器或者枚举类型强化Singleton属性 第4条:通过私有构造器强化不可实例化的能力 第5条:避免创建不必要的对象 第6条:消除过期的对象引用 第7条:避免使用终结方法 第3章 对于所有对象都通用的方法 第8条:覆盖equals时请遵守通用约定 第9条:覆盖equals时总要覆盖hashCode 第10条:始终要覆盖toString 第11条:谨慎地覆盖clone 第12条:考虑实现Comparable接口 第4章 类和接口 第13条:使类和成员的可访问性最小化 第14条:在公有类中使用访问方法而非公有域 第15条:使可变性最小化 第16条:复合优先于继承 第17条:要么为继承而设计,并提供文档说明,要么就禁止继承 第18条:接口优于抽象类 第19条:接口只用于定义类型 第20条:类层次优于标签类 第21条:用函数对象表示策略 第22条:优先考虑静态成员类 第5章 泛型 第23条:请不要在新代码中使用原生态类型 第24条:消除非受检警告 第25条:列表优先于数组 第26条:优先考虑泛型 第27条:优先考虑泛型方法 第28条:利用有限制通配符来提升API的灵活性 第29条:优先考虑类型安全的异构容器 第6章 枚举和注解 第30条:用enum代替int常量 第31条:用实例域代替序数 第32条:用EnumSet代替位域 第33条:用EnumMap代替序数索引 第34条:用接口模拟可伸缩的枚举 第35条:注解优先于命名模式 第36条:坚持使用Override注解 第37条:用标记接口定义类型 第7章 方法 第38条:检查参数的有效性 第39条:必要时进行保护性拷贝 第40条:谨慎设计方法签名 第41条:慎用重载 第42条:慎用可变参数 第43条:返回零长度的数组或者集合,而不是:null 第44条:为所有导出的API元素编写文档注释 第8章 通用程序设计 第45条:将局部变量的作用域最小化 第46条:for-each循环优先于传统的for循环 第47条:了解和使用类库 第48条:如果需要精确的答案,请避免使用float和double 第49条:基本类型优先于装箱基本类型 第50条:如果其他类型更适合,则尽量避免使用字符串 第51条:当心字符串连接的性能 第52条:通过接口引用对象 第53条:接口优先于反射机制 第54条:谨慎地使用本地方法 第55条:谨慎地进行优化 第56条:遵守普遍接受的命名惯例 第9章 异常 第57条:只针对异常的情况才使用异常 第58条:对可恢复的情况使用受检异常,对编程错误使用运行时异常 第59条:避免不必要地使用受检的异常 第60条:优先使用标准的异常 第61条:抛出与抽象相对应的异常 第62条:每个方法抛出的异常都要有文档 第63条:在细节消息中包含能捕获失败的信息 第64条:努力使失败保持原子性 第65条:不要忽略异常 第10章 并发 第66条:同步访问共享的可变数据 第67条:避免过度同步 第68条:executor和task优先干线程 第69条:并发工具优先于wait和notify 第70条:线程安全性的文档化 第71条:慎用延迟初始化 第72条:不要依赖于线程调度器 第73条:避免使用线程组 第11章 序列化 第74条:谨慎地实现Serializable接口 第75条:考虑使用自定义的序列化形式 第76条:保护性地编写readObject方法 第77条:对于实例控制,枚举类型优先于readResolve 第78条:考虑用序列化代理代替序列化实例 附录 第1版与第2版条目对照 中英文术语对照 参考文献
©️2020 CSDN 皮肤主题: 黑客帝国 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值