שלום! היום נדבר על מושג חשוב בג'אווה - ממשקים. המילה בוודאי מוכרת לך. לדוגמה, לרוב תוכניות המחשב והמשחקים יש ממשקים. במובן הרחב, ממשק הוא סוג של "שליטה מרחוק" המחבר בין שני צדדים המקיימים אינטראקציה זה עם זה. דוגמה פשוטה לממשק מחיי היומיום היא שלט רחוק לטלוויזיה. הוא מחבר בין שני חפצים, אדם וטלוויזיה, ומבצע משימות שונות: להגביר או להנמיך את הווליום, להחליף ערוצים, להדליק או לכבות את הטלוויזיה. צד אחד (האדם) צריך לגשת לממשק (לחץ על כפתור השלט הרחוק) כדי שהצד השני יבצע את הפעולה. לדוגמה, שהטלוויזיה תעביר את הערוץ לערוץ הבא. במקרה זה, המשתמש אינו צריך לדעת את המכשיר של הטלוויזיה וכיצד תהליך החלפת הערוץ מיושם בתוכו. כל מה שיש למשתמש גישה אליו הוא הממשק . המשימה העיקרית היא להשיג את התוצאה הרצויה. מה זה קשור לתכנות וג'אווה? ישיר :) יצירת ממשק דומה מאוד ליצירת מחלקה רגילה, אך במקום המילה
class
אנו מציינים את המילה interface
. בואו נסתכל על ממשק Java הפשוט ביותר ונבין איך הוא עובד ולמה הוא נחוץ:
public interface Swimmable {
public void swim();
}
יצרנו ממשק Swimmable
שיכול לשחות . זה משהו כמו השלט הרחוק שלנו, שיש לו "כפתור" אחד: השיטה swim()
היא "שחות". כיצד נוכל להשתמש ב"שלט רחוק " זה? לצורך כך השיטה, דהיינו. יש ליישם את הכפתור בשלט הרחוק שלנו. כדי להשתמש בממשק, השיטות שלו חייבות להיות מיושמות על ידי כמה מחלקות של התוכנית שלנו. בואו נמציא כיתה שהאובייקטים שלה מתאימים לתיאור "יכול לשחות". לדוגמה, מחלקת הברווזים מתאימה Duck
:
public class Duck implements Swimmable {
public void swim() {
System.out.println("Duck, swim!");
}
public static void main(String[] args) {
Duck duck = new Duck();
duck.swim();
}
}
מה אנחנו רואים כאן? מחלקה Duck
משויכת לממשק Swimmable
באמצעות מילת המפתח implements
. אם אתה זוכר, השתמשנו במנגנון דומה כדי לחבר שתי מחלקות בירושה, רק שהייתה המילה " מרחיב ". " public class Duck implements Swimmable
" ניתן לתרגם מילולית לצורך הבהירות: "מחלקה ציבורית Duck
מיישמת את הממשק Swimmable
." המשמעות היא שמחלקה הקשורה לממשק חייבת ליישם את כל השיטות שלה. שימו לב: בכיתה שלנו, Duck
בדיוק כמו בממשק , Swimmable
יש שיטה swim()
, ובתוכה יש איזושהי היגיון. זוהי דרישה מחייבת. אם רק נכתב " public class Duck implements Swimmable
" ולא ניצור מתודה swim()
במחלקה Duck
, המהדר היה נותן לנו שגיאה: Duck אינו מופשט ואינו עוקף את השיטה המופשטת swim() ב-Swimmable מדוע זה קורה? אם נסביר את השגיאה באמצעות הדוגמה של טלוויזיה, מסתבר שאנו נותנים לאדם שלט עם כפתור "שינוי ערוץ" מטלוויזיה שלא יודעת להחליף ערוצים. בשלב זה, לחץ על הכפתור כמה שאתה רוצה, שום דבר לא יעבוד. השלט הרחוק עצמו אינו מחליף ערוצים: הוא רק נותן אות לטלוויזיה, שבתוכה מיושם תהליך מורכב של החלפת ערוץ. כך זה עם הברווז שלנו: עליו להיות מסוגל לשחות כדי שניתן יהיה לגשת אליו באמצעות הממשק Swimmable
. אם היא לא תדע לעשות זאת, הממשק Swimmable
לא יחבר בין שני הצדדים - האדם והתוכנית. אדם לא יוכל להשתמש בשיטה swim()
כדי לגרום לאובייקט Duck
בתוך תוכנית לצוף. עכשיו ראית בצורה ברורה יותר למה מיועדים ממשקים. ממשק מתאר את ההתנהגות שחייבות להיות מחלקות שמיישמות ממשק זה. "התנהגות" היא אוסף של שיטות. אם ברצוננו ליצור מספר שליחים, הדרך הקלה ביותר לעשות זאת היא על ידי יצירת ממשק Messenger
. מה כל שליח אמור להיות מסוגל לעשות? בצורה פשוטה, קבלו ושלחו הודעות.
public interface Messenger{
public void sendMessage();
public void getMessage();
}
ועכשיו אנחנו יכולים פשוט ליצור את שיעורי המסנג'ר שלנו על ידי יישום הממשק הזה. המהדר עצמו "יאלץ" אותנו ליישם אותם בתוך מחלקות. מִברָק:
public class Telegram implements Messenger {
public void sendMessage() {
System.out.println("Sending a message to Telegram!");
}
public void getMessage() {
System.out.println("Reading the message in Telegram!");
}
}
WhatsApp:
public class WhatsApp implements Messenger {
public void sendMessage() {
System.out.println("Sending a WhatsApp message!");
}
public void getMessage() {
System.out.println("Reading a WhatsApp message!");
}
}
Viber:
public class Viber implements Messenger {
public void sendMessage() {
System.out.println("Sending a message to Viber!");
}
public void getMessage() {
System.out.println("Reading a message in Viber!");
}
}
אילו יתרונות זה מספק? החשוב שבהם הוא צימוד רופף. תארו לעצמכם שאנחנו מעצבים תוכנית שבה נאסוף נתוני לקוחות. למחלקה Client
חייב להיות שדה המציין באיזה מסנג'ר הלקוח משתמש. בלי ממשקים זה היה נראה מוזר:
public class Client {
private WhatsApp whatsApp;
private Telegram telegram;
private Viber viber;
}
יצרנו שלושה שדות, אבל ללקוח יכול בקלות להיות רק שליח אחד. אנחנו פשוט לא יודעים איזה. וכדי לא להישאר ללא תקשורת עם הלקוח, אתה צריך "לדחוף" את כל האפשרויות האפשריות לתוך הכיתה. מסתבר שאחד או שניים מהם תמיד יהיו שם null
, ואין צורך בהם כלל כדי שהתוכנית תעבוד. במקום זאת, עדיף להשתמש בממשק שלנו:
public class Client {
private Messenger messenger;
}
זוהי דוגמה ל"צימוד רופף"! במקום לציין מחלקת מסנג'ר ספציפית במחלקה Client
, אנו פשוט מזכירים שללקוח יש מסנג'ר. איזה מהם ייקבע במהלך התוכנית. אבל למה אנחנו צריכים ממשקים בשביל זה? למה הם נוספו לשפה בכלל? השאלה טובה ונכונה! ניתן להגיע לאותה תוצאה באמצעות ירושה רגילה, נכון? הכיתה Messenger
היא כיתת ההורים, ו Viber
, Telegram
והם WhatsApp
היורשים. אכן, אפשר לעשות זאת. אבל יש מלכוד אחד. כפי שאתה כבר יודע, אין ירושה מרובה ב-Java. אבל יש מימושים מרובים של ממשקים. מחלקה יכולה ליישם כמה ממשקים שהיא רוצה. תארו לעצמכם שיש לנו מחלקה Smartphone
שיש בה שדה Application
- אפליקציה המותקנת בסמארטפון.
public class Smartphone {
private Application application;
}
האפליקציה והמסנג'ר דומים, כמובן, אבל עדיין מדובר בדברים שונים. Messenger יכול להיות גם נייד וגם שולחני, בעוד Application היא אפליקציה לנייד. לכן, אם השתמשנו בירושה, לא נוכל להוסיף אובייקט Telegram
למחלקה Smartphone
. אחרי הכל, כיתה Telegram
לא יכולה לרשת מ- Application
ומ Messenger
! וכבר הצלחנו לרשת אותו מ Messenger
, ולהוסיף אותו לכיתה בצורה זו Client
. אבל כיתה Telegram
יכולה ליישם את שני הממשקים בקלות! לכן, במחלקה Client
נוכל ליישם אובייקט Telegram
כ- Messenger
ובמחלקה Smartphone
כ- Application
. הנה איך זה נעשה:
public class Telegram implements Application, Messenger {
//...methods
}
public class Client {
private Messenger messenger;
public Client() {
this.messenger = new Telegram();
}
}
public class Smartphone {
private Application application;
public Smartphone() {
this.application = new Telegram();
}
}
כעת נוכל להשתמש בכיתה Telegram
כרצוננו. איפשהו הוא יפעל בתפקיד של Application
, איפשהו בתפקיד של Messenger
. בטח כבר שמתם לב ששיטות בממשקים תמיד "ריקות", כלומר אין להן יישום. הסיבה לכך פשוטה: ממשק מתאר התנהגות, לא מיישם אותה. "כל האובייקטים של מחלקות שמיישמות את הממשק Swimmable
חייבים להיות מסוגלים לצוף": זה כל מה שהממשק אומר לנו. איך בדיוק ישחו דג, ברווז או סוס יש שאלה לשיעורים Fish
, Duck
ולא Horse
לממשק. בדיוק כמו שהחלפת ערוץ היא המשימה של טלוויזיה. השלט פשוט נותן לך כפתור לעשות את זה. עם זאת, ל-Java8 יש תוספת מעניינת - שיטות ברירת מחדל. לדוגמה, לממשק שלך יש 10 שיטות. 9 מהם מיושמים בצורה שונה במחלקות שונות, אבל אחת מיושמת זהה בכולם. בעבר, לפני שחרורו של Java8, לשיטות בתוך ממשקים לא היה יישום כלל: המהדר השליך מיד שגיאה. עכשיו אתה יכול לעשות את זה ככה:
public interface Swimmable {
public default void swim() {
System.out.println("Swim!");
}
public void eat();
public void run();
}
באמצעות מילת המפתח default
, יצרנו שיטה בממשק עם מימוש ברירת מחדל. נצטרך ליישם את שתי השיטות האחרות, eat()
ואת run()
עצמנו בכל המחלקות שיטמיעו את Swimmable
. אין צורך לעשות זאת בשיטה swim()
: היישום יהיה זהה בכל המחלקות. אגב, נתקלת בממשקים יותר מפעם אחת במשימות עבר, למרות שלא שמת לב לזה בעצמך :) הנה דוגמה ברורה: עבדת עם ממשקים List
ו- Set
! ליתר דיוק, עם היישום שלהם - ArrayList
, LinkedList
, HashSet
ואחרים. אותה תרשים מציג דוגמה כאשר מחלקה אחת מיישמת מספר ממשקים בבת אחת. לדוגמה, LinkedList
הוא מיישם את הממשקים List
ו Deque
(תור דו צדדי). אתה גם מכיר את הממשק Map
, או ליתר דיוק, עם ההטמעות שלו - HashMap
. אגב, בתרשים הזה אפשר לראות תכונה אחת: ממשקים יכולים לעבור בירושה אחד מהשני. הממשק SortedMap
עובר בירושה מ- Map
, ועובר Deque
בירושה מהתור Queue
. זה הכרחי אם אתה רוצה להראות את הקשר בין ממשקים, אבל ממשק אחד הוא גרסה מורחבת של אחר. בואו נסתכל על דוגמה עם ממשק Queue
- תור. עוד לא עברנו על הקולקציות Queue
, אבל הן די פשוטות ומסודרות כמו תור רגיל בחנות. אתה יכול להוסיף אלמנטים רק לסוף התור, ולקחת אותם רק מההתחלה. בשלב מסוים, המפתחים היו צריכים גרסה מורחבת של התור כדי שניתן יהיה להוסיף ולקבל אלמנטים משני הצדדים. כך נוצר ממשק Deque
- תור דו כיווני. הוא מכיל את כל השיטות של תור רגיל, מכיוון שהוא ה"אב" של תור דו-כיווני, אך נוספו שיטות חדשות.
GO TO FULL VERSION