CodeGym /בלוג Java /Random-HE /מה ההבדל בין סריאליזציה לדה-סריאליזציה בג'אווה?
John Squirrels
רָמָה
San Francisco

מה ההבדל בין סריאליזציה לדה-סריאליזציה בג'אווה?

פורסם בקבוצה
היי! בשיעור של היום, אנחנו מדברים על סריאליזציה וסידריאליזציה בג'אווה. נתחיל בדוגמה פשוטה. נניח שיצרת משחק מחשב. אם גדלתם בשנות ה-90 וזוכרים את קונסולות המשחקים של אותה תקופה, אתם בטח יודעים שחסר להן משהו שאנחנו לוקחים כמובן מאליו היום - היכולת לשמור ולטעון משחקים :) אם לא, תארו לעצמכם! מה ההבדל בין סריאליזציה לדה-סריאליזציה בג'אווה?  - 1 אני חושש שהיום משחק בלי היכולות האלה יהיה נחרץ! מה זה אומר "לשמור" ו"לטעון" משחק בכל מקרה? ובכן, אנחנו מבינים את המשמעות הרגילה: אנחנו רוצים להמשיך את המשחק מהמקום שבו הפסקנו. לשם כך, אנו יוצרים מעין "נקודת ביקורת", שבה אנו משתמשים לאחר מכן לטעינת המשחק. אבל מה זה אומר למתכנת ולא לגיימר מזדמן? התשובה פשוטה: אנחנו שומרים את מצב התוכנית שלנו . נניח שאתה משחק בתור ספרד באסטרטגיה. למשחק שלך יש מדינה: מי הבעלים של אילו טריטוריות, למי יש כמה משאבים, מי בברית עם מי, מי במלחמה עם מי וכו'. עלינו איכשהו לשמור את המידע הזה, מצב התוכנית שלנו, כדי לשחזר אותו בעתיד ולהמשיך במשחק. שכן בשביל זה בדיוק נועדו ההמשכה והדה -ריאליזציה . סריאליזציה היא תהליך אחסון המצב של אובייקט ברצף של בתים. דה-סריאליזציה היא תהליך של שחזור אובייקט מבייטים אלו. ניתן להמיר כל אובייקט Java לרצף בתים. למה שנצטרך את זה? אמרנו יותר מפעם אחת שתוכניות לא קיימות בפני עצמן. לרוב, הם מקיימים אינטראקציה עם תוכניות אחרות, מחליפים נתונים וכו'. ורצף בתים הוא פורמט נוח ויעיל. לדוגמה, אנחנו יכולים להפוך SavedGameאת האובייקט שלנו לרצף של בתים, לשלוח את הבתים האלה דרך הרשת למחשב אחר, ואז במחשב השני להפוך את הבתים האלה בחזרה לאובייקט Java! נשמע קשה, נכון? ויישום התהליך הזה נראה כמו כאב :/ לשמחתנו, זה לא כך! :) ב-Java, Serializableהממשק אחראי על תהליך ההמשכה. הממשק הזה הוא פשוט ביותר: אתה לא צריך ליישם שיטה אחת כדי להשתמש בו! כך נראית הכיתה שלנו לשמירת משחקים פשוטה:
import java.io.Serializable;
import java.util.Arrays;

public class SavedGame implements Serializable {

   private static final long serialVersionUID = 1L;

   private String[] territoriesInfo;
   private String[] resourcesInfo;
   private String[] diplomacyInfo;

