Java中的方法调度:为什么不能在运行时区分接口和类参数呢?

gk7wooem  于 2023-05-15  发布在  Java
关注(0)|答案(4)|浏览(120)

我正在编写一个Java程序,它有一个接口I和实现它的类AB。我还有另一个实现C的类,它有两个方法和一个参数:一个接受对象A,另一个接受对象B。

interface I {}
  static class A implements I {}
  static class B implements I {}
  interface C {
    String map(A a);
    String map(B b);
  }

  public static void main(String[] args) {
    // Injected at runtime;
    C mapper = getInstance();
    I a = new A();
    I b = new B();
    // This fail at compilation 
    String result = mapper.map(a);
  }   
private static C getInstance() {
    return new C() {
      @Override
      public String map(A a) {
        return "a";
      }
      @Override
      public String map(B b) {
        return "b";
      }
    };
  }

当我尝试用A或B的对象调用C的方法时,我得到以下错误:

java: no suitable method found for map(Main.I)
    method Main.C.map(Main.A) is not applicable
      (argument mismatch; Main.I cannot be converted to Main.A)
    method Main.C.map(Main.B) is not applicable
      (argument mismatch; Main.I cannot be converted to Main.B)

即使我们说I类型的其他示例可以传递给mapper,sealed interface I permits A, B {}也应该解决这个问题。
为什么Java中没有运行时的方法调度?我的问题是为什么在java中不可能,而不是“为什么它不工作”

okxuctiv

okxuctiv1#

对于“因为重载选择是在编译时根据参数的静态类型完成的,而不是在运行时根据参数的动态类型完成的”的效果,有几个答案。这些语句是100%正确的;这就是重载的工作原理,也是它的设计工作原理。多分派(multimethods)在Java选择单(基于接收者)分派的时候是众所周知的。
一个合理的下一个问题是:为什么要这样做?这在1995年是一个扣篮的原因有几个:

***可预测性。**如果给定的调用点总是对应于完全相同的(虚拟)方法,则这是一个简单的好处。
***安全性。**有些情况下没有“最佳”过载。在编译时确定这一点,并给出一个明确的错误消息,这比只有在传递了意外的输入组合时才让程序在运行时失败要好。让每个方法分派都潜在地抛出“没有最特定的重载”异常会使语言不那么可靠。
***性能。**重载选择可能代价高昂;最好是在编译时预先执行此操作,而不是加重运行时的负担。
***简单性。**重载和覆盖的交互将比现在复杂得多,因为,例如,我们需要更多的规则来打破“超类中更具体的方法和子类中被覆盖的候选者”之间的联系。这将导致更多令人惊讶的互动。
***实用性。**真正的多次调度很少;采用多参数分派可能会使语言和运行时复杂化,超过其表达价值。

在2023年,编译器和运行时技术有了很大的改进,因此编译时重载选择和运行时分派之间的平衡可能会有所改变,但总体而言,这仍然是一个明智和务实的权衡。

plupiseo

plupiseo2#

由于在java中不可能有这种重载,我找到了一个解决方案,可以解决类似的问题,使用访问者模式。
首先,假设下面的代码表示我们的域对象,我们不会向它添加任何外部依赖项或实现细节(如Map细节)

// Domain Layer
 interface I {
    void accept(VisitorMapper visitorMapper);
  }

  static final class A implements I {
    @Override
    public void accept(VisitorMapper visitorMapper) {
      visitorMapper.visit(this);
    }
  }
  static final class B implements I {
    @Override
    public void accept(VisitorMapper visitorMapper) {
      visitorMapper.visit(this);
    }
  }

interface VisitorMapper {

    void visit(A a);

    void visit(B b);
  }

正如我们在域层中看到的,没有外部依赖关系或Map逻辑的实现细节。
在下面的代码中,我们将表示Web层,我们希望将域对象Map到JSON表示中,并将其打印到PrintStream。

// Web Layer 
  static class JsonVisitorMapper implements VisitorMapper {

    private final PrintStream out;

    JsonVisitorMapper(PrintStream out) {
      this.out = out;
    }

    @Override
    public void visit(A a) {
      out.println("""
          {"a": "a"}
          """);
    }

    @Override
    public void visit(B b) {
      out.println("""
          {"b": "b"}
          """);
    }
  }

现在让我们结束吧!以下是我们的主要应用:

public static void main(String[] args) {
    VisitorMapper mapper = new JsonVisitorMapper(System.out);
    I a = new A();
    I b = new B();
    List<I> domainsObjects = List.of(a, b);
    domainsObjects.forEach(obj -> obj.accept(mapper));
  }

现在我们可以将不同领域对象的列表Map到同一个流中的不同表示。
PS:PrintStream实际上可以是ResponseWriterList或任何可变的。

uidvcgyl

uidvcgyl3#

在java中,方法的“身份”是而不仅仅是它的名称。它实际上是它的名称,* 和 * 是它所在的类型,* 和 * 是参数名的类型(在类文件级别,甚至是返回类型)。
因此,给定map(I i)map(A a),这些方法 * 完全不同 *。这是不可能得到动态分派去出于同样的原因,它是不可能的,使调用.foo(x)以某种方式导致调用.bar(x)
当然,Java确实有动态分派。重写(进行动态分派)和重载(不进行动态分派)之间存在差异。
这就是java * 做 * 动态分派的地方:

class Parent {
  void foo() { System.out.println("Parent!"); }
  void bar(Number n) { System.out.println("Parent!"); }
}

class Child extends Parent {
  void foo() { System.out.println("Child!"); }
  void bar(Integer n) { System.out.println("Child!"); }
}

Parent p = new Child();
Number n = 5;
p.foo(); // prints 'Child!'
p.bar(n); // prints 'Parent!'

Child中的foo()override。你可以把@Override粘在上面,javac会毫无怨言地编译它。(@Override不会使方法成为覆盖;它仅仅是编译器检查文档:如果没有覆盖任何内容,javac将拒绝编译带有该注解的方法。这就是它的作用这是“错误或无操作”)。
然而,bar(Integer) * 不是一个覆盖 * -如果你坚持注解,它会导致编译错误。假设bar(Integer)是一个完全不同的方法,p.bar(expr),其中expr的类型是Number(即使该表达式解析为对Integer类型对象的引用,这是不相关的,编译器无法知道,这是运行时的事情)。
如果你想要这个,它..看起来有点丑会是这样的:

interface C {
  String map(I i);
}

private static C getInstance() {
  return new C() {
    public String map(I i) {
      if (i instanceof A) { ... }
      else if (i instanceof B ) { .... }
      // and so on
    }
  };
}

注意,java 20引入了一些方便的特性,比如i instanceof A a(它声明了一个名为aA类型的变量),以及pattern-switch,它允许你为每个类型编写一个case语句(也可以为你创建一个“预转换”的变量)。

5lhxktic

5lhxktic4#

对象a的类型在编译时未知,编译器无法确定调用哪个版本的map方法,因此它是不明确的
为了解决这个问题
选项1:将a的类型更改为更具体

  • (例如,A a = new A()或B b = new B())

选项2:修改C接口的map方法以接受实现I接口的任何对象

  • (例如,String map(I i))。

相关问题