היי! אנו ממשיכים בסדרת השיעורים שלנו בנושא גנריקה. בעבר
קיבלנו מושג כללי על מה הם ולמה הם נחוצים. היום נלמד עוד על כמה מהתכונות של תרופות גנריות ועל העבודה איתן. בוא נלך! בשיעור האחרון
דיברנו על ההבדל בין טיפוסים גנריים לסוגים גולמיים . סוג גולמי הוא מחלקה גנרית שהסוג שלה הוסר.
התיעוד אומר, "T - סוג המחלקה שעוצב על ידי אובייקט Class זה." כשמתרגמים זאת משפת התיעוד לדיבור פשוט, אנו מבינים שהמעמד של האובייקט
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 היא גנרית!
https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html
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#). אגב, לא סיימנו ללמוד גנריקה! בשיעור הבא תכירו עוד כמה תכונות של גנריות. לעת עתה, יהיה טוב לפתור כמה משימות! :)
GO TO FULL VERSION