Java“双Brace初始化”的效率?


Answers

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

这一点,以及即使一个普通的HashSet工作得很好(甚至更好),一个新类创建的事实,使我不想使用这个构造(即使我真的渴望语法糖)。

第二个问题:新的HashSet必须是实例初始化程序中使用的“this”......任何人都可以阐明该机制? 我会天真地期待“this”引用初始化“flavor”的对象。

这就是内部类的工作方式。 他们获得了他们自己的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){

            }

        };
    }
Question

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,可变参数方法,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




1) This will call add() for each member. If you can find a more efficient way to put items into a hash set, then use that. Note that the inner class will likely generate garbage, if you're sensitive about that.

2) It seems to me as if the context is the object returned by "new," which is the HashSet.

3) If you need to ask... More likely: will the people who come after you know this or not? Is it easy to understand and explain? If you can answer "yes" to both, feel free to use it.




容易泄漏

我已决定插入。性能影响包括:磁盘操作+ unzip(用于jar),类验证,perm-gen空间(用于Sun的Hotspot JVM)。 然而,最糟糕的是:它容易泄漏。 你不能简单地返回。

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

因此,如果该集转义到其他类加载器加载的其他部分,并且引用保留在那里,则类+ classloader的整个树将被泄漏。 为了避免这种情况,需要拷贝到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



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

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

这是我的结论

我惊讶地发现,在大多数运行测试中,内部启动实际上更快(在某些情况下几乎是两倍)。 在大量工作时,好处似乎消失。

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

然而,很显然,在大多数情况下,即使数量庞大,单个项目建设的开销也不会太大。

其中一个原因是每个双花括号启动创建一个新的类文件,它将整个磁盘块添加到我们的应用程序的大小(或压缩时约为1k)。 占地面积小,但如果它在很多地方使用,它可能会产生影响。 使用这1000次,你可能会添加一个完整的MiB给你的应用程序,这可能与嵌入式环境有关。

我的结论是? 只要没有被滥用,就可以使用。

让我知道你的想法 :)




虽然这个语法可以很方便,但它也增加了很多$ 0引用,因为这些引用变得嵌套,并且除非在每个引用上设置了断点,否则可能很难将其调试到初始化程序中。 For that reason, I only recommend using this for banal setters, especially set to constants, and places where anonymous subclasses don't matter (like no serialization involved).




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

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

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

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




Links