ירושה מול קומפוזיציה: איך לבחור

ירושה והרכב הן שתי טכניקות תכנות שמפתחים משתמשים בהן כדי ליצור קשרים בין מחלקות לאובייקטים. בעוד שירושה נגזרת ממעמד אחד מהשני, הרכב מגדיר מעמד כסכום חלקיו.

כיתות וחפצים שנוצרו באמצעות ירושה משולבים בחוזקה מכיוון ששינוי ההורה או מעמד העל ביחסי ירושה עלול לשבור את הקוד שלך. מחלקות ואובייקטים שנוצרו באמצעות קומפוזיציה משולבים באופן רופף , כלומר תוכלו לשנות בקלות רבה יותר את חלקי הרכיב מבלי לשבור את הקוד שלכם.

מכיוון שקוד משולב באופן רופף מציע יותר גמישות, מפתחים רבים למדו כי קומפוזיציה היא טכניקה טובה יותר מירושה, אך האמת מורכבת יותר. הבחירה בכלי תכנות דומה לבחירת כלי המטבח הנכון: לא היית משתמש בסכין חמאה כדי לחתוך ירקות, ובאותה צורה לא צריך לבחור קומפוזיציה לכל תרחיש תכנות. 

בצ'לנג'ר ג'אווה זה תלמדו את ההבדל בין ירושה להרכב וכיצד להחליט מה נכון לתוכנית שלכם. לאחר מכן אציג בפניך כמה היבטים חשובים אך מאתגרים בירושת Java: עקיפת שיטה, superמילת המפתח וסוג הליהוק. לבסוף, תבדוק מה למדת על ידי עבודה דרך דוגמה לירושה שורה אחר שורה כדי לקבוע מה צריכה להיות הפלט.

מתי להשתמש בירושה ב- Java

בתכנות מונחה עצמים נוכל להשתמש בירושה כאשר אנו יודעים שיש קשר "הוא" בין ילד לבין כיתת ההורים שלו. כמה דוגמאות יהיו:

  • אדם הוא בן אדם.
  • חתול הוא חיה.
  • מכונית היא   רכב.

בשני המקרים, הילד או תת המעמד הם גרסה מיוחדת של ההורה או מחלקת העל. ירושה ממעמד העל היא דוגמה לשימוש חוזר בקוד. כדי להבין טוב יותר את הקשר הזה, הקדש רגע ללמוד את 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(); } } 

כשאתה שוקל להשתמש בירושה, שאל את עצמך האם תת-המעמד באמת הוא גרסה מיוחדת יותר של מעמד-העל. במקרה זה, מכונית היא סוג של רכב, כך שקשרי הירושה הגיוניים. 

מתי להשתמש בקומפוזיציה בג'אווה

בתכנות מונחה עצמים, נוכל להשתמש בקומפוזיציה במקרים שבהם אובייקט אחד "יש" או הוא חלק מאובייקט אחר. כמה דוגמאות יהיו:

  • ברכב יש מצבר (מצבר הוא חלק ממכונית).
  • לאדם יש לב (לב הוא חלק מאדם).
  • לבית יש סלון (סלון הוא חלק מבית).

כדי להבין טוב יותר סוג זה של יחסים, שקול את ההרכב של 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 מלאה בדוגמאות טובות לירושה:

 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נקבל את מיאו של החתול. 

עקיפת השיטה היא פולימורפיזם

אולי תזכור מהפוסט האחרון שלי שעקיפת שיטה היא דוגמה לפולימורפיזם, או הפעלת שיטה וירטואלית.

האם לג'אווה יש ירושה מרובה?

בניגוד לשפות מסוימות, כגון 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 {} 

באמצעות 'סופר' כדי לגשת לשיטות של כיתות הורים

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עם סופר -טיפ 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 

קח את אתגר הירושה בג'אווה!

למדתם כמה מושגים חשובים של ירושה, אז עכשיו הגיע הזמן לנסות אתגר בירושה. כדי להתחיל, למד את הקוד הבא: