有人能解释一下这个Java作用域的谜团吗?

4

一位同事扩展了LinkedHashMap,并覆盖了removeEldestEntry,类似于以下代码:

import java.util.LinkedHashMap;
import java.util.Map.Entry;

public class CompileTest {
    static class MyMap<K, V> extends LinkedHashMap<K, V> {
        protected boolean removeEldestEntry(Entry<K, V> eldest) {
            return true;
        }
    }
}

请注意参数类是Entry,而不是Map.Entry
Eclipse生成了方法签名。IntelliJ显示错误,抱怨Entryjava.util.LinkedHashMap中具有私有访问权限,并更喜欢Map.Entry。但无论哪种方式编译都可以。

我写了一个较小的示例来进行实验:

public class CompileTest {

    static class A{
        public class Inner {
        }

        public void doStuff(Inner a){}
    }

    static class B extends A{
        private class Inner {
        }
    }

    static class C extends B {
        public void doStuff(Inner a) { }
    }
}

现在IntelliJ没有显示错误,但是该类无法编译。这里有两种看似相同的情况,其中IDE和编译器似乎都交替执行,永远也不会达成一致。

有人能解释一下吗?


1
编译器始终是正确的参考... 另外,您的示例与 Entry vs Map.Entry 不执行相同操作,其中 EntryLinkedList 的内部类,使 Entry 成为 Map 的内部类 - user177800
这两个示例中的静态导入有区别吗? - Kirby
它们看起来一样吗?你的测试用例中的import语句在哪里?导入实际上建立了别名,消除了给定未限定名称引用的完全限定名称的歧义。你的测试用例没有这个。 - erickson
+1 给Jarrod。你的示例并没有反映出你在问题中最初提到的作用域问题。首先,你的内部类定义深度过深,而且你还将Inner类声明为B类中的私有类,从而有效地隐藏了它,使其对C类不可见。这就是你编译错误的源头。 - Perception
顺便说一下,这个错误出现在Eclipse中。它生成的代码是错误的。HashMap.Entry在Eclipse生成的代码中优先于Map.Entry。 - M Platvoet
@M Platvoet。但它可以编译。正如我在另一条评论中所说,使用@Override注释在编译时不会产生错误,表明适当的方法已被覆盖。 - Mark Bolusmjak
3个回答

4

从Java语言规范 - 8.5成员类型声明

如果类声明了一个具有特定名称的成员类型,则该类型的声明被视为隐藏类的超类和超接口中具有相同名称的所有可访问成员类型的声明。

类从其直接超类和直接超接口继承所有非私有成员类型,这些成员类型既对类中的代码可访问,又没有在类中的声明中被隐藏。

我们可以推断出:

B.Inner 隐藏了 A.InnerB 不会继承 A.InnerA.Inner 不是 B 的成员(8.2)类型。 C 无法从 B 继承 A.Inner。C 无法从 B 继承 B.Inner,因为它是私有的。

因此,C没有成员类型Inner。假设在C的封闭作用域(外部类;编译单元;包)中没有其他Inner类型,则类型名称Inner无法解析。 javac试图报告更详细的错误,但那只是对您意图的猜测。一个更好的错误消息可能应该包括上述所有解释。
在关于Entry的第一个例子中,导入语句在整个编译单元范围内声明了Entry(即java文件),因此Entry被解析为Map.Entry IntelliJ 10.5不会抱怨第一个例子;显然这个错误已经被修复了。在第二个例子中仍然是错误的。
关于private有一件有趣的事情。为什么规范明确排除private成员,而它已经要求成员“可访问”?实际上,在整个CompileTest(6.6.1),包括C在内,B.Inner都是可以访问的。C可以有一个doStuff(B.Inner),并且它会编译。
这可能就是为什么IntelliJ在第二个示例中出错的原因;它认为B.Inner对C是可访问的,因此C继承了B.Inner。它忽略了继承中private成员的额外排除条款。
这揭示了关于private声明范围的两种冲突观点。观点#1希望范围是直接封闭的类,观点#2希望它是顶级封闭的类。程序员很少知道#2,他们会惊讶地发现C可以访问B.Inner。#2可以通过以下方式进行证明:它们都在同一个文件中,因此不需要封装,我们没有通过禁止访问来保护任何人;在大多数用例中,允许访问更加方便。

第二种观点可能也认为C应该继承B.Inner——会有什么问题呢?有趣的是,第二种观点在一般访问控制规则上获胜,但在继承规则上失败了。

规范真的非常混乱,幸运的是我们通常不会遇到这些复杂的情况。相反,LinkedHashMapHashMap的错误在于将公共名称重新用于私人用途。


1
那是因为 EntryMap.Entry 是两个不同的类。第一个的默认范围是定义在 HashMap 中,而后者是一个公共接口。所以您正在方法中使用具有更广泛范围的私有参数。这确实可以编译,但可能不是您的意图。

是的,A.Inner和B.Inner是两个不同的类。然而,一个可以编译通过,另一个却不能。两种情况有什么区别?此外,IntelliJ在这两种情况下与编译器意见不一致。我猜我刚发现了2个IntelliJ的bug?还是更微妙的问题?这就是谜团。 - Mark Bolusmjak
就像我说的,这是作用域问题。由于Entry具有默认访问权限,因此您实际上只能从该类或任何继承者中调用此方法。但是,在同一包中的类(通过将方法定义为“protected”而授予访问权限)实际上无法调用此方法,因为它们无法访问参数。因此,编译此代码是合法的,但IntelliJ会发出警告,因为这是无用的。 - M Platvoet
使用 @override 注释表示它确实覆盖了所需的方法。它并不无用,而且 IntelliJ 没有理由抱怨(就我所知)。 - Mark Bolusmjak

0

你只需要指定你正在引用哪个“Inner”。 IDE 只是一种辅助工具;编译器始终是最终的参考。

...
static class C extends B {

    public void doStuff(A.Inner a) {
    }
}
...

为了进一步澄清,您可以使用标准变量获得相同的“微妙/奇怪”的行为。私有定义遮盖了公共定义。

public class CompileTest {

    static class A {

        public static Object foo;

    }

    static class B extends A {

        private static Object foo;

    }

    static class C extends B {

        public void doStuff() {
            foo = new Object(); // Will not compile - 'foo' is not visible
            A.foo = new Object(); // Works
        }
    }
}

我知道如何让它工作。我询问的是微妙而奇怪的行为。我的问题是“有人能解释一下吗?”而不是“我该如何更改它以使其工作?” - Mark Bolusmjak
我在这里没有看到任何微妙或奇怪的东西。 - hoipolloi
一个世界级的IDE的静态代码分析居然错了两次,这真是奇怪。 - Mark Bolusmjak
世界级的... 呵呵 :P 这样有趣的陈述都是个人观点。 - Tim Bender
根据Tiobe和类似的排名,Java是最常用的前1或2种语言之一。IntelliJ是可用的前2或3个Java SE/EE(以及Scala/Clojure)IDE之一(与Eclipse和Netbeans一起)。我认为这并没有什么非常个人化或有趣的东西。是否有一个“Java IDE的实用哲学”stackexchange我们可以继续这个对话? - Mark Bolusmjak

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