1. ภาพรวม
ระบบประเภท Java ประกอบด้วยสองประเภท: แบบดั้งเดิมและการอ้างอิง
เรากล่าวถึงการแปลงแบบดั้งเดิมในบทความนี้และเราจะเน้นไปที่การส่งข้อมูลอ้างอิงที่นี่เพื่อให้เข้าใจถึงวิธีที่ Java จัดการกับประเภทต่างๆ
2. ดั้งเดิมเทียบกับการอ้างอิง
แม้ว่าการแปลงแบบดั้งเดิมและการคัดเลือกตัวแปรอ้างอิงอาจดูคล้ายกัน แต่ก็มีแนวคิดที่แตกต่างกันมาก
ในทั้งสองกรณีเรากำลัง "เปลี่ยน" ประเภทหนึ่งไปเป็นอีกประเภทหนึ่ง แต่ในทางที่เรียบง่ายตัวแปรดั้งเดิมมีค่าของมันและการแปลงตัวแปรดั้งเดิมหมายถึงการเปลี่ยนแปลงที่ไม่สามารถย้อนกลับได้ในมูลค่า:
double myDouble = 1.1; int myInt = (int) myDouble; assertNotEquals(myDouble, myInt);
หลังจากการแปลงในตัวอย่างข้างต้นตัวแปรmyIntคือ1และเราไม่สามารถเรียกคืนค่าก่อนหน้า1.1จากมันได้
ตัวแปรอ้างอิงที่แตกต่างกัน ; ตัวแปรอ้างอิงอ้างถึงออบเจ็กต์เท่านั้น แต่ไม่มีอ็อบเจ็กต์เอง
และการแคสต์ตัวแปรอ้างอิงไม่ได้สัมผัสกับออบเจ็กต์ที่อ้างถึง แต่จะติดป้ายกำกับวัตถุนี้ด้วยวิธีอื่นเท่านั้นขยายหรือ จำกัด โอกาสในการทำงานกับมัน Upcasting จำกัด รายการวิธีการและคุณสมบัติที่มีให้กับวัตถุนี้และการดาวน์คาสต์สามารถขยายได้
การอ้างอิงเปรียบเสมือนการควบคุมระยะไกลไปยังวัตถุ รีโมทคอนโทรลมีปุ่มมากขึ้นหรือน้อยลงขึ้นอยู่กับประเภทและวัตถุนั้นจะถูกเก็บไว้ในฮีป เมื่อทำการแคสต์เราจะเปลี่ยนประเภทของรีโมทคอนโทรล แต่จะไม่เปลี่ยนวัตถุนั้นเอง
3. การอัปโหลด
การแคสต์จากคลาสย่อยไปยังซูเปอร์คลาสเรียกว่าการอัปคาสต์ โดยปกติแล้วการอัปคาสต์จะดำเนินการโดยปริยายโดยคอมไพเลอร์
การอัปคาสต์มีความเกี่ยวข้องอย่างใกล้ชิดกับการสืบทอดซึ่งเป็นอีกแนวคิดหลักใน Java เป็นเรื่องปกติที่จะใช้ตัวแปรอ้างอิงเพื่ออ้างถึงประเภทที่เฉพาะเจาะจงมากขึ้น และทุกครั้งที่เราทำเช่นนี้การอัปเดตโดยปริยายจะเกิดขึ้น
เพื่อสาธิตการ upcasting ให้กำหนดคลาสAnimal :
public class Animal { public void eat() { // ... } }
ตอนนี้ขอขยายสัตว์ :
public class Cat extends Animal { public void eat() { // ... } public void meow() { // ... } }
ตอนนี้เราสามารถสร้างออบเจ็กต์ของคลาสCatและกำหนดให้กับตัวแปรอ้างอิงประเภทCat :
Cat cat = new Cat();
และเรายังสามารถกำหนดให้กับตัวแปรอ้างอิงประเภทAnimal :
Animal animal = cat;
ในการมอบหมายข้างต้นการอัปเดตโดยปริยายจะเกิดขึ้น เราสามารถทำได้อย่างชัดเจน:
animal = (Animal) cat;
แต่ไม่จำเป็นต้องทำการโยนต้นไม้มรดกอย่างชัดเจน คอมไพเลอร์รู้ว่าcatเป็นสัตว์และไม่แสดงข้อผิดพลาดใด ๆ
หมายเหตุการอ้างอิงนั้นสามารถอ้างถึงประเภทย่อยของประเภทที่ประกาศ
การใช้ upcasting เราได้ จำกัด จำนวนวิธีที่ใช้ได้กับอินสแตนซ์Catแต่ยังไม่ได้เปลี่ยนอินสแตนซ์เอง ตอนนี้เราไม่สามารถทำอะไรที่เฉพาะเจาะจงกับCat ได้ -เราไม่สามารถเรียกใช้meow ()ในตัวแปรสัตว์ได้
แม้ว่าวัตถุCatจะยังคงเป็นวัตถุCatแต่การเรียกmeow ()จะทำให้เกิดข้อผิดพลาดของคอมไพเลอร์:
// animal.meow(); The method meow() is undefined for the type Animal
ในการเรียกmeow ()เราจำเป็นต้องทำให้สัตว์หดหู่และเราจะดำเนินการนี้ในภายหลัง
แต่ตอนนี้เราจะอธิบายถึงสิ่งที่ทำให้เรามีทัศนคติที่ดีขึ้น ด้วยการอัปเดตเราสามารถใช้ประโยชน์จากความหลากหลาย
3.1. ความแตกต่าง
ให้กำหนดประเภทรองของผู้อื่นสัตว์เป็นสุนัขระดับ:
public class Dog extends Animal { public void eat() { // ... } }
ตอนนี้เราสามารถกำหนดวิธีการfeed ()ที่ปฏิบัติต่อแมวและสุนัขเหมือนสัตว์ :
public class AnimalFeeder { public void feed(List animals) { animals.forEach(animal -> { animal.eat(); }); } }
เราไม่ต้องการAnimalFeederการดูแลเกี่ยวกับการที่สัตว์อยู่ในรายชื่อที่ - เป็นแมวหรือสุนัข ในฟีด ()วิธีการที่พวกเขาทั้งหมดสัตว์
การอัปเดตโดยปริยายเกิดขึ้นเมื่อเราเพิ่มวัตถุประเภทเฉพาะในรายการสัตว์ :
List animals = new ArrayList(); animals.add(new Cat()); animals.add(new Dog()); new AnimalFeeder().feed(animals);
เราเพิ่มแมวและสุนัขและพวกเขาจะอัปเดตเป็นประเภทสัตว์โดยปริยาย แต่ละแมวเป็นสัตว์และแต่ละสุนัขเป็นสัตว์ พวกมันเป็นความหลากหลาย
โดยวิธีการที่วัตถุ Java เป็น polymorphic เพราะแต่ละวัตถุเป็นวัตถุอย่างน้อย เราสามารถกำหนดอินสแตนซ์ของAnimalให้กับตัวแปรอ้างอิงของObject type และคอมไพเลอร์จะไม่บ่น:
Object object = new Animal();
นั่นเป็นเหตุผลที่ทุกวัตถุ Java เราสร้างมีวัตถุวิธีการเฉพาะเช่นtoString ()
การอัปโหลดไปยังอินเทอร์เฟซเป็นเรื่องปกติ
We can create Mew interface and make Cat implement it:
public interface Mew { public void meow(); } public class Cat extends Animal implements Mew { public void eat() { // ... } public void meow() { // ... } }
Now any Cat object can also be upcast to Mew:
Mew mew = new Cat();
Cat is a Mew, upcasting is legal and done implicitly.
Thus, Cat is a Mew, Animal, Object, and Cat. It can be assigned to reference variables of all four types in our example.
3.2. Overriding
In the example above, the eat() method is overridden. This means that although eat() is called on the variable of the Animal type, the work is done by methods invoked on real objects – cats and dogs:
public void feed(List animals) { animals.forEach(animal -> { animal.eat(); }); }
If we add some logging to our classes, we'll see that Cat’s and Dog’s methods are called:
web - 2018-02-15 22:48:49,354 [main] INFO com.baeldung.casting.Cat - cat is eating web - 2018-02-15 22:48:49,363 [main] INFO com.baeldung.casting.Dog - dog is eating
To sum up:
- A reference variable can refer to an object if the object is of the same type as a variable or if it is a subtype
- Upcasting happens implicitly
- All Java objects are polymorphic and can be treated as objects of supertype due to upcasting
4. Downcasting
What if we want to use the variable of type Animal to invoke a method available only to Cat class? Here comes the downcasting. It’s the casting from a superclass to a subclass.
Let’s take an example:
Animal animal = new Cat();
We know that animal variable refers to the instance of Cat. And we want to invoke Cat’s meow() method on the animal. But the compiler complains that meow() method doesn’t exist for the type Animal.
To call meow() we should downcast animal to Cat:
((Cat) animal).meow();
The inner parentheses and the type they contain are sometimes called the cast operator. Note that external parentheses are also needed to compile the code.
Let’s rewrite the previous AnimalFeeder example with meow() method:
public class AnimalFeeder { public void feed(List animals) { animals.forEach(animal -> { animal.eat(); if (animal instanceof Cat) { ((Cat) animal).meow(); } }); } }
Now we gain access to all methods available to Cat class. Look at the log to make sure that meow() is actually called:
web - 2018-02-16 18:13:45,445 [main] INFO com.baeldung.casting.Cat - cat is eating web - 2018-02-16 18:13:45,454 [main] INFO com.baeldung.casting.Cat - meow web - 2018-02-16 18:13:45,455 [main] INFO com.baeldung.casting.Dog - dog is eating
Note that in the above example we're trying to downcast only those objects which are really instances of Cat. To do this, we use the operator instanceof.
4.1. instanceof Operator
We often use instanceof operator before downcasting to check if the object belongs to the specific type:
if (animal instanceof Cat) { ((Cat) animal).meow(); }
4.2. ClassCastException
If we hadn't checked the type with the instanceof operator, the compiler wouldn't have complained. But at runtime, there would be an exception.
To demonstrate this let’s remove the instanceof operator from the above code:
public void uncheckedFeed(List animals) { animals.forEach(animal -> { animal.eat(); ((Cat) animal).meow(); }); }
This code compiles without issues. But if we try to run it we’ll see an exception:
java.lang.ClassCastException: com.baeldung.casting.Dog cannot be cast to com.baeldung.casting.Cat
This means that we are trying to convert an object which is an instance of Dog into a Cat instance.
ClassCastException's always thrown at runtime if the type we downcast to doesn't match the type of the real object.
Note, that if we try to downcast to an unrelated type, the compiler won't allow this:
Animal animal; String s = (String) animal;
The compiler says “Cannot cast from Animal to String”.
For the code to compile, both types should be in the same inheritance tree.
Let's sum up:
- Downcasting is necessary to gain access to members specific to subclass
- Downcasting is done using cast operator
- To downcast an object safely, we need instanceof operator
- If the real object doesn't match the type we downcast to, then ClassCastException will be thrown at runtime
5. cast() Method
There's another way to cast objects using the methods of Class:
public void whenDowncastToCatWithCastMethod_thenMeowIsCalled() { Animal animal = new Cat(); if (Cat.class.isInstance(animal)) { Cat cat = Cat.class.cast(animal); cat.meow(); } }
In the above example, cast() and isInstance() methods are used instead of cast and instanceof operators correspondingly.
It's common to use cast() and isInstance() methods with generic types.
Let's create AnimalFeederGeneric class with feed() method which “feeds” only one type of animals – cats or dogs, depending on the value of the type parameter:
public class AnimalFeederGeneric { private Class type; public AnimalFeederGeneric(Class type) { this.type = type; } public List feed(List animals) { List list = new ArrayList(); animals.forEach(animal -> { if (type.isInstance(animal)) { T objAsType = type.cast(animal); list.add(objAsType); } }); return list; } }
The feed() method checks each animal and returns only those which are instances of T.
Note, that the Class instance should also be passed to the generic class as we can't get it from the type parameter T. In our example, we pass it in the constructor.
Let's make T equal to Cat and make sure that the method returns only cats:
@Test public void whenParameterCat_thenOnlyCatsFed() { List animals = new ArrayList(); animals.add(new Cat()); animals.add(new Dog()); AnimalFeederGeneric catFeeder = new AnimalFeederGeneric(Cat.class); List fedAnimals = catFeeder.feed(animals); assertTrue(fedAnimals.size() == 1); assertTrue(fedAnimals.get(0) instanceof Cat); }
6. Conclusion
ในแบบฝึกหัดพื้นฐานนี้เราได้สำรวจว่าอะไรคือการอัปเดตการดาวน์คาสติ้งวิธีใช้และแนวคิดเหล่านี้จะช่วยให้คุณใช้ประโยชน์จากความหลากหลายได้อย่างไร
เช่นเคยรหัสสำหรับบทความนี้มีอยู่ใน GitHub