การเลือกเมธอดที่โอเวอร์โหลดขึ้นอยู่กับประเภทจริงของพารามิเตอร์


115

ฉันกำลังทดลองกับรหัสนี้:

interface Callee {
    public void foo(Object o);
    public void foo(String s);
    public void foo(Integer i);
}

class CalleeImpl implements Callee
    public void foo(Object o) {
        logger.debug("foo(Object o)");
    }

    public void foo(String s) {
        logger.debug("foo(\"" + s + "\")");
    }

    public void foo(Integer i) {
        logger.debug("foo(" + i + ")");
    }
}

Callee callee = new CalleeImpl();

Object i = new Integer(12);
Object s = "foobar";
Object o = new Object();

callee.foo(i);
callee.foo(s);
callee.foo(o);

สิ่งนี้พิมพ์foo(Object o)สามครั้ง ฉันคาดหวังว่าการเลือกวิธีการจะพิจารณาประเภทพารามิเตอร์จริง (ไม่ใช่ประเภทที่ประกาศ) ฉันพลาดอะไรไปรึเปล่า? จะมีวิธีการแก้ไขรหัสนี้เพื่อที่ว่ามันจะพิมพ์foo(12), foo("foobar")และfoo(Object o)?

คำตอบ:


97

ฉันคาดหวังว่าการเลือกเมธอดจะคำนึงถึงประเภทพารามิเตอร์จริง (ไม่ใช่ประเภทที่ประกาศ) ฉันพลาดอะไรไปรึเปล่า?

ใช่. ความคาดหวังของคุณผิด ใน Java การจัดส่งเมธอดไดนามิกเกิดขึ้นเฉพาะกับอ็อบเจ็กต์ที่เมธอดถูกเรียกใช้ไม่ใช่สำหรับประเภทพารามิเตอร์ของเมธอดที่โอเวอร์โหลด

การอ้างถึงข้อกำหนดภาษา Java :

เมื่อมีการเรียกใช้เมธอด (§15.12) จำนวนอาร์กิวเมนต์จริง (และอาร์กิวเมนต์ประเภทที่ชัดเจน) และประเภทเวลาคอมไพล์ของอาร์กิวเมนต์ จะถูกใช้ในเวลาคอมไพล์เพื่อกำหนดลายเซ็นของเมธอดที่จะเรียกใช้ ( §15.12.2) ถ้าเมธอดที่จะเรียกใช้เป็นวิธีอินสแตนซ์เมธอดจริงที่จะถูกเรียกใช้จะถูกกำหนด ณ รันไทม์โดยใช้การค้นหาเมธอดแบบไดนามิก (§15.12.4)


4
คุณช่วยอธิบายข้อมูลจำเพาะที่คุณยกมาได้ไหม ทั้งสองประโยคดูเหมือนจะขัดแย้งกัน ตัวอย่างข้างต้นใช้วิธีการอินสแตนซ์ แต่วิธีการที่เรียกใช้นั้นไม่ได้ถูกกำหนดอย่างชัดเจนในขณะรัน
Alex Worden

16
@ Alex Worden: ประเภทเวลาคอมไพล์ของพารามิเตอร์วิธีใช้เพื่อกำหนดลายเซ็นของวิธีการที่จะเรียกในกรณีfoo(Object)นี้ ที่รันไทม์คลาสของอ็อบเจ็กต์ที่เมธอดถูกเรียกใช้จะเป็นตัวกำหนดว่าการนำเมธอดนั้นไปใช้งานใดโดยคำนึงว่ามันอาจเป็นอินสแตนซ์ของคลาสย่อยของชนิดที่ประกาศซึ่งแทนที่เมธอด
Michael Borgwardt

86

ตามที่กล่าวไว้ก่อนที่จะดำเนินการแก้ปัญหาการโอเวอร์โหลดในเวลาคอมไพล์

Java Puzzlersมีตัวอย่างที่ดีสำหรับสิ่งนั้น:

ปริศนา 46: กรณีของตัวสร้างที่สับสน

ปริศนานี้นำเสนอผู้สร้างที่สับสนสองคน วิธีการหลักเรียกใช้ตัวสร้าง แต่วิธีใด? ผลลัพธ์ของโปรแกรมขึ้นอยู่กับคำตอบ โปรแกรมพิมพ์อะไรหรือแม้กระทั่งถูกกฎหมาย?

public class Confusing {

    private Confusing(Object o) {
        System.out.println("Object");
    }

    private Confusing(double[] dArray) {
        System.out.println("double array");
    }

    public static void main(String[] args) {
        new Confusing(null);
    }
}

โซลูชันที่ 46: กรณีของตัวสร้างที่สับสน

... กระบวนการแก้ปัญหาโอเวอร์โหลดของ Java ทำงานในสองขั้นตอน ระยะแรกจะเลือกวิธีการหรือตัวสร้างทั้งหมดที่สามารถเข้าถึงและใช้ได้ ระยะที่สองจะเลือกวิธีการหรือตัวสร้างที่เฉพาะเจาะจงที่สุดที่เลือกไว้ในระยะแรก วิธีการหรือตัวสร้างหนึ่งมีความเฉพาะเจาะจงน้อยกว่าอีกวิธีหนึ่งหากสามารถยอมรับพารามิเตอร์ใด ๆ ที่ส่งผ่านไปยังอีกวิธีหนึ่ง [JLS 15.12.2.5]

ในโปรแกรมของเราผู้สร้างทั้งสองสามารถเข้าถึงและใช้งานได้ ตัวสร้าง Confusing (Object)ยอมรับพารามิเตอร์ใด ๆ ที่ส่งผ่านไปยังConfusing (double [])ดังนั้น Confusing (Object)จึงมีความเฉพาะเจาะจงน้อยกว่า ( อาร์เรย์คู่ทุกตัวเป็นObjectแต่ไม่ใช่ทุกObject ที่เป็นอาร์เรย์คู่ ) ตัวสร้างที่เฉพาะเจาะจงที่สุดจึงสับสน (double [])ซึ่งจะอธิบายผลลัพธ์ของโปรแกรม

พฤติกรรมนี้เหมาะสมถ้าคุณส่งค่าประเภทdouble [] ; มันเป็น counterintuitive ถ้าคุณผ่านnull กุญแจสำคัญในการทำความเข้าใจปริศนานี้คือการทดสอบวิธีการหรือตัวสร้างที่เฉพาะเจาะจงที่สุดไม่ใช้พารามิเตอร์จริง : พารามิเตอร์ที่ปรากฏในการเรียกใช้ ใช้เพื่อพิจารณาว่าการบรรทุกเกินพิกัดใดที่ใช้ได้เท่านั้น เมื่อคอมไพลเลอร์พิจารณาว่าการโอเวอร์โหลดใดที่สามารถใช้ได้และสามารถเข้าถึงได้มันจะเลือกการโอเวอร์โหลดที่เฉพาะเจาะจงที่สุดโดยใช้เฉพาะพารามิเตอร์ที่เป็นทางการเท่านั้น: พารามิเตอร์ที่ปรากฏในการประกาศ

หากต้องการเรียกใช้คอนสตรัคเตอร์Confusing (Object)ด้วยพารามิเตอร์nullให้เขียนConfusing ((Object) null)ใหม่ เพื่อให้แน่ใจว่าConfusing (Object)เท่านั้นที่ใช้ได้ โดยทั่วไปในการบังคับให้คอมไพเลอร์เลือกการโอเวอร์โหลดเฉพาะให้ส่งพารามิเตอร์จริงไปยังประเภทพารามิเตอร์ทางการที่ประกาศไว้


4
ฉันหวังว่ามันจะไม่สายเกินไปที่จะพูด - "หนึ่งในคำอธิบายที่ดีที่สุดเกี่ยวกับ SOF" ขอบคุณ :)
TheLostMind

5
ฉันเชื่อว่าถ้าเราเพิ่มตัวสร้าง 'private Confusing (int [] iArray)' มันจะไม่สามารถรวบรวมได้หรือไม่? เนื่องจากตอนนี้มีตัวสร้างสองตัวที่มีความจำเพาะเหมือนกัน
Risser

ถ้าฉันใช้ประเภทผลตอบแทนแบบไดนามิกเป็นอินพุตฟังก์ชันจะใช้ค่าที่เฉพาะเจาะจงน้อยกว่าเสมอ ... กล่าวว่าวิธีการที่สามารถใช้สำหรับค่าการส่งคืนที่เป็นไปได้ทั้งหมด ...
kaiser

16

ความสามารถในการส่งการเรียกร้องให้วิธีการขึ้นอยู่กับประเภทของการขัดแย้งที่เรียกว่าจัดส่งหลาย ใน Java นี้จะทำกับรูปแบบของผู้เข้าชม

อย่างไรก็ตามเนื่องจากคุณกำลังจัดการกับIntegers and Strings คุณจึงไม่สามารถรวมรูปแบบนี้ได้อย่างง่ายดาย (คุณไม่สามารถแก้ไขคลาสเหล่านี้ได้) ดังนั้นยักษ์switchบนเวลาวิ่งของวัตถุจะเป็นอาวุธที่คุณเลือก


11

ใน Java เมธอดที่จะเรียก (เช่นเดียวกับที่จะใช้ลายเซ็นของเมธอด) จะถูกกำหนดในเวลาคอมไพล์ดังนั้นจึงเป็นไปตามประเภทเวลาคอมไพล์

รูปแบบทั่วไปสำหรับการแก้ไขปัญหานี้คือการตรวจสอบประเภทวัตถุในเมธอดด้วยลายเซ็น Object และมอบหมายให้กับเมธอดด้วย cast

    public void foo(Object o) {
        if (o instanceof String) foo((String) o);
        if (o instanceof Integer) foo((Integer) o);
        logger.debug("foo(Object o)");
    }

หากคุณมีหลายประเภทและไม่สามารถจัดการได้การโอเวอร์โหลดเมธอดอาจไม่ใช่แนวทางที่ถูกต้อง แต่วิธีสาธารณะควรใช้ Object และใช้รูปแบบกลยุทธ์บางอย่างเพื่อมอบหมายการจัดการที่เหมาะสมตามประเภทอ็อบเจ็กต์


4

ฉันมีปัญหาคล้าย ๆ กันกับการเรียกตัวสร้างที่ถูกต้องของคลาสที่เรียกว่า "พารามิเตอร์" ซึ่งสามารถใช้ Java พื้นฐานหลายประเภทเช่น String, Integer, Boolean, Long เป็นต้นเมื่อพิจารณาจากอาร์เรย์ของวัตถุฉันต้องการแปลงเป็นอาร์เรย์ ของออบเจ็กต์พารามิเตอร์ของฉันโดยเรียกตัวสร้างที่เฉพาะเจาะจงที่สุดสำหรับแต่ละอ็อบเจ็กต์ในอาร์เรย์อินพุต ฉันยังต้องการกำหนด constructor Parameter (Object o) ที่จะโยน IllegalArgumentException แน่นอนฉันพบว่าวิธีนี้ถูกเรียกใช้สำหรับทุก Object ในอาร์เรย์ของฉัน

วิธีแก้ปัญหาที่ฉันใช้คือค้นหาตัวสร้างผ่านการสะท้อน ...

public Parameter[] convertObjectsToParameters(Object[] objArray) {
    Parameter[] paramArray = new Parameter[objArray.length];
    int i = 0;
    for (Object obj : objArray) {
        try {
            Constructor<Parameter> cons = Parameter.class.getConstructor(obj.getClass());
            paramArray[i++] = cons.newInstance(obj);
        } catch (Exception e) {
            throw new IllegalArgumentException("This method can't handle objects of type: " + obj.getClass(), e);
        }
    }
    return paramArray;
}

ไม่จำเป็นต้องมีอินสแตนซ์ของคำสั่งสลับหรือรูปแบบผู้เยี่ยมชมที่น่าเกลียด! :)


2

Java ดูประเภทการอ้างอิงเมื่อพยายามกำหนดว่าจะเรียกใช้เมธอดใด หากคุณต้องการบังคับรหัสของคุณคุณเลือกวิธีการ 'ถูกต้อง' คุณสามารถประกาศเขตข้อมูลของคุณเป็นอินสแตนซ์ของประเภทเฉพาะ:

Integeri = new Integer(12);
String s = "foobar";
Object o = new Object();

คุณยังสามารถใช้พารามิเตอร์ของคุณเป็นประเภทของพารามิเตอร์:

callee.foo(i);
callee.foo((String)s);
callee.foo(((Integer)o);

1

หากมีการจับคู่แบบตรงทั้งหมดระหว่างจำนวนและประเภทของอาร์กิวเมนต์ที่ระบุในการเรียกใช้เมธอดและลายเซ็นเมธอดของเมธอดที่โอเวอร์โหลดนั่นคือเมธอดที่จะถูกเรียกใช้ คุณกำลังใช้การอ้างอิง Object ดังนั้น java จึงตัดสินใจในขณะคอมไพล์ว่าสำหรับ Object param มีวิธีการที่ยอมรับ Object โดยตรง จึงเรียกวิธีนั้นว่า 3 ครั้ง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.