【问题标题】:Method reference to private interface method私有接口方法的方法引用
【发布时间】:2018-06-21 06:27:03
【问题描述】:

考虑以下代码:

public class A {
    public static void main(String[] args) {
        Runnable test1 = ((I)(new I() {}))::test;  // compiles OK
        Runnable test2 = ((new I() {}))::test;     // won't compile 
    }

    interface I {
        private void test() {}
    }
}

我不太明白...我知道test() 方法是private。但是,如果我们将匿名类强制转换为它的接口((I)(new I() {})),会发生什么变化?更准确地说,我希望看到一个允许该技巧的特定 JLS 点。

P.S. 我已将其报告为编译器的错误(ID:9052217)。在我看来,Runnable test2 = ((new I() {}))::test; 在这种特殊情况下应该编译得很好。

P.P.S. 到目前为止,根据我的报告创建了一个错误:https://bugs.openjdk.java.net/browse/JDK-8194998。可能会以“无法修复”或其他方式关闭。

【问题讨论】:

  • ((new I() {}))::test这个丑陋的代码是什么,它是从哪里来的?
  • @insidesin 你想怎么做? :)
  • 您的问题标题存在误导性错误。 “带有私有方法的接口”显然是I,它既不是,也不是功能接口,也不是“作为功能接口”使用的。
  • @JarrodRoberson 您不能将这些答案推断为接口中的私有方法作为绝对新功能
  • 标题建议:“私有接口方法的方法引用”。方法引用将被转换为功能接口的实例(在运行时),但这不是这个函数的重点。如果 Java 中有真正的函数类型,那么问题是相同的,因此方法引用在没有函数接口的情况下也能正常工作。

标签: java language-lawyer java-9


【解决方案1】:

这不是一个新问题,与私有接口方法或方法引用无关。

如果您更改代码以扩展类而不是实现接口,并调用方法而不是引用它,您仍然会遇到完全相同的问题。

class A {
    public static void main(String[] args) {
        ((I)(new I() {})).test();  // compiles OK
        ((new I() {})).test();     // won't compile 
    }

    class I {
        private void test() {}
    }
}

但是,该代码可以应用于较旧的 Java 版本,我尝试了 Java 9、8、7、6、5 和 1.4。都一样!!

问题是私有方法没有被继承1,所以匿名类根本没有这个方法。由于匿名类中甚至不存在私有方法,因此无法调用。

当您转换为 I 时,该方法现在存在供编译器查看,并且由于 I 是一个内部类,因此您被授予访问权限(通过合成方法),即使它是私有的。

在我看来,这不是一个错误。这就是私有方法在继承上下文中的工作方式。

1) 作为JLS 6.6-5 中的found by Jorn Vernee“[A private class member] 不被子类继承”

【讨论】:

  • @Andremoniy 为什么会这样?接口中的私有方法是供接口使用的,没有其他人使用。通过创建内部接口来绕过该规则,因此顶级类可以访问,这确实是一种技巧,在我看来是糟糕的代码。
  • 我想我明白这是怎么回事了,但是,我发现这里有一个矛盾:由于匿名类中甚至不存在私有方法,所以不能调用它 VS 当您转换为 I 时,该方法现在存在供编译器查看。无论有没有强制转换,实例的实际类型都是不可表示的匿名类型。所以,如果方法被执行,那么它就在那里。
  • @FedericoPeraltaSchaffner 假设你是编译器,程序员要求在某个表达式上调用test。您查找表达式的类型,打开它的方法列表,并没有找到 test 那里(因为它不是继承的)。但是,随着演员的转换,表达式的类型会发生变化,因此您获得的方法列表也会发生变化,现在您可以在那里找到test
  • @FedericoPeraltaSchaffner 澄清:编译器看到只有当类型是声明方法的地方时,因为私有方法在所有其他方法上都是不可见的类型。对方法的访问是一个单独的事情,即如果您将接口移出,成为顶级接口,那么编译器仍然可以通过强制转换看到它,但您现在将无法访问了。就 JVM 而言,即使接口是内部类型,您也无权访问,因此编译器通过创建隐藏的合成非私有桥接方法来伪造对私有方法的访问,并调用它。
  • 这条规则的一个结果是,不可能在类型参数的引用上调用private 方法。这太不直观了,以至于编译器在 Java 5(即使在 Java 6 中也是 Eclipse)中弄错了,并且在将源代码级别设置为这些旧版本时仍然保持错误的行为,以实现兼容性。
