来自匿名静态实例的私有实例成员访问

17
考虑以下代码:

Consider the following code:

enum E {
    A { public int get() { return i; } },
    B { public int get() { return this.i; } },
    C { public int get() { return super.i; } },
    D { public int get() { return D.i; } };

    private int i = 0;
    E() { this.i = 1; }
    public abstract int get();
}

我在前两个枚举常量(A和B)的声明处得到编译时错误,但最后两个(C和D)通过编译正常。错误如下:

 

第一个错误发生在A行: 非静态变量i不能从静态上下文引用
  第二个错误发生在B行: i在E中具有私有访问权限

由于get是一个实例方法,我不明白为什么我无法按照我想要的方式访问实例变量i

注意: 从i的声明中去掉private关键字也可以使代码可编译,这也让我感到困惑。

使用Oracle JDK 7u9。

编辑

正如评论中指出的那样,这与枚举无关,下面的代码产生了相同的行为:

class E {
    static E a = new E() { public int get() { return i; } };
    static E b = new E() { public int get() { return this.i; } };
    static E c = new E() { public int get() { return super.i; } };
    static E d = new E() { public int get() { return d.i; } };

    private int i = 0;
}

1
这里也有同样的问题。经过很长一段时间,我仍然对自己说我知道Java中的枚举类型 :) - mtk
2
为了更有趣,尝试使用return this.i;代替A.i - Marko Topolnik
1
@MarkoTopolnik return this.i;return i; 无法编译,但是 return super.i;return A.i; 可以。 - assylias
4
没错,而且每个错误都不同!这就是其中“更有趣”的部分 :) - Marko Topolnik
assylias,请注意,你的错误并不是枚举特定的。尝试使用Evgeniy的答案中的代码,它会产生与常规类相同的编译器错误。 - Marko Topolnik
关于为什么删除“private”后代码可以正常工作,我认为这个问题可以得到充分的解释。在解析变量i时有一定的搜索顺序,将其作为静态变量查找是最后的选择。这一步骤会找到i,但会得出无法访问的结论,并将原因归因于错误的原因。这是错误报告中的一个漏洞,其根本原因仍然是访问规则的不当实现。 - Marko Topolnik
4个回答

6
观察到的行为是由Java语言规范所要求的,特别是对封闭类型的字段进行隐式访问以及私有成员不被继承的规则。
未经限定的字段访问。
A { public int get() { return i; } }

该规范强制规定

枚举常量的可选类体隐式定义了一个匿名类声明(§15.9.5),该声明扩展了直接封闭的枚举类型。类体受匿名类的通常规则控制;特别是它不能包含任何构造函数。

这使得表达式i有些模棱两可:我们是在引用封闭实例的字段,还是内部实例?遗憾的是,内部实例不会继承该字段

私有声明的类成员不会被该类的子类继承。

因此,编译器认为我们要访问封闭实例的字段 - 但由于位于静态块中,因此没有封闭实例,因此会出现错误。

通过this访问字段

B { public int get() { return this.i; } },

规范规定

当关键字this用作主表达式时,它表示一个值,该值是对调用实例方法(§15.12)的对象或正在构造的对象的引用。

因此,很明显我们想要内部类的字段而不是外部类的字段。

编译器拒绝访问字段的表达式this.i 的原因是

一个类声明为private的成员在该类的子类中不会被继承。

也就是说,私有字段只能通过声明该字段的类型的引用而不是其子类型的引用来访问。确实,

B { public int get() { return ((E)this).i; } },

编译完全正常。

通过super访问

像这样,super 指的是方法被调用的对象(或正在构造的对象)。因此,很明显我们指的是内部实例。

此外,super的类型为E,因此声明可以看到。

通过其他字段访问

D { public int get() { return D.i; } };

在这里,D是对在E中声明的静态字段D的未限定访问。由于它是一个静态字段,因此使用哪个实例的问题是毫无意义的,访问是有效的。
然而,它非常脆弱,因为只有在枚举对象完全构建完成后才分配该字段。如果有人在构建期间调用get(),则会抛出NullPointerException建议 正如我们所见,访问其他类型的私有字段受到相当复杂的限制。由于很少需要,开发人员可能不知道这些微妙之处。
虽然将字段设置为protected将削弱访问控制(即允许包中的其他类访问该字段),但可以避免这些问题。

我基本上同意,除了“在静态块中,没有封闭实例,因此会出现错误” => get是一个实例方法。 变量分配发生在静态上下文中(A = new E() {...}),但语句return i;则不是。 - assylias
1
@assylias 我认为meriton的逻辑是无可挑剔的:确实没有封闭实例,语句return i出现在没有继承i的实例上下文中。 - Marko Topolnik
@assylias:Marko回答了你的问题吗?还是我需要更详细地阐述某个方面? - meriton
@meriton 我想我需要明天早上冷静地重新阅读一遍,但似乎你的答案是正确的。 - assylias
1
@assylias 私有成员不被继承的规则和匿名类没有封闭实例的事实的组合解释了一切。当我写答案时,我忽略了这两个事实中的第一个。 - Marko Topolnik

