ברוכים הבאים ליחידה מרתקת וקריטית בקורס "תכנות מתקדם בשפת Java" – "תכנות גנרי ואוספים". יחידה זו תצייד אתכם בכלים רבי עוצמה לכתיבת קוד Java גמיש, בטוח יותר מבחינת סוגים (Type-Safe), ורב-שימוש (Reusable). נצלול לעקרונות התכנות הגנרי, נבין כיצד הוא מונע שגיאות זמן ריצה, ונכיר את מסגרת האוספים (Collections Framework) העשירה של Java, שהיא אבן יסוד בכל יישום מודרני.
מבוא לתכנות גנרי: גמישות ובטיחות סוגים
תכנות גנרי (Generics) הוא מאפיין מרכזי ב-Java המאפשר למחלקות, ממשקים ומתודות לפעול עם סוגים שונים של אובייקטים, תוך שמירה על בטיחות סוגים בזמן קומפילציה. לפני Generics, עבודה עם אוספים דרשה שימוש ב-Object, מה שהוביל לצורך בהטלות סוגים (Type Casting) ושגיאות ClassCastException בזמן ריצה.
היתרונות העיקריים של תכנות גנרי הם:
- בטיחות סוגים חזקה יותר: שגיאות סוגים נתפסות בזמן קומפילציה ולא בזמן ריצה.
- הימנעות מהטלות סוגים: אין צורך להטיל סוגים באופן ידני, מה שמפשט את הקוד ומפחית שגיאות.
- קוד רב-שימוש: ניתן לכתוב אלגוריתמים ומבני נתונים הפועלים עם כל סוג, מבלי לשכפל קוד.
יסודות התכנות הגנרי: מחלקות, מתודות ו-Wildcards
מחלקות גנריות (Generic Classes)
מחלקות גנריות מאפשרות להגדיר מחלקה עם פרמטרי סוג. לדוגמה, מחלקה Box שיכולה להכיל כל סוג של אובייקט:
public class Box<T> {
private T content;
public Box(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
}
// שימוש:
// Box<String> stringBox = new Box<>("Hello");
// Box<Integer> intBox = new Box<>(123);
מתודות גנריות (Generic Methods)
ניתן להגדיר גם מתודות גנריות, בין אם הן חלק ממחלקה גנרית או לא. פרמטר הסוג מוגדר לפני סוג ההחזרה של המתודה.
public class Util {
public static <T> T getFirstElement(List<T> list) {
if (list != null && !list.isEmpty()) {
return list.get(0);
}
return null;
}
}
// שימוש:
// List<String> names = Arrays.asList("Alice", "Bob");
// String first = Util.getFirstElement(names);
Wildcards (תווי ריבוי)
Wildcards מספקים גמישות נוספת בעבודה עם סוגים גנריים, במיוחד כאשר מתודות מקבלות או מחזירות אוספים גנריים.
<?>(Unbounded Wildcard): מייצג כל סוג. שימושי כאשר איננו מעוניינים במידע על הסוג, למשל, להדפסת אוסף.<? extends T>(Upper Bounded Wildcard): מייצג את סוגTאו כל תת-סוג שלו. משמש לקריאת נתונים מאוסף (Producer).<? super T>(Lower Bounded Wildcard): מייצג את סוגTאו כל סוג-על שלו. משמש לכתיבת נתונים לאוסף (Consumer).
- כאשר אוסף משמש כ"מקור" (Producer) של נתונים (כלומר, אנו קוראים ממנו), השתמשו ב-
<? extends T>. זה מאפשר לכם לקבל אוספים שלTאו כל תת-סוג שלT. - כאשר אוסף משמש כ"יעד" (Consumer) של נתונים (כלומר, אנו כותבים אליו), השתמשו ב-
<? super T>. זה מאפשר לכם לקבל אוספים שלTאו כל סוג-על שלT.
מבוא למסגרת האוספים של Java (Java Collections Framework)
מסגרת האוספים של Java (JCF) היא סט של ממשקים ומחלקות המספקות ארכיטקטורה אחידה לייצוג ולתפעול של אוספים. היא חוסכת מאיתנו את הצורך לממש מבני נתונים בסיסיים בעצמנו ומספקת פתרונות אופטימליים ומוכחים.
Iterable.ה-JCF מבוססת על מספר ממשקים עיקריים, כאשר שלושת החשובים ביותר הם List, Set ו-Map.
אוספים מרכזיים: List, Set, Map
List
מאפיינים: אוסף מסודר (Ordered Collection) המאפשר כפילויות. ניתן לגשת לאלמנטים לפי אינדקס.
שימושים: שמירת רשימת פריטים בסדר מסוים, למשל רשימת קניות, היסטוריית פעולות.
מימושים נפוצים:
ArrayList: מבוסס מערך דינמי. גישה מהירה לפי אינדקס, הוספה/הסרה בסוף מהירה. הוספה/הסרה באמצע יקרה.LinkedList: מבוסס רשימה מקושרת. הוספה/הסרה מהירה מההתחלה/הסוף/האמצע. גישה לפי אינדקס איטית.
Set
מאפיינים: אוסף שאינו מאפשר כפילויות (No Duplicates). הסדר אינו מובטח (למעט מימושים ספציפיים).
שימושים: שמירת קבוצה של אובייקטים ייחודיים, למשל, רשימת משתמשים ייחודיים, מילים ייחודיות במסמך.
מימושים נפוצים:
HashSet: מבוסס טבלת גיבוב (Hash Table). ביצועים מהירים מאוד להוספה, הסרה ובדיקת קיום (O(1) בממוצע). אינו שומר סדר.LinkedHashSet: שומר על סדר ההכנסה (Insertion Order). ביצועים דומים ל-HashSet.TreeSet: מבוסס עץ חיפוש בינארי מאוזן (Red-Black Tree). שומר על סדר ממוין טבעי או לפיComparator. ביצועים של O(log n).
Map
מאפיינים: אוסף של זוגות מפתח-ערך (Key-Value Pairs). כל מפתח חייב להיות ייחודי. ערכים יכולים להיות כפולים.
שימושים: מילון, טבלת חיפוש, מיפוי מזהים לאובייקטים.
מימושים נפוצים:
HashMap: מבוסס טבלת גיבוב. ביצועים מהירים מאוד (O(1) בממוצע) למפתח, ערך, הוספה והסרה. אינו שומר סדר.LinkedHashMap: שומר על סדר ההכנסה. ביצועים דומים ל-HashMap.TreeMap: מבוסס עץ חיפוש בינארי מאוזן. שומר על סדר ממוין של המפתחות. ביצועים של O(log n).
הבנות מתקדמות ושיקולי תכנון
מחיקת סוגים (Type Erasure)
Object, תוך הוספת הטלות סוגים היכן שצריך.משמעות הדבר היא שמידע הסוגים הגנריים אינו זמין בזמן ריצה. לדוגמה, List<String> ו-List<Integer> הופכים שניהם ל-List בזמן ריצה. זהו פשרה עיצובית שנועדה לשמור על תאימות לאחור עם קוד Java ישן.
השלכות של מחיקת סוגים:
- לא ניתן ליצור מופע של פרמטר סוג:
new T()אינו חוקי. - לא ניתן להשתמש ב-
instanceofעם פרמטר סוג:obj instanceof Tאינו חוקי. - לא ניתן ליצור מערכים של פרמטרי סוג:
new T[size]אינו חוקי.
סוגים גולמיים (Raw Types)
List במקום List<String>).בעוד שסוגים גולמיים עדיין נתמכים ב-Java (לצורך תאימות לאחור), יש להימנע מהם ככל האפשר. שימוש בהם מבטל את בטיחות הסוגים של Generics ומחזיר את הבעיות של הטלות סוגים ושגיאות ClassCastException בזמן ריצה. המהדר יפיק אזהרות (warnings) בעת שימוש בסוגים גולמיים.
שאלות לדיון
- הסבירו מדוע תכנות גנרי נחשב לשיפור משמעותי לעומת שימוש ב-
Objectוהטלות סוגים ידניות. - תארו את ההבדל העיקרי בין
ArrayListל-LinkedListומתי תבחרו להשתמש בכל אחד מהם. - הסבירו את עקרון PECS (Producer Extends, Consumer Super) ותנו דוגמה קצרה לשימוש בכל אחד מה-Wildcards.
- מהי מחיקת סוגים (Type Erasure) וכיצד היא משפיעה על היכולות שלנו בעבודה עם Generics ב-Java?
- מתי תבחרו להשתמש ב-
Setבמקום ב-List, ומתי ב-Mapבמקום באחד מהם?
נקודות לתשובת מודל
- תכנות גנרי: בטיחות סוגים בזמן קומפילציה, מניעת
ClassCastException, קוד רב-שימוש, הימנעות מהטלות סוגים. - מחלקות/מתודות גנריות: הגדרת פרמטרי סוג
<T>, דוגמאות לשימוש. - Wildcards:
<?>,<? extends T>,<? super T>. - PECS: הסבר ברור על Producer Extends (לקריאה) ו-Consumer Super (לכתיבה) עם דוגמאות.
- Collection Framework: היררכיה, ממשק
Collection. - List: אוסף מסודר, כפילויות מותרות, גישה לפי אינדקס.
ArrayList(מערך, מהיר לגישה),LinkedList(מקושרת, מהיר להוספה/הסרה). - Set: אוסף לא מסודר (בדרך כלל), ללא כפילויות.
HashSet(גיבוב, מהיר),TreeSet(ממוין, O(log n)). - Map: זוגות מפתח-ערך, מפתחות ייחודיים.
HashMap(גיבוב, מהיר),TreeMap(ממוין לפי מפתח, O(log n)). - מחיקת סוגים: הסרת מידע סוגים בזמן קומפילציה, הופך ל-
Objectאו Raw Type. מגבלות (new T(),instanceof T). - סוגים גולמיים:
ListבמקוםList<T>. יש להימנע מהם עקב אובדן בטיחות סוגים.