使用匿名类有什么风险?

36

在阅读这个问题 - 如何在Java中连接两个列表的答案时,我产生了一个问题。这个答案提供了解决方案

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

阅读评论,用户们表示这是恶意的、丑陋的,不应在生产中使用。

我想知道使用它有什么危害?为什么在生产中使用它会被认为是丑陋、恶意或不好的?


1
或许这会让其他开发者感到困惑,毕竟它不仅仅是一行代码。 - Subin Sebastian
2
可能是Java“双括号初始化”的效率问题的重复问题。 - assylias
4
链接的问题主要涉及性能。我认为这不是重复的问题。 - Joachim Sauer
2
@assylias,你把这个关于丑陋问题的问题标记为与一个关于性能的问题 完全相同 吗?是的,它涉及相同的主题。不,你没有阅读过任何一篇文章。 - Daniel A.A. Pelsmaeker
@Virtlink 我已经阅读了两个问题。这个问题关注的是邪恶、丑陋和糟糕,本来可以被关闭,因为它不具有建设性。现在事实是,这个习语在功能上等同于其他替代方案,并且如果你知道Java语言,它也很容易阅读。因此,唯一客观的区别就是实现效率。顺便说一句,这也是最受欢迎的答案所关注的。只是我的个人看法。 - assylias
6个回答

51

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

给出源代码的一个示例:

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模式中的模型的一部分,并且捕获的封闭类是一个具有许多字段的较大类,例如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://dev59.com/pnNA5IYBdhLWcg3wh-YC#924536

3
同样也是正确的。滥用继承,而不是设置已经完美可用的数据属性(值/或集合项),就是非常邪恶的行为。 - Thomas W
2
+1 表明在“静态”方法中使用 DBI 会造成更少的损害。 - gaborsch
1
此外,DBI 意味着许多“equals”比较将失败,因为它们检查类,而这些类在技术上是不同的(不是ArrayList,而是MyClass$1)。 - wchargin
1
@WChargin: ArrayList.equals() 不会检查类,因为这会破坏 Listequals() 契约! - Joachim Sauer

20

"丑陋"和"不要在生产中使用"的评论是指特定使用匿名类的情况,而不是匿名类本身。

这个特定的用法将newList赋值为ArrayList<String>的一个匿名子类,完全是为了初始化一个列表,并填充两个具体列表的内容。这种方式不太易读(即使对有经验的读者来说也需要花费几秒钟才能理解),但更重要的是,它可以在同样数量的操作中实现而无需子类化。

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


+1 是指出问题不在于匿名类本身。 - Joachim Sauer

16

使用匿名类的这种特殊用法存在几个问题:

  1. 这是一个鲜为人知的习惯用法。不熟悉该用法(或者知道但不经常使用它的)开发人员在阅读和/或修改使用该用法的代码时会减慢速度。
  2. 实际上,这是在误用语言特性:您并没有尝试定义一种新的 ArrayList,而只是想要一些带有某些现有值的数组列表。
  3. 它创建了一个占用资源的新类:磁盘空间用于保存类定义、解析 / 验证 / ... 它的时间、持有类定义的永久代等等。
  4. 即使 "真正的代码" 稍微长一些,也可以轻松地将其移动到一个合适命名的实用方法中 (joinLists(listOne, listTwo))。

在我看来,避免第一点是最重要的原因,紧随其后的是第二点。第三点通常不是那么大的问题,但也不应该被忘记。


3
当不需要继承时,却使用继承是错误的。 - Thomas W
@ThomasW:是的,我认为那是我的第二选择,或者你有不同的理解吗? - Joachim Sauer
  1. [...] 在加载“.class”文件期间,JVM验证类的时间,...
- Joker_vD
@Joker_vD:我在第三点中添加了验证。但这不应该是一个详尽的列表,我只是想举一些资源作为例子。关于这种方法所需资源的精确计算超出了本答案的范围。 - Joachim Sauer
2
谢谢Joachim。我的观点是#2是关键--当仅仅改变/设置可变对象的数据可以正确地实现目标时,不应使用继承。 - Thomas W

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;
}

创建不必要的子类是一种不好的做法。你不需要扩展或者继承行为,只需改变数据值即可。


你可以用 <T> 替换 <String>,并在 static 后添加一个 <T>,这样就可以在所有的列表上运行。另外,加入可变参数会很好,这样你就可以连接任意数量的列表。 - Joachim Sauer
1
我在我的库中有这个 :) 但是人们会犹豫展示任何复杂的东西,因为一开始就可能出现错误。正确性应该像简单的闪电一样显而易见。 - Thomas W

2

这种方法本身并不是不好的,比如在性能方面等,但这种方法有点晦涩难懂,而且使用类似的东西时,你总是(比如说99%)需要解释这种方法。我认为这是不使用这种方法的最大原因之一,在打字时也是如此。

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

虽然需要输入较多的代码,但阅读起来更容易,这对于理解或调试代码非常有帮助。


0
在你的例子中,它看起来真的很邪恶和丑陋,至少对我来说 - 很难理解代码中发生了什么。但是有一些使用匿名类的模式,人们习惯于使用它们,因为他们经常看到它们,例如:
    Arrays.sort(args, new Comparator<String>() {
        public int compare(String o1, String o2) {
            return  ... 
        }});

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接