3

看一下这段代码:

public class E 
{
  final int i;
  private final int j;
  final E a;

  E() { i = j = 0; a = null; }

  E(int p_i) {
    this.i = this.j = p_i;
    a = new E() {
      int getI() { return i; }
      int getJ() { return j; }
    };
  }

  int getI() { throw new UnsupportedOperationException(); }
  int getJ() { throw new UnsupportedOperationException(); }

  public static void main(String[] args) {
    final E ea = new E(1).a;
    System.out.println(ea.getI());
    System.out.println(ea.getJ());
  }
}

这将打印

0
1

唯一的区别在于 ij 的访问级别!

这很令人惊讶,但这是正确的行为。


1

更新

看起来是因为它在静态块中定义了。请看以下内容:

    private E works = new E("A", 0) {

        public int get() {
            return i; // Compiles
        }
    };

    static {
        A = new E("A", 0) {

            public int get() {
                return i; // Doesn't Compile
            }

        };
    }

翻译

我编译了枚举,然后使用Jad反编译它,以查看代码可能的样子:

static abstract class E extends Enum
{

    public static E[] values()
    {
        return (E[])$VALUES.clone();
    }

    public static E valueOf(String s)
    {
        return (E)Enum.valueOf(Foo$E, s);
    }

    public abstract int get();

    public static final E A;
    private int i;
    private static final E $VALUES[];

    static
    {
        A = new E("A", 0) {

            public int get()
            {
                return A.i;
            }

        }
;
        $VALUES = (new E[] {
            A
        });
    }


    private E(String s, int j)
    {
        super(s, j);
        i = 0;
        i = 1;
    }

}

这让我更清楚地了解到A是在类型E的静态初始化块中定义的匿名内部类。在寻找匿名内部类中私有成员可见性时,我在这个答案中发现了以下内容(为什么只有final变量可以在匿名类中访问?):

当您创建匿名内部类的实例时,该类中使用的任何变量都会通过自动生成的构造函数进行复制。这避免了编译器必须自动生成各种额外类型来保存“局部变量”的逻辑状态,例如C#编译器所做的那样。

从这里我可以得出结论,A.i指的是A中的复制变量,而不是在E中声明的i。要获取E中的i,唯一的方法是将其设置为静态或非私有。



  1. 我们都知道这是一个匿名内部类。
  2. 在你的引用中,“变量”意味着“局部变量”,并不适用于这里。
- Marko Topolnik
@MarkoTopolnik 在(1)处我错了一点打字。让我惊讶的是它是在静态块中定义的匿名内部类。这让我认为A无法访问E的私有实例变量。 - John Farrelly
准确来说,它不是一个匿名的内部类,而是一个嵌套类(它没有封闭实例),这就是为什么你在示例中的 works 编译通过:它调用了不同的访问规则,即封闭实例字段的规则。这并不能解释 OP 的代码为什么会产生编译错误的原因。 - Marko Topolnik
@JohnFarrelly 注意,在我的第二个示例中没有静态块。 - assylias
@assylias 在你的第二个例子中,所有变量都被声明为静态的,因此会有类似的问题。如果将它们设置为非静态,则可以编译所有变量。 - John Farrelly

0

private方法可以在嵌套类中访问,前提是它们在同一个类文件中。

因此,即使A是E的匿名子类,第一个示例也可以正常工作。有趣的是,为什么第二个示例无法编译,但我怀疑这是误导性错误消息,因为您可以执行以下操作

A { public int get() { return super.i; } };

编译但是

A { public int get() { return i; } };

提供

error: non-static variable i cannot be referenced from a static context

这显然是不正确的,因为如果这是一个静态上下文,super.i 将没有意义。

正如马尔科所指出的那样。

A { public int get() { return this.i; } };

产生错误信息

error: i has private access in E

这可能更合适。也就是说,您可以显式地访问此字段,但不能隐式地访问。


实际上,return this.i; 也会给我编译错误。但是 super.i 可以编译通过。 - assylias
那么你的解释是“这是编译器的错误”吗? - Denys Séguret
1
this.i 不会出现 "非静态" 错误,但会出现 "E 中的 i 具有私有访问权限"。 - Marko Topolnik
@MarkoTopolnik 我的IDE应该抱怨时却没有。 ;) - Peter Lawrey
1
也许是你的IDE是正确的 :) 无论如何,不同的方法到达相同实例变量之间绝不能存在差异。这是错误的行为。 - Marko Topolnik
显示剩余2条评论

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