היי! היום נדבר על מושג חשוב בג'אווה: ממשקים. המילה בוודאי מוכרת לך. לדוגמה, לרוב תוכניות המחשב והמשחקים יש ממשקים. במובן הרחב, ממשק הוא מעין 'שליטה מרחוק' המחבר בין שני צדדים המקיימים אינטראקציה. דוגמה פשוטה לממשק בחיי היומיום היא שלט רחוק לטלוויזיה. הוא מחבר בין שני אובייקטים - אדם וטלוויזיה - ומבצע משימות שונות: להגביר או להנמיך את עוצמת הקול, להחליף ערוצים ולהדליק או לכבות את הטלוויזיה. צד אחד (האדם) צריך לגשת לממשק (לחץ על כפתור בשלט רחוק) כדי לגרום לצד השני לבצע את הפעולה. לדוגמה, כדי להפוך את הטלוויזיה לערוץ הבא. מה גם שהמשתמש לא צריך לדעת איך הטלוויזיה מאורגנת או איך תהליך החלפת הערוצים מיושם באופן פנימי. הדבר היחיד שיש למשתמש גישה אליו הוא הממשק. המטרה העיקרית היא להגיע לתוצאה הרצויה. מה זה קשור לתכנות וג'אווה? הכל :) יצירת ממשק דומה מאוד ליצירת מחלקה רגילה, אך במקום זאת באמצעות המילה class , אנו מציינים את המילה interface . בואו נסתכל על ממשק Java הפשוט ביותר, נראה איך הוא עובד ולמה נצטרך אותו:
public interface CanSwim {
public void swim();
}
יצרנו ממשק CanSwim . זה קצת כמו השלט הרחוק שלנו, אבל עם 'כפתור' אחד: שיטת swim() . אבל איך אנחנו משתמשים בשלט רחוק זה? לשם כך, עלינו ליישם שיטה, כלומר כפתור השלט הרחוק שלנו. כדי להשתמש בממשק, חלק מהשיעורים בתוכנית שלנו חייבים ליישם את השיטות שלו. בואו נמציא מחלקה שהאובייקטים שלה 'יכולים לשחות'. לדוגמה, שיעור ברווז מתאים:
public class Duck implements CanSwim {
public void swim() {
System.out.println("Duck, swim!");
}
public static void main(String[] args) {
Duck duck = new Duck();
duck.swim();
}
}
"מה אנחנו רואים כאן? המחלקה Duck 'משויכת' לממשק CanSwim על ידי מילת המפתח implements . אתה אולי זוכר שהשתמשנו במנגנון דומה כדי לשייך שתי מחלקות דרך ירושה, אבל במקרה כזה השתמשנו במילה extends. עבור בהירות מלאה, אנו יכולים לתרגם את ' מחלקה ציבורית Duck מיישמת CanSwim ' מילולית כ: ' מחלקת Duck הציבורית מיישמת את ממשק CanSwim '. זה אומר שמחלקה הקשורה לממשק חייבת ליישם את כל השיטות שלה. הערה: Duck
המחלקה שלנו, בדיוק כמו הממשק CanSwim
, יש swim()
שיטה, והוא מכיל קצת היגיון. זו דרישת חובה. אם רק נכתוב public class Duck implements CanSwim
בלי ליצור swim()
מתודה במחלקה Duck
, המהדר יתן לנו שגיאה: Duck לא מופשט ולא עוקף שחייה של מתודה מופשטת () ב-CanSwim למה? למה זה קורה? אם נסביר את השגיאה באמצעות דוגמה לטלוויזיה, זה יהיה כמו להעביר למישהו שלט רחוק לטלוויזיה עם כפתור 'שנה ערוץ' שאינו יכול לשנות ערוצים. אתה יכול ללחוץ על הכפתור כמה שאתה רוצה, אבל זה לא יעבוד. השלט הרחוק לא מחליף ערוצים בעצמו: הוא רק שולח אות לטלוויזיה, שמיישמת את התהליך המורכב של החלפת ערוצים. וכך גם לגבי הברווז שלנו: עליו לדעת לשחות כדי שניתן יהיה לקרוא לו באמצעות CanSwim
הממשק. אם הוא לא יודע איך, CanSwim
הממשק לא מחבר בין שני הצדדים - האדם והתוכנית. האדם לא יוכל להשתמש בשיטה 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 Telegram message!");
}
public void getMessage() {
System.out.println("Receiving a Telegram message!");
}
}
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 Viber message!");
}
public void getMessage() {
System.out.println("Receiving a Viber message!");
}
}
אילו יתרונות זה מספק? החשוב שבהם הוא צימוד רופף. תארו לעצמכם שאנחנו מעצבים תוכנית שתאסוף נתוני לקוחות. המחלקה 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
. אכן, זה אפשרי. אבל יש תקלה אחת. כפי שאתה כבר יודע, לג'אווה אין ירושה מרובה. אבל יש תמיכה במספר ממשקים. מחלקה יכולה ליישם כמה ממשקים שתרצה. תארו לעצמכם שיש לנו Smartphone
מחלקה שיש בה App
שדה אחד, שמייצג אפליקציה המותקנת בסמארטפון.
public class Smartphone {
private App app;
}
כמובן, אפליקציה ומסנג'ר דומים, אבל הם עדיין דברים שונים. יכולות להיות גרסאות לנייד ולשולחן העבודה של מסנג'ר, אבל האפליקציה מייצגת ספציפית אפליקציה לנייד. הנה העסקה - אם היינו משתמשים בירושה, לא היינו יכולים להוסיף אובייקט Telegram
למחלקה Smartphone
. אחרי הכל, Telegram
המחלקה לא יכולה לרשת בו זמנית App
ו Messenger
! וכבר העברנו אותו בירושה Messenger
והוספנו אותו לכיתה Client
. אבל Telegram
הכיתה יכולה ליישם את שני הממשקים בקלות! בהתאם לכך, נוכל לתת Client
למחלקה Telegram
אובייקט בתור Messenger
, ונוכל לתת אותו למחלקה Smartphone
בתור App
. הנה איך אתה עושה את זה:
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
בכיתה איך שאנחנו רוצים. במקומות מסוימים הוא פועל כ- App
. במקומות אחרים הוא פועל כ- Messenger
. ודאי כבר שמתם לב ששיטות ממשק הן תמיד 'ריקות', כלומר אין להן יישום. הסיבה לכך פשוטה: הממשק מתאר התנהגות, אך אינו מיישם אותה. 'כל האובייקטים שמיישמים את CanSwim
הממשק חייבים להיות מסוגלים לשחות': זה כל מה שהממשק אומר לנו. הדרך הספציפית שבה דגים, ברווזים וסוסים שוחים היא שאלה עבור ה- Fish
, Duck
, ושיעורים Horse
, לא לממשק. בדיוק כמו שהחלפת ערוץ היא משימה עבור הטלוויזיה. השלט פשוט נותן לך כפתור בשביל זה. עם זאת, תוספת מעניינת הופיעה ב-Java 8 - שיטות ברירת מחדל. לדוגמה, לממשק שלך יש 10 שיטות. ל-9 מהם יש יישומים שונים במחלקות שונות, אבל אחד מיושם זהה לכולם. בעבר, לפני Java 8, לשיטות ממשק לא היה מימוש כלשהו: המהדר מיד נתן שגיאה. עכשיו אתה יכול לעשות משהו כזה:
public interface CanSwim {
public default void swim() {
System.out.println("Swim!");
}
public void eat();
public void run();
}
באמצעות default
מילת המפתח, יצרנו שיטת ממשק עם מימוש ברירת מחדל. אנחנו צריכים לספק יישום משלנו עבור שתי שיטות אחרות - eat()
ו- run()
בכל המחלקות שמיישמות CanSwim
. אנחנו לא צריכים לעשות זאת בשיטה swim()
: היישום יהיה זהה בכל מחלקה. אגב, כבר נתקלתם בממשקים במשימות העבר, גם אם לא שמתם לב :) הנה דוגמה חיה: עבדתם עם ממשקי ה- List
ו Set
! ליתר דיוק, עבדת עם ההטמעות שלהם - ArrayList
, LinkedList
, HashSet
וכו'. אותה תרשים נותן בבירור דוגמה שבה מחלקה אחת מיישמת מספר ממשקים בו-זמנית. לדוגמה, LinkedList
מיישמת את הממשקים List
ו- Deque
(תור כפול). אתה מכיר את Map
הממשק, או ליתר דיוק, את HashMap
היישום שלו. אגב, דיאגרמה זו ממחישה תכונה: ממשקים יכולים לרשת ממשקים אחרים. הממשק SortedMap
יורש Map
, בעוד Deque
יורש Queue
. זה הכרחי אם ברצונך להציג את הקשר בין ממשקים, כאשר ממשק אחד הוא גרסה מורחבת של ממשק אחר. הבה נשקול דוגמה עם Queue
הממשק. עדיין לא סקרנו Queues
, אבל זה די פשוט ועובד כמו תור רגיל, או תור, בחנות. אתה יכול להוסיף פריטים רק לסוף התור, ויכול לקחת אותם רק מההתחלה. בשלב מסוים, מפתחים נזקקו לגרסה משופרת של התור כדי להוסיף ולקחת פריטים בשני הקצוות. אז הם יצרו Deque
ממשק, שהוא תור כפול. יש לו את כל השיטות של תור רגיל. אחרי הכל, זה האב של התור הכפול, אבל הוא גם מוסיף שיטות חדשות.
GO TO FULL VERSION