Smart-World Surf

יחידה 3: פולימורפיזם וממשקים

גמישות קוד באמצעות פולימורפיזם ומימוש ממשקים.
פולימורפיזםמחלקות אב מופשטותממשקיםהשוואת אובייקטים (equalshashCode)

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

פולימורפיזם: "צורות רבות" בקוד

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

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

פולימורפיזם בזמן ריצה (Runtime Polymorphism)

זהו המופע הנפוץ ביותר של פולימורפיזם ב-Java, המכונה גם Overriding (דריסה). כאשר מחלקת בן דורסת מתודה ממחלקת האב שלה, והפניה מסוג מחלקת האב מצביעה על אובייקט מסוג מחלקת הבן, המתודה שתיקרא בזמן ריצה היא זו של מחלקת הבן.

  • טיפוס הפניה (Reference Type): הטיפוס המוצהר של המשתנה. הוא קובע אילו מתודות ניתן לקרוא.
  • טיפוס אובייקט (Object Type): הטיפוס האמיתי של האובייקט שנוצר בזיכרון. הוא קובע איזו מתודה תתבצע בפועל.

מחלקות אב מופשטות (Abstract Classes)

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

מחלקת אב מופשטת: מחלקה המוצהרת עם המילה השמורה abstract. היא יכולה להכיל מתודות מופשטות (ללא מימוש) ומתודות קונקרטיות (עם מימוש). לא ניתן ליצור מופע ישיר של מחלקה מופשטת.

מאפיינים מרכזיים:

  • חייבת להיות מוצהרת כ-abstract.
  • יכולה להכיל אפס או יותר מתודות מופשטות (abstract methods), שאין להן גוף (מימוש).
  • יכולה להכיל גם מתודות קונקרטיות (רגילות), שדות, בנאים וכו'.
  • מחלקת בן היורשת ממחלקה מופשטת חייבת לממש את כל המתודות המופשטות שלה, אלא אם כן היא עצמה מוגדרת כמחלקה מופשטת.
  • מטרתה העיקרית היא לספק שלד (framework) למחלקות הבן, תוך אכיפת מבנה מסוים.

ממשקים (Interfaces)

ממשקים הם דרך נוספת להשיג אבסטרקציה ופולימורפיזם ב-Java. הם מגדירים "חוזה" של התנהגות שכל מחלקה המממשת אותם חייבת לציית לו.

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

מאפיינים מרכזיים:

  • מוצהר עם המילה השמורה interface.
  • לפני Java 8, כל המתודות היו חייבות להיות public abstract (ומכאן ללא גוף). החל מ-Java 8, ניתן להוסיף מתודות default (עם מימוש) ומתודות static.
  • כל השדות בממשק הם public static final באופן מרומז (קבועים).
  • מחלקה יכולה לממש מספר ממשקים (implements Interface1, Interface2), ובכך להשיג "ירושה מרובה של התנהגות".
  • מטרתו העיקרית היא להגדיר יכולות (capabilities) או חוזים (contracts) שמחלקות שונות יכולות לממש.

השוואה: מחלקות מופשטות מול ממשקים

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

מחלקה מופשטת

משמשת כאשר יש קשר "הוא סוג של" (is-a) חזק בין מחלקות, ויש צורך לשתף קוד (מתודות עם מימוש) וגם לאכוף מבנה מסוים (מתודות מופשטות). יכולה להכיל בנאים, שדות שאינם קבועים, ומתודות עם מימוש. מחלקה יכולה לרשת רק ממחלקת אב אחת.

ממשק

משמש כאשר רוצים להגדיר "יכולת" או "חוזה" שמחלקות שונות ובלתי קשורות בהיררכיית ירושה יכולות לממש. מאפשר "ירושה מרובה של טיפוסים". כל המתודות (למעט default/static ב-Java 8+) הן מופשטות, וכל השדות הם קבועים. מתאים למצבים כמו Comparable, Runnable.

השוואת אובייקטים: equals ו-hashCode

השוואת אובייקטים ב-Java היא נושא קריטי, במיוחד כאשר עובדים עם אוספים (Collections). המתודות equals() ו-hashCode(), המוגדרות במחלקת Object, הן הבסיס לכך.

equals(Object obj): מתודה המשמשת להשוואה סמנטית בין שני אובייקטים. כברירת מחדל (במחלקת Object), היא משווה הפניות (==), אך יש לדרוס אותה במחלקות מותאמות אישית כדי להגדיר שוויון לוגי.
hashCode(): מתודה המחזירה ערך שלם (int) המייצג את קוד הגיבוב (hash code) של האובייקט. היא משמשת בעיקר באוספים מבוססי גיבוב כמו HashMap ו-HashSet כדי למקם ולאחזר אובייקטים ביעילות.
החוזה בין equals() ל-hashCode(): אם שני אובייקטים נחשבים שווים על פי המתודה equals(), אזי חייב להיות להם אותו קוד גיבוב (hash code) על פי המתודה hashCode(). ההפך אינו נכון: לאובייקטים עם אותו קוד גיבוב לא בהכרח יהיו שווים. הפרת חוזה זה תוביל להתנהגות בלתי צפויה ולבאגים קשים לאיתור באוספים.

מדוע זה חשוב? כאשר מוסיפים אובייקט ל-HashSet או משתמשים בו כמפתח ב-HashMap, ה-JVM קודם כל משתמש ב-hashCode() כדי למצוא את "הדלי" (bucket) הפוטנציאלי שבו האובייקט אמור להיות. רק לאחר מכן, הוא משתמש ב-equals() כדי לוודא אם האובייקט כבר קיים באותו דלי. אם hashCode() לא עקבי עם equals(), אובייקטים שווים עלולים להיות ממוקמים בדליים שונים או לא להימצא כלל.

שאלות לדיון

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

נקודות לתשובת מודל

  • הבחירה בין מחלקה מופשטת לממשק תלויה בקשר הסמנטי: "is-a" (מחלקה מופשטת) מול "can-do" (ממשק).
  • פולימורפיזם מאפשר לכתוב קוד גנרי יותר, המטפל באובייקטים שונים באופן אחיד, ומפחית את הצורך בבדיקות instanceof רבות.
  • הפרת החוזה בין equals() ל-hashCode() תוביל לכך שאובייקטים שווים לא יזוהו ככאלה באוספים מבוססי גיבוב (HashSet, HashMap), מה שיגרום להוספת כפילויות או אי-מציאת אובייקטים קיימים.
  • לא ניתן ליצור מופע ישיר של מחלקה מופשטת או ממשק; יש לממש אותם על ידי מחלקה קונקרטית כדי ליצור אובייקטים.
מצאתם טעות או שחסר משהו?
→ הקודמת
ירושה
הבאה ←
טיפול בחריגות