【问题标题】:Can't access private variable from own class via subclass instance无法通过子类实例从自己的类访问私有变量
【发布时间】:2015-04-07 06:12:13
【问题描述】:
class A {
    private int foo;
    void bar(B b) { b.foo = 42; }
}

class B extends A { }

编译失败,报错:

A.java:3: error: foo has private access in A
    void bar(B b) { b.foo = 42; }
                     ^
1 error

向基类添加强制转换使其工作。

void bar(B b) { ((A) b).foo = 42; }

有人能解释一下为什么第一个 sn-p 是非法的吗?被禁止的原因是什么? JLS 是这样说的:

否则,成员或构造函数被声明为private,并且当且仅当它出现在包含成员或构造函数声明的顶级类(第 7.6 节)的主体内时才允许访问。

据我所知,我的代码符合这个措辞。那么这是 Java 编译器的错误,还是我对 JLS 的解释不正确?

(注意:我不是在寻找变通方法,比如创建变量protected。我知道如何解决这个问题。)

【问题讨论】:

  • @John Kugelman:您的陈述“为什么我不能从A 类的方法访问A.foo”不太正确。实际上,您正在尝试从 A 类的方法访问 B.foo。但是根据 JLS §8,2,子类 B 不会继承私有成员。 JLS §6.6 说通过限定名称访问成员需要应用访问控制。因此编译器会查看B 中的类成员foo 是否可访问,但B 中没有这样的成员。尽管如此,它还是好心地告诉您,您的预期继承失败了,因为foo has private access in A

标签: java private


【解决方案1】:

Java 对通过不应访问该变量的引用类型 访问私有变量很挑剔。您应该可以通过写((A) b).foo = 42 合法地做到这一点。

【讨论】:

  • 我不确定除了“JLS 这么说”,“规则这样更简单,因为你不必特殊情况”之外,会有一个好的答案,如果该字段最初来自该课程,没关系...,'”或“只有 James Gosling 可以回答。”
【解决方案2】:

您不能说b.foo,因为foo 是私有的,因此不会被继承,因此B 类看不到foo 变量并且不知道是否有名为@ 的变量987654328@ 甚至存在 - 除非它标记为受保护(如您所说)或默认(我猜它们在同一个包中)或公共。

如果您想使用foo 而不使用像第二个示例那样的显式转换,您必须使用this.foo 或只使用具有隐式thisfoo。正如Javadocs 指定的那样,this 关键字的主要原因是为了防止:

使用 this 关键字最常见的原因是因为一个字段 被方法或构造函数参数遮蔽。

当您使用((A) b) 时,您正在转换引用类型,编译器会看到它就像您使用A 引用变量类型一样,换句话说,A aa.foo 是完全合法的.

对超类私有实例变量的可见性和访问权的图解总结:

【讨论】:

  • 我了解编译器在做什么。我想知道我的用例是否被故意禁止,或者这是否是 JLS 或 Java 编译器中的缺陷。根据@Alex 评论中的 JLS 引用,在我看来,我的代码符合语言规范。
  • 不,这是关于私有变量的。 “否则,成员或构造函数被声明为private,并且当且仅当它出现在包含成员或构造函数声明的顶级类(第 7.6 节)的主体内时才允许访问。”
  • 但是如何在类中调用变量?你有没有在没有指定引用类型的情况下调用它们?如果不是,那么您使用的是关键字 this!
  • @JohnKugelman 我认为错误消息可能会误导您。实际错误可能应该是“foo 无法解析或不是字段”,因为它适用于对象中不存在的任何字段。试图在B 中查找foo 是没有意义的。只有编译器试图友好并告诉你为什么B 中没有foo。一旦你转换它,你就有一个A 类型的值,并且就A 而言,这个值肯定有一个foo 变量。
  • 这个答案应该是公认的答案。如果您在类之外引用私有变量,则无法访问它。 “为什么不编译?”您不能访问类类型之外的私有变量。 “但我是从一个子类调用它,代码在类中带有私有变量。”您仍然无法访问它,因为您的子类不是超类的强引用类型。它是遗传的。这就是类型转换起作用的原因。 docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html
【解决方案3】:

错误消息“在 A 中有私有访问权限”是一个很长一段时间的 java bug。

