CodeGym /בלוג Java /Random-HE /ההבדל בין מחלקות מופשטות לממשקים
John Squirrels
רָמָה
San Francisco

ההבדל בין מחלקות מופשטות לממשקים

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

    לדוגמה, קח את Birdהמחלקה המופשטת ואת CanFlyהממשק:

    public abstract class Bird {
       private String species;
       private int age;
    
       public abstract void fly();
    
       public String getSpecies() {
           return species;
       }
    
       public void setSpecies(String species) {
           this.species = species;
       }
    
       public int getAge() {
           return age;
       }
    
       public void setAge(int age) {
           this.age = age;
       }
    }

    בואו ניצור MockingJayכיתת ציפורים ונעביר אותה בירושה Bird:

    public class MockingJay extends Bird {
    
       @Override
       public void fly() {
           System.out.println("Fly, bird!");
       }
    
       public static void main(String[] args) {
    
           MockingJay someBird = new MockingJay();
           someBird.setAge(19);
           System.out.println(someBird.getAge());
       }
    }

    כפי שאתה יכול לראות, אנו יכולים לגשת בקלות למצב של המחלקה המופשטת - שלה speciesומשתנים age.

    אבל אם ננסה לעשות את אותו הדבר עם ממשק, התמונה שונה. נוכל לנסות להוסיף לו משתנים:

    public interface CanFly {
    
       String species = new String();
       int age = 10;
    
       public void fly();
    }
    
    public interface CanFly {
    
       private String species = new String(); // Error
       private int age = 10; // Another error
    
       public void fly();
    }

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

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

    Java 8 הציגה שיטות ברירת מחדל עבור ממשקים שיש להם יישום. אתם כבר יודעים עליהם, אז לא נחזור על עצמנו.

  2. מחלקה מופשטת מחברת ומאחדת כיתות הקשורות מאוד. במקביל, ממשק יחיד יכול להיות מיושם על ידי מחלקות שאין להן שום דבר במשותף.

    נחזור לדוגמה שלנו עם ציפורים.

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

    ההבדל בין מחלקות מופשטות לממשקים - 2

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

    ההבדל בין מחלקות מופשטות לממשקים - 3

    4 הישויות הללו אינן קשורות זו לזו. הם אפילו לא כולם חיים. עם זאת, כולם CanFly.

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

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

  3. מחלקות יכולות ליישם כמה ממשקים שתרצו, אבל הן יכולות לרשת רק מחלקה אחת.

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

ממשקים סטנדרטיים: InputStream ו-OutputStream

כבר עברנו על מחלקות שונות האחראיות על זרמי קלט ופלט. בואו נשקול InputStreamו OutputStream. באופן כללי, לא מדובר בממשקים כלל, אלא במחלקות מופשטות אמיתיות לחלוטין. עכשיו אתה יודע מה זה אומר, אז יהיה הרבה יותר קל לעבוד איתם :) InputStreamהיא מחלקה מופשטת שאחראית על קלט בתים. ל- Java יש מספר מחלקות שיורשות את InputStream. כל אחד מהם נועד לקבל נתונים ממקורות שונים. מכיוון InputStreamשהוא האב, הוא מספק מספר שיטות המקלות על העבודה עם זרמי נתונים. לכל צאצא של InputStreamיש את השיטות הבאות:
  • int available()מחזירה את מספר הבתים הזמינים לקריאה;
  • close()סוגר את זרם הקלט;
  • int read()מחזיר ייצוג מספר שלם של הבת הזמין הבא בזרם. אם הושג סוף הזרם, -1 יוחזר;
  • int read(byte[] buffer)מנסה לקרוא בתים לתוך מאגר, ומחזיר את מספר הבתים שנקראו. כאשר הוא מגיע לסוף הקובץ, הוא מחזיר -1;
  • int read(byte[] buffer, int byteOffset, int byteCount)כותב חלק מבלוק של בתים. הוא משמש כאשר מערך הבתים לא התמלא במלואו. כאשר הוא מגיע לסוף הקובץ, הוא מחזיר -1;
  • long skip(long byteCount)מדלג על byteCount בתים בזרם הקלט, ומחזיר את מספר הבתים שהתעלמו מהם.
אני ממליץ לך ללמוד את רשימת השיטות המלאה . למעשה יש יותר מעשר כיתות ילדים. לדוגמה, הנה כמה:
  1. FileInputStream: הסוג הנפוץ ביותר של InputStream. הוא משמש לקריאת מידע מקובץ;
  2. StringBufferInputStream: עוד סוג מועיל של InputStream. זה ממיר מחרוזת ל- InputStream;
  3. BufferedInputStream: זרם קלט מאוחסן. הוא משמש לרוב להגברת הביצועים.
זוכרים כשניגשנו BufferedReaderואמרנו שאתם לא חייבים להשתמש בזה? כשאנחנו כותבים:
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))
...אתה לא צריך להשתמש ב- BufferedReader: InputStreamReaderיכול לעשות את העבודה. אבל BufferedReaderמשפר את הביצועים ויכול גם לקרוא שורות שלמות של נתונים ולא תווים בודדים. אותו דבר חל על BufferedInputStream! המחלקה צוברת נתוני קלט במאגר מיוחד מבלי לגשת כל הזמן להתקן הקלט. הבה נשקול דוגמה:
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;

public class BufferedInputExample {

   public static void main(String[] args) throws Exception {
       InputStream inputStream = null;
       BufferedInputStream buffer = null;

       try {

           inputStream = new FileInputStream("D:/Users/UserName/someFile.txt");

           buffer = new BufferedInputStream(inputStream);

           while(buffer.available()>0) {

               char c = (char)buffer.read();

                System.out.println("Character read: " + c);
           }
       } catch(Exception e) {

           e.printStackTrace();

       } finally {

           inputStream.close();
           buffer.close();
       }
   }
}
בדוגמה זו, אנו קוראים נתונים מקובץ שנמצא במחשב בכתובת ' D:/Users/UserName/someFile.txt '. אנו יוצרים 2 אובייקטים - a FileInputStreamו-a BufferedInputStreamש'עוטפים' אותו. לאחר מכן אנו קוראים בתים מהקובץ וממירים אותם לתווים. ואנחנו עושים את זה עד שהקובץ מסתיים. כפי שאתה יכול לראות, אין כאן שום דבר מסובך. אתה יכול להעתיק את הקוד הזה ולהריץ אותו על קובץ אמיתי במחשב שלך :) המחלקה OutputStreamהיא מחלקה אבסטרקטית שמייצגת זרם פלט של בתים. כפי שאתה כבר יודע, זה ההפך מ- InputStream. היא אינה אחראית לקריאת נתונים מאיפשהו, אלא על שליחת נתונים למקום כלשהו . כמו InputStream, מחלקה מופשטת זו נותנת לכל צאצאיה קבוצה של שיטות נוחות:
  • void close()סוגר את זרם הפלט;
  • void flush()מנקה את כל מאגרי הפלט;
  • abstract void write(int oneByte)כותב 1 בייט לזרם הפלט;
  • void write(byte[] buffer)כותב מערך בתים לזרם הפלט;
  • void write(byte[] buffer, int offset, int count)כותב טווח של בתים ספירה ממערך, החל ממיקום ההיסט.
הנה כמה מצאצאי הכיתה OutputStream:
  1. DataOutputStream. זרם פלט הכולל שיטות לכתיבת סוגי נתונים סטנדרטיים של Java.

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

    import java.io.*;
    
    public class DataOutputStreamExample {
    
       public static void main(String[] args) throws IOException {
    
           DataOutputStream dos = new DataOutputStream(new FileOutputStream("testFile.txt"));
    
           dos.writeUTF("SomeString");
           dos.writeInt(22);
           dos.writeDouble(1.21323);
           dos.writeBoolean(true);
    
       }
    }

    יש לו שיטות נפרדות לכל סוג - writeDouble(), writeLong(), writeShort(), וכן הלאה.


  2. FileOutputStream. מחלקה זו מיישמת מנגנון לשליחת נתונים לקובץ בדיסק. אגב, כבר השתמשנו בזה בדוגמה האחרונה. האם שמתם לב? העברנו אותו ל-DataOutputStream, שפעל כ'עטיפה'.

  3. BufferedOutputStream. זרם פלט מאוחסן. גם אין כאן שום דבר מסובך. מטרתו מקבילה ל BufferedInputStream(או BufferedReader). במקום הקריאה הרציפה הרגילה של נתונים, הוא כותב נתונים באמצעות מאגר 'מצטבר' מיוחד. המאגר מאפשר לצמצם את מספר הפעמים שהגישה ל-Data Sink, ובכך להגדיל את הביצועים.

    import java.io.*;
    
    public class DataOutputStreamExample {
    
         public static void main(String[] args) throws IOException {
    
               FileOutputStream outputStream = new FileOutputStream("D:/Users/Username/someFile.txt");
               BufferedOutputStream bufferedStream = new BufferedOutputStream(outputStream);
    
               String text = "I love Java!"; // We'll convert this string to a byte array and write it to a file
    
               byte[] buffer = text.getBytes();
    
               bufferedStream.write(buffer, 0, buffer.length);
         }
    }

    שוב, אתה יכול לשחק עם הקוד הזה בעצמך ולוודא שהוא יעבוד על קבצים אמיתיים במחשב שלך.

יהיה לנו שיעור נפרד על FileInputStream, FileOutputStreamו BuffreredInputStream, אז זה מספיק מידע להיכרות ראשונה. זהו זה! אנו מקווים שאתה מבין את ההבדלים בין ממשקים לשיעורים מופשטים ומוכנים לענות על כל שאלה, אפילו שאלות טריק :)
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION