การถ่ายทอดทางพันธุกรรมกับองค์ประกอบ: วิธีการเลือก

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

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

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

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

เมื่อใดควรใช้การสืบทอดใน Java

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

  • คนก็คือมนุษย์
  • แมวเป็นสัตว์
  • รถยนต์คือ   ยานพาหนะ

ในแต่ละกรณีคลาสย่อยหรือคลาสย่อยเป็นเวอร์ชันพิเศษของพาเรนต์หรือซูเปอร์คลาส การสืบทอดจากซูเปอร์คลาสเป็นตัวอย่างของการใช้โค้ดซ้ำ เพื่อให้เข้าใจความสัมพันธ์นี้ดีขึ้นให้ใช้เวลาสักครู่เพื่อศึกษาCarชั้นเรียนซึ่งสืบทอดมาจากVehicle:

 class Vehicle { String brand; String color; double weight; double speed; void move() { System.out.println("The vehicle is moving"); } } public class Car extends Vehicle { String licensePlateNumber; String owner; String bodyStyle; public static void main(String... inheritanceExample) { System.out.println(new Vehicle().brand); System.out.println(new Car().brand); new Car().move(); } } 

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

เมื่อใดควรใช้องค์ประกอบใน Java

ในการเขียนโปรแกรมเชิงวัตถุเราสามารถใช้การจัดองค์ประกอบในกรณีที่วัตถุหนึ่ง "มี" (หรือเป็นส่วนหนึ่งของ) วัตถุอื่น ตัวอย่างบางส่วนจะเป็น:

  • รถยนต์มีแบตเตอรี่ (แบตเตอรี่เป็นส่วนหนึ่งของรถยนต์)
  • บุคคลมีหัวใจ (หัวใจเป็นส่วนหนึ่งของบุคคล)
  • บ้านมีห้องนั่งเล่น (ห้องนั่งเล่นเป็นส่วนหนึ่งของบ้าน)

