为什么更喜欢非静态内部类而不是静态内部类?


37

这个问题是关于将Java中的嵌套类做成静态嵌套类还是内部嵌套类。我在这里和Stack Overflow上进行了搜索,但是实际上找不到关于此决定的设计含义的任何问题。

我发现的问题是关于静态和内部嵌套类之间的区别的,这对我来说很明显。但是,我还没有找到令人信服的理由在Java中使用静态嵌套类-匿名类除外,我不考虑这个问题。

这是我对使用静态嵌套类的影响的理解:

  • 更少的耦合:由于类不能直接访问其外部类的属性,因此通常得到的耦合较少。较少的耦合通常意味着更好的代码质量,更容易的测试,重构等。
  • Single Class类加载器不需要每次都照顾一个新类,我们创建外部类的对象。我们只是一遍又一遍地获得同一类的新对象。

对于一个内部类,我通常会发现人们认为访问外部类的属性是有利的。从设计的角度来看,我希望在这方面有所不同,因为这种直接访问意味着我们具有很高的耦合度,并且如果我们想将嵌套类提取到其单独的顶层类中,则只能在本质上转向将其转换为静态嵌套类。

所以我的问题归结为:我是否认为非静态内部类可用的属性访问会导致较高的耦合度,从而导致较低的代码质量,我是错误的,并且据此我推断(非匿名)嵌套类应一般是静态的?

或者换句话说:为什么有一个令人信服的理由为什么人们更喜欢嵌套的内部类?


2
Java教程“使用非静态嵌套类(或内部类),如果你需要访问一个类实例的非公共字段和方法使用一个静态嵌套类,如果你不需要这样的访问。”
gna 2014年

5
感谢您的教程报价,但这只是重申了区别,而不是为什么您要访问实例字段。
2014年

这只是一个一般性的指导,暗示了您更喜欢什么。我在此答案中
2014年

1
如果内部类没有与封闭类紧密耦合,那么一开始它可能不应该是内部类。
凯文·克鲁姆维德

Answers:


23

约书亚·布洛赫(Joshua Bloch)在他的《有效的Java第二版》一书的第22条中,讲述了何时使用哪种嵌套类以及为什么使用。以下是一些引号:

静态成员类的一种常见用法是用作公共帮助程序类,仅在与其外部类结合使用时才有用。例如,考虑一个描述计算器支持的操作的枚举。Operation枚举应为该类的公共静态成员Calculator类。Calculator然后,的客户可以使用类似Calculator.Operation.PLUS和的名称来引用操作Calculator.Operation.MINUS

非静态成员类的一种常见用法是定义一个Adapter,该适配器允许将外部类的实例视为某些不相关类的实例。例如,所述的实施方式中Map接口通常使用非静态成员类来实现他们的集合视图,这是由返回MapkeySetentrySetvalues方法。类似地,集合接口的实现(例如SetList)通常使用非静态成员类来实现其迭代器:

// Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E> {
    ... // Bulk of the class omitted

    public Iterator<E> iterator() {
        return new MyIterator();
    }

    private class MyIterator implements Iterator<E> {
        ...
    }
}

如果声明不需要访问封闭实例的成员类,请始终static修饰符放在其声明中,使其成为静态成员,而不是非静态成员类。


1
我不太确定此适配器如何/是否更改外部类MySet实例的视图?我可以设想类似于路径依赖类型的东西,即myOneSet.iterator.getClass() != myOtherSet.iterator.getClass(),但是再说一次,在Java中这实际上是不可能的,因为每个类的类都相同。
2014年

@Frank这是一个非常典型的适配器类-它使您可以将集合视为其元素流(迭代器)。
user253751

@immibis,谢谢,我很了解迭代器/适配器的功能,但是这个问题是关于如何实现的。
2015年

@Frank我在说它如何改变外部实例的视图。这正是适配器所做的-它改变了您查看某些对象的方式。在这种情况下,该对象是外部实例。
user253751

10

您正确地假设非静态内部类可用的属性访问会导致较高的耦合,从而导致较低的代码质量,并且(非匿名和非本地)内部类通常应该是静态的。

使Java内部类变为非静态的决策的设计含义在Java PuzzlersPuzzle 90中列出(以下引号中的粗体是我的):

每当您编写成员类时,都要问自己:此类是否真的需要一个封闭的实例?如果答案是否定的,请回答static。内部类有时很有用,但它们很容易引入复杂性,使程序难以理解。它们与泛型(难题89),反射(难题80)和继承(此难题)具有复杂的交互。如果您声明Inner1static,问题就解决了。如果您还声明Inner2static,则实际上可以理解该程序的功能:确实是一个不错的奖励。

总而言之,将一个类同时作为另一个的内部类和子类是非常不合适的。更一般地说,扩展内部类很少是合适的。如果必须的话,请仔细考虑该封闭实例。此外,static与non-相比,更喜欢嵌套类static。可以并且应该声明大多数成员类static

如果你有兴趣,更详细的分析拼图90中提供了这个答案在堆栈溢出


值得注意的是,以上内容实质上是Java 类和对象教程中给出的指南的扩展版本:

如果您需要访问封闭实例的非公共字段和方法,请使用非静态嵌套类(或内部类)。如果您不需要此访问,请使用静态嵌套类。

因此,换句话说,按照本教程所要回答的问题的答案是,使用非静态的唯一令人信服的理由是,当需要访问封闭实例的非公共字段和方法

教程的措词有些宽泛(这可能是Java Puzzlers试图对其进行增强和缩小的原因)。特别是,根据我的经验,直接访问封闭的实例字段从未真正需要 -在某种意义上,诸如将这些作为构造函数/方法参数传递的替代方法始终变得更易于调试和维护。


总的来说,我(非常痛苦)在调试内部类时遇到了直接访问封闭实例字段的情况,这给我留下了深刻的印象,即这种做法类似于使用全局状态,以及与之相关的已知弊端

当然,Java做到了在封闭类中包含此类“准全局”的损坏,但是当我不得不调试特定的内部类时,感觉像这样的创可贴并没有帮助减轻痛苦:我仍然有记住“外来”语义和细节,而不是全神贯注于对特定麻烦对象的分析。


为了完整起见,在某些情况下上述推理可能不适用。例如,根据我对map.keySet javadocs的阅读,此功能建议紧密耦合,结果使针对非静态类的参数无效:

返回Set此映射中包含的键的视图。该集合由地图支持,因此对地图的更改会反映在集合中,反之亦然。

上面的内容并不能以某种方式使所涉及的代码更易于维护,测试和调试,但是它至少可以使人们认为复杂性与预期功能相符。


我分享了您对由这种高耦合引起的问题的经验,但是我对本教程对require的用法不满意。您说自己,您从未真正需要现场访问,所以很遗憾,这也不能完全解决我的问题。
弗兰克

@Frank就像您看到的那样,我对教程指南的解释(似乎由Java Puzzlers支持)就像仅在可以证明这是必需的时才使用非静态类,尤其是证明替代方法是更糟。值得一提的是,根据我的经验,替代方法总是会更好
2014年

这才是重点。如果总是更好,那为什么还要打扰呢?
弗兰克(Frank)

@Frank 另一个答案提供了引人注目的示例,但事实并非总是如此。根据我对map.keySet javadocs的阅读,此功能建议紧密耦合,结果使针对非静态类的参数无效“该集合由地图支持,因此对地图的更改反映在该集合中,反之亦然...”
gna 2014年

1

一些误解:

更少的耦合:由于[静态内部]类无法直接访问其外部类的属性,因此我们通常会得到较少的耦合。

IIRC-确实可以。(当然,如果它们是对象字段,那么它就没有外部对象可供查看。尽管如此,如果确实从任何地方都得到了这样的对象,那么它可以直接访问那些字段)。

单个类:类加载器不必每次都照顾一个新类,我们创建外部类的对象。我们只是一遍又一遍地获得同一类的新对象。

我不太确定你在这里的意思。类加载器总共只涉及两次,每个类一次(加载类时)。


乱码如下:

在Javaland中,我们似乎有点害怕创建类。我认为它必须是一个重要的概念。它通常属于自己的文件。它需要大量的样板。它需要注意其设计。

与其他语言(例如Scala或C ++)相比:任何时候只要有两个数据位属于同一类,就绝对不会创建一个微小的持有人(类,结构等)。灵活的访问规则可通过减少样板来帮助您,从而使相关代码在物理上更紧密。这样可以减少您在两个班级之间的“思维距离”。

通过保持内部类的私有性,我们可以消除对直接访问的设计关注。

(...如果您问“为什么要选择一个非静态公共内部类?”,这是一个很好的问题-我认为我还没有找到适合这些人的正当理由)。

您可以随时使用它们,这有助于提高代码的可读性,避免DRY违规和样板。我没有现成的示例,因为根据我的经验,它并不会经常发生,但是确实会发生。


静态内部类仍然是一个很好的默认方法,顺便说一句。非静态具有一个额外的隐藏字段,内部类通过该字段来引用外部。


0

可用于创建工厂模式。当创建的对象需要附加的构造函数参数来运行时,通常会使用工厂模式,但每次都提供这些代码很麻烦:

考虑:

public class QueryFactory {
    @Inject private Database database;

    public Query create(String sql) {
        return new Query(database, sql);
    }
}

public class Query {
    private final Database database;
    private final String sql;

    public Query(Database database, String sql) {
        this.database = database;
        this.sql = sql;
    } 

    public List performQuery() {
        return database.query(sql);
    }
}

用作:

Query q = queryFactory.create("SELECT * FROM employees");

q.performQuery();

使用非静态内部类可以实现相同的目的:

public class QueryFactory {
    @Inject private Database database;

    public class Query {
        private final String sql;

        public Query(String sql) {
            this.sql = sql;
        }

        public List performQuery() {
            return database.query(sql);
        }
    }
}

用于:

Query q = queryFactory.new Query("SELECT * FROM employees");

q.performQuery();

工厂从具有特别命名的方法,如受益createcreateFromSQLcreateFromTemplate。在这里也可以以多个内部类的形式进行相同的操作:

public class QueryFactory {

    public class SQL { ... }
    public class Native { ... }
    public class Builder { ... }

}

从工厂到构造的类传递的参数较少,但是可读性可能会有所下降。

相比:

Queries.Native q = queries.new Native("select * from employees");

Query q = queryFactory.createNative("select * from employees");
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.