CodeGym /בלוג Java /Random-HE /הקלד מחיקה
John Squirrels
רָמָה
San Francisco

הקלד מחיקה

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

"Unchecked call to add(E) as a member of raw type of java.util.List".
אבל דיברנו גם על העובדה שהגנריות הופיעו רק ב-Java 5. בזמן שהגרסה הזו שוחררה, מתכנתים כבר כתבו חבורה של קוד באמצעות סוגי גולמיים, כך שהתכונה הזו של השפה לא יכלה להפסיק לעבוד, והיכולת יצירת סוגי גלם ב-Java נשמר. עם זאת, התברר שהבעיה נפוצה יותר. כידוע, קוד Java מומר לפורמט הידור מיוחד הנקרא bytecode, אשר מבוצע לאחר מכן על ידי המכונה הוירטואלית של 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>, כלומר Integerהארגומנט type מחליף את Integerפרמטר ה-type. אנו מעבירים גם a Doubleו- Stringל השיטה createAndAdd2Values(). האם לדעתך התוכנית שלנו תעבוד? אחרי הכל, ציינו Integerכארגומנט הסוג, אבל Stringבהחלט לא ניתן להטיל ארגומנט ל- Integer! בואו נריץ את main()השיטה ונבדוק. פלט מסוף:

22.111 
Test String
זה היה בלתי צפוי! למה זה קרה? זו תוצאה של מחיקת סוג. מידע על Integerארגומנט הסוג המשמש ליצירת TestClass<Integer> testהאובייקט שלנו נמחק כאשר הקוד הודר. השדה הופך ל TestClass<Object> test. Doubleהטיעונים שלנו Stringוהומרו בקלות Objectלאובייקטים (הם לא מומרים Integerלאובייקטים כפי שציפינו!) ונוספו בשקט ל- TestClass. הנה עוד דוגמה פשוטה אך מאוד חושפנית למחיקת סוג:
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) {

       // Compilation error! 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אובייקטים ואובייקטים מכל תת-המחלקות X. בהתאם, אתה יכול לשים כל דבר בכלל במערך Object. בשורה 4, אנו מחליפים את האלמנט היחיד של objects()המערך (a List<String>) ב-a List<Integer>. לפיכך, שמנו List<Integer>במערך שנועד רק לאחסון List<String>חפצים! ניתקל בשגיאה רק כאשר אנו מבצעים שורה 5. A ClassCastExceptionייזרק בזמן ריצה. בהתאם לכך, נוסף ל-Java איסור על יצירת מערכים כאלה. זה מאפשר לנו להימנע ממצבים כאלה.

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

ובכן, למדנו על מחיקת סוגים. בואו ננסה להערים על המערכת! :) משימה: יש לנו TestClass<T>שיעור גנרי. אנחנו רוצים לכתוב createNewT()שיטה עבור המחלקה הזו שתיצור ותחזיר Tאובייקט חדש. אבל זה בלתי אפשרי, נכון? כל המידע על Tהסוג נמחק במהלך ההידור, ובזמן הריצה אנחנו לא יכולים לקבוע איזה סוג אובייקט אנחנו צריכים ליצור. למעשה יש דרך מסובכת אחת לעשות זאת. אתה בטח זוכר שלג'אווה יש 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

https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html

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

   public static void main(String[] args) {

       Class<Integer> classInt = Integer.class;
       // Compilation error!
       Class<String> classInt2 = Integer.class;


       Class<String> classString = String.class;
       // Compilation error!
       Class<Double> classString2 = String.class;
   }
}
ועכשיו, באמצעות הידע הזה, אנחנו יכולים לעקוף את מחיקת הסוג ולבצע את המשימה שלנו! בואו ננסה לקבל מידע על פרמטר סוג. טיעון הסוג שלנו יהיה MySecretClass:
public class MySecretClass {

   public MySecretClass() {

       System.out.println("A MySecretClass object was created successfully!");
   }
}
והנה איך אנחנו משתמשים בפתרון שלנו בפועל:
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();

   }
}
פלט מסוף:

A MySecretClass object was created successfully!
זה עתה העברנו את ארגומנט המחלקה הנדרש לבנאי המחלקה הגנרית שלנו:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
זה אפשר לנו לשמור את המידע על ארגומנט הסוג, ומנע ממנו להימחק לחלוטין. כתוצאה מכך, הצלחנו ליצור Tאובייקט! :) בכך מסתיים השיעור של היום. עליך לזכור תמיד את מחיקת הסוג בעת עבודה עם תרופות גנריות. הדרך לעקיפת הבעיה לא נראית נוחה במיוחד, אבל אתה צריך להבין שהגנריות לא היו חלק משפת Java כאשר היא נוצרה. תכונה זו, שעוזרת לנו ליצור אוספים עם פרמטרים ולתפוס שגיאות במהלך הקומפילציה, טופלה מאוחר יותר. בכמה שפות אחרות שכללו גנריות מהגרסה הראשונה, אין מחיקת סוג (לדוגמה, ב-C#). אגב, לא סיימנו ללמוד גנריקה! בשיעור הבא תכירו עוד כמה תכונות של גנריות. לעת עתה, יהיה טוב לפתור כמה משימות! :)
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION