การคัดเลือกอย่างชัดเจนจากคลาสซูเปอร์ไปเป็นคลาสย่อย


161
public class Animal {
    public void eat() {}
}

public class Dog extends Animal {
    public void eat() {}

    public void main(String[] args) {
        Animal animal = new Animal();
        Dog dog = (Dog) animal;
    }
}

ได้รับมอบหมายDog dog = (Dog) animal;ไม่ได้สร้างรวบรวมข้อผิดพลาด ClassCastExceptionแต่ที่รันไทม์มันสร้าง ทำไมคอมไพเลอร์ตรวจไม่พบข้อผิดพลาดนี้?


51
คุณกำลังบอกคอมไพเลอร์ว่าไม่พบข้อผิดพลาด
Mauricio

คำตอบ:


326

เมื่อใช้นักแสดงคุณต้องบอกคอมไพเลอร์ว่า "เชื่อฉันฉันเป็นมืออาชีพฉันรู้ว่าฉันกำลังทำอะไรอยู่และฉันรู้ว่าถึงแม้ว่าคุณจะไม่สามารถรับประกันได้ แต่ฉันบอกคุณว่าanimalตัวแปรนี้แน่นอน จะเป็นสุนัข "

เนื่องจากสัตว์ไม่ได้เป็นสุนัข (เป็นสัตว์คุณสามารถทำได้Animal animal = new Dog();และเป็นสุนัข) VM ส่งข้อยกเว้นขณะทำงานเนื่องจากคุณละเมิดความเชื่อใจนั้น (คุณบอกว่าคอมไพเลอร์ทุกอย่างจะใช้ได้และก็ ไม่!)

คอมไพเลอร์นั้นฉลาดกว่าที่จะยอมรับทุกสิ่งทุกอย่างเล็กน้อยหากคุณลองและโยนวัตถุในลำดับชั้นการสืบทอดที่แตกต่างกัน (ยกตัวอย่างเช่น Dog to a String) คอมไพเลอร์จะส่งกลับมาที่คุณเพราะมันรู้ว่าอาจจะไม่ทำงาน

เนื่องจากคุณเพียงแค่หยุดคอมไพเลอร์ไม่ให้บ่นทุกครั้งที่คุณส่งเป็นเรื่องสำคัญที่จะต้องตรวจสอบว่าคุณจะไม่ทำให้เกิด a ClassCastExceptionโดยใช้instanceofในข้อความสั่ง if (หรือบางสิ่งที่มีผลกระทบนั้น)


ขอบคุณ แต่คุณต้องสุนัขขยายจากสัตว์หากไม่ได้ไม่ทำงาน :)
delive

66
ฉันชอบที่คุณทำให้มันดูน่าทึ่ง
Hendra Anggrian

3
@ delive แน่นอนคุณทำ แต่ตามคำถามDog ไม่ขยายจากAnimal!
Michael Berry

52

เพราะในทางทฤษฎีAnimal animal อาจเป็นสุนัข:

Animal animal = new Dog();

โดยทั่วไปการดาวน์สตรีมไม่ใช่ความคิดที่ดี คุณควรหลีกเลี่ยง หากคุณใช้คุณควรรวมเช็ค:

if (animal instanceof Dog) {
    Dog dog = (Dog) animal;
}

แต่รหัสต่อไปนี้สร้างข้อผิดพลาดในการรวบรวม Dog dog = new Animal (); (ประเภทที่เข้ากันไม่ได้). แต่ในสถานการณ์เช่นนี้คอมไพเลอร์ระบุว่าสัตว์เป็นซูเปอร์คลาสและสุนัขเป็นคลาสย่อยดังนั้นการมอบหมายนั้นไม่ถูกต้อง แต่เมื่อเราร่าย Dog Dog = (Dog) สัตว์; ยอมรับ. โปรดอธิบายฉันเกี่ยวกับสิ่งนี้
saravanan

3
ใช่เพราะ Animal เป็น superclass ไม่ใช่สัตว์ทุกตัวใช่ไหม คุณสามารถอ้างถึงชั้นเรียนตามประเภทหรือประเภทของพวกเขาเท่านั้น ไม่ใช่ชนิดย่อย
Bozho

43

เพื่อหลีกเลี่ยง ClassCastException ชนิดนี้หากคุณมี:

class A
class B extends A

คุณสามารถกำหนดนวกรรมิกใน B ที่รับวัตถุของ A. ด้วยวิธีนี้เราสามารถทำ "cast" เช่น:

public B(A a) {
    super(a.arg1, a.arg2); //arg1 and arg2 must be, at least, protected in class A
    // If B class has more attributes, then you would initilize them here
}

24

อธิบายคำตอบของ Michael Berry อย่างละเอียด

Dog d = (Dog)Animal; //Compiles but fails at runtime

ที่นี่คุณกำลังพูดกับคอมไพเลอร์ "เชื่อฉันฉันรู้ว่าdมันหมายถึงDogวัตถุ" แม้ว่ามันจะไม่ใช่ โปรดจำไว้ว่าคอมไพเลอร์ถูกบังคับให้ความไว้วางใจเราเมื่อเราทำตาละห้อย

คอมไพเลอร์เท่านั้นที่รู้เกี่ยวกับประเภทอ้างอิงที่ประกาศไว้ JVM ที่รันไทม์รู้ว่าวัตถุคืออะไร

ดังนั้นเมื่อ JVM ที่รันไทม์คิดว่าDog dจริง ๆ แล้วเป็นการอ้างถึงวัตถุAnimalไม่ใช่Dogวัตถุ เฮ้ ... ClassCastExceptionคุณโกหกคอมไพเลอร์และพ่นไขมันใหญ่

ดังนั้นหากคุณกำลังใจร้อนคุณควรใช้instanceofการทดสอบเพื่อหลีกเลี่ยงการทำให้ผิดพลาด

if (animal instanceof Dog) { Dog dog = (Dog) animal; }

ตอนนี้คำถามมาถึงใจของเรา ทำไมคอมไพเลอร์นรกจึงอนุญาตให้ downcast เมื่อในที่สุดมันจะขว้างjava.lang.ClassCastException ?

คำตอบก็คือว่าทุกคอมไพเลอร์สามารถทำได้คือการตรวจสอบว่าทั้งสองประเภทอยู่ในต้นไม้มรดกเดียวกันดังนั้นขึ้นอยู่กับสิ่งที่อาจจะมีรหัสมาก่อนที่จะเศร้าใจก็เป็นไปได้ว่าเป็นประเภทanimaldog

คอมไพเลอร์ต้องอนุญาตสิ่งต่าง ๆ ที่อาจเป็นไปได้ในการทำงานขณะรันไทม์

พิจารณาตัวอย่างรหัสต่อไปนี้:

public static void main(String[] args) 
{   
    Dog d = getMeAnAnimal();// ERROR: Type mismatch: cannot convert Animal to Dog
    Dog d = (Dog)getMeAnAnimal(); // Downcast works fine. No ClassCastException :)
    d.eat();

}

private static Animal getMeAnAnimal()
{
    Animal animal = new Dog();
    return animal;
}

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

String s = (String)d; // ERROR : cannot cast for Dog to String

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

Dog d = new Dog(); Animal animal1 = d; // Works fine with no explicit cast Animal animal2 = (Animal) d; // Works fine with n explicit cast

ทั้งสอง upcast ข้างต้นจะทำงานได้ดีโดยไม่มีข้อยกเว้นเพราะ Dog IS-A Animal, anithing สัตว์สามารถทำได้สุนัขสามารถทำได้ แต่มันไม่ใช่ความจริงในทางกลับกัน


1

รหัสสร้างข้อผิดพลาดในการคอมไพล์เนื่องจากอินสแตนซ์ของคุณเป็นสัตว์ชนิด:

Animal animal=new Animal();

ไม่อนุญาตให้ดาวน์สตรีมใน Java ด้วยเหตุผลหลายประการ ดูรายละเอียดที่นี่


5
ไม่มีข้อผิดพลาดในการรวบรวมนั่นคือเหตุผลสำหรับคำถามของเขา
Clarence Liu

1

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


0

หากต้องการพัฒนาคำตอบของ @Caumons:

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

ตอนนี้ลองดูวิธีนี้

พ่อสามารถรับสิ่งของตนเองจากเด็กแต่ละคน นี่คือคลาสพ่อ:

public class Father {

    protected String fatherField;

    public Father(Father a){
        fatherField = a.fatherField;
    }

    //Second constructor
    public Father(String fatherField){
        this.fatherField = fatherField;
    }

    //.... Other constructors + Getters and Setters for the Fields
}

นี่คือคลาสลูกของเราที่ควรนำคอนสตรัคเตอร์ของพ่อมาให้ในกรณีนี้คอนสตรัคเตอร์ดังกล่าว:

public class Child extends Father {

    protected String childField;

    public Child(Father father, String childField ) {
        super(father);
        this.childField = childField;
    }

    //.... Other constructors + Getters and Setters for the Fields

    @Override
    public String toString() {
        return String.format("Father Field is: %s\nChild Field is: %s", fatherField, childField);
    }
}

ตอนนี้เราทดสอบการสมัคร:

public class Test {
    public static void main(String[] args) {
        Father fatherObj = new Father("Father String");
        Child child = new Child(fatherObj, "Child String");
        System.out.println(child);
    }
}

และนี่คือผลลัพธ์:

เขตข้อมูลพ่อคือ: สตริงพ่อ

เขตข้อมูลย่อยคือ: สตริงเด็ก

ตอนนี้คุณสามารถเพิ่มเขตข้อมูลใหม่ในชั้นพ่อโดยไม่ต้องกังวลว่ารหัสลูก ๆ ของคุณจะแตก

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