JavaRush /בלוג Java /Random-HE /פולימורפיזם וחבריו
Viacheslav
רָמָה

פולימורפיזם וחבריו

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

מבוא

אני חושב שכולנו יודעים ששפת התכנות Java שייכת לאורקל. לכן, הדרך שלנו מתחילה באתר: www.oracle.com . יש "תפריט" בעמוד הראשי. בו, בסעיף "תיעוד" יש תת-סעיף "Java". כל מה שקשור לפונקציות הבסיסיות של השפה שייך ל"תיעוד Java SE", ולכן נבחר בסעיף זה. מדור התיעוד ייפתח עבור הגרסה העדכנית ביותר, אך לעת עתה הקטע "מחפש מהדורה אחרת?" בוא נבחר את האפשרות: JDK8. בעמוד נראה אפשרויות רבות ושונות. אבל אנחנו מעוניינים ללמוד את השפה: " נתיבי לימוד Java Tutorials ". בדף זה נמצא סעיף נוסף: " לימוד שפת ג'אווה ". זהו הקודש שבקודשים, מדריך על יסודות ג'אווה מ-Oracle. Java היא שפת תכנות מונחה עצמים (OOP), ולכן לימוד השפה אפילו באתר אורקל מתחיל בדיון במושגים הבסיסיים של " מושגי תכנות מונחה עצמים ". מהשם עצמו ברור ש-Java מתמקדת בעבודה עם אובייקטים. מקטע המשנה " מהו אובייקט? ", ברור שאובייקטים ב-Java מורכבים ממצב והתנהגות. תאר לעצמך שיש לנו חשבון בנק. כמות הכסף בחשבון היא מדינה, ושיטות עבודה עם מדינה זו הן התנהגות. אובייקטים צריכים להיות מתוארים איכשהו (ספר איזה מצב והתנהגות עשויים להיות להם) והתיאור הזה הוא המחלקה . כאשר אנו יוצרים אובייקט של מחלקה כלשהי, אנו מציינים מחלקה זו וזו נקראת " סוג אובייקט ". מכאן נאמר ש-Java היא שפה עם הקלדה חזקה, כפי שמצוין במפרט שפת Java בסעיף " פרק 4. סוגים, ערכים ומשתנים ". שפת Java עוקבת אחר מושגי OOP ותומכת בירושה באמצעות מילת המפתח extends. למה הרחבה? כי עם ירושה, כיתת ילד יורשת את ההתנהגות והמצב של כיתת ההורים ויכולה להשלים אותם, כלומר. להרחיב את הפונקציונליות של מחלקת הבסיס. ניתן לציין ממשק גם בתיאור הכיתה באמצעות מילת המפתח implements. כאשר מחלקה מיישמת ממשק, זה אומר שהמחלקה תואמת חוזה כלשהו – הצהרה של המתכנת לשאר הסביבה שלמחלקה יש התנהגות מסוימת. לדוגמה, לנגן יש כפתורים שונים. כפתורים אלו מהווים ממשק לשליטה בהתנהגות הנגן, וההתנהגות תשנה את המצב הפנימי של הנגן (לדוגמה, עוצמת הקול). במקרה זה, המצב וההתנהגות כתיאור יתנו שיעור. אם מחלקה מיישמת ממשק, אז אובייקט שנוצר באמצעות מחלקה זו יכול להיות מתואר על ידי סוג לא רק על ידי המחלקה, אלא גם על ידי הממשק. בואו נסתכל על דוגמה:
public class MusicPlayer {

    public static interface Device {
        public void turnOn();
        public void turnOff();
    }

    public static class Mp3Player implements Device {
        public void turnOn() {
            System.out.println("On. Ready for mp3.");
        }
        public void turnOff() {
            System.out.println("Off");
        }
    }

    public static class Mp4Player extends Mp3Player {
        @Override
        public void turnOn() {
            System.out.println("On. Ready for mp3/mp4.");
        }
    }

    public static void main(String []args) throws Exception{
        // Какое-то устройство (Тип = Device)
        Device mp3Player = new Mp3Player();
        mp3Player.turnOn();
        // У нас есть mp4 проигрыватель, но нам от него нужно только mp3
        // Пользуемся им How mp3 проигрывателем (Тип = Mp3Player)
        Mp3Player mp4Player = new Mp4Player();
        mp4Player.turnOn();
    }
}
סוג הוא תיאור חשוב מאוד. זה אומר איך אנחנו הולכים לעבוד עם האובייקט, כלומר. לאיזו התנהגות אנו מצפים מהאובייקט. התנהגויות הן שיטות. לכן, בואו נבין את השיטות. באתר אורקל, לשיטות יש סעיף משלהן במדריך של אורקל: " הגדרת שיטות ". הדבר הראשון שצריך לקחת מהמאמר: החתימה של שיטה היא שם השיטה וסוגי הפרמטרים :
פולימורפיזם וחבריו - 2
לדוגמה, כאשר מכריזים על מתודה public void method(Object o), החתימה תהיה שם המתודה וסוג הפרמטר Object. סוג ההחזרה אינו כלול בחתימה. זה חשוב! לאחר מכן, בואו נקמפל את קוד המקור שלנו. כידוע, לשם כך יש לשמור את הקוד בקובץ עם שם המחלקה והסיומת java. קוד Java מורכב באמצעות מהדר " javac " לפורמט ביניים כלשהו שניתן להפעיל על ידי ה-Java Virtual Machine (JVM). פורמט ביניים זה נקרא bytecode והוא כלול בקבצים עם סיומת .class. הבה נריץ את הפקודה להידור: javac MusicPlayer.java לאחר הידור של קוד ה-Java, נוכל להפעיל אותו. שימוש בכלי השירות " java " כדי להתחיל, תהליך המכונה הוירטואלית של java יופעל כדי להפעיל את קוד הבתים שעבר בקובץ הכיתה. הבה נריץ את הפקודה כדי להפעיל את היישום: java MusicPlayer. נראה על המסך את הטקסט שצוין בפרמטר הקלט של שיטת println. מעניין לציין שקוד ה-byte נמצא בקובץ עם סיומת .class, אנו יכולים להציג אותו באמצעות כלי השירות " javap ". בואו נריץ את הפקודה <ocde>javap -c MusicPlayer:
פולימורפיזם וחבריו - 3
מה-bytecode אנחנו יכולים לראות שקריאה למתודה דרך אובייקט שהסוג שלו צוינה המחלקה מתבצעת באמצעות invokevirtual, והקומפיילר חישב באיזו חתימת מתודה יש ​​להשתמש. למה invokevirtual? כי יש קריאה (invoke מתורגם כקריאה) של שיטה וירטואלית. מהי שיטה וירטואלית? זוהי שיטה שניתן לעקוף את הגוף שלה במהלך ביצוע התוכנית. פשוט דמיינו שיש לכם רשימה של התאמה בין מפתח מסוים (חתימת שיטה) לבין גוף (קוד) השיטה. וההתכתבות הזו בין המפתח לגוף השיטה עשויה להשתנות במהלך הפעלת התוכנית. לכן השיטה היא וירטואלית. כברירת מחדל, ב-Java, שיטות שאינן סטטיות, לא סופיות ולא פרטיות הן וירטואליות. הודות לכך, Java תומכת בעיקרון התכנות מונחה עצמים של פולימורפיזם. כפי שאולי כבר הבנתם, על זה עוסקת הסקירה שלנו היום.

רב צורתיות

באתר אורקל במדריך הרשמי שלהם יש קטע נפרד: " פולימורפיזם ". בואו נשתמש במהדר Java Online כדי לראות כיצד פועל פולימורפיזם ב-Java. לדוגמה, יש לנו מספר מחלקה מופשטת המייצגת מספר ב-Java. מה זה מאפשר? יש לו כמה טכניקות בסיסיות שיהיו לכל היורשים. כל מי שיורש מספר אומר פשוטו כמשמעו - "אני מספר, אתה יכול לעבוד איתי כמספר." לדוגמה, עבור כל יורש אתה יכול להשתמש בשיטת intValue() כדי לקבל את הערך השלם שלה. אם תסתכלו על ה-Java API של Number, תוכלו לראות שהשיטה מופשטת, כלומר כל יורש של Number חייב ליישם את השיטה הזו בעצמו. אבל מה זה נותן לנו? בואו נסתכל על דוגמה:
public class HelloWorld {

    public static int summ(Number first, Number second) {
        return first.intValue() + second.intValue();
    }

    public static void main(String []args){
        System.out.println(summ(1, 2));
        System.out.println(summ(1L, 4L));
        System.out.println(summ(1L, 5));
        System.out.println(summ(1.0, 3));
    }
}
כפי שניתן לראות מהדוגמה, הודות לפולימורפיזם, נוכל לכתוב שיטה שתקבל ארגומנטים מכל סוג כקלט, שתהיה צאצא של Number (לא נוכל לקבל Number, כי זו מחלקה מופשטת). כפי שהיה במקרה בדוגמה של השחקן, במקרה הזה אנחנו אומרים שאנחנו רוצים לעבוד עם משהו, כמו Number. אנו יודעים שכל מי שהוא מספר חייב להיות מסוגל לספק את הערך השלם שלו. וזה מספיק לנו. אנחנו לא רוצים להיכנס לפרטים של יישום של אובייקט ספציפי ורוצים לעבוד עם אובייקט זה באמצעות שיטות משותפות לכל צאצאי Number. רשימת השיטות שיהיו זמינות לנו תיקבע לפי סוג בזמן הקומפילציה (כפי שראינו קודם ב-bytecode). במקרה זה, הסוג שלנו יהיה Number. כפי שניתן לראות מהדוגמה, אנו מעבירים מספרים שונים מסוגים שונים, כלומר, שיטת הסיכום תקבל אינטגר, ארוך וכפול כקלט. אבל המשותף לכולם הוא שהם צאצאים של המספר המופשט, ולכן גוברים על התנהגותם בשיטת intValue, כי כל טיפוס ספציפי יודע להטיל את הטיפוס הזה למספר שלם. פולימורפיזם כזה מיושם באמצעות מה שנקרא overriding, באנגלית Overriding.
פולימורפיזם וחבריו - 4
פולימורפיזם עוקף או דינמי. אז בואו נתחיל בשמירת הקובץ HelloWorld.java עם התוכן הבא:
public class HelloWorld {
    public static class Parent {
        public void method() {
            System.out.println("Parent");
        }
    }
    public static class Child extends Parent {
        public void method() {
            System.out.println("Child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child();
        parent.method();
        child.method();
    }
}
בוא נעשה javac HelloWorld.javaו javap -c HelloWorld:
פולימורפיזם וחבריו - 5
כפי שניתן לראות, ב-bytecode עבור השורות עם קריאת מתודה, מצוינת אותה הפניה לשיטה לקריאה invokevirtual (#6). בוא נעשה את זה java HelloWorld. כפי שאנו יכולים לראות, המשתנים parent ו-child מוכרזים עם סוג Parent, אך המימוש עצמו נקרא לפי איזה אובייקט הוקצה למשתנה (כלומר איזה סוג אובייקט). במהלך הפעלת התוכנית (אומרים גם בזמן ריצה), ה-JVM, בהתאם לאובייקט, בעת קריאה לשיטות באמצעות אותה חתימה, הוציא לפועל שיטות שונות. כלומר, באמצעות המפתח של החתימה המתאימה, קיבלנו תחילה גוף שיטה אחד, ולאחר מכן קיבלנו אחר. תלוי איזה אובייקט נמצא במשתנה. קביעה זו בזמן הפעלת התוכנית של איזו שיטה תיקרא נקראת גם כריכה מאוחרת או כריכה דינמית. כלומר, ההתאמה בין החתימה לגוף השיטה מתבצעת באופן דינמי, בהתאם לאובייקט עליו נקראת השיטה. באופן טבעי, אינך יכול לעקוף חברים סטטיים של כיתה (חבר בכיתה), כמו גם חברי כיתה עם סוג גישה פרטי או סופי. הערות @Override באות לעזרת המפתחים. זה עוזר למהדר להבין שבשלב זה אנחנו הולכים לעקוף את ההתנהגות של שיטת אב קדמון. אם עשינו טעות בחתימת השיטה, המהדר יספר לנו עליה מיד. לדוגמה:
public static class Parent {
        public void method() {
            System.out.println("parent");
        }
}
public static class Child extends Parent {
        @Override
        public void method(String text) {
            System.out.println("child");
        }
}
לא מבצע קומפילציה עם שגיאה: שגיאה: השיטה לא עוקפת או מיישמת שיטה מסופר-טיפוס
פולימורפיזם וחבריו - 6
הגדרה מחדש קשורה גם למושג " שיתופיות ". בואו נסתכל על דוגמה:
public class HelloWorld {
    public static class Parent {
        public Number method() {
            return 1;
        }
    }
    public static class Child extends Parent {
        @Override
        public Integer method() {
            return 2;
        }
    }

    public static void main(String[] args) {
        System.out.println(new Child().method());
    }
}
למרות המעורפלות לכאורה, המשמעות מסתכמת בכך שכאשר מדרסים, נוכל להחזיר לא רק את הטיפוס שצוין באב הקדמון, אלא גם טיפוס ספציפי יותר. לדוגמה, האב הקדמון החזיר את Number, ואנחנו יכולים להחזיר Integer - הצאצא של Number. כך גם לגבי חריגים שהוכרזו בהשלכות השיטה. היורשים יכולים לעקוף את השיטה ולחדד את החריגה שנזרקה. אבל הם לא יכולים להרחיב. כלומר, אם ההורה זורק IOException, אז נוכל לזרוק את EOFException המדויק יותר, אבל אנחנו לא יכולים לזרוק Exception. כמו כן, אינך יכול לצמצם את ההיקף ואין באפשרותך להטיל מגבלות נוספות. לדוגמה, לא ניתן להוסיף סטטי.
פולימורפיזם וחבריו - 7

הַסתָרָה

יש גם דבר כזה " הסתרה ". דוגמא:
public class HelloWorld {
    public static class Parent {
        public static void method() {
            System.out.println("Parent");
        }
    }
    public static class Child extends Parent {
        public static void method() {
            System.out.println("Child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child();
        parent.method();
        child.method();
    }
}
זה דבר די ברור אם חושבים על זה. חברים סטטיים של כיתה שייכים לכיתה, כלומר. לסוג המשתנה. לכן, זה הגיוני שאם הילד הוא מסוג Parent, אז השיטה תיקרא על הורה, ולא על הילד. אם נסתכל על ה-bytecode, כפי שעשינו קודם, נראה שהמתודה הסטטית נקראת באמצעות invokestatic. זה מסביר ל-JVM שהוא צריך להסתכל על הסוג, ולא על טבלת המתודות, כפי שעשו invokevirtual או invokeinterface.
פולימורפיזם וחבריו - 8

שיטות עומס יתר

מה עוד אנחנו רואים במדריך Java Oracle? בחלק שנלמד קודם לכן " הגדרת שיטות " יש משהו על עומס יתר. מה זה? ברוסית זה "עומס יתר של שיטה", ושיטות כאלה נקראות "עומס יתר". אז, עומס יתר על השיטה. במבט ראשון הכל פשוט. בואו נפתח מהדר Java מקוון, למשל tutorialspoint מהדר java מקוון .
public class HelloWorld {

	public static void main(String []args){
		HelloWorld hw = new HelloWorld();
		hw.say(1);
		hw.say("1");
	}

	public static void say(Integer number) {
		System.out.println("Integer " + number);
	}
	public static void say(String number) {
		System.out.println("String " + number);
	}
}
אז הכל נראה פשוט כאן. כפי שנאמר במדריך של אורקל, שיטות עומס יתר (במקרה זה שיטת say) נבדלות במספר ובסוג הארגומנטים המועברים לשיטה. אתה לא יכול להצהיר על אותו שם ועל אותו מספר של סוגים זהים של ארגומנטים, כי המהדר לא יוכל להבחין ביניהם. כדאי לשים לב לדבר חשוב מאוד מיד:
פולימורפיזם וחבריו - 9
כלומר, בעת עומס יתר, המהדר בודק תקינות. זה חשוב. אבל איך בעצם המהדר קובע שצריך לקרוא למתודה מסוימת? הוא משתמש בכלל "השיטה הספציפית ביותר" המתואר במפרט שפת Java: " 15.12.2.5. בחירת השיטה הספציפית ביותר ". כדי להדגים איך זה עובד, ניקח דוגמה מתכנת Java מקצועי מוסמך של Oracle:
public class Overload{
  public void method(Object o) {
    System.out.println("Object");
  }
  public void method(java.io.FileNotFoundException f) {
    System.out.println("FileNotFoundException");
  }
  public void method(java.io.IOException i) {
    System.out.println("IOException");
  }
  public static void main(String args[]) {
    Overload test = new Overload();
    test.method(null);
  }
}
קח דוגמה מכאן: https://github.com/stokito/OCPJP/blob/master/src/ru/habrahabr/blogs/java/OCPJP1/question1/Overload.j... כפי שאתה יכול לראות, אנחנו עוברים אפס לשיטה. המהדר מנסה לקבוע את הסוג הספציפי ביותר. חפץ לא מתאים כי הכל עובר בירושה ממנו. לך על זה. ישנם 2 מחלקות חריגים. בואו נסתכל על java.io.IOException ונראה שיש FileNotFoundException ב-"Direct Known Subclasses". כלומר, מסתבר ש-FileNotFoundException הוא הסוג הספציפי ביותר. לכן, התוצאה תהיה הפלט של המחרוזת "FileNotFoundException". אבל אם נחליף את IOException ב-EOException, מסתבר שיש לנו שתי שיטות באותה רמה של ההיררכיה בעץ הסוג, כלומר, עבור שתיהן, IOException הוא האב. המהדר לא יוכל לבחור לאיזו שיטה לקרוא ויזרוק שגיאת קומפילציה: reference to method is ambiguous. עוד דוגמה אחת:
public class Overload{
    public static void method(int... array) {
        System.out.println("1");
    }

    public static void main(String args[]) {
        method(1, 2);
    }
}
זה יוציא 1. אין כאן שאלות. הסוג int... הוא vararg https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html והוא באמת לא יותר מ"סוכר תחבירי" והוא למעשה int. .. מערך ניתן לקרוא כמערך int[]. אם נוסיף כעת שיטה:
public static void method(long a, long b) {
	System.out.println("2");
}
אז הוא יציג לא 1, אלא 2, כי אנחנו מעבירים 2 מספרים, ו-2 ארגומנטים מתאימים יותר ממערך אחד. אם נוסיף שיטה:
public static void method(Integer a, Integer b) {
	System.out.println("3");
}
אז עדיין נראה 2. כי במקרה הזה הפרימיטיבים הם התאמה מדויקת יותר מאשר איגרוף ב-Integer. עם זאת, אם נבצע, method(new Integer(1), new Integer(2));זה ידפיס 3. קונסטרוקטורים בג'אווה דומים לשיטות, ומכיוון שניתן להשתמש בהם גם כדי להשיג חתימה, אותם כללי "רזולוציית עומס יתר" חלים עליהם כמו שיטות עומס יתר. מפרט שפת Java אומר לנו זאת ב" 8.8.8. Constructor Overloading ". עומס יתר של שיטות = כריכה מוקדמת (המכונה גם כריכה סטטית) לעתים קרובות ניתן לשמוע על כריכה מוקדמת ומאוחרת, הידועה גם בשם כריכה סטטית או כריכה דינמית. ההבדל ביניהם הוא פשוט מאוד. מוקדם הוא הידור, מאוחר הוא הרגע שבו התוכנית מבוצעת. לכן, כריכה מוקדמת (כריכה סטטית) היא הקביעה של איזו שיטה תיקרא על מי בזמן הקומפילציה. ובכן, כריכה מאוחרת (כריכה דינמית) היא הקביעה לאיזו שיטה להתקשר ישירות בזמן ביצוע התוכנית. כפי שראינו קודם (כששינו את IOException ל-EOFException), אם נעמיס על שיטות כך שהקומפיילר לא יכול להבין היכן לבצע איזו קריאה, אז נקבל שגיאת זמן קומפילציה: ההתייחסות למתודה אינה ברורה. המילה דו-משמעית בתרגום מאנגלית פירושה דו-משמעי או לא ודאי, לא מדויק. מסתבר שעומס יתר הוא מחייב מוקדם, כי הבדיקה מתבצעת בזמן הקומפילציה. כדי לאשר את המסקנות שלנו, בואו נפתח את מפרט שפת Java בפרק " 8.4.9. עומס יתר ":
פולימורפיזם וחבריו - 10
מסתבר שבמהלך הקומפילציה ישמש מידע על סוגי ומספר הארגומנטים (שזמין בזמן הקומפילציה) לקביעת החתימה של השיטה. אם השיטה היא אחת מהשיטות של האובייקט (כלומר, שיטת מופע), קריאת המתודה בפועל תיקבע בזמן הריצה באמצעות חיפוש מתודה דינמית (כלומר, כריכה דינמית). כדי להבהיר זאת, ניקח דוגמה הדומה לזו שנידונה קודם לכן:
public class HelloWorld {
    public void method(int intNumber) {
        System.out.println("intNumber");
    }
    public void method(Integer intNumber) {
        System.out.println("Integer");
    }
    public void method(String intNumber) {
        System.out.println("Number is: " + intNumber);
    }

    public static void main(String args[]) {
        HelloWorld test = new HelloWorld();
        test.method(2);
    }
}
בוא נשמור את הקוד הזה בקובץ HelloWorld.java ונרכיב אותו באמצעות javac HelloWorld.java עכשיו בוא נראה מה המהדר שלנו כתב ב-bytecode על ידי הפעלת הפקודה: javap -verbose HelloWorld.
פולימורפיזם וחבריו - 11
כאמור, המהדר קבע שיטה וירטואלית כלשהי תיקרא בעתיד. כלומר, גוף השיטה יוגדר בזמן הריצה. אבל בזמן הקומפילציה, מבין כל שלוש השיטות, המהדר בחר את השיטות המתאימה ביותר, אז הוא ציין את המספר:"invokevirtual #13"
פולימורפיזם וחבריו - 12
איזה סוג של מתודה זה? זה קישור לשיטה. באופן גס, זהו רמז שבאמצעותו, בזמן ריצה, ה-Java Virtual Machine יכול לקבוע איזו שיטה לחפש לביצוע. פרטים נוספים ניתן למצוא במאמר העל: " כיצד JVM מטפל בעומס יתר של שיטות ועקיפה פנימית ".

תִמצוּת

אז גילינו ש-Java, כשפה מונחה עצמים, תומכת בפולימורפיזם. פולימורפיזם יכול להיות סטטי (Static Binding) או דינמי (Dynamic Binding). עם פולימורפיזם סטטי, הידוע גם בשם כריכה מוקדמת, המהדר קובע לאיזו שיטה יש לקרוא והיכן. זה מאפשר שימוש במנגנון כמו עומס יתר. עם פולימורפיזם דינמי, הידוע גם ככריכה מאוחרת, המבוסס על החתימה שחושבה קודם לכן של שיטה, שיטה תחושב בזמן ריצה על סמך האובייקט שבו נעשה שימוש (כלומר, איזו שיטת אובייקט נקראת). ניתן לראות כיצד המנגנונים הללו פועלים באמצעות bytecode. עומס היתר בוחן את חתימות השיטה, וכשפותרים את עומס היתר, בוחרים באפשרות הספציפית ביותר (הכי מדויקת). דריסה מסתכלת על הסוג כדי לקבוע אילו שיטות זמינות, והשיטות עצמן נקראות על סמך האובייקט. כמו גם חומרים בנושא: #ויאצ'סלב
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION