Java中的重载和多分派

Posted

技术标签:

【中文标题】Java中的重载和多分派【英文标题】:Overloading in Java and multiple dispatch 【发布时间】:2012-04-03 06:53:47 【问题描述】:

我有一个集合(或列表或数组列表),我想在其中放置字符串值和双精度值。我决定让它成为一个对象集合并使用重载和多态性,但我做错了。

我运行了一个小测试:

public class OOP 
    void prova(Object o)
        System.out.println("object");
    

    void prova(Integer i)
    System.out.println("integer");
    

    void prova(String s)
        System.out.println("string");
    

    void test()
        Object o = new String("  ");
        this.prova(o); // Prints 'object'!!! Why?!?!?
    

    public static void main(String[] args) 
        OOP oop = new OOP();
        oop.test(); // Prints 'object'!!! Why?!?!?
    

在测试中,参数类型似乎是在编译时而不是在运行时决定的。这是为什么呢?

这个问题与:

Polymorphism vs Overriding vs Overloading Try to describe polymorphism as easy as you can

编辑:

好的,要调用的方法是在编译时决定的。是否有避免使用instanceof 运算符的解决方法?

【问题讨论】:

【参考方案1】:

这篇文章秒 voo 的回答,并提供有关后期绑定/替代方案的详细信息。

一般 JVM 只使用 single dispatch:运行时类型只考虑接收者对象;对于方法的参数,考虑静态类型。使用 method tables(类似于 C++ 的虚拟表)可以轻松实现高效的优化。您可以找到详细信息,例如在the HotSpot Wiki。

如果您希望参数多次调度,请查看

groovy。但据我最新了解,它有一个过时的、缓慢的多次调度实现(参见例如this performance comparison),例如没有缓存。 clojure,但这与 Java 完全不同。 MultiJava,它为 Java 提供了多重调度。此外,您可以使用 this.resend(...) 而不是 super(...) 来调用封闭方法的最具体的重写方法; 值调度(下面的代码示例)。

如果你想坚持使用 Java,你可以

通过在更细粒度的类层次结构上移动重载方法来重新设计您的应用程序。 Josh Bloch's Effective Java,第 41 条(明智地使用重载)中给出了一个示例; 使用一些设计模式,例如Strategy、Visitor、Observer。这些通常可以解决与多次分派相同的问题(即,在那些情况下,对于那些使用多次分派的模式,您有一些简单的解决方案)。

值调度:

class C 
  static final int INITIALIZED = 0;
  static final int RUNNING = 1;
  static final int STOPPED = 2;
  void m(int i) 
    // the default method
  
  void m(int@@INITIALIZED i) 
    // handle the case when we're in the initialized `state'
  
  void m(int@@RUNNING i) 
    // handle the case when we're in the running `state'
  
  void m(int@@STOPPED i) 
    // handle the case when we're in the stopped `state'
  

【讨论】:

Multijava 非常有趣,但最后一个版本是 2006 年的。Josh Bloch 的 Effective Java 只是说您必须使用一种方法创建许多类,这是我会避免的。模式是要走的路。 哦 - Multijava 不再维护,这很有趣也很遗憾。我猜这些人更关注其他项目,比如 Java 建模语言。关于 Effective Java 中的示例:您不能将其转移到更一般的情况吗?例如。如果您需要在示例中区分 3 种行为,请使用 GeneralDecorator 和两个子类 NumberDecorator 和 TextualDecorator?然后你只需要 OOP.prova(GeneralDecorator),它从 GeneralDecorator 调用 someMethod(),它是单调度的。还是您已经将其称为一种模式(例如控制反转)? 顺便说一句,here is a blog post on how to solve your problem in Scala,它也只有一个调度。但是,该解决方案在 Java 中是不可能的,因为它使用了 Scala 的特性。 我想到了另一个问题:我原以为 Java 的具有类型擦除的泛型与多分派不兼容。扫视plrg.kaist.ac.kr/_media/research/publications/oopsla11.pdf,它似乎在某种程度上起作用:-0 是的,我认为装饰器模式是我所需要的。 :) Scala 是一门非常好的语言,我希望它会变得更加主流。关于泛型和类型擦除,我不知道类型擦除,我得稍微研究一下才能理解那篇文章。【参考方案2】:

