JavaRush /בלוג Java /Random-HE /סריאליזציה כמו שהיא. חלק 1
articles
רָמָה

סריאליזציה כמו שהיא. חלק 1

פורסם בקבוצה
במבט ראשון, סריאליזציה נראית כמו תהליך טריוויאלי. באמת, מה יכול להיות יותר פשוט? הכריז על הכיתה ליישם את הממשק java.io.Serializable- וזהו. אתה יכול לעשות סדרה בכיתה ללא בעיות. סריאליזציה כמו שהיא.  חלק 1 - 1תיאורטית, זה נכון. בפועל, יש הרבה דקויות. הם קשורים לביצועים, לסידריאליזציה, לבטיחות הכיתה. ועם עוד היבטים רבים. דקויות כאלה יידונו. ניתן לחלק מאמר זה לחלקים הבאים:
  • דקויות של מנגנונים
  • למה זה נחוץ?Externalizable
  • ביצועים
  • אבל מצד שני
  • אבטחת מידע
  • סידור אובייקטיםSingleton
נעבור לחלק הראשון -

דקויות של מנגנונים

קודם כל שאלה קצרה. כמה דרכים יש להפוך אובייקט לסידרה? התרגול מראה שיותר מ-90% מהמפתחים עונים על שאלה זו בערך באותו אופן (עד הניסוח) - יש רק דרך אחת. בינתיים, יש שניים מהם. לא כולם זוכרים את השני, שלא לדבר על אומר משהו מובן על תכונותיו. אז מהן השיטות האלה? כולם זוכרים את הראשון. זהו היישום שכבר הוזכר java.io.Serializableואינו דורש מאמץ. השיטה השנייה היא גם יישום של ממשק, אבל שונה: java.io.Externalizable. שלא כמו java.io.Serializable, הוא מכיל שתי שיטות שצריך ליישם - writeExternal(ObjectOutput)ו readExternal(ObjectInput). שיטות אלו מכילות את ההיגיון בהמשכה/דה-סריאליזציה. תגובה.Serializableבהמשך , לפעמים אתייחס לסדרה עם יישום כסטנדרט, ויישום Externalizableמורחב. אַחֵרתגובה. אני בכוונה לא נוגע כעת באפשרויות בקרת סדרה סטנדרטיות כמו הגדרה readObjectו writeObject, כי אני חושב ששיטות אלה אינן נכונות במקצת. שיטות אלו אינן מוגדרות בממשק Serializableוהן, למעשה, אביזרים לעקוף את המגבלות ולהגמיש את ההסדרה הסטנדרטית. Externalizableשיטות המספקות גמישות מובנות בהן כבר מההתחלה . בוא נשאל עוד שאלה אחת. איך בעצם עובדת סדרה סטנדרטית, באמצעות java.io.Serializable? וזה עובד דרך ה-Reflection API. הָהֵן. המחלקה מנותחת כקבוצה של שדות, שכל אחד מהם נכתב לזרם הפלט. אני חושב שברור שהפעולה הזו לא אופטימלית מבחינת ביצועים. כמה בדיוק נגלה בהמשך. יש הבדל גדול נוסף בין שתי שיטות הסידרה שהוזכרו. כלומר, במנגנון הדה-סריאליזציה. בשימוש, דה Serializable-סריאליזציה מתרחשת כך: זיכרון מוקצה לאובייקט, ולאחר מכן השדות שלו מתמלאים בערכים מהזרם. הבנאי של האובייקט לא נקרא. כאן עלינו לשקול את המצב הזה בנפרד. אוקיי, הכיתה שלנו ניתנת לסידרה. וההורה שלו? אופציונלי לחלוטין! יתרה מכך, אם אתה יורש מחלקה Object- ההורה בהחלט לא ניתן לסידרה. ולמרות Objectשאיננו יודעים דבר על תחומים, ייתכן שהם קיימים בכיתות ההורים שלנו. מה יהיה איתם? הם לא ייכנסו לזרם ההסדרה. אילו ערכים הם ייקחו בעת דה-סריאליזציה? בואו נסתכל על הדוגמה הזו:
package ru.skipy.tests.io;

import java.io.*;

/**
 * ParentDeserializationTest
 *
 * @author Eugene Matyushkin aka Skipy
 * @since 05.08.2010
 */
public class ParentDeserializationTest {

    public static void main(String[] args){
        try {
            System.out.println("Creating...");
            Child c = new Child(1);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            c.field = 10;
            System.out.println("Serializing...");
            oos.writeObject(c);
            oos.flush();
            baos.flush();
            oos.close();
            baos.close();
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            System.out.println("Deserializing...");
            Child c1 = (Child)ois.readObject();
            System.out.println("c1.i="+c1.getI());
            System.out.println("c1.field="+c1.getField());
        } catch (IOException ex){
            ex.printStackTrace();
        } catch (ClassNotFoundException ex){
            ex.printStackTrace();
        }
    }

    public static class Parent {
        protected int field;
        protected Parent(){
            field = 5;
            System.out.println("Parent::Constructor");
        }
        public int getField() {
            return field;
        }
    }

    public static class Child extends Parent implements Serializable{
        protected int i;
        public Child(int i){
            this.i = i;
            System.out.println("Child::Constructor");
        }
        public int getI() {
            return i;
        }
    }
}
זה שקוף - יש לנו כיתת הורים שאינה ניתנת לסידרה וכיתת ילד הניתנת לסידרה. וזה מה שקורה:
Creating...
Parent::Constructor
Child::Constructor
Serializing...
Deserializing...
Parent::Constructor
c1.i=1
c1.field=5
כלומר, במהלך דה-סריאליזציה, הבנאי ללא פרמטרים של מחלקת האב NON-serialization נקרא . ואם אין בנאי כזה, תתרחש שגיאה במהלך הדה-סריאליזציה. הקונסטרוקטור של אובייקט הילד, זה שאנו עושים דה-סיריאליזציה, אינו נקרא, כפי שנאמר לעיל. כך מתנהגים מנגנונים סטנדרטיים בשימוש Serializable. כאשר משתמשים בו, Externalizableהמצב שונה. ראשית, הבנאי ללא פרמטרים נקרא, ולאחר מכן נקראת המתודה readExternal על האובייקט שנוצר, שלמעשה קורא את כל הנתונים שלו. לכן, כל מחלקה המיישמת את הממשק Externalizable חייבת להיות בנאי ציבורי ללא פרמטרים! יתרה מכך, מכיוון שכל הצאצאים של מחלקה כזו ייחשבו גם למיישמת הממשק Externalizable, עליהם להיות גם בנאי חסר פרמטרים! בוא נלך רחוק יותר. יש שינוי שדה כזה כמו transient. זה אומר שאסור לעשות שדה זה בסידרה. עם זאת, כפי שאתה בעצמך מבין, הוראה זו משפיעה רק על מנגנון ההסדרה הסטנדרטי. כשמשתמשים בו, Externalizableאף אחד לא טורח לעשות שדה זה בסידרה, כמו גם להחסיר אותו. אם שדה מוכרז כחולף, אז כאשר האובייקט עובר דה-serialized, הוא מקבל את ערך ברירת המחדל. עוד נקודה די עדינה. עם סדרה רגילה, שדות שיש להם את השינוי staticאינם מסודרים. בהתאם לכך, לאחר דה-סריאליזציה שדה זה אינו משנה את ערכו. כמובן שבמהלך ההטמעה Externalizableאף אחד לא טורח לעשות את התחום הזה בסידרה וסידריאליזציה, אבל אני ממליץ בחום לא לעשות זאת, כי זה יכול להוביל לשגיאות עדינות. שדות עם משנה finalמסודרים כמו שדות רגילים. למעט יוצא מן הכלל אחד - לא ניתן לבטל אותם בסידריאליות בעת שימוש ב-Externalizable. כי final-поляיש לאתחל אותם בקונסטרוקטור, ואחרי זה יהיה בלתי אפשרי לשנות את הערך של השדה הזה ב-readExternal. בהתאם לכך, אם אתה צריך לעשות סדרה של אובייקט שיש לו final-שדה, תצטרך להשתמש רק בסריאליזציה רגילה. עוד נקודה שהרבה אנשים לא יודעים. סדרה רגילה לוקחת בחשבון את סדר ההכרזה על שדות במחלקה. בכל מקרה, זה היה המקרה בגרסאות קודמות; ב-JVM גרסה 1.6 של היישום של Oracle, הסדר כבר לא חשוב, סוג ושם השדה חשובים. סביר מאוד שהרכב השיטות ישפיע על המנגנון הסטנדרטי, למרות העובדה שבדרך כלל השדות עשויים להישאר זהים. כדי להימנע מכך, יש את המנגנון הבא. לכל מחלקה המיישמת את הממשק Serializableמתווסף עוד שדה אחד בשלב הקומפילציה -private static final long serialVersionUID. שדה זה מכיל את מזהה הגרסה הייחודי של המחלקה המסודרת. הוא מחושב על סמך תוכן המחלקה - שדות, סדר ההצהרה שלהם, שיטות, סדר ההצהרה שלהם. בהתאם, עם כל שינוי במחלקה, שדה זה ישנה את ערכו. שדה זה נכתב לזרם כאשר המחלקה מסודרת. אגב, זה אולי המקרה היחיד שידוע לי כאשר static-שדה מסודר. במהלך דה-סריאליזציה, הערך של שדה זה מושווה לזה של המחלקה במכונה הוירטואלית. אם הערכים אינם תואמים, נזרק חריג כמו זה:
java.io.InvalidClassException: test.ser2.ChildExt;
    local class incompatible: stream classdesc serialVersionUID = 8218484765288926197,
                                   local class serialVersionUID = 1465687698753363969
עם זאת, יש דרך, אם לא לעקוף, אז להונות את המחאה הזו. זה יכול להיות שימושי אם קבוצת שדות המחלקה והסדר שלהם כבר מוגדרים, אך שיטות המחלקה עשויות להשתנות. במקרה זה, הסדרת סדרה אינה בסיכון, אך המנגנון הסטנדרטי לא יאפשר דה-סריאליזציה של נתונים באמצעות ה-bytecode של המחלקה ששונתה. אבל, כפי שאמרתי, אפשר לרמות אותו. כלומר, הגדר באופן ידני את השדה במחלקה private static final long serialVersionUID. באופן עקרוני, הערך של שדה זה יכול להיות כל דבר. יש אנשים שמעדיפים להגדיר אותו שווה לתאריך שבו הקוד השתנה. חלקם אפילו משתמשים ב-1L. כדי לקבל את הערך הסטנדרטי (זה שמחושב באופן פנימי), אתה יכול להשתמש בכלי השירות הסדרתי הכלול ב-SDK. לאחר שהוגדר כך, ערך השדה יהיה קבוע, ומכאן תמיד תאפשר דה-סריאליזציה. יתרה מכך, בגרסה 5.0 הופיעו בערך הדברים הבאים בתיעוד: מומלץ מאוד שכל המחלקות הניתנות לסידרה יצהירו במפורש על שדה זה, מכיוון שחישוב ברירת המחדל רגיש מאוד לפרטים של מבנה המחלקה, אשר עשויים להשתנות בהתאם ליישום המהדר, ובכך לגרום InvalidClassExceptionלתוצאות בלתי צפויות. דה-סריאליזציה. עדיף להכריז על שדה זה כ- private, כי זה מתייחס אך ורק למחלקה שבה הוא מוצהר. למרות שהשינוי אינו מצוין במפרט. הבה נבחן כעת היבט זה. נניח שיש לנו את המבנה המחלקה הזה:
public class A{
    public int iPublic;
    protected int iProtected;
    int iPackage;
    private int iPrivate;
}

