119 Star 1K Fork 296

lin-mt / effective-java-third-edition

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
第65项:接口优先于反射机制.md 7.67 KB
一键复制 编辑 原始数据 按行查看 历史
lin-mt 提交于 2020-01-01 10:59 . 修订65、66项

接口优先于反射机制

  核心反射机制:java.lang.reflect,提供对任意类的编程式访问(The core reflection facility, java.lang.reflect, offers programmatic access to arbitrary classes.)。给定一个 Class 对象,你可以获得 Constructor、Mtehod 和 Field 实例,分别代表了该 Class 实例所表示的类的构造器、方法和域。这些对象提供了通过编程的方式(programmatic)访问类的成员名称、域类型、方法签名等信息的能力。

  而且,Constructor,Method 和 Field 实例使你能够通过*反射机制(reflectively)*操作它们的底层对等体:通过调用 Constructor,Method 和 Field 实例上的方法,可以构造底层类的实例、调用底层类的方法,并访问底层类中的域。例如,Method.invoke 使你可以调用任何类的的任何对象上的任何方法(遵从常规的安全限制)。反射机制(reflection)允许一个类使用另一个类,即使当前者被编译的时候后者根本还不存在。然而,这种能力也要付出代价:

  • 丧失了编译时类型检查的好处 ,包括异常检查。如果程序企图用反射方式调用不存在的或者不可访问的方法,在运行时它将会失败,除非采取了特别的预防措施。

  • 执行反射访问所需要的代码非常笨拙和冗长 。编写这样的代码非常乏味,阅读起来也很困难。

  • 性能损失 。反射方法调用比普通方法调用慢了许多。具体慢了多少,这很难说,因为受到了多个因素的影响。在我的机器上,当反射完成时,调用一个没有输入参数、返回【类型为】int 的方法会慢 11 倍。

  有一些复杂的应用程序需要反射。示例包括代码分析工具和依赖注入框架。即便是这样的后来工具也已经不再使用反射了,因为它的缺点是显而易见的。如果你对应用程序是否需要反射有任何疑问,那么可能是不需要。

  如果只是以非常有限的形式使用反射机制,虽然也要付出少许代价,但是可以获得许多好处 。对于有些程序,它们必须用到在编译时无法获得的类,但是在编译时存在的适当的接口或者超类,通过它们可以引用这个类(第 64 项)。如果是这种情况,你可以通过反射方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例

  例如,下面的程序创建了一个 Set实例,它的类是由第一个命令行参数指定的。该程序把其余的命令行参数插入到这个集合中,然后打印该集合。不管第一个参数是什么,程序都会打印出余下的命令行参数,其中重复的参数会被消除掉。这些参数的打印顺序取决于第一个参数中指定的类。如果指定“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 实现。同样地,它也可以变成一个通用的集合性能分析工具。实际上,它所演示的这种足以实现一个成熟的服务提供者框架(service provider framework)(第 1 项)。绝大多数情况下,使用反射机制时需要的也正是这种方法。

  这个示例演示了反射机制的两个缺点。第一,该示例可以在运行时生成 6 个不同的异常,如果不使用反射实例化,则所有这些异常都是编译时错误。(为了好玩,你可以通过传入适当的命令行参数使程序生成 6 个异常中的每一个【调皮!】)。第 2 个缺点是需要 25 行繁琐的代码才能从类的名称生成类的实例,而构造函数调用则可以整齐地放在 1 行上。通过捕获 ReflectiveOperationException 异常可以减少程序的代码长度,ReflectiveOperationException 是 Java 7 中引入的各种反射异常的超类。这两个缺点都局限于实例化对象的那部分代码。一旦对象被实例化,它与其他的 Set 实例就无法区分。在实际的程序中,通过使用这种限定的反射方法,绝大部分代码可以不受影响。

  如果你编译此程序,你将获得未受检的强制转换警告。【出现】这个警告是合理的,因为强转为 Class<? extends Set>才会成功【编译】,即使指定名称的类不是 Set 的一种实现,在这种情况下,程序在实例化类时会抛出 ClassCastException 异常。要了解有关压制警告的相关信息,请阅读第 27 项。

  类对于在运行时可能不存在的其他类、方法或者域的依赖性,用反射法进行管理,这种用法是合理的,但是很少使用。如果你要编写一个包,并且它运行的时候必须依赖其他某个包的多个版本,这种做法可能就非常有用。这种做法就是,在所支持的包所需要的最小环境下对它进行编译,通常是最老的版本,然后以反射方式访问任何更加新的类或者方法。如果企图访问的新的类或者新的方法在运行时不存在,为了使这种方法有效你还必须采取适当的动作。所谓适当的动作,可能包括使用某种其他可替换的办法来达到同样的目的,或者使用简化的功能进行处理。

  简而言之,反射机制是一种功能强大的机制,对于复杂系统中特定的编程任务,它是非常必要的,但它也有一些缺点。如果你编写的程序必须要与编译时未知的类一起工作,如果有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类。

其他
1
https://gitee.com/lin-mt/effective-java-third-edition.git
git@gitee.com:lin-mt/effective-java-third-edition.git
lin-mt
effective-java-third-edition
effective-java-third-edition
master

搜索帮助