JDK 1.1:

JDK-4096353:JLS 6.6.1:当子类引用用于访问超类的私有时

包含代码 sn-p 完全符合问题一

class X{
  private static int i = 10;
  void f()     {
    Y oy = new  Y();
    oy.i = 5;  // Is this an error? Is i accessable through a reference to Y?
  }
}
class Y extends X {}

他们试图修复它并导致

JDK-4122297 : javac 的错误消息不适用于私有字段。

======TP1======
1  class C extends S {
2      void f(){
3          java.lang.System.out.println("foo");
4      }
5  }
6
7  class S {
8      private int java;
9  }
======
% javac C.java
C.java:3: Variable java in class S not accessible from class C.
    java.lang.System.out.println("foo");
    ^
C.java:3: Attempt to reference field lang in a int.
   java.lang.System.out.println("foo");
       ^
2 errors
======

但是按照规范,java 没有在 C 中继承,这个程序应该可以编译。

它在 1.2 中修复,但在 1.3 中再次出现

JDK-4240480 : name00705.html: JLS6.3 私有成员不应该从超类继承

JDK-4249653 : new javac 假定私有字段由子类继承

当泛型出现时

JDK-6246814 : 类型变量的私有成员被错误地访问

JDK-7022052 : 私有方法和泛型的无效编译器错误


但是,对于 JLS,这个成员根本不存在于继承的类型中。

JLS 8.2。班级成员

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

所以b.foo 是非法的,因为类B 没有名为foo 的字段。 没有限制,是B中的一个缺字段。

Java 具有强类型,我们无法访问不存在于 B 中的字段,即使它们存在于超类 A 中。

转换(A) b 是合法的,因为BA 的子类。

A 有一个名为foo 的字段,我们可以访问这个私有字段,因为b(B b)A 类中的一个函数,即使b != this 由于

JLS 6.6.1。确定可访问性

否则,如果成员或构造函数被声明为私有,则当且仅当它出现在包含成员或构造函数声明的顶级类(第 7.6 节)的主体内时,才允许访问。

如果我们写

class A {
  private int foo;
  void baz(A b) { b.foo = 42; }
}

class B extends A { }

class T {
  void x() {
    B b = new B();
    b.baz(b);
  }
}

它将编译,因为 Java 推断多态调用的类型参数。

JLS 15.12.2.7。根据实际参数推断类型参数:

超类型约束 T :> X 暗示解是 X 的超类型之一。给定 T 上的几个这样的约束,我们可以与每个约束隐含的超类型集合相交,因为类型参数必须是成员他们所有人中。然后我们可以选择交叉路口中最具体的类型

【讨论】:

  • "所以 b.foo 是非法的,因为 B 类没有名为 foo 的字段。" -- 请注意编译器错误消息是foo 不可访问,而不是它不存在。 “......当且仅当访问发生在包含成员或构造函数声明的顶级类(第 7.6 节)的主体内时,才允许访问。” -- 如我所见,我的代码满足了这个条件。
  • 编译器行为符合规范,但错误消息不符合规范。很有趣...
  • 让我感到困惑的是B 的实例有一个字段foo 但类型B 没有,我引用的JLS 部分仅适用于成员的类型。
  • 一切都与继承有关。当您使用私有成员对类型进行子类化时,您不应该关心您的成员和变量名称与它的冲突。看看答案中的TP1测试。例如,您可以将新字段 foo 添加到 B 类型。在这种情况下B 的实例可以有两个独立的字段foo,一个在A 类型中,一个在B 类型中。
【解决方案4】:

specification for field access expressions, chapter 15.11 说:

如果标识符没有命名类型 T 中的可访问成员字段, 则字段访问未定义并发生编译时错误。

从超类的角度来看,查看类型,我认为该成员是不可访问,因此是错误的。

我认为您介绍的情况更接近于将成员作为字段访问,如示例 15.11-1-1 所示。

class S           { int x = 0; }
class T extends S { int x = 1; }
class Test1 {
    public static void main(String[] args) {
        T t = new T();
        System.out.println("t.x=" + t.x + when("t", t));
        S s = new S();
        System.out.println("s.x=" + s.x + when("s", s));
        s = t;
        System.out.println("s.x=" + s.x + when("s", s));
    }
    static String when(String name, Object t) {
        return " when " + name + " holds a "
                        + t.getClass() + " at run time.";
    }
}

只是为了回答你的问题:

请解释一下限制保护了什么样的错误代码 反对。

请考虑关注sn-p。

public class X {
    private int a;

    public void bar(Z z) {
        z.a // not visible, but if was, what 'a' 
            // would you actually access at this point 'X' or 'Z'
    }
}

public class Z extends X {
    private int a;
}

【讨论】:

  • 你在 X 内部和 Z 外部,你的代码应该只看到 X 的 a。
  • @Magnamag,是的,但我认为它的工作方式与访问 X 和 Z 之外的字段的静态绑定一致,这使得它更直观(至少对我而言)。考虑docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.11 的 15.11 和示例示例 15.11.1-1 - 我的意思是要指出,您只是在访问作为参数传递的变量的成员,它恰好是扩展的类型X,但不继承 a,因为 JSL 8.2。
  • 当然!实际上,如果 OP 在他的问题中提出的内容是有效的,那么代码将是一团糟。我的观点是规范没有正确涵盖那个“奇怪”的用例。但是,您提供的链接澄清了这个问题,特别是:“如果标识符未命名类型 T 中的可访问成员字段,则该字段访问未定义并且发生编译时错误”。您应该将该链接添加到您的答案中,以及引用和一些论点。
【解决方案5】:

我们不能继承private 字段或方法。所以在你的代码中 Class B 完全不知道变量 foo 即使你从它自己的类 A 访问。

【讨论】:

  • 其他答案已经涵盖了这一点。另外,请引用您的来源。
【解决方案6】:

这不是默认访问修饰符的用途吗?

试试这个:

public class blah{
    static class A {
        int foo;
        void bar(B b) {b.foo=42;}
    }
    static class B extends A {

    }
}

您不能直接从祖先访问私有成员,这就是私有的意思。现在为什么在你施放时它会起作用?这是否意味着文档不正确?

我向一位同事提到 java 文档可能是错误的,他指出您实际上是从 A 类内部设置 foo 的值。所以一切都是正确的。您不能(因为它是私有的)从后代访问 foo ,因此您必须强制转换。而且你不能在 A 的身体之外做到这一点。

我相信这是正确的答案。

【讨论】:

  • 我不确定这是一种解决方法。这个问题从未被“修复”的原因是因为这是默认访问修饰符的用途。
  • 如果你把这两个类放在不同的包里,你会遇到同样的问题:“foo 在 A 中不是公共的;不能从包外访问”。
  • 对吗?但默认不仅仅意味着“包私有”。它还处理继承,这就是它在这种情况下工作的原因。您能否详细解释一下您认为问题究竟是什么?
  • 我的问题是关于我写的代码。修改不在讨论范围内。我认为/认为它应该编译,所以我的问题是,“为什么不编译?”不是,“我怎样才能让它编译?”
  • 我认为这是设计的。您可以在转换时访问私有成员变量,因为您在对象上(就像使用反射一样)。如果您希望成员变量可在类之间访问但不能对后代访问,则将友好的类放在同一个包中并使用默认值。所有降级类都在该包之外。我认为关于 Java 访问的事情是它实际上是一个“指南”,因为在 Java 中没有什么是真正私有的,因为反射。
【解决方案7】:

在我看来,规范不一致。正如 John 所说,规范正文指出

否则,成员或构造函数被声明为私有,并且当且仅当它出现在包含成员或构造函数声明的顶级类(第 7.6 节)的主体内时才允许访问。

并且没有提到子类。所以A类应该正确编译。但是示例 6.6-5 状态

私有类成员或构造函数只能在包含成员或构造函数声明的顶级类(第 7.6 节)的主体内访问。 不被子类继承

第二个语句更弱(不仅是 if),但将子类带到了表中。根据这个 A 不应该编译。

【讨论】:

    猜你喜欢
    • 2010-10-23
    • 1970-01-01
    • 1970-01-01
    • 2013-09-20
    • 1970-01-01
    • 2015-06-05
    • 1970-01-01
    • 1970-01-01
    • 2011-11-05
    相关资源
    最近更新 更多