   public SavedGame(String[] territoriesInfo, String[] resourcesInfo, String[] diplomacyInfo){
       this.territoriesInfo = territoriesInfo;
       this.resourcesInfo = resourcesInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   public String[] getTerritoriesInfo() {
       return territoriesInfo;
   }

   public void setTerritoriesInfo(String[] territoriesInfo) {
       this.territoriesInfo = territoriesInfo;
   }

   public String[] getResourcesInfo() {
       return resourcesInfo;
   }

   public void setResourcesInfo(String[] resourcesInfo) {
       this.resourcesInfo = resourcesInfo;
   }

   public String[] getDiplomacyInfo() {
       return diplomacyInfo;
   }

   public void setDiplomacyInfo(String[] diplomacyInfo) {
       this.diplomacyInfo = diplomacyInfo;
   }

   @Override
   public String toString() {
       return "SavedGame{" +
               "territoriesInfo=" + Arrays.toString(territoriesInfo) +
               ", resourcesInfo=" + Arrays.toString(resourcesInfo) +
               ", diplomacyInfo=" + Arrays.toString(diplomacyInfo) +
               '}';
   }
}
שלושת המערכים אחראים למידע על טריטוריות, משאבים ודיפלומטיה. ממשק ה-Serializable אומר למכונה הוירטואלית של Java: " הכל בסדר - במידת הצורך, ניתן לבצע סדרה של אובייקטים מהמחלקה הזו ". ממשק ללא ממשק אחד נראה מוזר :/ למה זה נחוץ? ניתן לראות את התשובה לשאלה זו לעיל: היא משמשת רק לספק את המידע הדרוש למכונה הוירטואלית של Java. באחד השיעורים הקודמים שלנו, הזכרנו בקצרה ממשקי סמן . אלו הם ממשקי מידע מיוחדים שפשוט מסמנים את השיעורים שלנו במידע נוסף שיהיה שימושי למכונת Java בעתיד. אין להם שיטות שאתה צריך ליישם. Serializableהוא אחד מהממשקים האלה. עוד נקודה חשובה: למה אנחנו צריכים את private static final long serialVersionUIDהמשתנה שהגדרנו בכיתה? למה זה נחוץ? שדה זה מכיל מזהה ייחודי עבור הגרסה של המחלקה בסידרה . לכל מחלקה המיישמת את Serializableהממשק יש versionמזהה. הוא מחושב על סמך תוכן המחלקה: השדות שלה, סדר ההצהרה שלהם, שיטות וכו'. אם נשנה את סוג השדה ו/או את מספר השדות במחלקה שלנו, אזי מיד משתנה מזהה הגרסה. . serialVersionUIDנכתב גם כאשר הכיתה מסודרת. כאשר אנו מנסים לבצע דה-סריאליזציה, כלומר לשחזר אובייקט מקבוצה של בתים, serialVersionUIDהמשוות את המשויך לערך של serialVersionUIDעבור המחלקה בתוכנית שלנו. אם הערכים אינם תואמים, אז java.io. InvalidClassException ייזרק. נראה דוגמה לכך להלן. כדי להימנע מכך, אנו פשוט מגדירים את מזהה הגרסה באופן ידני בכיתה שלנו. במקרה שלנו, הוא פשוט יהיה שווה ל-1 (אבל אתה יכול להחליף כל מספר אחר שתרצה). ובכן, הגיע הזמן לנסות לעשות סדרה של SavedGameהאובייקט שלנו ולראות מה קורה!
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {

   public static void main(String[] args) throws IOException {

       // Create our object
       String[] territoryInfo = {"Spain has 6 provinces", "Russia has 10 provinces", "France has 8 provinces"};
       String[] resourcesInfo = {"Spain has 100 gold", "Russia has 80 gold", "France has 90 gold"};
       String[] diplomacyInfo = {"France is at war with Russia, Spain has taken a neutral position"};

       SavedGame savedGame = new SavedGame(territoryInfo, resourcesInfo, diplomacyInfo);

       // Create 2 streams to serialize the object and save it to a file
       FileOutputStream outputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);

       // Save the game to a file
       objectOutputStream.writeObject(savedGame);

       // Close the stream and free resources
       objectOutputStream.close();
   }
}
כפי שאתה יכול לראות, יצרנו 2 זרמים: FileOutputStreamו ObjectOutputStream. הראשון יכול לכתוב נתונים לקובץ, והשני ממיר אובייקטים לבייטים. כבר ראיתם מבנים "מקוננים" דומים, למשל, , new BufferedReader(new InputStreamReader(...))בשיעורים קודמים, אז אלה לא צריכים להפחיד אתכם :) על ידי יצירת "שרשרת" כזו של שני זרמים, אנו מבצעים את שתי המשימות: אנו ממירים את האובייקט SavedGameלסט של בתים ושמור אותו בקובץ באמצעות writeObject()השיטה. ודרך אגב, אפילו לא הסתכלנו מה קיבלנו! הגיע הזמן להסתכל על הקובץ! *הערה: אין צורך ליצור את הקובץ מראש. אם קובץ בשם זה לא קיים, הוא ייווצר אוטומטית* והנה התוכן שלו!

