למה קיימים generics
טיפוס גנרי מאפשר לכתוב קוד פעם אחת ולהשתמש בו מחדש עם טיפוסים רבים, בלי לוותר על בטיחות טיפוסים. במקום לכתוב IntBox, StringBox ו-UserBox נפרדים, כותבים Box<T> אחד, שבו T הוא ממלא מקום שהקורא ממלא.
כבר השתמשתם ב-generics בכל פעם שכתבתם ArrayList<String> או HashMap כמו HashMap<String, Integer>. החלק <...> הוא ארגומנט טיפוס. העמוד הזה מראה איך לכתוב generics משלכם.
החלופה, לשמור הכול כ-Object, זורקת את מידע הטיפוס ומחייבת המרות מכוערות שמזמינות טעויות:
ההמרה בשורה האחרונה זורקת ClassCastException בזמן ריצה: בדיוק סוג ה-exception ש-generics נועדו להפוך לבלתי אפשרי.
מחלקה גנרית
מצהירים על פרמטר טיפוס בסוגריים משולשים אחרי שם המחלקה. לפי המוסכמה זו אות גדולה אחת: T ל-"type", E ל-"element", K/V ל-key/value.
בתוך Box, כל T הופך למה שהקורא סיפק. Box<String> היא קופסה שמחזיקה ומחזירה רק String. הקומפיילר דוחה את name.set(99) עוד לפני שהתוכנית רצה.
ה-<> הריק בצד ההשמה (diamond operator) מאפשר לקומפיילר להסיק את ארגומנט הטיפוס מהצד של ההצהרה, כך שלא צריך לכתוב <String> פעמיים.
מתודות גנריות
למתודה בודדת יכול להיות פרמטר טיפוס משלה, בלי תלות במחלקה. שימו את הפרמטר <T> לפני טיפוס ההחזרה:
אף פעם לא מעבירים את T במפורש: הקומפיילר מסיק אותו מהארגומנט. מתודות גנריות הן הסיבה שכלים כמו Collections.sort או List.of נשארים בטוחים מבחינת טיפוסים עם כל טיפוס איבר.
פרמטרי טיפוס מוגבלים
לפעמים טיפוס גנרי הגיוני רק עבור חלק מהטיפוסים. extends מגביל את הפרמטר כך שאפשר לקרוא למתודות של הגבול. כאן T extends Number אומר ש-T הוא Number או כל תת-מחלקה שלו (Integer, Double, ...), ולכן doubleValue() זמינה:
שימו לב ש-extends כאן אומר "הוא תת-טיפוס של", והוא עובד גם למחלקות וגם לממשקים: <T extends Comparable<T>> נפוץ מאוד כשצריך להשוות בין איברים.
Wildcards: ? extends ו-? super
מלכודת עדינה: List<Integer> היא לא List<Number>, למרות ש-Integer הוא Number. Generics הם invariant. Wildcards מרפים את זה כשצריך רק לקרוא או רק לכתוב.
השתמשו ב-? extends T עבור יצרן שקוראים ממנו, וב-? super T עבור צרכן שכותבים אליו (כלל ה-"PECS": Producer Extends, Consumer Super):
רשימה מסוג ? extends Number מאפשרת לקרוא איברים כ-Number אבל לא להוסיף לה (הקומפיילר לא יכול לדעת את טיפוס האיבר המדויק). רשימה מסוג ? super Integer מאפשרת להוסיף Integer, אבל מה שקוראים ממנה חוזר כ-Object. בחרו את ה-wildcard שמתאים לכיוון הזרימה של הנתונים.
Type erasure והמגבלות שלו
Generics הם יכולת של זמן הקומפילציה. אחרי הקומפילציה פרמטר הטיפוס נמחק: בזמן ריצה Box<String> ו-Box<Integer> הם שניהם סתם Box. זה שומר על תאימות של generics לקוד ישן, אבל מטיל מגבלות אמיתיות.
// אף אחד מאלה לא מתקמפל: פרמטר הטיפוס לא קיים בזמן ריצה:
T value = new T(); // אי אפשר ליצור מופע של פרמטר טיפוס
T[] array = new T[10]; // אי אפשר ליצור מערך גנרי
if (list instanceof List<String>) { } // אי אפשר לבדוק את ארגומנט הטיפוס
מכיוון שהטיפוס נעלם בזמן ריצה, אי אפשר לשאול "מה היה T?" דרך reflection, ואי אפשר להעמיס מתודות שנבדלות רק בארגומנט הגנרי שלהן (foo(List<String>) ו-foo(List<Integer>) נמחקות לאותה חתימה). כשבאמת צריך את הטיפוס בזמן ריצה, העבירו Class<T> (type token) כפרמטר של בנאי או מתודה.
הבא בתור: ביטויי Lambda
ראיתם שמתודה גנרית מקבלת טיפוס כפרמטר. השלב הבא הוא להתייחס להתנהגות כפרמטר. ביטויי lambda מאפשרים להעביר קטע קוד, פונקציה, לתוך מתודה, וזו בדיוק הדרך שבה תמיינו, תסננו ותמירו את האוספים הגנריים שלמדתם עכשיו להגדיר עם בטיחות טיפוסים.
שאלות נפוצות
מה הם generics ב-Java?
Generics מאפשרים לכתוב מחלקה או מתודה שעובדות עם טיפוס שתציינו מאוחר יותר, במקום לנעול אותן לטיפוס קונקרטי אחד. מצהירים על פרמטר טיפוס בסוגריים משולשים, class Box<T>, והקורא ממלא את הטיפוס בפועל, Box<String>. אז הקומפיילר אוכף את הטיפוס הזה בכל מקום, כך שאי-התאמות נתפסות בזמן הקומפילציה ואין צורך בהמרות ידניות.
למה להשתמש ב-generics במקום ב-Object?
שימוש ב-Object מאבד את כל מידע הטיפוס: הקומפיילר לא יכול למנוע מכם להכניס את הדבר הלא נכון, וחייבים להמיר כל ערך שיוצא (עם סיכון ל-ClassCastException בזמן ריצה). Generics מעבירים את הבדיקה הזו לזמן הקומפילציה. List<String> פשוט לא תקבל Integer, ו-get() כבר מחזירה String: בלי המרה ובלי הפתעות בזמן ריצה.
מה זה type erasure ב-generics של Java?
Type erasure אומר שמידע הטיפוס הגנרי קיים רק בזמן הקומפילציה. אחרי הקומפילציה, List<String> ו-List<Integer> הן שתיהן סתם List בזמן ריצה: פרמטר הטיפוס נמחק. בגלל זה אי אפשר לכתוב new T[10], לבדוק list instanceof List<String> או לקרוא פרמטר טיפוס דרך reflection. Generics נותנים בטיחות בזמן קומפילציה, לא מידע על טיפוסים בזמן ריצה.