היי! בואו נמשיך במחקר הגנרי. כבר צברת ידע נרחב עליהם משיעורים קודמים (על שימוש ב-varargs בעבודה עם גנריות ועל מחיקת סוגים ), אבל ישנו נושא חשוב שעדיין לא שקלנו - תווים כלליים . זו תכונה חשובה מאוד של תרופות גנריות. עד כדי כך שהקדשנו לזה שיעור נפרד! עם זאת, אין שום דבר מסובך במיוחד בתווים כלליים. אתה תראה את זה מיד :) תווים כלליים בגנריות - 1בואו נסתכל על דוגמה:
public class Main {

   public static void main(String[] args) {

       String str = new String("Test!");
       // No problem
       Object obj = str;

       List<String> strings = new ArrayList<String>();
       // Compilation error!
       List<Object> objects = strings;
   }
}
מה קורה פה? אנו רואים שני מצבים דומים מאוד. במקרה, אנו מטילים חפץ Stringלחפץ Object. אין כאן בעיות - הכל עובד כמצופה. אבל במצב השני, המהדר יוצר שגיאה. אבל אנחנו עושים את אותו הדבר, לא? הפעם אנחנו פשוט משתמשים באוסף של כמה חפצים. אבל למה השגיאה מתרחשת? מה ההבדל? האם אנו יצוקים Stringחפץ אחד לאובייקט Objectאו 20 חפצים? יש הבחנה חשובה בין חפץ לאוסף של חפצים . אם Bהכיתה היא ילדה של Aהכיתה, אז Collection<B>היא אינה ילדה של Collection<A>. זו הסיבה שלא הצלחנו להטיל את עצמנו List<String>ל- List<Object>. Stringהוא ילד של Object, אבל List<String>אינו ילד של List<Object>. זה אולי לא נראה סופר אינטואיטיבי. מדוע עשו זאת יוצרי השפה? בואו נדמיין שהמהדר לא נותן לנו שגיאה:
List<String> strings = new ArrayList<String>();
List<Object> objects = strings;
במקרה זה, נוכל, למשל, לבצע את הפעולות הבאות:
objects.add(new Object());
String s = strings.get(0);
מכיוון שהמהדר לא נתן לנו שום שגיאה ואיפשר לנו ליצור הפניה List<Object>שמפנה ל- strings, נוכל להוסיף כל Objectאובייקט ישן לאוסף strings! לפיכך, איבדנו את הערבות שהאוסף שלנו מכיל רק את Stringהאובייקטים שצוינו על ידי ארגומנט ה-type בהזמנת הסוג הגנרית . במילים אחרות, איבדנו את היתרון העיקרי של תרופות גנריות - בטיחות סוג. ומכיוון שהמהדר לא מנע מאיתנו לעשות זאת, נקבל שגיאה רק בזמן הריצה, שהיא תמיד הרבה יותר גרועה משגיאת קומפילציה. כדי למנוע מצבים כאלה, המהדר נותן לנו שגיאה:
// Compilation error
List<Object> objects = strings;
...ומזכיר לנו List<String>שאיננו צאצא של List<Object>. זהו חוק מגובש לגנריות, ויש לזכור אותו כשעובדים איתם. בוא נמשיך הלאה. נניח שיש לנו היררכיית מחלקות קטנה:
public class Animal {

   public void feed() {

       System.out.println("Animal.feed()");
   }
}

public class Pet extends Animal {

   public void call() {

       System.out.println("Pet.call()");
   }
}

public class Cat extends Pet {

   public void meow() {

       System.out.println("Cat.meow()");
   }
}
בראש ההיררכיה יש כיתת חיות פשוטה, אותה ירש פט. לחיות מחמד יש 2 קבוצות משנה: כלב וחתול. עכשיו נניח שאנחנו צריכים ליצור iterateAnimals()שיטה פשוטה. השיטה צריכה לקחת אוסף של בעלי חיים כלשהם ( Animal, Pet, Cat, Dog), לחזור על כל האלמנטים ולהציג הודעה בקונסולה במהלך כל איטרציה. בואו ננסה לכתוב שיטה כזו:
public static void iterateAnimals(Collection<Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Another iteration in the loop!");
   }
}
נראה שהבעיה נפתרה! עם זאת, כפי שלמדנו לאחרונה, List<Cat>, List<Dog>ואינם List<Pet>צאצאים של List<Animal>! זה אומר שכאשר אנו מנסים לקרוא לשיטה iterateAnimals()עם רשימה של חתולים, אנו מקבלים שגיאת קומפילציה:
import java.util.*;

public class Main3 {


   public static void iterateAnimals(Collection<Animal> animals) {

       for(Animal animal: animals) {

           System.out.println("Another iteration in the loop!");
       }
   }

