אובייקט אוריינטד תכנות ב-Java: מחלקות, אובייקטים, אינקפסולציה, ירושה ואבסטרקציה

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

הערה על ארגון והרצת קוד Java

לפני שנתחיל לכתוב קוד, בואו נבצע קצת הגדרה.

כמו עם התחביר שלה, ל־Java יש כללים מחמירים לגבי ארגון הקוד.

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

כל הפרויקטים ב-Java חייבים לכלול קובץ Main.java עם קלאס ה-Main. זהו המקום בו אתה בודק את הקלאסים שלך על ידי יצירת אובייקטים מהם.

כדי להפעיל קוד Java, נשתמש ב- IntelliJ IDEA, סביר להניח, סביר להניח. לאחר התקנת IntelliJ:

  1. צרו פרויקט Java חדש (קובץ > חדש > פרויקט)
  2. לחץ על הספרייה src כדי ליצור את ה-Main.java והדבק את התוכן הבא:
public class Main { public static void main(String[] args) { // יצירת ובדיקת אובייקטים כאן } }

כאשר אנו מדברים על כיתות, אנו כותבים קוד בקבצים אחרים מאשר בקובץ Main.java. אך אם אנו מדברים על יצירה ובדיקת אובייקטים, אנו מחליפים אל Main.java.

כדי להריץ את התוכנית, ניתן ללחוץ על כפתור הניגון הירוק ליד השיטה הראשית:

הפלט יוצג בחלון הריצה בחלקו התחתון.

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

אחרת, בוא נתפס את זה.

מחלקות ואובייקטים ב-Java

אז, מהן בדיוק כיתות?

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

public class MenuItem { public String name; public double price; }

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

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

אבל איך אנו יוצרים אובייקטים ששייכים למחלקה זו? ג'אווה מאפשרת זאת דרך שיטות בונות:

public class MenuItem { public String name; public double price; // בונה public MenuItem(String name, double price) { this.name = name; this.price = price; } }

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

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

לאחר שכתבת את המחלקה שלך ואת בנאיה, אתה יכול ליצור מופעים (אובייקטים) שלה בשיטת ה-main שלך:

public class Main { public static void main(String[] args) { // צור אובייקטים כאן MenuItem burger = new MenuItem("Burger", 3.5); MenuItem salad = new MenuItem("Salad", 2.5); System.out.println(burger.name + ", " + burger.price); } }

פלט:

Burger, 3.5

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

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

public class MenuItem { public String name; public double price; // בונה public MenuItem(String name, double price) { this.name = name; this.price = price; } // שיטה לחישוב מחיר לאחר מס public double getPriceAfterTax() { double taxRate = 0.08; // שיעור מס של 8% return price + (price * taxRate); } }

כעת ניתן לחשב את המחיר הכולל, כולל מס:

public class Main { public static void main(String[] args) { MenuItem burger = new MenuItem("Burger", 3.5); System.out.println("Price after tax: $" + burger.getPriceAfterTax()); } }

פלט:

Price after tax: $3.78

קיבוץ

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

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

// בתוך Main.java MenuItem applePie = new MenuItem("Apple Pie", -5.99); // מחיר שלילי! MenuItem sandwich = new MenuItem("Sandwich", 1000000); // יקר מדי System.out.println("Apple pie price: $" + applePie.price); System.out.println("Sandwich price: $" + sandwich.price);

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

Java מאפשרת לנו לבצע זאת באמצעות שימוש בשיטות setter:

public class MenuItem { private String name; private double price; private static final double MAX_PRICE = 100.0; public MenuItem(String name, double price) { this.name = name; setPrice(price); } public void setPrice(double price) { if (price < 0) { throw new IllegalArgumentException("Price cannot be negative"); } if (price > MAX_PRICE) { throw new IllegalArgumentException("Price cannot exceed $" + MAX_PRICE); } this.price = price; } }

בואו נבחן מה חדש בבלוק הקוד למעלה:

1. עשינו את המאפיינים פרטיים על ידי הוספת המילה המפתחית private. זה אומר שאפשר לגשת אליהם רק בתוך מחלקת MenuItem. האחסון מתחיל עם שלב חשוב זה.

2. הוספנו קבוע חדש MAX_PRICE שהוא:

  • פרטי (נגיש רק בתוך המחלקה)
  • סטטי (משותף לכל המופעים)
  • סופי (לא ניתן לשנות אחרי האתחול)
  • מוגדר ל- $100.0 כמחיר מרבי סביר

3. הוספנו setPrice() שיטה ש:

  • מקבלת פרמטר מחיר
  • מאמת כי המחיר אינו שלילי
  • מאמת כי המחיר אינו חורג מ-MAX_PRICE
  • זורק IllegalArgumentException עם הודעות תיאוריות אם האימות נכשל
  • מגדיר את המחיר רק אם כל האימותים עוברים בהצלחה

4. שינינו את הבנאי כך שישתמש ב-setPrice() במקום להקצות ישירות את המחיר. זה מבטיח שאימות המחיר יתבצע במהלך יצירת העצם.

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

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

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

// שאר המחלקה כאן ... private static final String[] VALID_NAMES = {"latte", "cappuccino", "espresso", "americano", "mocha"}; private String name; public void setName(String name) { String lowercaseName = name.toLowerCase(); for (String validName : VALID_NAMES) { if (validName.equals(lowercaseName)) { this.name = name; return; } } throw new IllegalArgumentException("Invalid drink name. Must be one of: " + String.join(", ", VALID_NAMES)); }

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

1. ראשית, הוא מגדיר מערך סטטי סופי פרטי VALID_NAMES המכיל את שמות המשקאות המותרים בלבד: לאטה, קפוצ'ינו, אספרסו, אמריקנו, ומוקה. מערך זה הוא:

  • פרטי: נגיש רק בתוך המחלקה
  • סטטי: משותף לכל המופעים
  • סופי: לא ניתן לשנות אחרי האתחול

2. הוא מצהיר על שדה String פרטי בשם name כדי לאחסן את שם המשקה

3. שיטת setName() מיישמת את הלוגיקה לאימות:

  • מקבלת פרמטר String בשם name
  • ממירה אותו לאותיות קטנות כדי לבצע השוואה לא תלוית אותיות
  • עוברת על מערך VALID_NAMES
  • אם נמצאה התאמה, מגדיר את השם ומחזיר
  • אם לא נמצאה התאמה, זורק IllegalArgumentException עם הודעת תיאור המפרטת את כל האפשרויות התקפות

כאן כל המחלקה עד כה:

public class MenuItem { private String name; private double price; private static final String[] VALID_NAMES = {"latte", "cappuccino", "espresso", "americano", "mocha"}; private static final double MAX_PRICE = 100.0; public MenuItem(String name, double price) { setName(name); setPrice(price); } public void setName(String name) { String lowercaseName = name.toLowerCase(); for (String validName : VALID_NAMES) { if (validName.equals(lowercaseName)) { this.name = name; return; } } throw new IllegalArgumentException("Invalid drink name. Must be one of: " + String.join(", ", VALID_NAMES)); } public void setPrice(double price) { if (price < 0) { throw new IllegalArgumentException("Price cannot be negative"); } if (price > MAX_PRICE) { throw new IllegalArgumentException("Price cannot exceed $" + MAX_PRICE); } this.price = price; } }

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

public class MenuItem { // קוד נוסף כאן ... public String getName() { return name; } public double getPrice() { return price; } }

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

לדוגמא, ללא getters, יתכן ונגיע למאפיינים ישירות:

MenuItem item = new MenuItem("Latte", 4.99); String name = item.name; // גישה ישירה למאפיין item.name = "INVALID"; // ניתן לשנות ישית, בעילת עוברת על אימות

עם getters, אנו מאפשרים גישה נכונה:

MenuItem item = new MenuItem("Latte", 4.99); String name = item.getName(); // גישה בקרתית דרך getter // item.name = "INVALID"; // לא מותר - יש להשתמש ב setName() שמאמת

האחסון בתוך מחלקה:

  1. מגן על תקינות הנתונים על ידי מניעת שינויים לא תקינים
  2. מאפשר לנו לשנות את היישום הפנימי ללא השפעה על קוד שמשתמש במחלקה
  3. מספק נקודת גישה יחידה שיכולה לכלול לוגיקה נוספת אם נדרש
  4. הופך את הקוד לניתן לתחזוקה יותר ופחות נוטה לבאגים

ירושה

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

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

המורשת מספקת פתרון אלגנטי לכל אלה הבעיות. זה מאפשר לנו ליצור גרסאות מתמחות של פריטי תפריט על ידי 定義 מחלקת מקור MenuItem עם מאפיינים משותפים ואז יצירת מחלקות ילד שמורשות את אלה היסודות בזמן שהן מוסיפות תכונות ייחודיות.

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

הרחבת מחלקות

בואו נממש את הרעיונות האלה החל מDrink:

public class Drink extends MenuItem { private boolean isCold; public Drink(String name, double price, boolean isCold) { this.name = name; this.price = price; this.isCold = isCold; } public boolean getIsCold() { return isCold; } public void setIsCold(boolean isCold) { this.isCold = isCold; } }

כדי להגדיר מחלקת ילד שמורשת ממחלקת הורה, אנו משתמשים במילת המפתח extends אחרי שם המחלקה הילד עם שם המחלקה ההורה. לאחר הגדרת המחלקה, אנו מגדירים את המאפיינים החדשים של הילד ומיישמים את הבונה שלו.

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

public class Drink extends MenuItem { private boolean isCold; public Drink(String name, double price, boolean isCold) { super(name, price); this.isCold = isCold; } public boolean getIsCold() { return isCold; } public void setIsCold(boolean isCold) { this.isCold = isCold; } }

המילת מפתח super משמשת לקריאה לבנאי של מחלקת ההורה. במקרה זה, super(name, price) קוראת לבנאי של MenuItem כדי לאתחל את המאפיינים האלה, ממנה נמנעת כפילות בקוד. אנו רק צריכים לאתחל את המאפיין החדש isCold הספציפי למחלקת Drink.

המילת מפתח היא גמישה מאוד מאחר ואפשר להשתמש בה להתייחס למחלקת ההורה בכל חלק במחלקת הילד. לדוגמה, כדי לקרוא לשיטת ההורה, אתה משתמשsuper.methodName() בזמן ש-super.attributeName היא למאפיינים.

התעלמות משיטה

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

כך זה נראה:

public class MenuItem { // שאר חלקי מחלקת MenuItem public double calculateTotalPrice() { // שיעור מס ברירת מחדל של 10% return price * 1.10; } } public class Food extends MenuItem { private boolean isVegetarian; public Food(String name, double price, boolean isVegetarian) { super(name, price); this.isVegetarian = isVegetarian; } @Override public double calculateTotalPrice() { // מזון עם מס של 15% return super.getPrice() * 1.15; } } public class Drink extends MenuItem { private boolean isCold; public Drink(String name, double price, boolean isCold) { super(name, price); this.isCold = isCold; } @Override public double calculateTotalPrice() { // משקאות עם מס של 8% return super.getPrice() * 1.08; } }

בדוגמה זו, דריסת שיטה מאפשרת לכל תת-מחלקה לספק את המימוש שלה calculateTotalPrice():

המחלקה הבסיסית MenuItem מגדירה חישוב מס ברירת מחדל של 10%.

כאשר Food ו- Drink מרחיבים MenuItem, הם מחליפים את השיטה זו על מנת ליישם שיעורי מס משלהם:

  • פריטי מזון משוייכים לשיעור מס גבוה יותר של 15%
  • שתייה מחוייבת בשיעור מע"מ של 8%

The @Override הופעל כדי לציין באופן מפורש כי השיטות הללו מחליפות את שיטת ההורה. זה עוזר לזהות שגיאות אם חתימת השיטה אינה תואמת לשל שיטת ההורה.

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

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

מחלקות מופשטות

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

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

כדי ליצור MenuItem אבסטרקטי, אנו מוסיפים את abstract מפתח לאחר מודיפיקטור הגישה שלו:

public abstract class MenuItem { private String name; private double price; public MenuItem(String name, double price) { setName(name); setPrice(price); } // הגטרים/סטטרים הקיימים נשארים אותו דבר // לעשות את המתודה הזו אבסטרקטית - כל תת מחלקה חייבת ליישם אותה public abstract double calculateTotalPrice(); }

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

אז, בואו נרשום מחדש אוכל ו שתייה עם השינויים הללו בדעת:

public class Food extends MenuItem { private boolean isVegetarian; public Food(String name, double price, boolean isVegetarian) { super(name, price); this.isVegetarian = isVegetarian; } @Override public double calculateTotalPrice() { return getPrice() * 1.15; // 15% מס } } public class Drink extends MenuItem { private boolean hasCaffeine; public Drink(String name, double price, boolean hasCaffeine) { super(name, price); this.hasCaffeine = hasCaffeine; } @Override public double calculateTotalPrice() { return getPrice() * 1.10; // 10% מס } }

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

מסקנה

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

כדי להפוך את המערכת הזו למוכנה לייצור, עליך עדיין ללמוד דברים רבים, כמו ממשקים (חלק מהסתירה), פולימורפיזם, ותבניות עיצוב מונחי עצמים. כדי ללמוד עוד על מושגים אלה, עיין ב הקורס Introduction to OOP in Java.

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

Source:
https://www.datacamp.com/tutorial/oop-in-java