11

我对一段代码有如下风格的评论:

Iterable<String> upperCaseNames = Iterables.transform(
    lowerCaseNames, new Function<String, String>() {
        public String apply(String input) {
            return input.toUpperCase();
        }
    });

该人说,每次我浏览这段代码时,我都会实例化这个匿名函数类,我宁愿在一个静态变量中拥有一个实例:

static Function<String, String> toUpperCaseFn =
    new Function<String, String>() {
        public String apply(String input) {
            return input.toUpperCase();
        }
    };
...
Iterable<String> upperCaseNames =
    Iterables.transform(lowerCaseNames, toUpperCaseFn);

在非常肤浅的层面上,这在某种程度上是有道理的。多次实例化一个类必须浪费内存或其他东西,对吧?

另一方面,人们在代码中间实例化匿名类,就像没有明天一样,编译器将其优化掉是微不足道的。

这是一个有效的担忧吗?

4

3 回答 3

8

关于 Hot Spot JVM 优化的有趣事实,如果您实例化一个未在当前方法之外传递的对象,JVM 将在字节码级别执行优化。

通常,堆栈分配与公开内存模型的语言相关联,例如 C++。您不必delete在 C++ 中堆叠变量,因为它们会在退出范围时自动释放。这与堆分配相反,堆分配要求您在完成后删除指针。

在 Hot Spot JVM 中,字节码被分析以确定对象是否可以“逃脱”线程。逃生分为三个级别

  1. 没有转义 - 对象仅在它创建的方法/范围内使用,并且无法在线程外部访问该对象。
  2. 本地/参数转义 - 对象由创建它的方法返回或传递给它调用的方法,但这些方法都不会将该对象放在可以在线程之外访问的某个位置。
  3. 全局转义 - 将对象放在可以在另一个线程中访问的某个位置。

这基本上类似于问题,1)我是否将它传递给另一个方法或返回它,以及 2)我是否将它与附加到 GC 根的ClassLoader东西(如 a 或存储在static字段中的东西)相关联?

在您的特定情况下,匿名对象将被标记为“本地转义”,这仅意味着synchronized对象上的任何锁(阅读:使用 of )都将被优化掉。(为什么要在另一个线程中永远不会使用的东西上进行同步?)这与“不转义”不同,后者在堆栈上进行分配。需要注意的是,这种“分配”与堆分配不同。它真正做的是在堆栈上为非转义对象内的所有变量分配空间。如果你在 no-escape 对象中有 3 个字段 、intStringMyObject那么将分配三个堆栈变量:一个int、一个String引用和一个MyObject 引用——MyObject实例本身仍将存储在堆中,除非它也被分析为“无法逃脱”。然后优化对象分配,构造函数/方法将使用本地堆栈变量而不是堆变量运行。

话虽如此,对我来说这听起来像是过早的优化。除非后来证明代码很慢并且导致性能问题,否则您不应该做任何事情来降低其可读性。对我来说,这段代码非常易读,我会不理会它。当然,这完全是主观的,但是“性能”并不是更改代码的好理由,除非它与实际运行时间有关。通常,过早的优化会导致代码更难维护,而性能收益却很小。

Java 8+ 和 Lambda

如果分配匿名实例仍然困扰您,我建议切换到将 Lambdas 用于单一抽象方法 (SAM) 类型。Lambda 评估是使用 执行invokedynamic的,并且实现最终在第一次调用时只创建一个 Lambda 实例。更多细节可以在我的答案这里的答案中找到。对于非 SAM 类型,您仍然需要分配一个匿名实例。在大多数用例中,这里的性能影响可以忽略不计,但 IMO,这种方式更具可读性。

参考

于 2013-10-31T23:44:12.337 回答
1

简短的回答:不-别担心。

长答案:这取决于您实例化它的频率。如果在一个经常调用的紧密循环中,也许——尽管请注意,当函数被应用时,它会String.toUpperCase()为 an 中的每个项目调用一次Iterable——每个调用可能会创建一个 new String,这将产生更多的 GC 流失。

“过早的优化是万恶之源”——Knuth

于 2013-10-31T23:40:10.250 回答
1

找到这个线程:Java 匿名类效率的影响,你可能会觉得很有趣

做了一些微基准测试。微基准是在以下之间进行比较:每次循环迭代实例化一个(静态内部)类,实例化一个(静态内部)类一次并在循环中使用它,以及两个相似但具有匿名类的类。对于微基准测试,编译器似乎将匿名类从循环中提取出来,并且正如预测的那样,将匿名类提升为调用者的内部类。这意味着所有四种方法在速度上都无法区分。我还将它与外部课程进行了比较,同样的速度。具有匿名类的那个可能多占用了大约 128 位空间

您可以在http://jdmaguire.ca/Code/Comparing.javahttp://jdmaguire.ca/Code/OutsideComp.java查看我的微基准测试。我在 wordLen、sortTimes 和 listLen 的各种值上运行了这个。同样,JVM 的预热很慢,所以我改变了方法调用。请不要因为糟糕的未注释代码而评判我。我的编程比 RL 更好。而且微基准标记几乎和过早优化一样邪恶和无用。

于 2013-11-01T00:29:05.223 回答