Smart-World Surf

יחידה 5: תכנות גנרי ואוספים

כתיבת קוד גמיש ובטוח באמצעות תכנות גנרי ומבני נתונים.
מחלקות ומתודות גנריותWildcardsממשק CollectionListSetMap

ברוכים הבאים ליחידה מרתקת וקריטית בקורס "תכנות מתקדם בשפת Java" – "תכנות גנרי ואוספים". יחידה זו תצייד אתכם בכלים רבי עוצמה לכתיבת קוד Java גמיש, בטוח יותר מבחינת סוגים (Type-Safe), ורב-שימוש (Reusable). נצלול לעקרונות התכנות הגנרי, נבין כיצד הוא מונע שגיאות זמן ריצה, ונכיר את מסגרת האוספים (Collections Framework) העשירה של Java, שהיא אבן יסוד בכל יישום מודרני.

מבוא לתכנות גנרי: גמישות ובטיחות סוגים

תכנות גנרי (Generics) הוא מאפיין מרכזי ב-Java המאפשר למחלקות, ממשקים ומתודות לפעול עם סוגים שונים של אובייקטים, תוך שמירה על בטיחות סוגים בזמן קומפילציה. לפני Generics, עבודה עם אוספים דרשה שימוש ב-Object, מה שהוביל לצורך בהטלות סוגים (Type Casting) ושגיאות ClassCastException בזמן ריצה.

תכנות גנרי (Generic Programming): שיטה המאפשרת לכתוב קוד הפועל עם סוגים שונים של נתונים, מבלי לוותר על בטיחות סוגים בזמן קומפילציה.
בטיחות סוגים (Type Safety): היכולת של המהדר (Compiler) לאכוף כללי סוגים, ובכך למנוע שגיאות הקשורות לסוגי נתונים שגויים עוד לפני הרצת התוכנית.

היתרונות העיקריים של תכנות גנרי הם:

  • בטיחות סוגים חזקה יותר: שגיאות סוגים נתפסות בזמן קומפילציה ולא בזמן ריצה.
  • הימנעות מהטלות סוגים: אין צורך להטיל סוגים באופן ידני, מה שמפשט את הקוד ומפחית שגיאות.
  • קוד רב-שימוש: ניתן לכתוב אלגוריתמים ומבני נתונים הפועלים עם כל סוג, מבלי לשכפל קוד.

יסודות התכנות הגנרי: מחלקות, מתודות ו-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).
עקרון PECS (Producer Extends, Consumer Super): זהו עקרון מפתח ונושא שכיח במבחנים.
  • כאשר אוסף משמש כ"מקור" (Producer) של נתונים (כלומר, אנו קוראים ממנו), השתמשו ב-<? extends T>. זה מאפשר לכם לקבל אוספים של T או כל תת-סוג של T.
  • כאשר אוסף משמש כ"יעד" (Consumer) של נתונים (כלומר, אנו כותבים אליו), השתמשו ב-<? super T>. זה מאפשר לכם לקבל אוספים של T או כל סוג-על של T.
הבנה ויישום נכון של PECS קריטיים לכתיבת קוד גנרי נכון וגמיש.

מבוא למסגרת האוספים של Java (Java Collections Framework)

מסגרת האוספים של Java (JCF) היא סט של ממשקים ומחלקות המספקות ארכיטקטורה אחידה לייצוג ולתפעול של אוספים. היא חוסכת מאיתנו את הצורך לממש מבני נתונים בסיסיים בעצמנו ומספקת פתרונות אופטימליים ומוכחים.

ממשק Collection: הממשק הבסיסי ביותר במסגרת האוספים, המייצג קבוצה של אובייקטים. הוא מרחיב את ממשק 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)

מחיקת סוגים (Type Erasure): האופן שבו Java מממשת Generics. המהדר מסיר את כל מידע הסוגים הגנריים לאחר הקומפילציה, והופך אותם לסוגים גולמיים (Raw Types) או ל-Object, תוך הוספת הטלות סוגים היכן שצריך.

משמעות הדבר היא שמידע הסוגים הגנריים אינו זמין בזמן ריצה. לדוגמה, List<String> ו-List<Integer> הופכים שניהם ל-List בזמן ריצה. זהו פשרה עיצובית שנועדה לשמור על תאימות לאחור עם קוד Java ישן.

השלכות של מחיקת סוגים:

  • לא ניתן ליצור מופע של פרמטר סוג: new T() אינו חוקי.
  • לא ניתן להשתמש ב-instanceof עם פרמטר סוג: obj instanceof T אינו חוקי.
  • לא ניתן ליצור מערכים של פרמטרי סוג: new T[size] אינו חוקי.

סוגים גולמיים (Raw Types)

סוגים גולמיים (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>. יש להימנע מהם עקב אובדן בטיחות סוגים.
מצאתם טעות או שחסר משהו?
→ הקודמת
טיפול בחריגות
הבאה ←
קבצים וזרמי מידע