   public static void main(String[] args) {


       List<Cat> cats = new ArrayList<>();
       cats.add(new Cat());
       cats.add(new Cat());
       cats.add(new Cat());
       cats.add(new Cat());

       // Compilation error!
       iterateAnimals(cats);
   }
}
המצב לא נראה טוב עבורנו! האם עלינו לכתוב שיטות נפרדות למנות כל סוג של בעל חיים? למעשה, לא, אנחנו לא :) וכמו שזה קורה, תווים כלליים עוזרים לנו בזה! נוכל לפתור את הבעיה בשיטה פשוטה אחת באמצעות המבנה הבא:
public static void iterateAnimals(Collection<? extends Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Another iteration in the loop!");
   }
}
זהו תו כללי. ליתר דיוק, זהו הראשון מבין כמה סוגים של תווים כלליים. זה ידוע בתור תווים כלליים בגבול העליון והוא מתבטא על ידי ? מרחיב . מה המבנה הזה אומר לנו? המשמעות היא שהשיטה מקבלת אוסף של Animalאובייקטים או אוסף של אובייקטים מכל מחלקה שמקורה Animal(? מרחיב Animal). במילים אחרות, השיטה יכולה לקבל אוסף של Animal, Pet, Dog, או Catאובייקטים - זה לא משנה. בואו נשכנע את עצמנו שזה עובד:
public static void main(String[] args) {

   List<Animal> animals = new ArrayList<>();
   animals.add(new Animal());
   animals.add(new Animal());

   List<Pet> pets = new ArrayList<>();
   pets.add(new Pet());
   pets.add(new Pet());

   List<Cat> cats = new ArrayList<>();
   cats.add(new Cat());
   cats.add(new Cat());

   List<Dog> dogs = new ArrayList<>();
   dogs.add(new Dog());
   dogs.add(new Dog());

   iterateAnimals(animals);
   iterateAnimals(pets);
   iterateAnimals(cats);
   iterateAnimals(dogs);
}
פלט מסוף:

Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
יצרנו בסך הכל 4 אוספים ו-8 אובייקטים, ובקונסולה יש בדיוק 8 ערכים. הכל עובד מצוין! :) התו הכללי אפשר לנו להתאים בקלות את ההיגיון הדרוש הקשור לסוגים ספציפיים לשיטה אחת. ביטלנו את הצורך לכתוב שיטה נפרדת לכל סוג חיה. תארו לעצמכם כמה שיטות היינו צריכים אם האפליקציה שלנו הייתה בשימוש על ידי גן חיות או משרד וטרינרי :) אבל עכשיו בואו נסתכל על מצב אחר. היררכיית הירושה שלנו נשארת ללא שינוי: המחלקה ברמה העליונה היא Animal, כאשר Petהמחלקה ממש מתחת, והמחלקות Catו Dogברמה הבאה. עכשיו אתה צריך לשכתב את iterateAnimals()השיטה כך שתעבוד עם כל סוג של חיה, למעט כלבים . כלומר, זה צריך לקבל Collection<Animal>, Collection<Pet>או Collection<Car>, אבל זה לא צריך לעבוד עם Collection<Dog>. איך נוכל להשיג זאת? נראה ששוב עומדים בפנינו האפשרות לכתוב שיטה נפרדת לכל סוג :/ איך עוד נסביר למהדר מה אנחנו רוצים שיקרה? זה למעשה די פשוט! שוב, תווים כלליים באים לעזרתנו כאן. אבל הפעם נשתמש בסוג אחר של תווים כלליים - תווים כלליים בעלי גבול נמוך יותר , שמתבטא באמצעות סופר .
public static void iterateAnimals(Collection<? super Cat> animals) {

   for(int i = 0; i < animals.size(); i++) {

       System.out.println("Another iteration in the loop!");
   }
}
כאן העיקרון דומה. המבנה <? super Cat>אומר למהדר שהשיטה iterateAnimals()יכולה לקבל כקלט אוסף של Catאובייקטים או כל אב קדמון של Catהמחלקה כקלט. במקרה זה, Catהמחלקה, ההורה שלה Petוההורה של ההורה שלה, Animal, כולם תואמים לתיאור זה. המחלקה Dogאינה תואמת את ההגבלה שלנו, כך שניסיון להשתמש בשיטה עם ארגומנט List<Dog>יגרום לשגיאת קומפילציה:
public static void main(String[] args) {

   List<Animal> animals = new ArrayList<>();
   animals.add(new Animal());
   animals.add(new Animal());

   List<Pet> pets = new ArrayList<>();
   pets.add(new Pet());
   pets.add(new Pet());

   List<Cat> cats = new ArrayList<>();
   cats.add(new Cat());
   cats.add(new Cat());

   List<Dog> dogs = new ArrayList<>();
   dogs.add(new Dog());
   dogs.add(new Dog());

   iterateAnimals(animals);
   iterateAnimals(pets);
   iterateAnimals(cats);

   // Compilation error!
   iterateAnimals(dogs);
}
פתרנו את הבעיה שלנו, ושוב התווים כלליים התבררו כמועילים ביותר :) בכך, השיעור הגיע לסיומו. עכשיו אתה רואה כמה גנריות חשובות בלימוד ג'אווה - היו לנו 4 שיעורים שלמים עליהם! אבל עכשיו אתם בקיאים בנושא ותוכלו להוכיח את כישוריכם בראיונות עבודה :) ועכשיו, הגיע הזמן לחזור למשימות! המון הצלחה בלימודים! :)