【解决方案2】:

private 方法没有被继承(到目前为止我发现的最接近的是:JLS6.6-5“[A private class member] 没有被子类继承”)。这意味着您不能从子类型访问私有方法(因为它根本没有“拥有”该方法)。例如:

public static void main(String[] args) {
    I1 i1 = null;
    I2 i2 = null;

    i1.test(); // works
    i2.test(); // method test is undefined
}

interface I1 {
    private void test() {}
}

interface I2 extends I1 {}

这也意味着你不能通过匿名子类的类型直接访问test方法。 表达式的类型:

(new I() {})

不是I,实际上是匿名子类的不可表示类型,所以无法通过它访问test

但是,表达式的类型:

((I) (new I() {}))

I(当您明确地将其转换为I),因此您可以通过它访问test 方法。 (就像你可以在我上面的例子中做((I1) i2).test();

类似的规则适用于static 方法,因为它们也不是继承的。

【讨论】:

  • 不错的 JLS 参考。 +1。在我们之间(见my answer),我想我们已经讨论过了。
  • 到目前为止,根据我的报告创建了一个错误:bugs.openjdk.java.net/browse/JDK-8194998。可能会以“无法修复”或其他方式关闭。
【解决方案3】:

无论场景如何,都只能通过完全声明类型的表达式调用private 方法。

我们用最简单的例子来解释一下

public class A {
    public static void main(String[] args) {
        B b = new B();
        b.someMethod(); // does not compile
        A a = b;
        a.someMethod(); // no problem
    }
    private void someMethod() {}
}
class B extends A {
}

您可能希望它使用b.someMethod() 来编译以调用AsomeMethod()。但是如果B 被声明为

class B extends A {
    public void someMethod() {}
}

这是可能的,因为private void someMethod() 没有被继承,所以public void someMethod() 不会覆盖它。但应该清楚的是,现在b.someMethod() 应该调用B 的方法。

因此,如果允许b.someMethod()Aprivate 方法结束,这将取决于B 是否声明另一个someMethod(),调用将以哪个实际方法结束。这显然与private 方法的整个概念相矛盾。 private 方法不会被继承,也不会被覆盖,因此它不应该依赖于子类,无论调用是否以 private 方法或子类的方法结束。

您的示例类似。实现I 的匿名内部类可以声明自己的test() 方法,例如Runnable test2 = ((new I() {void test() {}}))::test; 所以它取决于那个匿名内部类,无论是调用Iprivate 方法还是那个匿名内部类的方法,这是不可接受的。当然,对于这样的内部类,直接在调用或方法引用之前,读者可以立即知道调用将在哪个方法处结束,但是如果允许匿名内部类但什么都没有,这将是非常不一致的否则。

Iprivate 方法可以被 A 访问,因为它是一个嵌套接口,但如上面更简单的示例所示,该规则与可访问性无关,因为该规则甚至适用于 @ 987654348@方法与调用者在同一个类中。

【讨论】:

    【解决方案4】:

    这是违反直觉的。首先让我们简化一下:

    static interface Inter {
        private void test() {
            System.out.println("test");
        }
    }
    
    
    public static void main(String[] args) {
        ((Inter) new Inter() {
        }).hashCode();
    }
    

    这是有道理的,因为您正在调用公共 hashCode 方法,这里是它的(仅重要部分)字节码:

    public static void main(java.lang.String[]);
    Code:
       0: new           #2   // class com/test/DeleteMe$1
       3: dup
       4: invokespecial #3   // Method com/test/DeleteMe$1."<init>":()V
       7: invokevirtual #4   // Method java/lang/Object.hashCode:()I
      10: pop
      11: return
    

    对我来说看起来很理智。现在让我们将其更改为调用test()

    public static void main(String[] args) {
        ((Inter) new Inter() {
        }).test();
    }
    

    这个的字节码:

     invokestatic  #4  // InterfaceMethod com/test/DeleteMe$Inter.access$000:(Lcom/test/DeleteMe$Inter;)V
    

    由于私有方法没有被继承,你实际上是通过access$n静态合成方法“去”那个方法。

    【讨论】:

    猜你喜欢
    • 2015-05-16
    • 2011-04-26
    • 1970-01-01
    • 2018-05-06
    • 2017-11-06
    • 2019-04-12
    • 2010-09-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多