¬н sr SavedGame [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territoriesInfoq ~ xpur [Ljava.lang.String;­ТVзй{G xp t pФранция воюет СЃ Россией, Испания заняла позицию нейтралитетаuq ~ t "РЈ Испании 100 золотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Испании 6 провинцийt %РЈ Р РѕСЃСЃРёРё 10 провинцийt &РЈ Франции 8 провинций
אה-אוי :( נראה שהתוכנית שלנו לא עבדה :( למעשה, היא כן עבדה. אתה זוכר ששלחנו קבוצה של בתים, לא רק אובייקט או טקסט, לקובץ? ובכן, זה מה זה קבוצה של בתים נראית כמו :) זה המשחק השמור שלנו! אם אנחנו רוצים לשחזר את האובייקט המקורי שלנו, כלומר להתחיל ולהמשיך את המשחק מהנקודה בה הפסקנו, אז אנחנו צריכים את התהליך ההפוך: דה-סריאליזציה. כך זה ייראה ב- מקרה:
import java.io.*;

public class Main {

   public static void main(String[] args) throws IOException, ClassNotFoundException {

       FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);

       SavedGame savedGame = (SavedGame) objectInputStream.readObject();

       System.out.println(savedGame);
   }
}
והנה התוצאה!

SavedGame{territoriesInfo=["Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces], resourcesInfo=[Spain has 100 gold, Russia has 80 gold, France has 90 gold], diplomacyInfo=[France is at war with Russia, Spain has taken a neutral position]}
מְעוּלֶה! הצלחנו קודם כל לשמור את מצב המשחק שלנו בקובץ, ולאחר מכן לשחזר אותו מהקובץ. עכשיו בואו ננסה לעשות את אותו הדבר, אבל בלי מזהה הגרסה של SavedGameהכיתה שלנו. לא נשכתב את שני השיעורים שלנו. הקוד שלהם יישאר זהה, אבל נסיר private static final long serialVersionUIDמהכיתה SavedGame. הנה האובייקט שלנו לאחר סידרה:

¬н sr SavedGameі€MіuОm‰ [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territoriesInfoq ~ xpur [Ljava.lang.String;­ТVзй{G xp t pФранция воюет СЃ Россией, Испания заняла позицию нейтралитетаuq ~ t "РЈ Испании 100 золотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Испании 6 провинцийt %РЈ Р РѕСЃСЃРёРё 10 провинцийt &РЈ Франции 8 провинций
אבל תראה מה קורה כשאנחנו מנסים לבטל את זה:

InvalidClassException: local class incompatible: stream classdesc serialVersionUID = -196410440475012755, local class serialVersionUID = -6675950253085108747
זהו היוצא מן הכלל שהזכרנו לעיל. אגב, פספסנו משהו חשוב. זה הגיוני שניתן להרכיב בקלות מחרוזות ופרימיטיבים: ל-Java יש כנראה מנגנון מובנה כלשהו לעשות זאת. אבל מה אם serializableלכיתה שלנו יש שדות שאינם פרימיטיביים, אלא התייחסויות לאובייקטים אחרים? לדוגמה, בואו ניצור TerritoriesInfo, ResourcesInfoוכיתות DiplomacyInfoנפרדות לעבודה עם SavedGameהכיתה שלנו.
public class TerritoriesInfo {

   private String info;

   public TerritoriesInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "TerritoriesInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}

public class ResourcesInfo {

   private String info;

   public ResourcesInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "ResourcesInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}

public class DiplomacyInfo {

   private String info;

   public DiplomacyInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "DiplomacyInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}
ועכשיו עולה שאלה: האם כל השיעורים האלה צריכים להיות Serializableאם ברצוננו לעשות סדרה SavedGameבמעמד השונה שלנו?
import java.io.Serializable;
import java.util.Arrays;

public class SavedGame implements Serializable {

   private TerritoriesInfo territoriesInfo;
   private ResourcesInfo resourcesInfo;
   private DiplomacyInfo diplomacyInfo;

   public SavedGame(TerritoriesInfo territoriesInfo, ResourcesInfo resourcesInfo, DiplomacyInfo diplomacyInfo) {
       this.territoriesInfo = territoriesInfo;
       this.resourcesInfo = resourcesInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   public TerritoriesInfo getTerritoriesInfo() {
       return territoriesInfo;
   }

   public void setTerritoriesInfo(TerritoriesInfo territoriesInfo) {
       this.territoriesInfo = territoriesInfo;
   }

   public ResourcesInfo getResourcesInfo() {
       return resourcesInfo;
   }

   public void setResourcesInfo(ResourcesInfo resourcesInfo) {
       this.resourcesInfo = resourcesInfo;
   }

   public DiplomacyInfo getDiplomacyInfo() {
       return diplomacyInfo;
   }

   public void setDiplomacyInfo(DiplomacyInfo diplomacyInfo) {
       this.diplomacyInfo = diplomacyInfo;
   }

   @Override
   public String toString() {
       return "SavedGame{" +
               "territoriesInfo=" + territoriesInfo +
               ", resourcesInfo=" + resourcesInfo +
               ", diplomacyInfo=" + diplomacyInfo +
               '}';
   }
}
ובכן, בואו נבחן את זה! בוא נשאיר הכל כמו שהוא וננסה לעשות אובייקט בסידרה SavedGame:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {

   public static void main(String[] args) throws IOException {

       // Create our object
       TerritoryInfo territoryInfo = new TerritoryInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
       ResourceInfo resourceInfo = new ResourceInfo("Spain has 100 gold, Russia has 80 gold, France has 90 gold");
       DiplomacyInfo diplomacyInfo =  new DiplomacyInfo("France is at war with Russia, Spain has taken a neutral position");


       SavedGame savedGame = new SavedGame(territoriesInfo, resourcesInfo, diplomacyInfo);

       FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

       objectOutputStream.writeObject(savedGame);

       objectOutputStream.close();
   }
}
תוֹצָאָה:

Exception in thread "main" java.io.NotSerializableException: DiplomacyInfo
זה לא עבד! בעיקרון, זו התשובה לשאלה שלנו. כאשר אובייקט מסודר, כל האובייקטים שאליהם מתייחסים משתני המופע שלו מסודרים. ואם האובייקטים האלה מתייחסים גם לאובייקטים אחרים, אז הם גם מסודרים. וכן הלאה עד אינסוף. כל השיעורים בשרשרת זו חייבים להיותSerializable , אחרת אי אפשר יהיה לבצע אותם בסידרה ויוצא חריג. אגב, זה יכול ליצור בעיות בהמשך הדרך. מה עלינו לעשות אם, למשל, אנחנו לא צריכים חלק משיעור כשאנחנו עושים סדרה? או, למשל, מה אם TerritoryInfoהכיתה הגיעה אלינו כחלק מספריית צד שלישי כלשהי. ותניח עוד שזה לא Serializable, ובהתאם, אנחנו לא יכולים לשנות את זה. מסתבר שאיננו יכולים להוסיף TerritoryInfoשדה לכיתה שלנו SavedGame, כי פעולה זו תהפוך את SavedGameהכיתה כולה לבלתי ניתנת לסידרה! זאת בעיה :/ מה ההבדל בין סריאליזציה לדה-סריאליזציה בג'אווה?  - 2ב-Java, בעיות מהסוג הזה נפתרות באמצעות transientמילת המפתח. אם תוסיף את מילת המפתח הזו לשדה בכיתה שלך, השדה הזה לא יעבור בסידרה. בואו ננסה להפוך את אחד SavedGameמשדות המופע של המחלקה לחולפים. לאחר מכן נבצע סדרה ונשחזר אובייקט אחד.
import java.io.Serializable;

public class SavedGame implements Serializable {

   private transient TerritoriesInfo territoriesInfo;
   private ResourcesInfo resourcesInfo;
   private DiplomacyInfo diplomacyInfo;

   public SavedGame(TerritoriesInfo territoriesInfo, ResourcesInfo resourcesInfo, DiplomacyInfo diplomacyInfo) {
       this.territoriesInfo = territoriesInfo;
       this.resourcesInfo = resourcesInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   // ...getters, setters, toString()
}



import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {

   public static void main(String[] args) throws IOException {

       // Create our object
       TerritoryInfo territoryInfo = new TerritoryInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
       ResourceInfo resourceInfo = new ResourceInfo("Spain has 100 gold, Russia has 80 gold, France has 90 gold");
       DiplomacyInfo diplomacyInfo =  new DiplomacyInfo("France is at war with Russia, Spain has taken a neutral position");


       SavedGame savedGame = new SavedGame(territoriesInfo, resourcesInfo, diplomacyInfo);

       FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

       objectOutputStream.writeObject(savedGame);

       objectOutputStream.close();
   }
}


import java.io.*;

public class Main {

   public static void main(String[] args) throws IOException, ClassNotFoundException {

       FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);

       SavedGame savedGame = (SavedGame) objectInputStream.readObject();

       System.out.println(savedGame);

       objectInputStream.close();


   }
}
והנה התוצאה:

SavedGame{territoriesInfo=null, resourcesInfo=ResourcesInfo{info='Spain has 100 gold, Russia has 80 gold, France has 90 gold'}, diplomacyInfo=DiplomacyInfo{info='France is at war with Russia, Spain has taken a neutral position'}}
בנוסף, קיבלנו תשובה לשאלה שלנו לגבי איזה ערך מוקצה לשדה transient. הוא מקבל את ערך ברירת המחדל. עבור חפצים, זהו null. אתה יכול לקרוא את המאמר המצוין הזה על סדרה כאשר יש לך כמה דקות פנויות. זה גם מזכיר את Externalizableהממשק, עליו נדבר בשיעור הבא. בנוסף, בספר "Head-First Java" יש פרק בנושא זה. תן לזה קצת תשומת לב :)
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION