2012-03-18 27 views
20

我有一個集合(或列表或數組列表),我想在其中放置字符串值和雙精度值。我決定把它作爲一個對象的集合,並使用重載多態性,但我做錯了什麼。在Java中重載和多次調度

我運行一個小測試:

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運算符的解決方法?

+0

因爲這就是Java重載的工作原理。 – 2012-03-18 14:35:16

+0

但是,該對象在運行時是一個字符串,並且有一個爲字符串定義的方法。我究竟做錯了什麼? – 2012-03-18 14:36:50

+3

你假定選擇哪個重載調用是在運行時進行的。不是;它是在編譯時根據參數的靜態類型決定的。 – 2012-03-18 14:37:51

回答

17

本帖子voo的回覆,並提供了關於/替代晚期綁定的詳細信息。

一般JVM只使用單個調度:運行時類型只考慮接收對象;對於方法的參數,考慮靜態類型。使用method tables(類似於C++的虛擬表),使用優化的高效實現非常簡單。你可以找到細節,例如在the HotSpot Wiki

如果你想多分派爲您的參數,看看

  • groovy。但就我的最新知識而言,它具有過時的,緩慢的多次調度實施(例如見this performance comparison),例如,沒有緩存。
  • clojure,但這與Java完全不同。
  • MultiJava,它爲Java提供多個派遣。此外,您可以使用
    • this.resend(...)而不是super(...)來調用封閉方法的最具體的重寫方法;
    • 值調度(代碼示例如下)。

如果你想棒與Java,你可以

  • 移動在更細粒度的類層次重載方法重新設計應用程序。 Josh Bloch's Effective Java,條目41(明智地使用重載)給出了一個例子。
  • 使用一些設計模式,如策略,訪問者,觀察者。這些通常可以解決與多次調度相同的問題(即在那些情況下,對於使用多次調度的那些模式而言,這些解決方案是微不足道的)。

值調度:

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([email protected]@INITIALIZED i) { 
    // handle the case when we're in the initialized `state' 
    } 
    void m([email protected]@RUNNING i) { 
    // handle the case when we're in the running `state' 
    } 
    void m([email protected]@STOPPED i) { 
    // handle the case when we're in the stopped `state' 
    } 
} 
+0

Multijava非常有趣,但最後一個版本是2006年。Josh Bloch的Effective Java只是說你必須用一種方法創建許多類,這是我會避免的。模式是要走的路。 – 2012-03-18 16:45:58

+0

哦 - 有趣和悲傷的是,Multijava不再維護。我猜這些人更關注於Java Modeling Language等其他項目。關於Effective Java的例子:你不能將它轉移到更一般的情況嗎?例如。如果您需要在您的示例中區分3種行爲,請使用GeneralDecorator和兩個子類NumberDecorator和TextualDecorator?然後你只需要OOP.prova(GeneralDecorator),它調用GeneralDecorator中的someMethod(),它是單個派發的。或者你會否稱這種模式已經存在(例如控制反轉)? – DaveFar 2012-03-18 17:54:55

+0

順便說一句,[這裏是關於如何解決您在斯卡拉的問題的博客文章](http://cleverlytitled.blogspot.de/2010/01/dynamic-dispatch-in-scala.html),它只有單一的發送,太。但是,該解決方案在Java中不可行,因爲它使用了Scala的特性。 – DaveFar 2012-03-18 18:05:04

1

這不是polymoprhism,你只是重載的方法與對象類型的參數調用它

3

當調用一個重載的方法,Java的挑選基礎上,的類型限制最多的類型變量傳遞給函數。它不使用實際實例的類型。

1

Java中的所有東西都是Object/object(原始類型除外)。您將字符串和整數存儲爲對象,然後當您調用prove方法時,它們仍稱爲對象。您應該查看instanceof關鍵字。 Check this link

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

你想要的是雙或更一般multiple dispatch,而這在其他語言中實際執行的(Common Lisp的想到)

推測主要原因Java中沒有它,是因爲它會導致性能損失,因爲重載解析必須在運行時完成,而不是編譯時間。通常的解決方法是visitor pattern - 非常醜陋,但事實就是這樣。

+0

我第二次回答你的答案(+1),儘管我不確定今天的編譯器技術對於多次調度來說要慢得多(請參閱下面的答案)。你有沒有關於這方面的最新統計數據? – DaveFar 2012-03-18 16:42:19

+0

@DaveBall沒有運行任何測試,我當然懷疑它在大多數情況下「慢得多」。但它肯定增加了優化的複雜度,我可以想到一些常見的情況,我們可以通過虛擬呼叫而不是直接呼叫。許多Java類實現一個接口作爲唯一的類 - 允許通常的優化。這依賴於許多接口僅由一個類實現的事實。與此相反,大多數方法在存在多個適用子類的情況下采用相當一般的參數(例如,所有集合類) – Voo 2012-03-18 18:08:05

+0

續。所以我們確實得到了一些開銷,它基本上歸結爲一些級聯,並在運行時爲所有參數加載類(這很便宜)。我認爲它不影響一般情況,雖然我們不需要多次調度,並且在我們這樣做的情況下避免了煩人的一類錯誤,並且我非常憎恨訪問者模式。 – Voo 2012-03-18 18:12:52

1

老問題,但沒有答案提供了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」上下文。
所以我們必須IntegerStringObjectaccept()方法來執行本次分派:

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

但不可能!訪問的課程是內置課程:String,Integer,Object
所以我們沒有辦法添加這個方法。
無論如何,我們不想補充說。

所以要實現雙派遣,我們必須能夠修改我們想要作爲第二派遣參數傳遞的類。
因此,我們不會操縱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 = another字符串

整數= 1

對象= 100


改進實施

實際實現需要對每個BUIT入式引入特定包裝類的我們想要包裝。如上所述,我們沒有選擇操作雙重派單。
但需要注意的是,在子類的重碼可避免:

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子類更輕申報:

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<Foo>聲明中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是在編譯時處理的自定義註解。