תכנות מתקדם ב-Java/אובייקטים
אובייקטים או עצמים, הם אבני היסוד של תכנות מונחה העצמים- Object Oriented Programming (OOP).
הקדמה - תכנות מונחה עצמים
[עריכה]תכנות מונחה עצמים - תפיסה בתחום התכנות, לפיה התוכנה מורכבת מאוסף של עצמים שונים. כל עצם הוא בעל תכונות ואפיונים משלו, ומכלול העצמים יוצר את התוכנה המוגמרת. תכנות מונחה עצמים מסייע לבניית קוד מודולרי, פשוט לשינוי, קל לשליטה (באופן יחסי) גם בתוכניות גדולות, ורב שימושי - קטע קוד אחד יכול להשתלב בתוכניות רבות.
דוגמה - מכולת
[עריכה]ניקח דוגמה פשוטה: נניח שאנו רוצים לבנות תוכנה לניהול מלאי של חנות מכולת. הדרישות הן פשוטות: אפשר ליצור סוגי מוצרים חדשים, ולשנות את המחיר של מוצרים חדשים. אפשר להוסיף ולהחסיר מהמלאי, ואפשר גם לחשב את ערך כל המוצרים במלאי ולהדפיס רשימת מלאי. כיצד נממש זאת בעזרת אובייקטים? נשתמש בשני סוגי אובייקטים:
- אובייקט מטיפוס "מוצר", שיכיל נתונים לגבי המוצר: שם, מחיר, תיאור, כמות, ותאריך תפוגה. אובייקט מטיפוס מוצר יתמוך בכמה פעולות: יצירה של מוצר, עדכון של מחיר המוצר, עדכון כמות המוצר, והדפסה של פרטי המוצר.
- אובייקט מטיפוס "מלאי", שיכיל אוסף של מוצרים. אובייקט מטיפוס מלאי יתמוך בפעולות משלו: הכנסה של סוג מוצר למלאי, הוספת והחסרת מוצרים מהמלאי, חישוב של כלל ערך המוצרים והדפסת רשימת מלאי.
קל לראות שקיימים קשרים בין האובייקטים: בעת הדפסת רשימת מלאי, למשל, פונה אובייקט המלאי לפונקציית ההדפסה של כל מוצר במלאי. בעת חישוב סך ערך המלאי ישתמש אובייקט המלאי, כחלק מהחישוב, בנתוני המחיר והכמות שמכיל כל מוצר.
מבנה
[עריכה]כל טיפוס של אובייקט נקרא "מחלקה" (Class). מחלקה מכילה בנאים (והורסים), מתודות/שיטות (Methods) ומשתנים פנימיים. בדוגמה שלנו, קיימות שתי מחלקות: מלאי, ומוצר. המלאי מכיל את השיטות "חשב ערך כולל", "הדפס מלאי", "הוסף מוצר למלאי", וכן הלאה. הוא מכיל את הנתונים לגבי המוצרים: מערך מטיפוס "מוצר". המוצר יכיל כמה שיטות משלו: "הדפס מוצר", "צור מוצר חדש", וכדומה. הוא יכיל משתנים פנימיים: משתנה שיכיל בתוכו את מחיר המוצר, משתנה שיכיל את שם המוצר, וכן הלאה.
בנאים
[עריכה]בנאי (באנגלית: Constructor), כשמו, הוא כלי באמצעותו יוצרים אובייקט. לכל אובייקט קיים בנאי, ולפעמים גם כמה סוגים של בנאים. חוץ מיצירת האובייקט, הבנאי יכול להכיל פעולות כאוות נפשו של המתכנת. הערה: בניגוד לשפות (כמו C++), בהן קיים צורך להשמיד (Destruct) כל אובייקט לאחר השימוש בו כדי לפנות את הזיכרון בו נעשה שימוש, בג'אווה אין צורך בכך - כלי בשם Garbage Collector (אוסף האשפה) אחראי על פינוי הזיכרון, ומסיר בכך את הנטל מעל כתפי המתכנתים. נעיר כי העדפה זו של הנוחות עולה במחיר מה של מהירות הריצה.
משתנים פנימיים
[עריכה]משתנים פנימיים הם המשתנים אותם מכיל האובייקט בתוכו. הם יכולים להיות מכל סוג שהוא - משתנים פשוטים כמו int או String, מערכים, או אף אובייקטים אחרים. לעיתים קרובות הם מכונים גם "שדות".
שיטות
[עריכה]החלק העיקרי ברוב האובייקטים הוא השיטות הנכללות בהם. מחוץ לפרמטרים הרגילים, השיטות (למעט הסטטיות) נקראות ומתבצעות על האובייקט עצמו. שיטות מכונות לעיתים קרובות גם "מתודות" או "פונקציות" או "שגרות" (ביחיד "שגרה"), או "פרוצדורות".
איך זה נראה בג'אווה
[עריכה]בג'אווה, כל קובץ הוא מחלקה. שם הקובץ חייב להיות כשם המחלקה הנמצאת בו, לכן יש מתאם בין שם הקובץ לשם המחלקה. כל מחלקה כזאת מכילה את המרכיבים שנזכרו לעיל. כל מחלקה מכילה בנאי. במחלקה בה לא כתב המתכנת בנאי - מוסיף המהדר, בצורה אוטומטית, בנאי ריק, שלא מקבל פרמטרים כלשהם. פרט לכך, מחלקה יכולה להיות ריקה: אין הכרח שיהיו בה משתנים פנימיים או שיטות. אפשר לדלג על ההסברים ולעיין בדוגמה, אך לשם הבנה נכונה יותר - כדאי לחזור אליהם לאחר מכן.
בנאי(Constructor)
[עריכה]הבנאי הוא שיטה ששמה כשם המחלקה, ושאינה מחזירה ערך משום סוג. אם נניח ששם המחלקה הוא MyClass, הבנאי יראה כך:
public MyClass() { // Some code }
בנאי יכול לקבל ערך, או ערכים רבים, מכל סוג, בצורה הבאה:
public MyClass(int x, float y) { // Some code }
הקריאה לבנאי מתבצעת בעזרת המילה השמורה new, בצורה הבאה:
MyClass obj = new MyClass();
או, אם הבנאי מקבל ערכים, למשל - בנאי שמקבל int ו-float:
MyClass obj = new MyClass(1, 2.3);
מרבית הקוראים בוודאי יתמהו: מדוע להכריז בצורה כזו על האובייקט? מדוע שלא להכריז על אובייקט כמו שמכריזים על משתנה פשוט - MyClass obj;
?
למען האמת, מורכבת ההכרזה שהצגנו כאן משני חלקים. החלק הראשון, השמאלי - MyClass obj
יוצר הפנייה (מצביע) לאובייקט מטיפוס MyClass, הפנייה שעד יצירת האובייקט מכוונת לערך ריק - null. האובייקט עצמו לא נוצר. החלק השני, הימני, בו אנו פונים לבנאי, הוא החלק שמקצה זיכרון עבור האובייקט, ויוצר אותו במקום שהוקצה עבורו בזיכרון. כשתלמדו כמה צדדים מתוחכמים יותר של תכנות מונחה העצמים תוכלו להבין טוב יותר מדוע קיימת ההפרדה הזו.
משתנים ושיטות
[עריכה]צורת העבודה עם המשתנים והשיטות דומה מאוד לזו שכבר הכרנו, אך ללא המילה static
. בעבודה עם אובייקטים נהוג להשתמש במשתנים גלובליים - של האובייקט.
פנייה למשתנים ושיטות
[עריכה]נקודת ההסתכלות צריכה לעבור מהפך, כאשר מתעסקים בתכנות מונחה עצמים. כעת, כל פעולה שמתבצעת - מתבצעת על אובייקט. כדי לבצע פעולה כלשהי על אובייקט, משתמשים בצורה
obj.action(arguments)
כדי לבצע את השיטה action (עם הארגומנטים arguments) על האובייקט obj, או
obj.variable
כדי לפנות למשתנה variable, שהוא משתנה פנימי של האובייקט obj.
שיטות בתוך האובייקט עצמו, כלומר - שיטות בתוך המחלקה - יכולות לפנות ישירות למשתני המחלקה (אך לא למשתנים סטטיים). שיטות מתוך האובייקט יכולות לפנות ישירות גם לשיטות (לא סטטיות) אחרות של המחלקה. גם הפניות האלה נעשות אל אובייקט - אל אותו האובייקט עצמו.
תוכנית המכולת
[עריכה]נראה כאן מימוש (עדיין לא מספק) בג'אווה של ה"מכולת" אותה תיארנו קודם. נעבוד עם שלושה קבצים: Item.java יכיל את המחלקה המטפלת במוצרים. Stock.java יכיל את המחלקה המטפלת במלאי. הקובץ השלישי אינו חלק מהמימוש עצמו, אך הוא יראה כיצד מחלקה אחרת ניגשת אל המחלקות שיצרנו ועושה בהן שימוש.
// Item.java
public class Item {
// Item's name
private String _name;
// Item's description
private String _description;
// Item's price
private double _price;
/*
* Constructor
*/
public Item(String name, String desc, double price) {
_name = name;
_description = desc;
_price = price;
}
// Get item's name
public String getName() {
return _name;
}
// Get item's price
public double getPrice() {
return _price;
}
// Print item details
public void printItem() {
System.out.println("Item: "+_name);
System.out.println("Description: "+_description);
System.out.println("Price: "+_price);
}
}
זהו המימוש של מוצר במכולת. המחלקה מכילה את כל השדות (המשתנים הפנימיים) וכמה שיטות נחוצות. המוצר מכיל רק נתונים שנוגעים ישירות אליו, אך לא נתונים כמו כמות המוצר במלאי, כיוון שזה עניין של המלאי ולא של המוצר עצמו. נראה כעת את המחלקה האחראית על המלאי:
// Stock.java
public class Stock {
// Items in stock
private Item _item1;
private Item _item2;
// Quantity of each product
private int _item1Q;
private int _item2Q;
// Days until expiration for each product
private int _exp1;
private int _exp2;
/*
* Constructor
*/
public Stock() {
_item1 = null;
_item2 = null;
}
// Add item to stock
public void addItem(Item it, int quantity, int expiration) {
if(_item1 == null) {
_item1 = it;
_item1Q = quantity;
_exp1 = expiration;
}
else if(_item2 == null) {
_item2 = it;
_item2Q = quantity;
_exp2 = expiration;
}
else
System.out.println("Stock is full, cannot add "+it.getName());
}
// Print all items in stock
public void printStock() {
if(_item1 != null) {
_item1.printItem();
System.out.println("Quantity: "+_item1Q+" Expiration: "+_exp1);
}
if(_item2 != null) {
_item2.printItem();
System.out.println("Quantity: "+_item2Q+" Expiration: "+_exp2);
}
}
// Calculates the value of all items in stock
public double sumStock() {
double sum = 0.0;
if(_item1 != null)
sum+=_item1.getPrice()*_item1Q;
if(_item2 != null)
sum+=_item2.getPrice()*_item2Q;
return sum;
}
}
הקלה נוספת לשם הפשטות: המלאי יכול להכיל רק עד שני מוצרים. זהו, כמובן, מימוש לא שימושי במיוחד, והוא נעשה לצרכי הדגמה בלבד. נראה כאן את המחלקה Grocery, שתשתמש במחלקות שיצרנו על מנת לנהל את המלאי:
// Grocery.java
public class Grocery {
public static void main(String[] args) {
// Build a stock object
Stock stck = new Stock();
// Print the stock - it is empty now
stck.printStock();
// Add items to stock:
Item it1 = new Item("Cheese","Smelly green cheese", 1.5);
Item it2 = new Item("Tomato","Fresh tomato", 2.6);
stck.addItem(it1, 2, 7);
stck.addItem(it2, 3, 5);
stck.printStock();
System.out.println("Total price of stock: " + stck.sumStock());
}
}
אופן הפעולה
[עריכה]שלוש המחלקות עובדות בתיאום, זו עם זו. המחלקה הראשונה, Item, אחראית על ייצוג המוצרים. כל אובייקט מסוג Item מייצג מוצר בודד. המחלקה השנייה, Stock, משתמשת במחלקה Item. היא מכילה שני שדות פנימיים מסוג Item, ומכאן שהיא מסוגלת להכיל שני מוצרים שונים. המחלקה האחרונה, Grocery, יוצרת אובייקט מלאי אחד (מסוג Stock), ומבצעת עליו פעולות שונות (ראו הסברים נוספים המופיעים בהערות).
הפעלת אובייקטים
[עריכה]המחלקה Grocery מעוניינת בשימוש באובייקט מלאי. לשם כך, נוצר אובייקט מטיפוס Stock בעזרת הבנאי הנתון של Stock, ואז ניתן היה לבצע על האובייקט החדש שיצרנו פעולות שונות. למשל, כדי להוסיף מוצר למלאי, השתמשנו בשיטה addItem בדרך הבאה:
ראשית, יצרנו אובייקט מוצר אחד בעזרת השורה
Item it1 = new Item("Cheese","Smelly green cheese", 1.5);
שורה זו פנתה לבנאי של המחלקה Item. לאחר מכן, הוספנו מוצר זה למלאי בעזרת הפקודה
stck.addItem(it1, 2, 7);
באופן דומה, במחלקה Stock, כדי לבצע את השיטה שמדפיסה מוצר על האובייקט _item1, השתמשנו בשורה
_item1.printItem();
הפעולה התבצעה על האובייקט המסויים item1_, והשתמשה בנתונים שלו. פעולה זהה על האובייקט item1_ ועל האובייקט item2_ לא תיתן תוצאה זהה, כי הנתונים של אובייקט item1_ שונים (בדרך כלל) מאלה של item2_.
תקשורת בין מחלקות
[עריכה]המחלקות אמנם מתקשרות ביניהן, אך אף מחלקה לא ניגשת בצורה ישירה לשדה של מחלקה אחרת, ואם יש צורך לדעת מה ערכו של שדה מסויים - קיימות שיטות מיוחדות לשם כך. בנוסף לזה, אפשר לראות שחלק מהמשתנים הוגדרו בעזרת המילה השמורה private. כל זה קשור לעיקרון הכימוס, שיוסבר בהמשך.
הסבר
[עריכה]ראינו כאן דוגמה, פשוטה יחסית, לתוכנית שנכתבה בצורה מונחית עצמים. המבנה שלנו מתחלק לשלושה חלקים נפרדים: המלאי, המוצרים, והמחלקה שמשתמשת במלאי. התוכנית הזו מדגימה את עיקרון המודולריות של התכנות מונחה העצמים: למרות קשרי הגומלין שמתקיימים בין המחלקות השונות, ניתן לשנות בקלות מחלקה אחת בלי להפריע לפעולת שאר המחלקות. נניח, לדוגמה, כי החלטנו לשנות את הצורה בה מודפסים פרטי כל מוצר. כל שצריך לעשות הוא לשנות את הקוד במקום אחד - בשיטה האחראית על הדפסת מוצר - והצורה בה מודפסים מוצרים תשתנה בכל מקום בתוכנית, בלי שנצטרך לשנות דבר נוסף. נניח כי נרצה לשנות את הצורה בה המלאי מחזיק את המוצרים ברשותו, למשל - לעבור לייצג את המוצרים בעזרת מערך - זה ידרוש עבודה רבה למדי בשיטות של המחלקה Stock, אבל המחלקה Item תוכל להישאר בדיוק כמו שהיא, וגם הלקוח הסופי שמשתמש במחלקה לא צריך לשנות דבר. עם זאת, המימוש הנוכחי רחוק מלהיות מושלם. נראה בהמשך כיצד ניתן לשפרו.
- | אובייקטים תרגילים |
הפרק הבא: עבודה לפי ממשק |