เพื่อให้เข้าใจความสัมพันธ์ประเภทนี้ได้ดีขึ้นให้พิจารณาองค์ประกอบของHouse:

 public class CompositionExample { public static void main(String... houseComposition) { new House(new Bedroom(), new LivingRoom()); // The house now is composed with a Bedroom and a LivingRoom } static class House { Bedroom bedroom; LivingRoom livingRoom; House(Bedroom bedroom, LivingRoom livingRoom) { this.bedroom = bedroom; this.livingRoom = livingRoom; } } static class Bedroom { } static class LivingRoom { } } 

ในกรณีนี้เรารู้ว่าบ้านหลังหนึ่งมีห้องนั่งเล่นและห้องนอนดังนั้นเราจึงสามารถใช้Bedroomและ  LivingRoomวัตถุในองค์ประกอบของกHouse

รับรหัส

รับซอร์สโค้ดสำหรับตัวอย่างใน Java Challenger นี้ คุณสามารถเรียกใช้การทดสอบของคุณเองในขณะที่ทำตามตัวอย่าง

การถ่ายทอดทางพันธุกรรมและองค์ประกอบ: สองตัวอย่าง

พิจารณารหัสต่อไปนี้ นี่เป็นตัวอย่างที่ดีของการสืบทอดหรือไม่?

 import java.util.HashSet; public class CharacterBadExampleInheritance extends HashSet { public static void main(String... badExampleOfInheritance) { BadExampleInheritance badExampleInheritance = new BadExampleInheritance(); badExampleInheritance.add("Homer"); badExampleInheritance.forEach(System.out::println); } 

ในกรณีนี้คำตอบคือไม่ คลาสย่อยสืบทอดวิธีการมากมายที่ไม่เคยใช้ส่งผลให้เกิดรหัสคู่ที่แน่นซึ่งทั้งสับสนและดูแลรักษายาก หากคุณมองอย่างใกล้ชิดจะเห็นได้ชัดว่ารหัสนี้ไม่ผ่านการทดสอบ

ตอนนี้เรามาลองใช้ตัวอย่างเดียวกันโดยใช้องค์ประกอบ:

 import java.util.HashSet; import java.util.Set; public class CharacterCompositionExample { static Set set = new HashSet(); public static void main(String... goodExampleOfComposition) { set.add("Homer"); set.forEach(System.out::println); } 

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

ตัวอย่างการสืบทอดใน JDK

Java Development Kit เต็มไปด้วยตัวอย่างที่ดีของการสืบทอด:

 class IndexOutOfBoundsException extends RuntimeException {...} class ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException {...} class FileWriter extends OutputStreamWriter {...} class OutputStreamWriter extends Writer {...} interface Stream extends BaseStream
    
      {...} 
    

โปรดสังเกตว่าในแต่ละตัวอย่างเหล่านี้คลาสย่อยเป็นเวอร์ชันพิเศษของพาเรนต์ ตัวอย่างเช่นIndexOutOfBoundsExceptionเป็นประเภทของRuntimeException.

วิธีการแทนที่ด้วยการสืบทอด Java

การสืบทอดทำให้เราสามารถนำวิธีการและคุณลักษณะอื่น ๆ ของคลาสหนึ่งมาใช้ซ้ำในคลาสใหม่ได้ซึ่งสะดวกมาก แต่เพื่อให้การสืบทอดใช้งานได้จริงเราจำเป็นต้องสามารถเปลี่ยนพฤติกรรมที่สืบทอดมาภายในคลาสย่อยใหม่ของเราด้วย ตัวอย่างเช่นเราอาจต้องการสร้างเสียงให้เป็นพิเศษCat:

 class Animal { void emitSound() { System.out.println("The animal emitted a sound"); } } class Cat extends Animal { @Override void emitSound() { System.out.println("Meow"); } } class Dog extends Animal { } public class Main { public static void main(String... doYourBest) { Animal cat = new Cat(); // Meow Animal dog = new Dog(); // The animal emitted a sound Animal animal = new Animal(); // The animal emitted a sound cat.emitSound(); dog.emitSound(); animal.emitSound(); } } 

นี่คือตัวอย่างของการสืบทอด Java ด้วยวิธีการแทนที่ ครั้งแรกที่เราขยายAnimalระดับที่จะสร้างใหม่Catระดับ ต่อไปเราจะแทนที่AnimalระดับของemitSound()วิธีการที่จะได้รับเฉพาะเสียงCatรถ แม้ว่าเราจะประกาศประเภทคลาสเป็นAnimalแต่เมื่อเราสร้างอินสแตนซ์Catเราจะได้แมวเหมียว 

วิธีการแทนที่คือความหลากหลาย

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

Java มีหลายมรดกหรือไม่?

แตกต่างจากบางภาษาเช่น C ++ Java ไม่อนุญาตให้มีการสืบทอดหลายคลาสด้วยกัน อย่างไรก็ตามคุณสามารถใช้การสืบทอดหลายรายการกับอินเทอร์เฟซได้ ความแตกต่างระหว่างคลาสและอินเทอร์เฟซในกรณีนี้คืออินเทอร์เฟซไม่คงสถานะ

หากคุณพยายามสืบทอดหลายรายการเหมือนที่ฉันมีด้านล่างรหัสจะไม่รวบรวม:

 class Animal {} class Mammal {} class Dog extends Animal, Mammal {} 

วิธีแก้ปัญหาโดยใช้คลาสคือการสืบทอดทีละคน:

 class Animal {} class Mammal extends Animal {} class Dog extends Mammal {} 

อีกวิธีหนึ่งคือการแทนที่คลาสด้วยอินเทอร์เฟซ:

 interface Animal {} interface Mammal {} class Dog implements Animal, Mammal {} 

ใช้ 'super' เพื่อเข้าถึงเมธอดคลาสพาเรนต์

When two classes are related through inheritance, the child class must be able to access every accessible field, method, or constructor of its parent class. In Java, we use the reserved word super to ensure the child class can still access its parent's overridden method:

 public class SuperWordExample { class Character { Character() { System.out.println("A Character has been created"); } void move() { System.out.println("Character walking..."); } } class Moe extends Character { Moe() { super(); } void giveBeer() { super.move(); System.out.println("Give beer"); } } } 

In this example, Character is the parent class for Moe.  Using super, we are able to access Character's  move() method in order to give Moe a beer.

Using constructors with inheritance

When one class inherits from another, the superclass's constructor always will be loaded first, before loading its subclass. In most cases, the reserved word super will be added automatically to the constructor.  However, if the superclass has a parameter in its constructor, we will have to deliberately invoke the super constructor, as shown below:

 public class ConstructorSuper { class Character { Character() { System.out.println("The super constructor was invoked"); } } class Barney extends Character { // No need to declare the constructor or to invoke the super constructor // The JVM will to that } } 

If the parent class has a constructor with at least one parameter, then we must declare the constructor in the subclass and use super to explicitly invoke the parent constructor. The super reserved word won't be added automatically and the code won't compile without it.  For example:

 public class CustomizedConstructorSuper { class Character { Character(String name) { System.out.println(name + "was invoked"); } } class Barney extends Character { // We will have compilation error if we don't invoke the constructor explicitly // We need to add it Barney() { super("Barney Gumble"); } } } 

Type casting and the ClassCastException

Casting is a way of explicitly communicating to the compiler that you really do intend to convert a given type.  It's like saying, "Hey, JVM, I know what I'm doing so please cast this class with this type." If a class you've cast isn't compatible with the class type you declared, you will get a ClassCastException.

In inheritance, we can assign the child class to the parent class without casting but we can't assign a parent class to the child class without using casting.

Consider the following example:

 public class CastingExample { public static void main(String... castingExample) { Animal animal = new Animal(); Dog dogAnimal = (Dog) animal; // We will get ClassCastException Dog dog = new Dog(); Animal dogWithAnimalType = new Dog(); Dog specificDog = (Dog) dogWithAnimalType; specificDog.bark(); Animal anotherDog = dog; // It's fine here, no need for casting System.out.println(((Dog)anotherDog)); // This is another way to cast the object } } class Animal { } class Dog extends Animal { void bark() { System.out.println("Au au"); } } 

When we try to cast an Animal instance to a Dog we get an exception. This is because the Animal doesn't know anything about its child. It could be a cat, a bird, a lizard, etc. There is no information about the specific animal. 

The problem in this case is that we've instantiated Animal like this:

 Animal animal = new Animal(); 

Then tried to cast it like this:

 Dog dogAnimal = (Dog) animal; 

Because we don't have a Dog instance, it's impossible to assign an Animal to the Dog.  If we try, we will get a ClassCastException

In order to avoid the exception, we should instantiate the Dog like this:

 Dog dog = new Dog(); 

then assign it to Animal:

 Animal anotherDog = dog; 

In this case, because  we've extended the Animal class, the Dog instance doesn't even need to be cast; the Animal parent class type simply accepts the assignment.

Casting with supertypes

เป็นไปได้ที่จะประกาศ a Dogด้วย supertype Animalแต่ถ้าเราต้องการเรียกใช้วิธีการเฉพาะจากDogเราจะต้องแคสต์มัน ตัวอย่างเช่นถ้าเราต้องการเรียกใช้bark()เมธอดจะเป็นอย่างไร? Animalsupertype มีวิธีที่จะรู้ว่าสิ่งที่เช่นสัตว์ที่เรากล่าวอ้างกำลังไม่ดังนั้นเราจะต้องหล่อDogด้วยตนเองก่อนที่เราจะเรียกbark()วิธีการ:

 Animal dogWithAnimalType = new Dog(); Dog specificDog = (Dog) dogWithAnimalType; specificDog.bark(); 

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

 System.out.println(((Dog)anotherDog)); // This is another way to cast the object 

รับความท้าทายในการสืบทอด Java!

คุณได้เรียนรู้แนวคิดที่สำคัญบางประการเกี่ยวกับการถ่ายทอดทางพันธุกรรมแล้วตอนนี้ถึงเวลาลองท้าทายการสืบทอด ในการเริ่มต้นให้ศึกษารหัสต่อไปนี้: