36

阅读这个问题的答案时出现了这个问题 - 如何在 java 中加入两个列表。这个答案给出了解决方案

List<String> newList = new ArrayList<String>() { { addAll(listOne); addAll(listTwo); } };

阅读评论,用户表示它是邪恶和丑陋的,不应该在生产中使用。

我想知道使用这个有什么害处?为什么在生产中使用它是丑陋、邪恶或不好的?

4

6 回答 6

50

除了已经提到的关于良好编程风格和继承滥用的问题之外,还有一个更微妙的问题——内部类和(非静态)匿名类实例充当闭包。这意味着它们保留对封闭类实例的隐式引用。这可能会导致阻止垃圾收集并最终导致内存泄漏。

给出一个示例源代码:

public interface Inner {
    void innerAction();
}

public class Outer {

    public void methodInOuter() {}

    private Inner inner = new Inner() {
        public void innerAction() {
            // calling a method outside of scope of this anonymous class
            methodInOuter();  
        }
    }
}

在编译时发生的事情是,编译器为新的匿名子类创建一个类文件,该子类Inner获取一个所谓的合成字段,并引用Outer类的实例。生成的字节码大致相当于这样:

public class Outer$1 implements Inner {

    private final Outer outer; // synthetic reference to enclosing instance

    public Outer$1(Outer outer) {
        this.outer = outer;
    }

    public void innerAction() {
        // the method outside of scope is called through the reference to Outer
        outer.methodInOuter();
    }
}

即使对于从未实际访问封闭类的任何方法或字段的匿名类(例如您问题中的双括号初始化(DBI)列表),也会发生对封闭实例的引用的这种捕获。

这导致 DBI 列表只要存在就保留对封闭实例的引用,从而防止封闭实例被垃圾收集。假设 DBI 列表恰好在应用程序中存在很长时间,例如作为 MVC 模式中模型的一部分,并且捕获的封闭类例如是 a JFrame,这是一个包含很多字段的相当大的类。如果您创建了几个 DBI 列表,您将很快发生内存泄漏。

一种可能的解决方案是仅在静态方法中使用 DBI,因为在它们的范围内没有可用的封闭实例。

另一方面,我仍然认为在大多数情况下仍然没有必要使用 DBI。至于列表加入,我会创建一个简单的可重用方法,不仅更安全,而且更简洁明了。

public static <T> List<T> join(List<? extends T> first, List<? extends T> second) {
    List<T> joined = new ArrayList<>();
    joined.addAll(first);
    joined.addAll(second);
    return joined;
}

然后客户端代码变得简单:

List<String> newList = join(listOne, listTwo);

进一步阅读: https ://stackoverflow.com/a/924536/1064809

于 2013-05-06T11:24:17.547 回答
20

“丑陋”和“不要在生产中使用”评论指的是匿名类的这种特定用途,而不是一般的匿名类。

这种特定用途分配newList了 的匿名子类ArrayList<String>,这是一个全新的类,其创建的目的只有一个 - 即用两个特定列表的内容初始化一个列表。这不是很易读(即使是有经验的读者也会花几秒钟的时间弄清楚),但更重要的是,它可以在不使用相同数量的操作进行子类化的情况下实现。

本质上,该解决方案通过创建一个新的子类来获得一点便利,这可能会导致以后出现问题,例如,在您尝试使用一个期望集合具有特定类型的自动化框架来持久化这个集合的情况下。

于 2013-05-06T10:50:51.137 回答
16

匿名类的这种特殊用途有几个问题:

  1. 这是一个鲜为人知的成语。不知道它(或知道它但不经常使用它)的开发人员在阅读和/或修改使用它的代码时会变慢。
  2. 它实际上是在滥用语言功能:您不是在尝试定义一种新的ArrayList,您只是想要一些包含一些现有值的数组列表
  3. 它创建了一个占用资源的新类:用于保存类定义的磁盘空间,解析/验证/...它的时间,用于保存类定义的 permgen,...
  4. 即使“真正的代码”稍长,也可以很容易地移入一个恰当命名的实用程序方法 ( joinLists(listOne, listTwo))

在我看来,#1 是避免它的最重要原因,紧随其后的是 #2。#3通常不是什么大问题,但不应忘记。

于 2013-05-06T10:48:56.250 回答
4

因为您不需要单独的子类 - 您只需要创建一个普通类的新 ArrayList ,并将addAll()两个列表都放入其中。

像这样:

public static List<String> addLists (List<String> a, List<String> b) {
    List<String> results = new ArrayList<String>();
    results.addAll( a);
    results.addAll( b); 
    return results;
}

创建一个不需要的子类是邪恶的。您不需要扩展或子类化行为- 只需更改数据值

于 2013-05-06T10:47:26.530 回答
2

这本身并不是一个糟糕的方法,比如说,在性能或类似的方面,但是这种方法有点晦涩,当使用这样的东西时,你总是(比如说,99%)必须解释这种方法。我认为这是不使用这种方法的最大原因之一,并且在输入时:

List<String> newList = new ArrayList<String>();
newList.addAll(listOne);
newList.addAll(listTwo);

打字多一点,阅读起来更容易一些,这对理解或调试代码有很大帮助。

于 2013-05-06T10:49:19.803 回答
0

在你的例子中,它看起来真的很邪恶和丑陋,至少对我来说 - 很难理解代码中发生了什么。但是有一些人们习惯于使用匿名类的模式,因为他们经常看到它们,例如

    Arrays.sort(args, new Comparator<String>() {
        public int compare(String o1, String o2) {
            return  ... 
        }});

我将上述称为最佳实践案例。

于 2013-05-06T11:05:48.867 回答