JavaRush /בלוג Java /Random-HE /סריאליזציה וביטול סדרה ב-Java

סריאליזציה וביטול סדרה ב-Java

פורסם בקבוצה
שלום! בהרצאה של היום נדבר על סריאליזציה ודה-סריאליזציה בג'אווה. נתחיל בדוגמה פשוטה. נניח שאתה היוצר של משחק מחשב. אם גדלתם בשנות ה-90 וזוכרים את קונסולות המשחקים של אותם זמנים, אתם בטח יודעים שחסר להם משהו ברור היום - שמירה וטעינת משחקים :) אם לא... תארו לעצמכם! אני חושש שהיום משחק בלי הזדמנות כזו נידון לכישלון! ובעצם, מה זה "לשמור" ו"טעינת" משחק? ובכן, במובן הרגיל, אנחנו מבינים מה זה: אנחנו רוצים להמשיך את המשחק מאיפה שהפסקנו בפעם הקודמת. לשם כך, אנו יוצרים מעין "מחסום", שבו אנו משתמשים לאחר מכן לטעינת המשחק. אבל מה זה אומר, לא במובן היומיומי, אלא במובן ה"מתכנת"? התשובה פשוטה: אנו שומרים את מצב התוכנית שלנו. נניח שאתה משחק משחק אסטרטגיה עבור ספרד. למשחק שלך יש מדינה: מי הבעלים של אילו שטחים, למי יש כמה משאבים, מי בברית עם מי, ומי, להיפך, במלחמה וכו'. מידע זה, מצב התוכנית שלנו, חייב להישמר איכשהו על מנת לשחזר מאוחר יותר את הנתונים ולהמשיך במשחק. בדיוק לשם כך משמשים מנגנוני ההמשכה והדה -סריאליזציה . סריאליזציה היא תהליך אחסון המצב של אובייקט ברצף של בתים. דה-סריאליזציה היא תהליך של שחזור אובייקט מבייטים אלה. כל אובייקט Java מומר לרצף של בתים. לשם מה זה? אמרנו יותר מפעם אחת שתכניות אינן קיימות בפני עצמן. לרוב הם מקיימים אינטראקציה זה עם זה, מחליפים נתונים וכו'. ופורמט הביטים נוח ויעיל לכך. אנחנו יכולים, למשל, להפוך את אובייקט המחלקה שלנו SavedGame(משחק שמור) לרצף של בתים, להעביר את אותם בתים דרך הרשת למחשב אחר, ובמחשב השני להפוך את אותם בתים בחזרה לאובייקט ג'אווה! קשה לשמוע, נכון? ככל הנראה, ארגון התהליך הזה לא יהיה קל :/ למרבה המזל, לא! :) ב-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 בעתיד. אין להם שיטות שצריך ליישם. אז, Serializable הוא אחד ממשקים כאלה. נקודה חשובה נוספת: המשתנה private static final long serialVersionUIDשהגדרנו בכיתה. למה זה נחוץ? שדה זה מכיל את מזהה הגרסה הייחודי של המחלקה בסידרה . לכל מחלקה המיישמת את ממשק ה-Serializable יש מזהה גרסה. זה מחושב על סמך תוכן המחלקה - שדות, סדר הצהרות, שיטות. ואם נשנה את סוג השדה ו/או מספר השדות במחלקה שלנו, מזהה הגרסה ישתנה באופן מיידי. 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 position of neutrality"};

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

       //create 2 threads 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 game to file
       objectOutputStream.writeObject(savedGame);

       // close the stream and release 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ФранцвоюесЁR ,Р▲ ЃРїР° РЅРёСЏ Р·Р°Ряла позицию нейтралитетаuq ~ t "РЈ Р˜СЃРх t "РЈ Р˜СЃРїРїРїРїРїРїР. Р°t Р J R РѕСЃСЃРёРё 80 золотаt !РЈ Франциии 90 золотаuq ~ t &РЈ Р˜СЃРїРЈ Р˜СЃРц РёРЅС †РёР№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=[ספרד יש 6 מחוזות, לרוסיה 10 מחוזות, לצרפת יש 8 מחוזות], resourcesInfo=[ספרד יש 100 זהב, לרוסיה 80 זהב, לצרפת יש 90 זהב], diplomacyInfo=[צרפת במלחמה עם רוסיה, ספרד תפסה עמדת נייטרליות]} נהדר! הצלחנו קודם כל לשמור את מצב המשחק שלנו בקובץ, ולאחר מכן לשחזר אותו מהקובץ. עכשיו בואו ננסה לעשות את אותו הדבר, אבל הסר SavedGameאת מזהה הגרסה מהכיתה שלנו. לא נשכתב את שני השיעורים שלנו, הקוד בהם יהיה זהה, פשוט נסיר SavedGameאת private static final long serialVersionUID. הנה האובייקט שלנו לאחר ההסדרה: ¬н sr SavedGameі€MіuОm‰ [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territoriesInfoq ~ xpur] Ѓ ѓ р р р ѓѓрёрμр№, рəѓmїm ° рmesmџm · р ° рmes »р ° rїрesр · ёmё † ёmµ in ~ † ~ ~ ~ с ~ с ~ ~ ~ ~ ~ ~ ~ ~ in ~ in ~ u 100 р · РѕР»РѕС‚Р°t РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 Р·РѕР & РёРё 6 R їСЂРѕРІРёРЅС† РёР№t %РЈ Р РѕСЃСЃРёРё 10 провнинцийt РёР№РёР№РёЅРЂ оввинцинА וכשמנסים לבטל את זה, זה מה שקרה: InvalidClassException: מחלקה מקומית לא תואמת : stream classdesc serialVersionUID = - 196410440475012755, local class serialVersionUID = -6675950253085108747 זה אותו חריג שהוזכר לעיל. אפשר לקרוא על כך עוד במאמר של אחד מתלמידינו. אגב, פספסנו נקודה אחת חשובה. ברור שחרוזים ופרימיטיביים ניתן להסיד בקלות: ל-Java בהחלט יש כמה - אז יש מנגנונים מובנים לכך. אבל מה אם serializableל-class שלנו יש שדות שמתבטאים לא כפרימיטיביים, אלא כהתייחסויות לאובייקטים אחרים? בואו, למשל , ניצור כיתות נפרדות TerritoriesInfoכדי לעבוד עם הכיתה שלנו . ResourcesInfoDiplomacyInfoSavedGame
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 + '\'' +
               '}';
   }
}
אבל עכשיו יש לנו שאלה: האם כל המחלקות הללו אמורות להיות ניתנות לסריאליזציה אם ברצוננו לעשות סדרה של המחלקה שהשתנה 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
       TerritoriesInfo territoriesInfo = new TerritoriesInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
       ResourcesInfo resourcesInfo = new ResourcesInfo("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 position of neutrality");


       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();
   }
}
תוצאה: חריגה בשרשור "ראשי" java.io.NotSerializableException: דיפלומטיה מידע נכשל! למעשה, הנה התשובה לשאלתנו. כאשר אתה מסדר אובייקט, כל האובייקטים שהוא מפנה במשתני המופע שלו מסודרים. ואם האובייקטים האלה מתייחסים גם לאובייקטים שלישיים, הם גם מסודרים. וכן הלאה עד אינסוף. כל המחלקות בשרשרת זו חייבים להיות ניתנים להמשכה, אחרת הם לא יהיו ניתנים לסידרה ויוצא חריג. זה, אגב, יכול ליצור בעיות בעתיד. מה עלינו לעשות, למשל, אם איננו זקוקים לחלק מהכיתה במהלך הסידרה? או, למשל, כיתה TerritoryInfoבתוכנית שלנו "עברה בירושה" כחלק מספריה כלשהי. עם זאת, זה לא ניתן להסדרה, ובהתאם, אנחנו לא יכולים לשנות את זה. מסתבר שאיננו יכולים להוסיף שדה TerritoryInfoלכיתה שלנו , כי אז כל הכיתה תהפוך לבלתי ניתנת לסידרה! בעיה:/ בעיות מסוג זה נפתרות ב-Java באמצעות מילת המפתח . אם תוסיף מילת מפתח זו לשדה מחלקה, הערך של שדה זה לא יעבור בסידרה. בואו ננסה ליצור אחד מהשדות של הכיתה שלנו , ולאחר מכן נבצע סדרה ונשחזר אובייקט אחד. SavedGameSavedGameסריאליזציה ודה-סריאליזציה ב-Java - 2transientSavedGame transient
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
       TerritoriesInfo territoriesInfo = new TerritoriesInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
       ResourcesInfo resourcesInfo = new ResourcesInfo("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 position of neutrality");


       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='ספרד יש 100 זהב, לרוסיה 80 זהב, לצרפת יש 90 זהב'}, diplomacyInfo=DiplomacyInfo{info='צרפת במלחמה עם רוסיה, ספרד נקטה בנייטרליות בעמדה'}} במקביל, קיבלנו תשובה לשאלה איזה ערך יוקצה transientלשדה. מוקצה לו ערך ברירת מחדל. במקרה של חפצים זה null. בשעות הפנאי שלך, אתה יכול לקרוא את המאמר המצוין הזה על סדרה . זה גם מדבר על הממשק Externalizable, עליו נדבר בהרצאה הבאה. בנוסף, יש פרק בנושא זה בספר "Head-First Java", שימו לב אליו :)
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION