【问题标题】:Difference between calling a class constructor and using Class.forName().newInstance调用类构造函数和使用 Class.forName().newInstance 之间的区别
【发布时间】:2014-10-14 02:30:42
【问题描述】:

我一直试图了解使用new 实例化对象与使用Class.forName("A").newInstance(); 之间的区别。

我为一个简单的类 A 运行了以下代码,它显示使用 Class.forname("A").newInstance() 比仅使用 new A() 慢 70-100 倍。

我很想知道为什么会有这样的时间差异,但无法弄清楚。请有人帮我理解原因。

public class Main4test {

    public Main4test() {
    }

    static int turns = 9999999;

    public static void main(String[] args) {
        new Main4test().run();
    }

    public void run() {
        System.out.println("method1: " + method1() + "");
        System.out.println("method2:" + method2() + "");
    }

    public long method2() {
        long t = System.currentTimeMillis();
        for (int i = 0; i < turns; i++) {
            try {
                A a = (A) Class.forName("A").newInstance();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return System.currentTimeMillis() - t;
    }

    public long method1() {
        long t = System.currentTimeMillis();
        for (int i = 0; i < turns; i++) {
            try {
                A a = new A();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return System.currentTimeMillis() - t;
    }

}

public class A {
    int a;
    public A() {
    a=0;
    }
}

【问题讨论】:

  • 我的猜测是它按名称查找类是一项昂贵的操作。编译器对类一无所知,因此无法进行任何优化。
  • 另外,您可能需要对实例进行一些操作,以确保编译器不会对其进行优化。
  • 它是反射,它允许按名称创建类的新实例。它也较慢。它加载类并初始化它的静态部分,因此由于副作用,它用于加载驱动程序。
  • 通过反射完成任务几乎总是比直接完成要慢。使用new A(),您有一个构造函数调用。使用Class.forName("A").newInstance(),您有两个方法调用,在构造函数调用之上,这两个方法都可能涉及对二进制文件结构的复杂检查。这只是一种更迂回的方式。
  • 在没有 try-catch-block 的情况下,method1 可能会更快,这根本不是必需的。

标签: java reflection


【解决方案1】:
A a = new A();

直接调用new 运算符和A 的构造函数,其中A 在源文本中被提及,因此已经被加载和初始化。

A a = (A) Class.forName("A").newInstance();
  • 查看A是否已经加载
  • 必要时加载它
  • 必要时初始化
  • 通过反射定位无参数构造函数
  • 通过反射调用new 运算符和无参数构造函数
  • 将结果类型转换为A

【讨论】:

  • 其实你不能假设在执行new A()的时候类A已经被加载并初始化了。相反,这可能正是A 第一次被加载和初始化的点,A 被使用。典型的 JVM(包括 Oracle 的 JVM)在加载类时是惰性的。
  • @Bakuriu 这不是一个常量表达式,除非编译器能以某种方式知道它总是产生相同的结果,但它不能,因为它不需要。
  • @EJP 那么编译器如何避免new A()中的类查找呢?如果与A 关联的类可以在运行时更改,则编译器必须每次都查找它,即使使用其他语法也是如此。如果类没有改变,那么编译器可以优化掉Class.forName("A"),只使用A(我的意思是对类对象的引用,从而避免按名称查找)。
  • 这个small piece of code 演示了Holger 所说的:该类可以按需加载。
  • @Bakuriu:仅仅因为调用了相同的代码并不意味着编译器知道调用Class.forName("A") a/ 会产生相同的结果并且 b/ 不会改变过程中的其他内容。由于new 关键字,编译器将new A 标识为给定类的构造函数。 Class.forName("A") 是对静态方法的任何调用,编译器对此一无所知。
【解决方案2】:

使用反射Class.forName("A").newInstance(); 不仅是一项代价高昂的操作(因为需要在运行时委派正确的类加载器并加载该类),而且还会使您的代码更难调试并且您失去了类型安全的所有优势(发生在编译期间)。

结论:
避免反射,除非您必须使用它(例如,如果您正在编写面向方面的插件/库)

【讨论】:

    【解决方案3】:

    new操作符和newInstance方法的区别如下:

    • new 运算符可以与类的任何构造函数一起使用,方法是传递构造函数接受的任意数量的参数。 newInstance 方法要求在调用它的类中存在无参数构造函数。如果要使用带有 newInstance 的构造函数,则需要为任何构造函数获取 Constructor 类的实例,然后调用 newInstance,如下所示:

      Class class = Class.forName("A");  
      Constructor const = class.getConstructor(A.class);  
      A a = null;  
      const.newInstance(a);
    • 使用 new 运算符不需要显式加载类,因为它由 JVM 内部处理。 对于 newInstance() 方法,需要该类的 Class 对象的实例(Class.forName(“A”).newInstance(); 或如上点所示)。通过调用forName方法获得引用底层类的Class对象。

    • 当类名在编译时已知时,建议使用 new 运算符。 由于newInstance使用反射来创建类的对象,所以建议在编译时不知道类而在运行时确定时使用。

    • 由于new运算符中没有像forName这样的方法调用相关的额外处理,所以比newInstance快。 使用 newInstance 会导致部分 JVM 进行额外处理(类型检查、安全检查),因此出于性能下降的原因不建议使用。(至少在使用 newInstance 创建数千个实例时)

    • 所有 Java 开发人员都应该了解 new 运算符,因为它是初学者级别的基本概念,因此没有什么特别需要学习的。 并非所有开发应用程序的开发人员都知道反射,因此对于使用 newInstance 方法处理代码的初学者来说,有一个学习曲线。

    • 您可以看到在任何普通 Java 程序中都使用了 new 运算符。 newInstance 正在 Java 内部的多个地方使用,尤其是在服务器端,例如加载和实例化 servlet、applet、JNDI 存根/骨架、JDBC 数据库驱动程序。

    • 使用new操作符,类加载和对象创建由JDK默认的类加载器完成。但是使用newInstance方法,可以显式指定类加载器用于加载类和对象实例化。

    • 使用 new 运算符时出现运行时异常的可能性非常小。只有极少数情况是该类在编译期间存在,但在运行时在类路径中不可用。 使用带有 Class.forName(String ...) 的 newInstance 方法可能会导致运行时异常,即使作为参数传递给 forName 方法的类名无效。

    • 使用 new 运算符会导致在 .class 文件中生成相应的字节码。 使用 newInstance 时,由于对象创建是动态处理的,因此类内部不会为创建对象生成额外的字节码。

    • 使用 new 运算符时,存在固有的类型检查,如果类不存在,则会显示编译器错误。 由于类名作为参数作为字符串传递给 Class.forName 方法,因此没有编译类型检查,并且通常会导致运行时异常,如前面几点所述。

    参考:http://www.javaexperience.com/difference-between-new-operator-and-class-forname-newinstance/

    【讨论】:

      【解决方案4】:

      传统的newnewInstance 之间的主要区别在于,newInstance 允许灵活地实例化一个直到运行时才知道的类,并使您的代码更加动态。如果直到运行时才知道该类,那么您应该使用反射。

      Javadoc,调用Class.forName(String)返回与具有给定字符串名称的类或接口关联的Class对象,即它返回Class A

      所以A a = (A) Class.forName(“A”).newInstance() 分解为:

      • Class.forName(“A”)
        返回 Class 类型的 Class A。

      • Class.forName(“A”).newInstance() 创建由此 Class 对象表示的类的新实例,因此您将获得 A 类型的实例。该类被实例化,就好像由具有空参数列表的新表达式一样。如果尚未初始化该类,则将其初始化。这实际上等同于 new A() 并返回 A 的新实例。

        重要提示: 使用此方法可以有效地绕过编译时异常检查,否则会由编译器执行。

      参考:

      【讨论】:

        【解决方案5】:

        您用来测量速度的测试不是有效的测试。 Java 性能非常复杂,首先涉及到热点虚拟机、智能编译器和垃圾收集器。

        在方法 1 中,java 通常足够聪明,只在内存中创建 1 个 A 实例并在每次迭代中重用它

        在方法 2 中,您强制 VM 使用反射和类加载器来生成 Object。 这本身就已经很慢了

        【讨论】:

        • 这是完全错误的。 Java 甚至会在method1() 中创建一个新实例。
        • 也许我没说清楚,当然它会创建一个新实例,但虚拟机不必为它分配新内存。重用之前的分配是明智之举
        • 即使这样,Java 也不会重用内存,因为这是 GC 要做的工作。没有这种“优化”,GC 逻辑本身就足够复杂了。
        • Java 在这两种情况下都可能重用相同的内存。它达到了需要 N 个字节的某个点,它们要么与上次在同一个地方,要么不在。这两种情况没有区别。
        猜你喜欢
        • 1970-01-01
        • 2011-01-06
        • 1970-01-01
        • 2012-08-25
        • 1970-01-01
        • 1970-01-01
        • 2015-02-24
        • 2019-03-28
        • 2011-12-28
        相关资源
        最近更新 更多