Java(匿名的或非匿名的)内部类:使用它们好吗?

25
在我的一些项目和一些书籍中,有说不要使用内部类(无论是匿名的还是非匿名的、静态的还是非静态的),除了像EventListenerRunnable这样的受限条件外。他们甚至在我第一个工业项目中被“禁止”使用。这是否真的是最佳实践?为什么?
(我必须说我经常使用它们...)
--编辑---
我不能在所有这些回复中选出正确答案:几乎所有回复都有一定程度的正确性:我仍然会使用内部类,但我会尽量少用!
16个回答

33

我认为,在Java代码中,90%的内部类要么是与单个类相关联的实体,因此作为内部类“塞进去”的,要么是由于Java不支持Lambda而存在的匿名内部类。

我个人不喜欢看到复杂的内部类。它们增加了源文件的复杂性,使其变得更大,在调试和分析方面处理起来很麻烦等等。我喜欢将我的项目分成许多包,在这种情况下,我可以将大多数实体作为仅限于该包的顶级类。

这留下了一些必要的内部类,例如动作监听器、虚假的“函数”编程等等。它们通常是匿名的,虽然我不是粉丝(在许多情况下更喜欢Lambda),但我能接受它们但不喜欢它们。

我已经好几年没做过C#了,但我想知道当他们引入Lambda时,内部类或C#等效物的普及率是否会降低。


5
需要翻译的内容:For the reference I have to mention that Java 8 introduces Lambda Expressions。我的翻译:提到参考文献,我必须提到Java 8引入了Lambda表达式 - vellotis

17

代码整洁化。如果将代码分成逻辑上的部分,而不是全部混在同一个文件中,那么它更容易被理解。

话虽如此,我认为明智地使用内部类并不是不恰当的。有时这些内部类只存在于一个目的中,因此我就不会对它们存在于唯一使用它们的文件中感到困扰。然而,在我的经验中,这种情况并不经常发生。


2
根据我的经验,一旦你创建了一个内部类,你总会在其他地方需要它 :) - Dolph
14
如果将代码分解成逻辑部分,确实更容易理解。但是在我的看法中,最好将某些东西定义在其使用的地方,而不是任意地分割成一个新文件。 ;) - Porculus
2
内部类不会阻止你在类外使用它们。枚举是一个很好的例子,枚举通常用于单个类,将其设置为public static,然后你就能明确知道该枚举在什么地方被使用了。工厂模式是内部类非常适合的一个场景,通过内部类来生产实现对象,避免了让一些只应该在某些情况下由工厂对象控制实例化的东西污染包名称空间。 - user177800

6
匿名类在事件驱动编程中特别是在swing中使用非常方便。

1
为什么在事件驱动编程中使用,而不是用于其他用途,比如用于重新组合多个参数的结构体...这更像是一种教条,而不是解释:-1。 - Guillaume

5

是的,禁止使用内部类是一种有用的做法,因为发现一个地方禁止使用它们是警告我不要在那里工作的好方法,从而保护我的未来健康。 :)

正如gicappa指出的那样,匿名内部类是Java最接近闭包的方式,并且非常适合在需要将行为传递到方法中的情况下使用,如果没有其他更好的选择。


1
至少这个让我微笑,而且这是在公司编码政策中发现的有时愚蠢规则的好用法! - Guillaume
关于事件处理程序没有什么特别的要求,嵌套类的主要目的是封装。 - bharatj

4

正如其他人所说,当你使用匿名内部类时,它也会在其他地方使用...

因此,您可能会将内部类代码轻松地复制到许多地方... 当您仅使用非常简单的内部类来过滤/排序集合时,使用谓词、比较器或类似方法似乎不是问题...

但是,您必须知道,当您三次使用匿名内部类来执行完全相同的操作(例如从集合中删除“”),您实际上正在创建3个新类于Java PermGen中。

因此,如果每个人都在各处使用内部类,则可能导致应用程序具有更大的PermGen。根据应用程序,这可能是一个问题...如果您正在工业界工作,则可能编写具有有限内存且需要优化的嵌入式应用程序...

请注意,这也是为什么双大括号语法(具有非静态初始化块的匿名内部类)有时被认为是反面模式的原因之一:

new ArrayList<String>() {{
     add("java");
     add("jsp");
     add("servlets");
  }}

如果有人禁止你使用它们,你应该询问他们的原因......在我看来,这完全取决于上下文......


这是反对内部类的一个好观点。至少在声明一些“标准”行为时如此。 - Guillaume
这是对内部类的荒谬使用。为什么不只是创建数组列表并添加字符串呢?展示一个愚蠢的内部类例子并不能使所有使用它们的做法都成为不良实践。 - ncmathsadist
@ncmathsadist 我不是说这很好,但有些人发现它更易读,并在单元测试中使用它,其中permgen不是那么重要。 - Sebastien Lorber

3

如果需要一个方法参数,我建议在使用它时要谨慎。我刚刚发现了与此相关的内存泄漏问题。这涉及到HttpServlet使用GrizzlyContinuation。
简而言之,以下是有缺陷的代码:

public void doGet(HttpServletRequest request, final HttpServletResponse response){
  createSubscription(..., new SubscriptionListener(){
    public void subscriptionCreated(final CallController controller) {
      response.setStatus(200);
      ...
      controller.resume();
    }

    public void subscriptionFailed(){
       ...
     }

    public void subscriptionTimeout(){
      ...
  }});
}

因为监听器由订阅所保留,所以 HttpServletResponse 也会被保留以便监听器需要它时使用(这一点不是很明显)。然后,只有当订阅被删除时,HttpServletResponse 实例才会被释放。如果使用在其构造函数中获取响应的内部类,则可以在调用恢复后将其设置为 null 以释放内存。
使用它们,但要小心!
Martin

3
匿名内部类具有查看“new”语句周围的字段和变量的优点。这可以使设计更加简洁,是一种非常好的(但有点冗长的)方法来“如何制作一个简单版本的lambda语句”。
命名内部类具有名称的优点,可以用通常的方式进行文档化,并与周围的类绑定在一起。一个非常好的例子是Builder模式,其中内部类负责为初始化过程提供状态,而不是拥有众多的构造函数。这样的构建器不能在类之间重复使用,因此将Builder紧密地绑定到父类是完全有意义的。

3
没有内部类的代码更易于维护和阅读。当从内部类访问外部类的私有数据成员时,JDK编译器会在外部类中创建“包访问”成员函数,以供内部类访问私有成员。这会留下一个安全漏洞。一般情况下,我们应避免使用内部类。
仅在内部类仅与外部类相关联的情况下使用内部类,或者可以将内部类设置为私有,以便只有外部类可以访问它。内部类主要用于实现辅助类(如迭代器、比较器等),这些辅助类在外部类的上下文中使用。

3

这里没有提到的一件事是,(非静态)内部类拥有对其封闭类的引用。更重要的是,内部类可以访问其封闭类的私有成员。这可能会破坏封装性。

如果有选择,请不要使用内部类。


静态内部类?如果它是从属于另一个类的话,就将其作为同一文件中的非公共类。如果不使用内部类作为监听器,那么就需要维护痛苦的getter链和相互链接的状态变量,这只会让代码变得丑陋。 - ncmathsadist

2
某些框架,如Wicket,确实需要使用匿名内部类。
说永远不用是愚蠢的。永远别说永远!一个好的使用例子可能是这样一种情况,你有一些遗留代码,由某个人编写,其中许多类直接在Collection字段上操作,但由于某种原因,你无法更改那些其他类,但需要有条件地将操作镜像到另一个Collection中。最简单的方法是通过匿名内部类添加此行为。
bagOfStuff = new HashSet(){
  @Override
  public boolean add(Object o) {
    boolean returnValue = super.add(o);
    if(returnValue && o instanceof Job)
    {
      Job job = ((Job)o);
      if(job.fooBar())
         otherBagOfStuff.add(job);
    }
    return returnValue;
  }
}

说到这里,它们绝对可以像穷人的闭包一样使用。

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