public class B extends A implements Serializable{}
במילים אחרות, יש לנו מחלקה שעברה בירושה מהורה שאינו ניתן לסידרה. האם אפשר לעשות סדרה של מחלקה זו, ומה צריך בשביל זה? מה יקרה למשתנים של כיתת האב? התשובה היא זו. כן, Bאתה יכול לעשות סדרה של מופע של מחלקה. מה צריך בשביל זה? אבל למחלקה צריך Aבנאי ללא פרמטרים, publicאו protected. לאחר מכן, במהלך הדה-סריאליזציה, כל משתני המחלקה Aיאותחלו באמצעות בנאי זה. משתני המחלקה Bיאותחלו עם הערכים מזרם הנתונים המסודר. תיאורטית, אפשר להגדיר במחלקה Bאת השיטות שדיברתי עליהן בהתחלה - readObjectו- writeObject, - שבתחילתן לבצע (דה-)הסדרה של משתני מחלקה Bדרך in.defaultReadObject/out.defaultWriteObject, ולאחר מכן (דה-)הסדרה של משתנים זמינים מהמחלקה A(במקרה שלנו אלה הם iPublic, iProtectedוכן iPackage, אם Bהוא נמצא באותה חבילה כמו A). עם זאת, לדעתי, עדיף להשתמש בסריאליזציה מורחבת בשביל זה. הנקודה הבאה שארצה לגעת בה היא סדרה של מספר אובייקטים. נניח שיש לנו את מבנה הכיתה הבא:
public class A implements Serializable{
    private C c;
    private B b;
    public void setC(C c) {this.c = c;}
    public void setB(B b) {this.b = b;}
    public C getC() {return c;}
    public B getB() {return b;}
}
public class B implements Serializable{
    private C c;
    public void setC(C c) {this.c = c;}
    public C getC() {return c;}
}
public class C implements Serializable{
    private A a;
    private B b;
    public void setA(A a) {this.a = a;}
    public void setB(B b) {this.b = b;}
    public B getB() {return b;}
    public A getA() {return a;}
}
סריאליזציה כמו שהיא.  חלק 1 - 2מה קורה אם אתה מסדר מופע של המחלקה A? זה יגרור מופע של המחלקה B, שבתורו, יגרור מופע Cשיש לו הפניה למופע A, אותו מופע שבו הכל התחיל. מעגל קסמים ורקורסיה אינסופית? למרבה המזל, לא. בואו נסתכל על קוד הבדיקה הבא:
// initiaizing
A a = new A();
B b = new B();
C c = new C();
// setting references
a.setB(b);
a.setC(c);
b.setC(c);
c.setA(a);
c.setB(b);
// serializing
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(a);
oos.writeObject(b);
oos.writeObject(c);
oos.flush();
oos.close();
// deserializing
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
A a1 = (A)ois.readObject();
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
// testing
System.out.println("a==a1: "+(a==a1));
System.out.println("b==b1: "+(b==b1));
System.out.println("c==c1: "+(c==c1));
System.out.println("a1.getB()==b1: "+(a1.getB()==b1));
System.out.println("a1.getC()==c1: "+(a1.getC()==c1));
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1.getA()==a1: "+(c1.getA()==a1));
System.out.println("c1.getB()==b1: "+(c1.getB()==b1));
מה אנחנו עושים? אנו יוצרים מופע של המחלקות A, Bונותנים Cלהם קישורים זה לזה, ואז עושים כל אחד מהם בסידרה. אחר כך אנחנו מוציאים אותם מהסידרה בחזרה ומבצעים סדרה של בדיקות. מה יקרה כתוצאה מכך:
a==a1: false
b==b1: false
c==c1: false
a1.getB()==b1: true
a1.getC()==c1: true
b1.getC()==c1: true
c1.getA()==a1: true
c1.getB()==b1: true
אז מה אפשר ללמוד מהמבחן הזה? ראשון. הפניות לאובייקט לאחר דה-סריאליזציה שונות מהפניות לפניה. במילים אחרות, במהלך סריאליזציה/דה-סריאליזציה האובייקט הועתק. שיטה זו משמשת לעתים לשכפול אובייקטים. המסקנה השנייה משמעותית יותר. בעת ביצוע סידורי/ביטול הסדרת אובייקטים מרובים שיש להם הפניות צולבות, הפניות אלו נשארות תקפות לאחר דה-סידריאליזציה. במילים אחרות, אם לפני ההמשכה הם הצביעו על אובייקט אחד, אז לאחר הדה-סריאליזציה הם יצביעו גם על אובייקט אחד. עוד בדיקה קטנה שתאשר זאת:
B b = new B();
C c = new C();
b.setC(c);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(b);
oos.writeObject(c);
oos.writeObject(c);
oos.writeObject(c);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
C c2 = (C)ois.readObject();
C c3 = (C)ois.readObject();
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1==c2: "+(c1==c2));
System.out.println("c1==c3: "+(c1==c3));
לאובייקט מחלקה Bיש הפניה לאובייקט מחלקה C. כאשר הוא מסודר, bהוא מסודר יחד עם מופע של המחלקה С, ולאחר מכן אותו מופע של c מסודר שלוש פעמים. מה קורה לאחר דה-סיריאליזציה?
b1.getC()==c1: true
c1==c2: true
c1==c3: true
כפי שאתה יכול לראות, כל ארבעת האובייקטים הבלתי מסודרים מייצגים למעשה אובייקט אחד - ההתייחסויות אליו שוות. בדיוק כפי שהיה לפני ההצגה בסידרה. עוד נקודה מעניינת - מה יקרה אם ניישם בו זמנית Externalizableו Serializable? כמו בשאלה ההיא - פיל מול לוויתן - מי יביס את מי? יתגבר Externalizable. מנגנון ההסדרה בודק תחילה את נוכחותו ורק לאחר מכן את נוכחותו. Serializableכך שאם מחלקה B, המיישמת Serializable, יורשת מ- Class A, המיישמת Externalizable, השדות של מחלקה B לא יעברו בסידרה. הנקודה האחרונה היא ירושה. בעת ירושה ממחלקה המיישמת Serializable, אין צורך לבצע פעולות נוספות. ההמשכה תרחיב גם לכיתת הילד. בעת בירושה ממחלקה המיישמת Externalizable, עליך לעקוף את המתודות readExternal ו-writeExternal של מחלקת האב. אחרת, השדות של כיתת הילד לא יועברו בסידרה. במקרה זה, תצטרך לזכור לקרוא לשיטות האב, אחרת שדות האב לא יועברו בסידרה. * * * כנראה שסיימנו עם הפרטים. עם זאת, יש נושא אחד שלא נגענו בו, בעל אופי גלובלי. כלומר -

למה אתה צריך Externalizable?

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

  private short year;
  private byte month;
  private byte day;
  private byte hours;
  private byte minutes;
  private byte seconds;

}
השאר לא חשוב. השדות יכולים להיות עשויים מסוג int, אבל זה רק ישפר את ההשפעה של הדוגמה. למרות שבמציאות השדות עשויים להיות מוקלדים intמטעמי ביצועים. בכל מקרה, הנקודה ברורה. השיעור מייצג תאריך ושעה. זה מעניין אותנו בעיקר מנקודת המבט של סריאליזציה. אולי הדבר הקל ביותר לעשות הוא לאחסן חותמת זמן פשוטה. הוא מסוג ארוך, כלומר. כשמועבר בסידרה זה ייקח 8 בתים. בנוסף, גישה זו דורשת שיטות להמרת רכיבים לערך אחד ובחזרה, כלומר. - אובדן תפוקה. היתרון בגישה הזו הוא דייט מטורף לחלוטין שיכול להתאים ל-64 ביט. זהו מרווח ביטחון עצום, לרוב אין צורך במציאות. המחלקה שניתנה למעלה ייקח 2 + 5*1 = 7 בתים. בנוסף תקורה לכיתה ו-6 שדות. האם יש דרך לדחוס את הנתונים האלה? בטוח. שניות ודקות הן בטווח 0-59, כלומר. כדי לייצג אותם, מספיקים 6 סיביות במקום 8. שעות – 0-23 (5 סיביות), ימים – 0-30 (5 סיביות), חודשים – 0-11 (4 סיביות). סך הכל, הכל בלי לקחת בחשבון את השנה - 26 ביטים. נותרו עדיין 6 ביטים לגודל של int. תיאורטית, במקרים מסוימים זה עשוי להספיק לשנה. אם לא, הוספת בית נוסף מגדילה את גודל שדה הנתונים ל-14 סיביות, מה שנותן טווח של 0-16383. זה די והותר ביישומים אמיתיים. בסך הכל, צמצמנו את גודל הנתונים הנדרשים לאחסון המידע הדרוש ל-5 בתים. אם לא עד 4. החיסרון זהה למקרה הקודם - אם מאחסנים את התאריך ארוז אז יש צורך בשיטות המרה. אבל אני רוצה לעשות את זה בדרך זו: לאחסן אותו בשדות נפרדים ולסדר אותו בצורה ארוזה. כאן הגיוני להשתמש ב Externalizable:
// data is packed into 5 bytes:
//  3         2         1
// 10987654321098765432109876543210
// hhhhhmmmmmmssssssdddddMMMMyyyyyy yyyyyyyy
public void writeExternal(ObjectOutput out){
    int packed = 0;
    packed += ((int)hours) << 27;
    packed += ((int)minutes) << 21;
    packed += ((int)seconds) << 15;
    packed += ((int)day) << 10;
    packed += ((int)month) << 6;
    packed += (((int)year) >> 8) & 0x3F;
    out.writeInt(packed);
    out.writeByte((byte)year);
}

public void readExternal(ObjectInput in){
    int packed = in.readInt();
    year = in.readByte() & 0xFF;
    year += (packed & 0x3F) << 8;
    month = (packed >> 6) & 0x0F;
    day = (packed >> 10) & 0x1F;
    seconds = (packed >> 15) & 0x3F;
    minutes = (packed >> 21) & 0x3F;
    hours = (packed >> 27);
}
בעצם, זה הכל. לאחר סריאליזציה, אנו מקבלים תקורה לכל מחלקה, שני שדות (במקום 6) ו-5 בתים של נתונים. וזה כבר יותר טוב משמעותית. ניתן להשאיר אריזה נוספת לספריות מיוחדות. הדוגמה שניתנה היא פשוטה מאוד. מטרתו העיקרית היא להראות כיצד ניתן להשתמש בסריאליזציה מתקדמת. למרות שהרווח האפשרי בנפח הנתונים בסידרה רחוק מלהיות היתרון העיקרי, לדעתי. היתרון העיקרי, בנוסף לגמישות... (עבור בצורה חלקה לסעיף הבא...) קישור למקור: Serialization as it is
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION