繁体   English   中英

Java“双括号初始化”的效率?

[英]Efficiency of Java "Double Brace Initialization"?

Hidden Features of Java 中,最佳答案提到了Double Brace Initialization ,其语法非常诱人:

Set<String> flavors = new HashSet<String>() {{
    add("vanilla");
    add("strawberry");
    add("chocolate");
    add("butter pecan");
}};

这个习惯用法创建了一个匿名内部类,其中只有一个实例初始值设定项,它“可以使用包含范围内的任何 [...] 方法”。

主要问题:这是否像听起来那么低效 它的使用是否应该仅限于一次性初始化? (当然还有炫耀!)

第二个问题:新的 HashSet 必须是实例初始值设定项中使用的“this”……谁能解释一下这个机制?

第三个问题:这个习语是否太晦涩而无法在生产代码中使用?

总结:非常非常好的答案,谢谢大家。 关于问题 (3),人们认为语法应该很清楚(尽管我建议偶尔发表评论,特别是如果您的代码将传递给可能不熟悉它的开发人员时)。

在问题 (1) 上,生成的代码应该运行得很快。 额外的 .class 文件确实会导致 jar 文件混乱,并且会稍微减慢程序启动速度(感谢 @coobird 对此进行了测量)。 @Thilo 指出垃圾收集可能会受到影响,在某些情况下,额外加载的类的内存成本可能是一个因素。

问题(2)对我来说是最有趣的。 如果我理解答案,那么 DBI 中发生的事情是匿名内部类扩展了由 new 运算符构造的对象的类,因此具有引用正在构造的实例的“this”值。 非常整洁。

总的来说,DBI 给我留下了一种知识上的好奇心。 Coobird 和其他人指出,您可以使用 Arrays.asList、varargs 方法、Google Collections 和提议的 Java 7 Collection 文字实现相同的效果。 Scala、JRuby 和 Groovy 等较新的 JVM 语言也为列表构造提供了简洁的符号,并且可以与 Java 很好地互操作。 鉴于 DBI 弄乱了类路径,稍微减慢了类加载速度,并使代码更加晦涩难懂,我可能会回避它。 但是,我计划将这个介绍给一个刚刚获得 SCJP 并且喜欢关于 Java 语义的善意较量的朋友! ;-) 谢谢大家!

7/2017:Baeldung 对双括号初始化做了很好的总结,并将其视为一种反模式。

12/2017:@Basil Bourque 指出,在新的 Java 9 中,您可以说:

Set<String> flavors = Set.of("vanilla", "strawberry", "chocolate", "butter pecan");

那肯定是要走的路。 如果您坚持使用早期版本,请查看Google Collections 的 ImmutableSet

当我对匿名内部类过于痴迷时,问题就出现了:

2009/05/27  16:35             1,602 DemoApp2$1.class
2009/05/27  16:35             1,976 DemoApp2$10.class
2009/05/27  16:35             1,919 DemoApp2$11.class
2009/05/27  16:35             2,404 DemoApp2$12.class
2009/05/27  16:35             1,197 DemoApp2$13.class

/* snip */

2009/05/27  16:35             1,953 DemoApp2$30.class
2009/05/27  16:35             1,910 DemoApp2$31.class
2009/05/27  16:35             2,007 DemoApp2$32.class
2009/05/27  16:35               926 DemoApp2$33$1$1.class
2009/05/27  16:35             4,104 DemoApp2$33$1.class
2009/05/27  16:35             2,849 DemoApp2$33.class
2009/05/27  16:35               926 DemoApp2$34$1$1.class
2009/05/27  16:35             4,234 DemoApp2$34$1.class
2009/05/27  16:35             2,849 DemoApp2$34.class

/* snip */

2009/05/27  16:35               614 DemoApp2$40.class
2009/05/27  16:35             2,344 DemoApp2$5.class
2009/05/27  16:35             1,551 DemoApp2$6.class
2009/05/27  16:35             1,604 DemoApp2$7.class
2009/05/27  16:35             1,809 DemoApp2$8.class
2009/05/27  16:35             2,022 DemoApp2$9.class

这些都是我在制作一个简单的应用程序时生成的所有类,并使用了大量匿名内部类——每个类都将被编译成一个单独的class文件。

正如已经提到的,“双括号初始化”是一个带有实例初始化块的匿名内部类,这意味着为每个“初始化”创建一个新类,通常都是为了创建单个对象。

考虑到 Java 虚拟机在使用它们时需要读取所有这些类,这可能会导致字节码验证过程等一些时间。 更不用说为了存储所有这些class文件所需的磁盘空间的增加。

使用双大括号初始化时似乎有一些开销,因此过度使用它可能不是一个好主意。 但正如埃迪在评论中指出的那样,不可能绝对确定影响。


仅供参考,双括号初始化如下:

List<String> list = new ArrayList<String>() {{
    add("Hello");
    add("World!");
}};

它看起来像是 Java 的一个“隐藏”特性,但它只是对以下内容的重写:

List<String> list = new ArrayList<String>() {

    // Instance initialization block
    {
        add("Hello");
        add("World!");
    }
};

所以它基本上是一个实例初始化块,它是匿名内部类的一部分


Joshua Bloch 对Project CoinCollection Literals 提案大致如下:

List<Integer> intList = [1, 2, 3, 4];

Set<String> strSet = {"Apple", "Banana", "Cactus"};

Map<String, Integer> truthMap = { "answer" : 42 };

遗憾的是,它既没有进入 Java 7 也没有进入 Java 8 并且被无限期搁置。


实验

这是我测试过的简单实验——使用元素"Hello""World!"制作 1000 个ArrayList s 通过add方法添加到它们中,使用两种方法:

方法一:双括号初始化

List<String> l = new ArrayList<String>() {{
  add("Hello");
  add("World!");
}};

方法二:实例化一个ArrayListadd

List<String> l = new ArrayList<String>();
l.add("Hello");
l.add("World!");

我创建了一个简单的程序来写出一个 Java 源文件来使用两种方法执行 1000 次初始化:

测试 1:

class Test1 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    List<String> l1 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    /* snip */

    List<String> l999 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    System.out.println(System.currentTimeMillis() - st);
  }
}

测试 2:

class Test2 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>();
    l0.add("Hello");
    l0.add("World!");

    List<String> l1 = new ArrayList<String>();
    l1.add("Hello");
    l1.add("World!");

    /* snip */

    List<String> l999 = new ArrayList<String>();
    l999.add("Hello");
    l999.add("World!");

    System.out.println(System.currentTimeMillis() - st);
  }
}

请注意,使用System.currentTimeMillis检查初始化 1000 个ArrayList和扩展ArrayList的 1000 个匿名内部类所用的时间,因此计时器的分辨率不是很高。 在我的 Windows 系统上,分辨率约为 15-16 毫秒。

两次测试运行 10 次的结果如下:

Test1 Times (ms)           Test2 Times (ms)
----------------           ----------------
           187                          0
           203                          0
           203                          0
           188                          0
           188                          0
           187                          0
           203                          0
           188                          0
           188                          0
           203                          0

可以看出,双括号初始化有一个明显的执行时间大约 190 毫秒。

同时, ArrayList初始化执行时间为 0 毫秒。 当然要考虑到定时器的分辨率,但很可能在15ms以下。

因此,这两种方法的执行时间似乎存在明显差异。 看起来这两种初始化方法确实存在一些开销。

是的,有 1000 个.class文件是通过编译Test1双括号初始化测试程序生成的。

迄今为止尚未指出的这种方法的一个特性是,因为您创建了内部类,所以整个包含类都被捕获在其范围内。 这意味着只要您的 Set 还活着,它就会保留一个指向包含实例( this$0 )的指针,并防止它被垃圾收集,这可能是一个问题。

这一点,以及尽管常规 HashSet 可以正常工作(甚至更好),但首先创建了一个新类的事实使我不想使用这个构造(即使我真的很渴望语法糖)。

第二个问题:新的 HashSet 必须是实例初始值设定项中使用的“this”……谁能解释一下这个机制? 我天真地期望“this”指代初始化“flavors”的对象。

这就是内部类的工作方式。 它们有自己的this ,但它们也有指向父实例的指针,因此您也可以调用包含对象的方法。 在命名冲突的情况下,内部类(在您的情况下为 HashSet)优先,但您也可以在“this”前加上类名以获取外部方法。

public class Test {

    public void add(Object o) {
    }

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // HashSet
              Test.this.add("hello"); // outer instance 
            }
        };
    }
}

要明确创建的匿名子类,您也可以在其中定义方法。 例如覆盖HashSet.add()

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // not HashSet anymore ...
            }

            @Override
            boolean add(String s){

            }

        };
    }

每次有人使用双括号初始化时,都会有一只小猫被杀死。

除了语法相当不寻常且不是真正惯用的(当然,品味值得商榷)之外,您在应用程序中不必要地创建了两个重大问题, 我最近刚刚在博客中更详细地介绍了这些问题

1. 你创建了太多匿名类

每次使用双括号初始化时,都会创建一个新类。 例如这个例子:

Map source = new HashMap(){{
    put("firstName", "John");
    put("lastName", "Smith");
    put("organizations", new HashMap(){{
        put("0", new HashMap(){{
            put("id", "1234");
        }});
        put("abc", new HashMap(){{
            put("id", "5678");
        }});
    }});
}};

...将产生这些类:

Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class

这对您的类加载器来说是相当多的开销 - 一无所获! 当然,如果你做一次,它不会花费太多初始化时间。 但是,如果您在整个企业应用程序中执行此操作 20'000 次...所有堆内存只是为了一点“语法糖”?

2. 您可能会造成内存泄漏!

如果您采用上述代码并从方法返回该映射,则该方法的调用者可能会毫无戒心地占用无法被垃圾收集的非常重的资源。 考虑以下示例:

public class ReallyHeavyObject {

    // Just to illustrate...
    private int[] tonsOfValues;
    private Resource[] tonsOfResources;

    // This method almost does nothing
    public Map quickHarmlessMethod() {
        Map source = new HashMap(){{
            put("firstName", "John");
            put("lastName", "Smith");
            put("organizations", new HashMap(){{
                put("0", new HashMap(){{
                    put("id", "1234");
                }});
                put("abc", new HashMap(){{
                    put("id", "5678");
                }});
            }});
        }};

        return source;
    }
}

返回的Map现在将包含对封闭的ReallyHeavyObject实例的ReallyHeavyObject 您可能不想冒险:

内存泄漏就在这里

图片来自http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/

3. 你可以假装 Java 有地图文字

为了回答您的实际问题,人们一直在使用这种语法来假装 Java 具有类似于地图文字的东西,类似于现有的数组文字:

String[] array = { "John", "Doe" };
Map map = new HashMap() {{ put("John", "Doe"); }};

有些人可能会发现这在句法上很刺激。

参加以下测试课程:

public class Test {
  public void test() {
    Set<String> flavors = new HashSet<String>() {{
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }};
  }
}

然后反编译类文件,我看到:

public class Test {
  public void test() {
    java.util.Set flavors = new HashSet() {

      final Test this$0;

      {
        this$0 = Test.this;
        super();
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
    };
  }
}

这在我看来并不是非常低效。 如果我担心这样的性能,我会分析它。 上面的代码回答了您的问题 #2:您在内部类的隐式构造函数(和实例初始值设定项)中,所以“ this ”指的是这个内部类。

是的,这个语法很晦涩,但是注释可以澄清晦涩的语法用法。 为了澄清语法,大多数人都熟悉静态初始化程序块(JLS 8.7 静态初始化程序):

public class Sample1 {
    private static final String someVar;
    static {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

您还可以使用类似的语法(没有“ static ”一词)用于构造函数的使用(JLS 8.6 实例初始化程序),尽管我从未见过在生产代码中使用过这种语法。 这是鲜为人知的。

public class Sample2 {
    private final String someVar;

    // This is an instance initializer
    {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

如果您没有默认构造函数,那么{}之间的代码块将被编译器转换为构造函数。 考虑到这一点,解开双括号代码:

public void test() {
  Set<String> flavors = new HashSet<String>() {
      {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
  };
}

最里面的大括号之间的代码块被编译器转换为构造函数。 最外面的大括号界定匿名内部类。 采取这最后一步,使一切都非匿名:

public void test() {
  Set<String> flavors = new MyHashSet();
}

class MyHashSet extends HashSet<String>() {
    public MyHashSet() {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }
}

出于初始化目的,我想说没有任何开销(或者小到可以忽略)。 但是,每次使用flavors都不会反对HashSet ,而是反对MyHashSet 这可能有很小的(很可能可以忽略不计)开销。 但同样,在我担心之前,我会分析它。

再次,对于您的问题 #2,上面的代码是双括号初始化的逻辑和显式等价物,并且很明显“ this ”指的是:扩展HashSet的内部类。

如果您对实例初始化程序的详细信息有疑问,请查看JLS文档中的详细信息。

容易泄漏

我决定插一句。性能影响包括:磁盘操作+解压缩(用于jar)、类验证、永久空间(用于Sun的Hotspot JVM)。 然而,最糟糕的是:它容易泄漏。 你不能简单地返回。

Set<String> getFlavors(){
  return Collections.unmodifiableSet(flavors)
}

因此,如果该集合转义到由不同类加载器加载的任何其他部分,并且引用保留在那里,则整个类+类加载器树将被泄漏。 为了避免这种情况,需要复制到 HashMap, new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}}) 。不再那么可爱了。我不使用习语,我自己,而是像new LinkedHashSet(Arrays.asList("xxx","YYY"));

加载许多类可能会在开始时增加几毫秒。 如果启动不是那么重要,并且您在启动后查看类的效率,则没有区别。

package vanilla.java.perfeg.doublebracket;

import java.util.*;

/**
 * @author plawrey
 */
public class DoubleBracketMain {
    public static void main(String... args) {
        final List<String> list1 = new ArrayList<String>() {
            {
                add("Hello");
                add("World");
                add("!!!");
            }
        };
        List<String> list2 = new ArrayList<String>(list1);
        Set<String> set1 = new LinkedHashSet<String>() {
            {
                addAll(list1);
            }
        };
        Set<String> set2 = new LinkedHashSet<String>();
        set2.addAll(list1);
        Map<Integer, String> map1 = new LinkedHashMap<Integer, String>() {
            {
                put(1, "one");
                put(2, "two");
                put(3, "three");
            }
        };
        Map<Integer, String> map2 = new LinkedHashMap<Integer, String>();
        map2.putAll(map1);

        for (int i = 0; i < 10; i++) {
            long dbTimes = timeComparison(list1, list1)
                    + timeComparison(set1, set1)
                    + timeComparison(map1.keySet(), map1.keySet())
                    + timeComparison(map1.values(), map1.values());
            long times = timeComparison(list2, list2)
                    + timeComparison(set2, set2)
                    + timeComparison(map2.keySet(), map2.keySet())
                    + timeComparison(map2.values(), map2.values());
            if (i > 0)
                System.out.printf("double braced collections took %,d ns and plain collections took %,d ns%n", dbTimes, times);
        }
    }

    public static long timeComparison(Collection a, Collection b) {
        long start = System.nanoTime();
        int runs = 10000000;
        for (int i = 0; i < runs; i++)
            compareCollections(a, b);
        long rate = (System.nanoTime() - start) / runs;
        return rate;
    }

    public static void compareCollections(Collection a, Collection b) {
        if (!a.equals(b) && a.hashCode() != b.hashCode() && !a.toString().equals(b.toString()))
            throw new AssertionError();
    }
}

印刷

double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 34 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns

要创建集合,您可以使用可变参数工厂方法而不是双括号初始化:

public static Set<T> setOf(T ... elements) {
    return new HashSet<T>(Arrays.asList(elements));
}

Google Collections 库有很多这样的便捷方法,以及大量其他有用的功能。

至于成语的晦涩难懂,我一直在生产代码中遇到并使用它。 我更关心那些被允许编写生产代码的习惯用法弄糊涂的程序员。

除了效率之外,我很少发现自己希望在单元测试之外创建声明式集合。 我确实相信双括号语法非常易读。

实现列表声明式构造的另一种方法是使用Arrays.asList(T ...)如下所示:

List<String> aList = Arrays.asList("vanilla", "strawberry", "chocolate");

这种方法的局限性当然是您无法控制要生成的特定类型列表。

双括号初始化是一种不必要的黑客攻击,可能会导致内存泄漏和其他问题

没有正当理由使用这个“技巧”。 Guava 提供了很好的不可变集合,包括静态工厂和构建器,允许您填充以干净、可读和安全语法声明的集合。

问题中的示例变为:

Set<String> flavors = ImmutableSet.of(
    "vanilla", "strawberry", "chocolate", "butter pecan");

这不仅更短更易于阅读,而且避免了其他答案中描述的双支撑模式的众多问题。 当然,它的性能类似于直接构造的HashMap ,但它很危险且容易出错,并且有更好的选择。

任何时候您发现自己正在考虑双支撑初始化,您应该重新检查您的 API 或引入新的API 以正确解决问题,而不是利用语法技巧。

Error-Prone现在标记这个反模式

通常没有什么特别低效的。 对于 JVM 来说,创建子类并向其添加构造函数通常无关紧要——这是在面向对象的语言中很正常的日常事情。 我可以想到一些非常人为的情况,您可能会通过这样做导致效率低下(例如,您有一个重复调用的方法,由于这个子类,该方法最终混合了不同的类,而普通的类传入将是完全可以预测的- - 在后一种情况下,JIT 编译器可以进行在第一种情况下不可行的优化)。 但实际上,我认为这很重要的情况非常人为。

我会更多地从您是否想用大量匿名类“弄乱”的角度来看待这个问题。 作为一个粗略的指南,请考虑使用这个习语,而不是像使用事件处理程序的匿名类一样。

在 (2) 中,您在对象的构造函数中,因此“this”指的是您正在构造的对象。 这与任何其他构造函数没有什么不同。

至于(3),我猜这真的取决于谁在维护你的代码。 如果您事先不知道这一点,那么我建议使用的基准测试是“您在 JDK 的源代码中看到了吗?” (在这种情况下,我不记得看到过很多匿名初始化程序,当然在匿名类的唯一内容的情况下也不会)。 在大多数中等规模的项目中,我认为您确实需要您的程序员在某些时候了解 JDK 源代码,因此在那里使用的任何语法或习语都是“公平游戏”。 除此之外,我想说,如果您可以控制谁维护代码,则可以对人们进行语法培训,否则评论或避免。

我正在研究这个并决定做一个比有效答案提供的更深入的测试。

这是代码: https : //gist.github.com/4368924

这是我的结论

我惊讶地发现,在大多数运行测试中,内部启动实际上更快(在某些情况下几乎翻了一番)。 当处理大量数据时,好处似乎逐渐消失。

有趣的是,在循环中创建 3 个对象的情况失去了它的好处,比其他情况更快。 我不确定为什么会发生这种情况,应该进行更多测试以得出任何结论。 创建具体的实现可能有助于避免重新加载类定义(如果发生了这种情况)

然而,很明显,在大多数情况下,对于单个项目构建,它观察到的开销并不大,即使数量很大。

一个挫折是这样一个事实,即每个双括号初始化都会创建一个新的类文件,该文件将整个磁盘块添加到我们的应用程序的大小(或压缩时约为 1k)。 占地面积小,但如果在很多地方使用它,它可能会产生影响。 使用此 1000 次,您可能会向您的应用程序添加整个 MiB,这可能与嵌入式环境有关。

我的结论? 只要不被滥用就可以使用。

让我知道你的想法 :)

Mario Gleichman 描述了如何使用 Java 1.5 泛型函数来模拟 Scala 列表文字,尽管遗憾的是您最终得到了不可变列表。

他定义了这个类:

package literal;

public class collection {
    public static <T> List<T> List(T...elems){
        return Arrays.asList( elems );
    }
}

并因此使用它:

import static literal.collection.List;
import static system.io.*;

public class CollectionDemo {
    public void demoList(){
        List<String> slist = List( "a", "b", "c" );
        List<Integer> iList = List( 1, 2, 3 );
        for( String elem : List( "a", "java", "list" ) )
            System.out.println( elem );
    }
}

Google Collections,现在是Guava 的一部分,支持类似的列表构建想法。 这次采访中,Jared Levy 说:

[...] 使用最频繁的特性,几乎出现在我编写的每个 Java 类中,都是静态方法,可以减少 Java 代码中重复击键的次数。 能够输入如下命令非常方便:

Map<OneClassWithALongName, AnotherClassWithALongName> = Maps.newHashMap();

List<String> animals = Lists.immutableList("cat", "dog", "horse");

2014 年 7 月 10 日:如果它可以像 Python 一样简单就好了:

animals = ['cat', 'dog', 'horse']

2/21/2020:在 Java 11 中,您现在可以说:

animals = List.of(“cat”, “dog”, “horse”)

虽然这种语法很方便,但它也添加了很多 this$0 引用,因为这些引用变得嵌套,除非在每个初始化器上都设置断点,否则很难逐步调试到初始化程序中。 出于这个原因,我只建议将它用于平庸的 setter,尤其是设置为常量,以及匿名子类无关紧要的地方(例如不涉及序列化)。

我第二个 Nat 的答案,除了我会使用循环而不是创建并立即从 asList(elements) 中抛出隐式列表:

static public Set<T> setOf(T ... elements) {
    Set set=new HashSet<T>(elements.size());
    for(T elm: elements) { set.add(elm); }
    return set;
    }
  1. 这将为每个成员调用add() 如果您能找到一种更有效的方法将项目放入哈希集中,请使用它。 请注意,如果您对此敏感,内部类可能会生成垃圾。

  2. 在我看来,上下文好像是new返回的对象,也就是HashSet

  3. 如果你需要问……更有可能:你后面来的人会知道吗? 容易理解和解释吗? 如果您可以对两者都回答“是”,请随意使用它。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM