Bloch的《Effective Java》- 静态类优于非静态类 - 有多少实例?

12
我想知道封闭类可以创建多少个静态成员类的实例。我原以为只有一个,但是 Bloch 的下面这段摘录让我感到困惑。
引用 Joshua Bloch 的《Effective Java》- 条款 22 *:优先使用静态成员类。
私有静态成员类的常见用途是表示其封闭类所表示对象的组件。例如,考虑一个 Map 实例,它将键与值关联起来。许多 Map 实现都有一个内部 Entry 对象,用于表示映射中的每个键值对。虽然每个条目与一个映射相关联,但是条目上的方法(getKey、getValue 和 setValue)不需要访问映射。因此,使用非静态成员类表示条目是浪费的:最好使用私有静态成员类。 如果在 entry 声明中意外省略了 static 修饰符,则地图仍将正常工作,但每个 entry 将包含一个多余的引用到 map,这会浪费空间和时间。
他指出地图为映射中的每个键值对创建一个 Entry 对象,即静态成员类的多个实例。
那么我的假设是错误的!那意味着我的静态成员类的理解是错误的。每个人都知道静态成员变量的行为,比如经典的静态 final 字符串 - 只有一个对象的实例。
这是否意味着当封闭对象被实例化时,并没有实际实例化静态成员类?
那么,在这种情况下,Map 使用静态成员类作为 Entry 的目的是什么?为什么不在 API 上使用接口?然后每个其他 Collections 类都可以提供自己的实现。

该类只有一个副本。但是您可以创建许多该类的实例。对于非静态类,概念上每个外部类实例都有一个类的副本(尽管出于效率原因,实际上并非如此实现)。 - user253751
我认为最容易理解静态内部类的方法是将其视为一个普通类,其名称中包含外部类。外部类和内部类之间唯一的关系在于访问修饰符。 - Dawood ibn Kareem
只有一个 Map,无论它是静态的还是动态的,但它可以包含多个元素。 - Stultuske
4个回答

10

这是对 static 关键字的一种常见误解。

当你在变量中使用 static ,意味着所有这个类的对象都只有一个该变量或者类似的意思。

static Object thereWillBeOnlyOne = new Object();

然而,在内部类的上下文中,它意味着完全不同的事情。 一个静态内部类与封闭类的对象没有任何关联,而非静态内部类则有

一个静态内部类:

public class TrieMap<K extends CharSequence, V> extends AbstractMap<K, V> implements Map<K, V> {

  private static class Entry<K extends CharSequence, V> implements Map.Entry<K, V> {

我使用的TrieMap类中使用的Map.Entry类不需要引用创建它的对象,因此可以将其设置为static以节省不必要的引用。


非静态内部类:

public final class StringWalker implements Iterable<Character> {
  // The iteree
  private final String s;
  // Where to get the first character from.
  private final int start;
  // What to add to i (usually +/- 1).
  private final int step;
  // What should i be when we stop.
  private final int stop;

  // The Character iterator.
  private final class CharacterIterator implements Iterator<Character> {
    // Where I am.
    private int i;
    // The next character.
    private Character next = null;

    CharacterIterator() {
      // Start at the start.
      i = start;
    }

    public boolean hasNext() {
      if (next == null) {
        if (step > 0 ? i < stop : i > stop) {
          next = s.charAt(i);
          i += step;
        }
      }
      return next != null;
    }
StringWalker对象内部的CharacterIterator称为s,它仅在StringWalker对象中存在一次,用于迭代的字符串。因此,我可以创建许多StringWalker的迭代器,它们都遍历同一个字符串。


为什么会出现这种奇怪的现象?

这种看似不合逻辑的二元性来源于在C中使用static关键字。

C中,您可以(或至少曾经可以)执行以下操作:

void doSomething () {
   static int x = 1;

   if ( x < 3 ) {
   } else {
   }
   x += 1;
}

每次调用函数时,x的值将与上一次相同-在这种情况下递增。
这个概念是,static关键字表示变量在其封闭块中具有作用域,但在其父块中具有语义封闭。也就是说,上述代码大致等同于:
int x = 1;
void doSomething () {
   if ( x < 3 ) {
   } else {
   }
   x += 1;
}

但在函数内部才允许引用x

把这个概念转移到Java中,事情现在变得更加清晰了。一个static内部类的行为就像它是在类外部声明一样,而非static内部类与其封闭实例更紧密地连接-实际上可以直接引用该实例。

另外:

class Thing {
   static Object thereWillBeOnlyOne = new Object();

表现得很像

Object thereWillBeOnlyOne = new Object();
class Thing {

如果它是合法的话。
课程到此结束。

5
我认为Java团队在这个问题上搞错了命名。 静态内部类(严格来说,它们的正确名称是“静态嵌套类”)与普通类没有任何不同,除了它有一个花哨的名称(Something.MyClass而不是MyClass),并且可以被设置为私有(即不能从其他类实例化)。
Map的情况下,之所以选择它,仅仅是因为名称Map.Entry清楚地表明EntryMap相关联。正如您建议的那样,使用普通类完全合理。唯一的区别是您无法编写Map.Entry
我认为他们应该使用“非静态”内部类的语法(即在封闭类中使用class)来创建静态嵌套类,并发明一个新关键字来创建“非静态”内部类,因为这些类与普通类的行为不同。也许像attached class这样的东西。据我所知,选择static关键字是为了避免有太多保留关键字,但我认为这只会引起混淆。

不存在静态内部类这种东西。内部类是一个嵌套的非静态类。我在初始版本的回答中也使用了错误的术语,现在已经更正过来了。 - peter.petrov

3

是的,您可以拥有许多嵌套类的实例,无论嵌套类是否为静态。

当嵌套类为静态时,您可以创建它的实例而无需具有封闭类的实例,这是其中之一的好处,也基本上是静态和非静态嵌套类之间的主要区别。

那么这是否意味着,当封闭对象被实例化时,并没有实际实例化静态成员类?

当调用其构造函数时,它会被实例化。与非静态类没有任何区别。当代码首次访问它时,JVM将加载嵌套类本身。再次与其他类相比,我认为这并没有任何不同(虽然我不确定,但您可以自己测试)。因此,我认为您有点混淆了“JVM通过加载类”和“实例化类”的术语。

那么,在这种情况下,Map使用静态成员类作为Entry有什么意义呢?为什么不在API上使用接口?

正如所说,创建静态嵌套类的实例更容易。您不需要一个封闭实例,这有时(也许大多数情况下)正是您想要的。

另请参阅:

(1) 嵌套类

(2) JVM如何判断一个类是否嵌套在另一个类中?

(3) JVM加载嵌套类的过程

您可以搜索其他相关的参考资料。
(2)号参考资料看起来比较高级,有些偏离您的问题。


2

为什么Map使用静态成员类作为Entry?

这是因为它使包结构在逻辑上更加正确。

为什么不在API上使用接口?

现在,这是一个设计讨论,没有人愿意被卷入其中。


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