你想要的是双重或更通用的multiple dispatch,实际上是用其他语言实现的东西(想到普通的 lisp)

大概java没有它的主要原因是因为它会带来性能损失,因为重载解析必须在运行时而不是编译时完成。解决这个问题的常用方法是 visitor pattern - 非常丑陋,但就是这样。

【讨论】:

我支持您的回答 (+1),但我不确定今天的编译器技术对于多次调度来说要慢得多(请参阅下面的回答)。你碰巧有这方面的最新统计数据吗? @DaveBall 没有运行任何测试,我当然怀疑它在大多数情况下“慢得多”。但它确实为优化增加了额外的复杂性,我可以想到我们会得到虚拟调用而不是直接调用的常见情况。许多 Java 类实现一个接口作为唯一的类 - 允许通常的优化。这依赖于许多接口仅由一个类实现的事实。与此相反,大多数方法在存在多个适用子类(例如所有集合类)的情况下采用相当通用的参数 续。所以我们确实得到了一些开销,它基本上归结为一些 if cascade 并在运行时为所有参数加载类(虽然这相当便宜)。我不认为它会影响一般情况,尽管我们不需要多次分派,并且在我们这样做的情况下避免了一类烦人的错误,我真的很讨厌访问者模式.. 我发现您的回答对我的特殊情况非常有用。但我不明白的是你说访问者风格的实现非常丑陋的原因。正如我们在 wiki 中看到的,它的目的之一是实现双重调度。【参考方案3】:

老问题但没有答案在 Java 中提供了一个具体的解决方案,以干净的方式解决问题。 事实上,这不是一个简单但非常有趣的问题。这是我的贡献。

好的,要调用的方法是在编译时决定的。有没有 避免使用 instanceof 运算符的解决方法?

正如出色的@DaveFar 回答中所说,Java 仅支持单调度方法。 在这种调度模式下,编译器通过依赖声明的参数类型而不是它们的运行时类型来限制方法在编译时立即调用。

我有一个要放入的集合(或列表或数组列表) 字符串值和双精度值。

为了以干净的方式解决答案并使用双重调度,我们必须为操作数据带来抽象。 为什么?

这里用一个幼稚的访问者方法来说明问题:

public class DisplayVisitor 

    void visit(Object o) 
        System.out.println("object"));
    

    void visit(Integer i) 
        System.out.println("integer");
    

    void visit(String s) 
        System.out.println("string"));
    


现在,问题:访问的类如何调用visit() 方法? 双重分派实现的第二个分派依赖于接受访问的类的“this”上下文。 所以我们需要在IntegerStringObject 类中有一个accept() 方法来执行第二次调度:

public void accept(DisplayVisitor visitor)
    visitor.visit(this);

但不可能!访问的课程是内置课程:StringIntegerObject。 所以我们没有办法添加这个方法。 无论如何,我们不想添加它。

所以要实现双重分派,我们必须能够修改我们想要在第二个分派中作为参数传递的类。 因此,我们不会将 ObjectList<Object> 作为声明的类型进行操作,而是将 FooList<Foo> 操作其中 Foo 类是保存用户值的包装器。

这里是Foo 接口:

public interface Foo 
    void accept(DisplayVisitor v);
    Object getValue();

getValue() 返回用户值。 它将 Object 指定为返回类型,但 Java 支持协方差返回(从 1.5 版本开始),因此我们可以为每个子类定义更具体的类型以避免向下转换。

ObjectFoo

public class ObjectFoo implements Foo 

    private Object value;

    public ObjectFoo(Object value) 
        this.value = value;
    

    @Override
    public void accept(DisplayVisitor v) 
        v.visit(this);
    

    @Override
    public Object getValue() 
        return value;
    


StringFoo

public class StringFoo implements Foo 

    private String value;

    public StringFoo(String string) 
        this.value = string;
    

    @Override
    public void accept(DisplayVisitor v) 
        v.visit(this);
    

    @Override
    public String getValue() 
        return value;
    


IntegerFoo

public class IntegerFoo implements Foo 

    private Integer value;

    public IntegerFoo(Integer integer) 
        this.value = integer;
    

    @Override
    public void accept(DisplayVisitor v) 
        v.visit(this);
    

    @Override
    public Integer getValue() 
        return value;
    


这是 DisplayVisitor 类访问Foo 子类:

public class DisplayVisitor 

    void visit(ObjectFoo f) 
        System.out.println("object=" + f.getValue());
    

    void visit(IntegerFoo f) 
        System.out.println("integer=" + f.getValue());
    

    void visit(StringFoo f) 
        System.out.println("string=" + f.getValue());
    


这是一个测试实现的示例代码:

public class OOP 

    void test() 

        List<Foo> foos = Arrays.asList(new StringFoo("a String"),
                                       new StringFoo("another String"),
                                       new IntegerFoo(1),
                                       new ObjectFoo(new AtomicInteger(100)));

        DisplayVisitor visitor = new DisplayVisitor();
        for (Foo foo : foos) 
            foo.accept(visitor);
        

    

    public static void main(String[] args) 
        OOP oop = new OOP();
        oop.test();
    

输出:

string=一个字符串

string=另一个字符串

整数=1

对象=100


改进实施

实际的实现需要为我们想要包装的每个内置类型引入一个特定的包装类。 如前所述,我们无法选择操作双重调度。 但请注意,可以避免 Foo 子类中的重复代码:

private Integer value; // or String or Object

@Override
public Object getValue() 
    return value;

我们确实可以引入一个抽象的泛型类来保存用户值并提供一个访问器:

public abstract class Foo<T> 

    private T value;

    public Foo(T value) 
        this.value = value;
    

    public abstract void accept(DisplayVisitor v);

    public T getValue() 
        return value;
    


现在Foo sublasses 更容易声明:

public class IntegerFoo extends Foo<Integer> 

    public IntegerFoo(Integer integer) 
        super(integer);
    

    @Override
    public void accept(DisplayVisitor v) 
        v.visit(this);
    



public class StringFoo extends Foo<String> 

    public StringFoo(String string) 
        super(string);
    

    @Override
    public void accept(DisplayVisitor v) 
        v.visit(this);
    



public class ObjectFoo extends Foo<Object> 

    public ObjectFoo(Object value) 
        super(value);
    

    @Override
    public void accept(DisplayVisitor v) 
        v.visit(this);
    


并且test()方法应该修改为在List&lt;Foo&gt;声明中为Foo类型声明一个通配符类型(?)。

void test() 

    List<Foo<?>> foos = Arrays.asList(new StringFoo("a String object"),
                                      new StringFoo("anoter String object"),
                                      new IntegerFoo(1),
                                      new ObjectFoo(new AtomicInteger(100)));

    DisplayVisitor visitor = new DisplayVisitor();
    for (Foo<?> foo : foos) 
        foo.accept(visitor);
    


事实上,如果真的需要,我们可以通过引入 java 代码生成来进一步简化 Foo 子类。

声明这个子类:

public class StringFoo extends Foo<String> 

    public StringFoo(String string) 
        super(string);
    

    @Override
    public void accept(DisplayVisitor v) 
        v.visit(this);
    


可以像声明一个类并添加注释一样简单:

@Foo(String.class)
public class StringFoo  

其中Foo 是在编译时处理的自定义注解。

【讨论】:

【参考方案4】:

当调用重载的方法时,Java 会根据传递给函数的变量 的类型选择最严格的类型。它不使用实际实例的类型。

【讨论】:

【参考方案5】:

这不是多态,你只是重载了一个方法并使用对象类型的参数调用它

【讨论】:

根据Wikipedia函数重载是Ad hoc多态性。【参考方案6】:

Java 中的一切都是Object/对象(基本类型除外)。您将字符串和整数存储为对象,然后当您调用prove 方法时,它们仍被称为对象。你应该看看instanceof 关键字。 Check this link

void prove(Object o)
   if (o instanceof String)
    System.out.println("String");
   ....

【讨论】:

以上是关于Java中的重载和多分派的主要内容,如果未能解决你的问题,请参考以下文章

[jvm解析系列][十二]分派,重载和重写,查看字节码带你深入了解分派的过程。

访问者模式

JVM 方法调用之静态分派

今日拾遗 20200627:java 的动态分派,到底包含哪些知识点?

(三十)分派调用:静态分派和动态分派

64.多态性实现机制—静态分派与动态分派(方法解析静态分派动态分派单分派和多分派)