Presentation is loading. Please wait.

Presentation is loading. Please wait.

Everything the light touches, Simba, will be yours

Similar presentations


Presentation on theme: "Everything the light touches, Simba, will be yours"— Presentation transcript:

1 Everything the light touches, Simba, will be yours
מבוא למדעי המחשב הורשה – Inheritance מטרת השיעור היא לבחון כיצד התפיסה של מתכנני שפת java את מושג ההורשה מתורגם להתנהגות של מחלקות ואובייקטים. נציג את מושג הפולימורפיזם (שיורחב בהמשך) בשעור זה נכנס לפרטים של הורשה. נברר מה המשמעות של הגדרת שיטות ומשתנים בעלי שם זהה במחלקת האב והבן. עבור כל דוגמה, נשאל: האם הקוד מתקמפל? האם הוא רץ? (לא מתרסק בזמן ריצה) מהי תוצאת הריצה?

2 מבוא למדעי המחשב הורשה – Inheritance
מטרת השיעור היא לבחון כיצד התפיסה של מתכנני שפת java את מושג ההורשה מתורגם להתנהגות של מחלקות ואובייקטים. נציג את מושג הפולימורפיזם (שיורחב בהמשך) בשעור זה נכנס לפרטים של הורשה. נברר מה המשמעות של הגדרת שיטות ומשתנים בעלי שם זהה במחלקת האב והבן. עבור כל דוגמה, נשאל: האם הקוד מתקמפל? האם הוא רץ? (לא מתרסק בזמן ריצה) מהי תוצאת הריצה?

3 כללי: בשיעור הקודם השתמשנו במושג הממשק על מנת לגרום לאלגוריתם שפיתחנו לעבוד על מגוון רחב יותר של עצמים. בשיעור זה נכיר את מנגנון ההורשה – מנגנון נוסף המאפשר התייחסות אחידה לעצמים ממחלקות שונות. נעמוד על הדמיון וההבדלים בין שני המנגנונים הנ"ל. לפני השיעור: לכתוב את קוד המחלקהStudent המקורית של סטודנט על הלוח.

4 class Student public class Student {
private static final int COURSE_PRICE = 1000; private String name; private int id; private int numOfCourses; public Student(int id, String name) { this.name = name; this.id = id; numOfCourses= 0; } public int computeTuitionFee(){ return numOfCourses * COURSE_PRICE;

5 class Student public String getName() {return name;}
public int getID() {return id;} public int getNumOfCourses() { return numOfCourses; } public void setNumOfCourses(int numOfCourses) { this.numOfCourses = numOfCourses;

6 class Student public boolean equals(Object other) {
return ((other instanceof Student) && (id == ((Student)other).id)); } public String toString() { return "Student name: " + getName() + ", ID:" + getID() + ", No. of courses: " + getNumOfCourses(); } //class Student

7 דוגמה: בתוכנית שלנו אנו משתמשים מחלקה Student. התוכנית משתמשת בשיטה public int computeTuitionFee() בכדי לחשב את שכר הלימוד שהסטודנט צריך לשלם, כפונקציה של מספר הקורסים שלו.

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

9 אפשרות ראשונה – שינוי Student
אנו יכולים לשנות את המחלקה Student. ניתן להוסיף לכל סטודנט את המילגה שלו (0 במקרה של סטודנט לא מילגאי) ונוסיף את השיטה public int getMilga(); המחזירה את המילגה של הסטודנט.

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

11 אפשרות שנייה – יצירת מחלקה חדשה
לבנות מחלקה חדשה עבור מלגאי: class Milgay{ // cut & paste Student, add more methods, and // change existing methods } חסרונות: שכפול קוד. בנוסף, כל עדכון שנירצה לבצע בעתיד במחלקה Student נאלץ לבצע פעמיים. לא ניתן ליצור מצביע כללי עבור אובייקט שהוא או סטודנט או מילגאי, אלא אם מגדירים ממשק משותף (ואז חזרנו לבעיה הראשונה – יש צורך לשנות ולקמפל מחדש קוד קיים). החיסרון העיקרי של גישה זו הוא בעצם השימוש בשכפול של קוד. שינוי הקוד של המחלקה Student יכריח אותנו לשנותו גם במחלקה Milgay וזהו מצב מועד לתקלות. אך אל יאוש, מנגנון ההורשה יספק לנו פתרון אידיאלי במקרה זה. (מנגנון ההורשה משמש גם במקרים נוספים, אך על כך נדון בהמשך הקורס)

12 פתרון בעזרת הורשה: מחלקה Milgay תירש את המחלקה Student:
Milgay תקבל "במתנה" את ההתנהגות של Student – ניתן לחשוב על כך כאילו שהשיטות הציבוריות של Student מגדירות ממשק אשר ממומש ע"י Milgay (ולכן ניתן לאמר כי "כל מלגאי הוא גם סטודנט" – "Every Milgay IS A Student". ) עם זאת, Milgay גם מקבלת את מימוש ההתנהגות במתנה – אין צורך לכתוב שנית את קוד השיטות המשותפות. 12

13 פתרון בעזרת הורשה: מחלקה Milgay תירש את המחלקה Student:
ניתן להוסיף ל-Milgay שדות ושיטות נוספים, וכן ניתן "לדרוס" (Overriding) שיטות של Student, ולממש אותן באופן שונה (בדומה למה שראינו לגבי דריסה של equals ו-toString של המחלקה Object) 13

14 המימוש הטכני: public class Milgay extends Student { private int milga;
המחלקה מילגאי "מרחיבה" את המחלקה סטודנט. ניתן לחשוב על זאת כך: "בתוך כל מילגאי נמצא סטודנט קטן". בנוסף, למלגאי יש שדה milga היחודי רק לו. 14

15 המימוש הטכני: כאשר בונים מלגאי חדש, יש לבנות גם את הסטודנט שבתוכו. בשביל להפעיל בנאי של מחלקה ממנה ירשנו, נשתמש בצורה super(…): public Milgay(int id, String name, int milga){ super(id, name); this.milga= milga; } בדומה ל-this(…), קריאה ל-super(…) חייבת להיות הפקודה הראשונה בבנאי. ניתן להפעיל כל בנאי של "מחלקת האב". אם לא כתבנו זאת באופן מפורש, תתבצע קריאה לבנאי ללא פרמטרים (ואם לא קיים כזה בנאי – שגיאת קומפילציה!). במקרה שלנו נקרא לבונה שכותרתו: public Student(int id, String name) הקריאה מתבצעת ע"י שימוש במילה השמורה super. כשם ש-this מאפשר לנו פנייה לשדות (שיטות ומשתני מחלקה) של העצם, כך super מאפשר פנייה לשדות שקיבלנו בירושה מהמחלקה האבא. בחלק של המלגאי אנו פשוט מאתחלים את משתנה המחלקה milga בהתאם לפרמטר שקיבלנו. ??? ומה אם לא היינו קוראים לבונה של Student ע"י שימוש ב-super? !!! ניסינו וקיבלנו מהקומפיילר את הודעת השגיאה הבאה: Implicit super constructor Student() is undefined. Must explicitly invoke another constructor Milgay.java line 5 ??? ומה פשר הודעת השגיאה הזאת? !!! לא ניתן לאתחל מופע של מחלקה יורשת ללא הפעלת הבונה של מחלקת האבא. אם לא כתובה פנייה מפורשת, מוכנסת אוטומטית הפנייה לבונה הריק שכותרתו: public Student() ??? אבל למדנו שאם לא כותבים בונה ריק אז מקבלים כזה באופן אוטומטי! !!! הדבר נכון רק לגבי מחלקות בהן אין בונים אחרים! במחלקה שלנו יש בונים אחרים ולכן הבונה הריק לא מסופק באופן אוטומטי. יש דמיון בין השורה: super(id, name); בבונה של Milgay, לשורה: this(s.getID(), s.getName()); בבונה ההעתקה של המחלקה Student. public Student(Student s) { this(s.getID(),s.getName()); courses = new SetAsArray(s.courses); } כשם שניתן להשתמש במילה השמורה this לצורך קריאה לבונה אחר, כן ניתן להשתמש במילה השומורה super לקריאה לבונה של המחלקה האבא. (בהמשך נראה שימוש נוסף ב-super). 15

16 המימוש הטכני: … public int getMilga() { return milga; }
אין צורך לכתוב את שאר השיטות של סטודנט (getName(), וכו') במחלקה מלגאי – הן יתווספו באופן אוטומטי. מאידך, ברצוננו לעדכן השיטה computeTuitionFee() בכדי שתתאים למלגאי. ניתן לעשות זאת בעזרת מנגנון "הדריסה" (Overriding), כפי שעשינו בעבר עם השיטות equals ו-toString. 16

17 ניסיון ראשון (שמעצם שמו, ניסיון ראשון, כנראה עתיד להיכשל! ) :
// this is the wrong first attempt that fails public int computeTuitionFee(){ return numOfCourses * COURSE_PRICE - milga; } מה הבעיה? במחלקה Milgay אין גישה למשתנים הפרטיים numOfCourses ו-COURSE_PRICE של המחלקה Student. ??? מה הבעיה? !!! לא מתקמפל! ??? מהי הודעת השגיאה של הקומפיילר? !!! Milgay.java:27: courses has private access in Student return (courses.getSize() * COURSE_PRICE) - milga; Milgay.java:28: COURSE_PRICE has private access in Student כלומר, משתני המחלקה הפרטיים של המחלקה ממנה ירשנו אינם חשופים לשימושינו! באופן דומה לא נוכל לפנות ישירות ל-name ו-id. ??? אז אולי נעשה את courses משתנה מהרשאת public במחלקה Student? !!! זה תכנות לא נכון! לא נעוות את המחלקה ממנה ירשנו כדי שתתאים לדרישותינו. המשתנה courses אינו חלק מהשירותים הציבוריים שהמחלקה Student מציעה! ??? אז איך בכל זאת נתגבר על הבעיה? !!! נראה שתי שיטות!

18 פתרון: public int computeTuitionFee(){ return Math.max(0,
super.computeTuitionFee() – milga); } } //class Milgay באותו אופן כמו ש-this מצביע על האובייקט הקורא לשיטה, super מצביע על "אבייקט האב" אשר ממנו ירשנו. בדוגמה הנוכחית super מצביע על הסטודנט הקטן בתוך המלגאי. ניתן עתה לפנות לשיטות ולמשתנים הציבוריים שלו. נשים לב כי הקריאה super.computeTuitionFee() מפעילה את הקוד הכתוב במחלקה Student. 18

19 דריסת שיטה של האב נתבונן בשיטה public int computeTuitionFee()
השיטה קיימת במחלקה Student, אך במימוש המחלקה Milgay דרסנו (override) שיטה זו וסיפקנו מימוש אלטרנטיבי. המימוש החדש הכתוב במחלקה Milgay הוא זה שיופעל כאשר טיפוס האובייקט המפעיל את השיטה הוא מילגאי. שימו לב כי חתימת השיטה המופיעה במחלקה Milgay חייבת להיות זהה לזו המופיעה במחלקה Student כי אחרת לא נקבל דריסה אלא שתי שיטות נפרדות. ??? זה נשמע לי מוכר, העניין הזה עם דריסה. !!! נכון! במחלקה Student ובמחלקות אחרות שמימשנו דרסנו את השיטות toString ו-equals של Object. ??? אבל לא רשמנו class Student extends Object... !!! לא סתם אמרנו את המשפט "כל Student הוא Object". כל מחלקה ב-java יורשת מהמחלקה Object ולכן אין צורך לציין זאת באופן מפורש. ??? האם מחלקה אחת יכולה לרשת משתי מחלקות שונות? !!! לא – ב-java מחלקה יכולה לרשת ממחלקה הורה אחת בלבד. ??? אז האם מלגאי הוא אינו אובייקט? !!! "כל מלגאי הוא סטודנט" וגם "כל סטודנט הוא אובייקט" ומכאן שגם "כל מלגאי הוא אובייקט". המחלקה אובייקט היא שורש עץ הירושה. באופן ציורי, ניתן להמחיש זאת כך:

20 "מודל הבצל" כאשר פונים לשיטות ציבוריות לא סטטיות של אובייקטים, אזי הקוד שיבוצע נקבע בזמן ריצה ולפי טיפוס האובייקט בפועל (ולא לפי טיפוס המצביע), "מבחוץ פנימה". לכן, ניסיון להפעיל את השיטה getName() על אובייקט מסוג מילגאי יגרום להפעלת השיטה הכתובה במחלקה Student. לעומת זאת, נסיון להפעיל את computeTuitionFee() יפעיל את הקוד במחלקה Milgay. Student getName(), computeTuitionFee(), addCourse(), getID(), etc… Object toString(), equals(…), etc… "בתוך כל אובייקט יושב אובייקט קטן ממחלקת האב, כאשר בגרעין יושב Object". Milgay getMilga(), computeTuitionFee()

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

22 הורשה מול ממשקים בדומה לממשקים, הורשה מאפשרת להתייחס למופעים ממחלקות שונות באופן אחיד, כאשר מעונינים לפעול על המכנה המשותף שלהם. על אובייקט ממחלקה X יכול להצביע מצביע מכל טיפוס Y, כך ש-Y מחלקה "על המסלול בעץ" מ-X ל-Object. לדוגמה, על אוביקט מהמחלקה Milgay יכולים להצביע מצביעים מהטיפוסים Milgay, Student, ו-Object. בשונה ממשקים, הורשה מאפשרת שיתוף גם במימוש ההתנהגות, כלומר שימוש חוזר בקוד קיים. מחלקה מסוימת יכולה לרשת מחלקה אחת בלבד, בעוד שהיא יכולה לממש מספר ממשקים. 22

23 הפעלת שיטה שנדרסה המקרה הפשוט
הפעלת שיטה שנדרסה המקרה הפשוט public static void main(String[] args) { Milgay m = new Milgay(11,"Moshe", 1000); m.setNumOfCourses(2); System.out.println("The milga of " + m.getName() + ” is " + m.getMilga()); System.out.println("The TF of “ + m.getName() + ” is " + m.computeTuitionFee()); במקרה זה אנו יודעים כי השיטה computeTuitionFee של המחלקה מלגאי היא שתרוץ. נכתוב פונקציית main נוסיף לה שורות קוד, ולגבי כל שורה נתעניין האם היא מתקמפלת. אם השורה אינה מתקמפלת, ננסה לחשוב מדוע. אם השורה מתקמפלת, ננסה להבין איך תרוץ השורה בזמן ריצה, ובפרט, מה יודפס למסך. נקודה לדיון: השורה האחרונה מפעילה את toString() של Student. יתכן והיינו רוצים לדרוס שיטה זו במחלקה Milgay.

24 הפעלת שיטה שנדרסה המקרה המסובך
הפעלת שיטה שנדרסה המקרה המסובך Student s1 = new Milgay(11,"Dani", 500); s1.setNumOfCourses(2); System.out.println("The TF of s1 is " + s1.computeTuitionFee()); טיפוס המצביע הוא Student, בעוד שטיפוס האובייקט בפועל הוא Milgay. עבור שיטות ציבוריות לא סטטיות (ורק עבורן!!!) השיטה המופעלת נקבעת בזמן ריצה, לפי טיפוס האובייקט בפועל. מבחינת קומפילציה המצביע s1 הוא מסוג Student. ל-Student יש את השיטה computeTuitionFee ולכן זה מתקמפל. בזמן הריצה השאלה הנשאלת היא מהי המחלקה של המופע עליו אנו עובדים. יש לחזור לרגע יצירת המופע, כלומר לפקודת ה-new שייצרה את המופע. במקרה שלנו הפקודה היא new Milgay(11,"Dani", 500) מרגע זה מתחיל חיפוש אחר חתימה מתאימה, כלומר חתימה מהסוג: computeTuitionFee() החיפוש מתחיל מהשכבה החיצונית ביותר של הבצל, כלומר במחלקה הנפנית, Milgay. במקרה זה השיטה נמצאת במחלקה Milgay וזו השיטה שתרוץ.

25 הפעלת שיטה יחודית למילגאי
Student s1 = new Milgay(11,"Dani", 500); s1.setNumOfCourses(2); System.out.println("The milga of s1 is “ + s1.getMilga()); לא עבר השורה הראשונה מתקמפלת כי מצביע מסוג Student יכול להצביע על כל מופע שהוא is a Student. בקוד כולו לא מתקמפל. נבחן את תהליך הקומפילציה: הקומפיילר שואל: על מי מתבצעת הפעולה? תשובה: s1 שאלה: מאיזה טיפוס הצבעה הוא s1? תשובה: Student (התשובה לשאלה זו תמיד נמצאת בקוד!) שאלה: האם למחלקה Student יש שיטה getMilga()? תשובה:לא!!! לכן זה לא מתקמפל!!

26 הפעלת שיטה יחודית למילגאי (מתוקנת)
Student s1 = new Milgay(11,"Dani", 500); s1.setNumOfCourses(2); System.out.println("The milga of s1 is “ + ((Milgay) s1).getMilga()); עבר הטיפוס המצביע משתנה ל-Milgay בגלל שעשינו casting. עתה ל-Milgay יש את השיטה getMilga() ולכן הקומפיילר מאשר את זה.

27 "הם עבדו עלי, דוד" התרסק Student s2 = new Student(11,"Dani");
s2.setNumOfCourses(2); System.out.println("The milga of (the non-milgay) s2 is " + ((Milgay) s2).getMilga());  המשמעות של Casting היא שהמתכנת לוקח על עצמו את האחריות לתקינות הטיפוסים. התרסק נעבור שוב על התהליך של הקומפיילר: הקומפיילר שואל: על מי מתבצעת הפעולה? תשובה: s2 שאלה: מאיזה טיפוס הצבעה הוא s2? תשובה: לאחר ה-casting טיפוס ההצבעה הוא Milgay. שאלה: האם למחלקה Milgay יש שיטה getMilga()? תשובה:כן!!! ולכן זה מתקמפל! ומה קורה בזמן ריצה? התוכנית עפה עם הודאת השגיאה: Exception in thread "main" java.lang.ClassCastException: Student at Lecture16.main(Lecture16.java:30) ClassCastException פירושו שבזמן ריצה ניסינו לבצע casting ל-Milgay עבור מופע שהוא אינו instanceof Milgay. ??? אז למה הקומפיילר נתן לזה לקרות? !!! לקומפיילר אין את הכלים להתמודד עם המצב הזה. הוא אינו יכול לדעת האם בזמן ריצה ה-casting יהיה חוקי או לא. ניתן לראות ב-casting כסוג של "העברת אחריות" מהקומפיילר למתכנת. הקומפיילר מסכים ל-casting אך "מסיר מעצמו אחריות" לתוצאות בזמן הריצה.

28 סטטי מול לא-סטטי public class Point{ private double x; private double y; … public void printPoint1(){ System.out.println("(" + x + "," + y + ")"); } public static void printPoint2(Point p){ System.out.println("(" + p.x + "," + p.y + ")"); } //class Point 28 28

29 סטטי מול לא-סטטי פרוצדורות סטטיות אינן משויכות לאובייקט מסוים, ולכן כאשר מבצעים קריאה לפרוצדורה כזו יש צורך להעביר מידע פנימי על האובייקט עליו היא מופעלת, או הצבעה לאובייקט עצמו. Point p1 = new Point(1,2); Point.printPoint2(p1); פרוצדורות סטטיות משויכות למחלקה מסוימת. למשל, הפונקציות של המחלקה Math. מכיוון שאין הגיון ליצור אובייקט אך ורק בשביל ביצוע פעולה מתמטית (פעולות אלו אינן תלויות במצב של אובייקט כלשהו שיכול להשתנות במהלך הריצה), הפונקציות מוגדרות כסטטיות וכך ניתן להשתמש בהן באופן ישיר (Math.pow(…), Math.random()). נהוג לאגד במחלקה מסוימת אוסף של פרוצדורות סטטיות הקשורות לאותו התחום, ובכך לאפשר שימוש באותו אוסף מתוך מספר רב של תוכניות שונות. לא ניתן לדרוס פרוצדורות סטטיות – הקוד שיופעל נקבע כבר בזמן קומפילציה. Math is a class that contains a collection of mathematical static methods, there is no need to define an object for this. 29 29

30 סטטי מול לא-סטטי שיטות מחלקה לעומת זאת, משויכות באופן ישיר לאובייקט אשר יזם את הקריאה להן. למשל: public static void printPoint3(Point p){ System.out.println("(" + p.getX() + "," + p.getY() + ")"); } public static void main(String[] args){ Point p = new Point(1,2); p.printPoint1(); printPoint3(p); Point.printPoint2(); //Compilation Error Point.printPoint2(p); p.printPoint2(p); //works – but it is the wrong way to write code שימו לב כי הפרוצדורה הסטטית printPoint2 יכלה לגשת לשדות הפרטיים של הארגומנט p, כיוון שהיא כתובה במחלקה Point. לעומת זאת, pringPoint3 כתובה במחלקה אחרת, והיא יכולה לגשת רק לשיטות הציבוריות של Point. ניתן להפעיל פרוצדורות סטייות גם מתוך אוביקטים (השורה האחרונה), אך זה שקול להפעלתם מתוך המחלקה 30 30

31 משתנים סטטיים מול שדות כאשר אנו עובדים עם אובייקטים, לכל אובייקט יש את השדות שלו. ב-java יש סוג נוסף של משתנים הנקראים משתנים סטטים. דוגמה למשתנה סטטי: Math.PI בניגוד לשדות, משתנים סטטים משויכים למחלקה עצמה, ולא למופעים שלה. לרוב משתמשים במשתנים סטטים לתיאור קבועים (כדוגמת Math.PI), ואז הם מוגדרים כ-static final. בד"כ לא נשתמש במשתנים מסוג זה באופן אחר, אך בכל זאת: 31

32 public static int numberOfPoints = 0; public Point(){ x=0; y=0;
אם נרצה לדוגמה, למנות את מספר האובייקטים מסוג Point שהוגדרו בתכנית, כלומר לספור את מספר הקריאות שבוצעו לבנאי המחלקה Point, נגדיר משתנה סטטי numberOfPoints , ונגדיל את ערכו בכל קריאה לבנאי: class Point{ public static int numberOfPoints = 0; public Point(){ x=0; y=0; numberOfPoints = numberOfPoints + 1; } }//class עתה נוכל לכתוב בתכנית: System.out.println(Point.numberOfPoints); וכמובן לכל שאר הבנאים נוסיף את אותה שורה. הערה: בפועל, מספר האובייקטים הקיימים לא זהה בהכרח למספר האובייקטים שנוצרו (java garbage collector), אבל לא נכנס לנושא זה בקורס. דוגמה נוספת למשתנים סטטיים תינתן בתירגול. 32

33


Download ppt "Everything the light touches, Simba, will be yours"

Similar presentations


Ads by Google