JavaRush /בלוג Java /Random-HE /מחיקת סוגים

מחיקת סוגים

פורסם בקבוצה
שלום! אנו ממשיכים בסדרת ההרצאות שלנו בנושא גנריקה. בעבר , הבנו באופן כללי מה זה ולמה זה נחוץ. היום נדבר על כמה מהתכונות של תרופות גנריות ונסתכל על כמה מלכודות כשעובדים איתן. ללכת! מחק סוגי - 1בהרצאה האחרונה, דיברנו על ההבדל בין סוגים גנריים לסוגים גולמיים . למקרה ששכחת, Raw Type היא מחלקה גנרית שהסוג שלה הוסר ממנה.
List list = new ArrayList();
הנה דוגמה. כאן איננו מציינים איזה סוג של חפצים יוצבו ב- List. אם ננסה ליצור אחד Listולהוסיף לו כמה אובייקטים, נראה אזהרה ב-IDEa:

“Unchecked call to add(E) as a member of raw type of java.util.List”.
אבל דיברנו גם על העובדה שהגנריות הופיעו רק בגרסת השפה Java 5. עד שיצאה, מתכנתים כתבו הרבה קוד באמצעות Raw Types, וכדי שזה לא יפסיק לעבוד, היכולת יצירה ועבודה עם Raw Types ב-Java נשמר. עם זאת, הבעיה הזו התבררה כרחבה הרבה יותר. קוד Java, כידוע, מומר לקוד בייט מיוחד, אשר מבוצע לאחר מכן על ידי המכונה הוירטואלית של Java. ואם במהלך תהליך התרגום שמים מידע על סוגי פרמטרים לתוך ה-bytecode, זה ישבור את כל הקוד שנכתב בעבר, כי לפני Java 5 לא היו קיימים סוגי פרמטרים! כאשר עובדים עם תרופות גנריות, יש תכונה אחת חשובה מאוד שעליך לזכור. זה נקרא מחיקת סוג. המהות שלו טמונה בעובדה שאין מידע על סוג הפרמטר שלו מאוחסן בתוך המחלקה. מידע זה זמין רק בשלב הקומפילציה ונמחק (הופך לבלתי נגיש) בזמן הריצה. אם תנסה להכניס אובייקט מהסוג הלא נכון ל- List<String>, המהדר יזרוק שגיאה. זה בדיוק מה שהשיגו יוצרי השפה ביצירת גנריות - בדיקות בשלב ההידור. אבל כאשר כל קוד ה-Java שאתה כותב הופך לבייטקוד, לא יהיה מידע על סוגי פרמטרים. בתוך ה-bytecode, רשימת List<Cat>החתולים שלך לא תהיה שונה ממחרוזות List<String>. שום דבר ב-bytecode לא יגיד שזו catsרשימה של אובייקטים Cat. מידע על זה יימחק במהלך ההידור, ורק המידע שיש לך רשימה מסוימת בתוכנית שלך יכנס לקוד הבתים List<Object> cats. בוא נראה איך זה עובד:
public class TestClass<T> {

   private T value1;
   private T value2;

   public void printValues() {
       System.out.println(value1);
       System.out.println(value2);
   }

   public static <T> TestClass<T> createAndAdd2Values(Object o1, Object o2) {
       TestClass<T> result = new TestClass<>();
       result.value1 = (T) o1;
       result.value2 = (T) o2;
       return result;
   }

   public static void main(String[] args) {
       Double d = 22.111;
       String s = "Test String";
       TestClass<Integer> test = createAndAdd2Values(d, s);
       test.printValues();
   }
}
יצרנו מחלקה גנרית משלנו TestClass. זה די פשוט: בעצם זה "אוסף" קטן של 2 חפצים, שממוקמים שם מיד עם יצירת האובייקט. יש לו 2 אובייקטים כשדות T. כאשר השיטה מבוצעת, createAndAdd2Values()יש להטיל את שני האובייקטים שעברו Object aלסוג Object bשלנו T, ולאחר מכן הם יתווספו לאובייקט TestClass. בשיטה main()שאנו יוצרים TestClass<Integer>, כלומר באיכות Tיהיה לנו Integer. אבל במקביל, createAndAdd2Values()אנו מעבירים מספר Doubleואובייקט לשיטה String. אתה חושב שהתוכנית שלנו תעבוד? אחרי הכל, ציינו כסוג פרמטר Integer, אבל Stringבהחלט לא ניתן להטיל אותו ל- Integer! בואו נריץ את השיטה main()ונבדוק. פלט מסוף: 22.111 מחרוזת בדיקה תוצאה לא צפויה! למה זה קרה? בדיוק בגלל מחיקת סוג. במהלך הידור הקוד, מידע על סוג הפרמטר Integerשל האובייקט שלנו TestClass<Integer> testנמחק. הוא הפך ל TestClass<Object> test. הפרמטרים שלנו הועברו ללא בעיות Double( ולא ל- , כפי שציפינו!) ונוספו בשקט ל- . הנה עוד דוגמה פשוטה אך ממחישה מאוד למחיקת סוגים: StringObjectIntegerTestClass
import java.util.ArrayList;
import java.util.List;

public class Main {

   private class Cat {

   }

   public static void main(String[] args) {

       List<String> strings = new ArrayList<>();
       List<Integer> numbers = new ArrayList<>();
       List<Cat> cats = new ArrayList<>();

       System.out.println(strings.getClass() == numbers.getClass());
       System.out.println(numbers.getClass() == cats.getClass());

   }
}
פלט מסוף: true true נראה שיצרנו אוספים עם שלושה סוגי פרמטרים שונים - String, Integer, והמחלקה שיצרנו Cat. אבל במהלך ההמרה ל-bytecode, כל שלוש הרשימות הפכו ל- List<Object>, כך שכאשר היא מבוצעת, התוכנית אומרת לנו שבכל שלושת המקרים אנו משתמשים באותה מחלקה.

הקלד מחיקה בעת עבודה עם מערכים וגנריות

יש נקודה אחת חשובה מאוד שחייבים להבין בבירור כאשר עובדים עם מערכים וגנריות (לדוגמה, List). זה גם שווה לשקול בעת בחירת מבנה נתונים עבור התוכנית שלך. גנריות כפופות למחיקת סוג. מידע על סוג הפרמטר אינו זמין במהלך הפעלת התוכנית. לעומת זאת, מערכים יודעים ויכולים להשתמש במידע על סוג הנתונים שלהם במהלך הפעלת התוכנית. ניסיון להכניס ערך מהסוג הלא נכון למערך יזרוק חריג:
public class Main2 {

   public static void main(String[] args) {

       Object x[] = new String[3];
       x[0] = new Integer(222);
   }
}
פלט מסוף:

Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
מכיוון שיש הבדל כל כך גדול בין מערכים לגנריות, ייתכן שיש להם בעיות תאימות. קודם כל, אתה לא יכול ליצור מערך של אובייקטים גנריים או אפילו רק מערך מודפס. נשמע קצת מבלבל? בואו נסתכל מקרוב. לדוגמה, אתה לא יכול לעשות כל זה ב-Java:
new List<T>[]
new List<String>[]
new T[]
אם ננסה ליצור מערך של רשימות List<String>, נקבל שגיאת קומפילציה כללית של יצירת מערך:
import java.util.List;

public class Main2 {

   public static void main(String[] args) {

       //ошибка компиляции! Generic array creation
       List<String>[] stringLists = new List<String>[1];
   }
}
אבל למה זה נעשה? מדוע אסורה יצירת מערכים כאלה? כל זה כדי להבטיח בטיחות סוג. אם המהדר היה מאפשר לנו ליצור מערכים כאלה מאובייקטים גנריים, היינו יכולים להסתבך בצרות רבות. הנה דוגמה פשוטה מספרו של יהושע בלוך "ג'אווה יעילה":
public static void main(String[] args) {

   List<String>[] stringLists = new List<String>[1];  //  (1)
   List<Integer> intList = Arrays.asList(42, 65, 44);  //  (2)
   Object[] objects = stringLists;  //  (3)
   objects[0] = intList;  //  (4)
   String s = stringLists[0].get(0);  //  (5)
}
בואו נדמיין שיצירת מערך List<String>[] stringListsתתאפשר, והמהדר לא יתלונן. הנה מה שנוכל לעשות במקרה זה: בשורה 1, אנו יוצרים מערך של גיליונות List<String>[] stringLists. המערך שלנו מכיל אחד List<String>. בשורה 2 אנו יוצרים רשימה של מספרים List<Integer>. בשורה 3 אנו מקצים את המערך שלנו List<String>[]למשתנה Object[] objects. שפת Java מאפשרת לך לעשות זאת: Xאתה יכול להכניס גם אובייקטים Xוגם אובייקטים של כל מחלקות הילד במערך של אובייקטים Х. בהתאם, Objectsאתה יכול להכניס כל דבר בכלל למערך. בשורה 4 אנו מחליפים את האלמנט הבודד של המערך objects (List<String>)ברשימה List<Integer>. כתוצאה מכך, הכנסנו List<Integer>למערך שלנו, שנועד רק לאחסון List<String>! אנו ניתקל בשגיאה רק כאשר הקוד יגיע לשורה 5. חריגה תזרק במהלך הפעלת התוכנית ClassCastException. לכן, האיסור על יצירת מערכים כאלה הוכנס לשפת ג'אווה - זה מאפשר לנו להימנע ממצבים כאלה.

איך אני יכול לעקוף את מחיקת הסוג?

ובכן, למדנו על מחיקת סוגים. בואו ננסה לרמות את המערכת! :) משימה: יש לנו שיעור גנרי TestClass<T>. אנחנו צריכים ליצור בו שיטה createNewT()שתיצור ותחזיר אובייקט חדש מסוג Т. אבל זה בלתי אפשרי לעשות, נכון? כל המידע על הסוג Тיימחק במהלך ההידור, ובזמן שהתוכנית פועלת, לא נוכל לגלות איזה סוג אובייקט עלינו ליצור. למעשה, יש דרך מסובכת אחת. אתה בטח זוכר שיש מחלקה ב-Java Class. באמצעותו נוכל לקבל את המחלקה של כל אחד מהאובייקטים שלנו:
public class Main2 {

   public static void main(String[] args) {

       Class classInt = Integer.class;
       Class classString = String.class;

       System.out.println(classInt);
       System.out.println(classString);
   }
}
פלט מסוף:

class java.lang.Integer
class java.lang.String
אבל הנה תכונה אחת שלא דיברנו עליה. בתיעוד של Oracle תראה ש-Class הוא מחלקה גנרית! מחק סוגי - 3התיעוד אומר: "T הוא סוג המחלקה שעוצב על ידי אובייקט Class זה." אם נתרגם את זה משפת תיעוד לשפה אנושית, זה אומר שהמחלקה של אובייקט Integer.classהיא לא רק Class, אלא Class<Integer>. סוג האובייקט string.classהוא לא רק Class, Class<String>וכו'. אם זה עדיין לא ברור, נסה להוסיף פרמטר סוג לדוגמה הקודמת:
public class Main2 {

   public static void main(String[] args) {

       Class<Integer> classInt = Integer.class;
       //ошибка компиляции!
       Class<String> classInt2 = Integer.class;


       Class<String> classString = String.class;
       //ошибка компиляции!
       Class<Double> classString2 = String.class;
   }
}
ועכשיו, באמצעות הידע הזה, אנחנו יכולים לעקוף את מחיקת הסוג ולפתור את הבעיה שלנו! בואו ננסה לקבל מידע על סוג הפרמטר. את תפקידו תמלא הכיתה MySecretClass:
public class MySecretClass {

   public MySecretClass() {

       System.out.println("Объект секретного класса успешно создан!");
   }
}
כך אנו משתמשים בפתרון שלנו בפועל:
public class TestClass<T> {

   Class<T> typeParameterClass;

   public TestClass(Class<T> typeParameterClass) {
       this.typeParameterClass = typeParameterClass;
   }

   public T createNewT() throws IllegalAccessException, InstantiationException {
       T t = typeParameterClass.newInstance();
       return t;
   }

   public static void main(String[] args) throws InstantiationException, IllegalAccessException {

       TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
       MySecretClass secret = testString.createNewT();

   }
}
פלט מסוף:

Объект секретного класса успешно создан!
פשוט העברנו את פרמטר המחלקה הנדרש לבנאי המחלקה הגנרית שלנו:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
הודות לכך, שמרנו מידע על סוג הפרמטר והגנתו מפני מחיקה. כתוצאה מכך, הצלחנו ליצור אובייקט T! :) זה מסיים את ההרצאה של היום. מחיקת סוג היא תמיד משהו שצריך לזכור כשעובדים עם תרופות גנריות. זה לא נראה מאוד נוח, אבל אתה צריך להבין שהגנריות לא היו חלק משפת ג'אווה כשהיא נוצרה. זוהי תכונה שנוספה מאוחר יותר שעוזרת לנו ליצור אוספים מוקלדים ולתפוס שגיאות בשלב ההידור. בכמה שפות אחרות שבהן גנריות קיימות מאז גרסה 1 אין מחיקת סוג (לדוגמה, C#). עם זאת, לא סיימנו ללמוד גנריקה! בהרצאה הבאה תכירו עוד כמה תכונות של עבודה איתם. בינתיים, זה יהיה נחמד לפתור כמה בעיות! :)
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION