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[] territoryInfo;
   private String[] resourceInfo;
   private String[] diplomacyInfo;

   public SavedGame(String[] territoryInfo, String[] resourceInfo, String[] diplomacyInfo){
       this.territoryInfo = territoryInfo;
       this.resourceInfo = resourceInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   public String[] getTerritoryInfo() {
       return territoryInfo;
   }

   public void setTerritoryInfo(String[] territoryInfo) {
       this.territoryInfo = territoryInfo;
   }

   public String[] getResourceInfo() {
       return resourceInfo;
   }

   public void setResourceInfo(String[] resourceInfo) {
       this.resourceInfo = resourceInfo;
   }

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

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

   @Override
   public String toString() {
       return "SavedGame{" +
               "territoryInfo=" + Arrays.toString(territoryInfo) +
               ", resourceInfo=" + Arrays.toString(resourceInfo) +
               ", diplomacyInfo=" + Arrays.toString(diplomacyInfo) +
               '}';
   }
}
שלושה מערכים אחראים למידע על טריטוריות, משאבים ודיפלומטיה, והממשק Serializable אומר למכונת ג'אווה: ' הכל בסדר אם ניתן לבצע סדרה של אובייקטים מהמחלקה הזו '. ממשק ללא ממשק אחד נראה מוזר :/ למה זה נחוץ? התשובה לשאלה זו ניתנה לעיל: זה נחוץ רק כדי לספק את המידע הדרוש למכונת Java. בשיעור שעבר הזכרנו בקצרה ממשקי סמן. אלו הם ממשקי מידע מיוחדים שפשוט מסמנים את השיעורים שלנו במידע נוסף שיהיה שימושי למכונת Java בעתיד. אין להם שיטות שאתה צריך ליישם. להלן Serializable - ממשק אחד כזה. הנה עוד נקודה חשובה: למה אנחנו צריכים את המשתנה הפרטי הסטטי הסופי הארוך 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[] resourceInfo = {"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, resourceInfo, 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 release resources
       objectOutputStream.close();
   }
}
כפי שאתה יכול לראות, יצרנו 2 זרמים: FileOutputStream ו- ObjectOutputStream . הראשון יודע לכתוב נתונים לקובץ, והשני ממיר אובייקטים לבייטים. כבר ראיתם מבנים מקננים דומים, למשל, new BufferedReader(new InputStreamReader(...)) , בשיעורים קודמים, אז הם לא צריכים להפחיד אתכם :) על ידי יצירת שרשרת זו של שני זרמים, אנו מבצעים את שתי המשימות: אנו ממירים את האובייקט SavedGame לרצף של בתים ושומרים אותו לקובץ באמצעות שיטת writeObject() . ודרך אגב, אפילו לא הסתכלנו מה קיבלנו! הגיע הזמן להסתכל על הקובץ! *הערה: אין צורך ליצור את הקובץ מראש. אם קובץ עם השם שצוין לא קיים, אז הוא ייווצר אוטומטית* והנה התוכן שלו: ¬н sr SavedGame [ diplomacyInfot [Ljava/lang/String;[ resourceInfoq ~ [ territoryInfoq ~ xpur [Ljava.lang. מחרוזת;¬ТVзй{G xp t pФранция воюет СЃ Россией, Р˜СЃРїР°ЅРёРяРїР°ЅРё ·РёС†РёСЋ нейтралит етаuq ~ t "РЈ Р˜СЃРїР°РЅРёРё 100 золотаt РЈ Р РѕСЃСЃРёРё 80 золоссии 80 золоссии 9 Рё 90 Р·РѕР »РѕС‚Р°uq ~ t &РЈ Р˜СЃРїР°РЅРёРё 6 провинцийt %РЈ Р РѕСЃСЃРёРёРёРРСЃРёРёРёРЅ Р№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{territoryInfo=[ספרד יש 6 מחוזות, לרוסיה 10 מחוזות, לצרפת יש 8 מחוזות], resourceInfo=[ספרד יש 100 זהב, לרוסיה 80 זהב, לצרפת יש 90 זהב], diplomacyInfo=[צרפת במלחמה עם רוסיה, ספרד נקטה בעמדה ניטרלית]} מצוין! הצלחנו קודם כל לשמור את מצב המשחק שלנו בקובץ, ולאחר מכן לשחזר אותו מהקובץ. עכשיו בואו ננסה לעשות את אותו הדבר, אבל נסיר את מזהה הגרסה ממחלקת SavedGame שלנו . לא נשכתב את שני השיעורים שלנו. הקוד שלהם יהיה זהה. אנחנו פשוט נסיר פרטי סטטי סופית ארוכה serialVersionUID מהמחלקה SavedGame . הנה האובייקט שלנו לאחר ההסדרה: ¬н sr SavedGameі€MіuОm‰ [ diplomacyInfot [Ljava/lang/String;[ resourceInfoq ~ [ territoryInfoq ~ xpur [Ljava.lang.String;¬ТVзй{G xp t pР¤БСЂР°РцРЅСЂР°РцРЅ ‚ СЃ Россией, Р˜СЃРїР°РЅРёСЏ заняла позициССЋ РЅРµ»Рu t "РЈ Р˜СЃРїР°РЅРёРё 100 Рё ·РѕР»РѕС‚Р°t РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 Р·РѕР&Рuѕ ЅРёРё 6 РїСЂРѕРІРёРЅС †РёР№t %РЈ Р РѕСЃСЃРёРё 10 провинцийt &РЈ ФранциРцРцРцРцРЅ № אבל תראה מה קורה כשאנחנו מנסים להפוך את זה לסידריאליזציה: InvalidClassException: מחלקה מקומית לא תואמת: stream classdesc serialVersionUID = - 196410440475012755, מחלקה מקומית serialVersionUID = -6675950253085108747 אגב, פספסנו משהו חשוב. ברור, מחרוזות ופרימיטיביות מסודרות בקלות: ל-Java בהחלט יש מנגנון מובנה לזה. אבל מה אם לשדות הפרימיטיביים שלנו, שהם לא ניתנים להסדרה, אין מחלקה אלא התייחסויות לאובייקטים אחרים? לדוגמה, בואו ניצור מחלקות TerritoryInfo , ResourceInfo ו- DiplomacyInfo נפרדות כדי לעבוד עם מחלקה SavedGame שלנו .
public class TerritoryInfo {

   private String info;

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

   public String getInfo() {
       return info;
   }

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

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

public class ResourceInfo {

   private String info;

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

   public String getInfo() {
       return info;
   }

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

   @Override
   public String toString() {
       return "ResourceInfo{" +
               "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 TerritoryInfo territoryInfo;
   private ResourceInfo resourceInfo;
   private DiplomacyInfo diplomacyInfo;

   public SavedGame(TerritoryInfo territoryInfo, ResourceInfo resourceInfo, DiplomacyInfo diplomacyInfo) {
       this.territoryInfo = territoryInfo;
       this.resourceInfo = resourceInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   public TerritoryInfo getTerritoryInfo() {
       return territoryInfo;
   }

   public void setTerritoryInfo(TerritoryInfo territoryInfo) {
       this.territoryInfo = territoryInfo;
   }

   public ResourceInfo getResourceInfo() {
       return resourceInfo;
   }

   public void setResourceInfo(ResourceInfo resourceInfo) {
       this.resourceInfo = resourceInfo;
   }

   public DiplomacyInfo getDiplomacyInfo() {
       return diplomacyInfo;
   }

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

   @Override
   public String toString() {
       return "SavedGame{" +
               "territoryInfo=" + territoryInfo +
               ", resourceInfo=" + resourceInfo +
               ", 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(territoryInfo, resourceInfo, diplomacyInfo);

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

       objectOutputStream.writeObject(savedGame);

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

public class SavedGame implements Serializable {

   private transient TerritoryInfo territoryInfo;
   private ResourceInfo resourceInfo;
   private DiplomacyInfo diplomacyInfo;

   public SavedGame(TerritoryInfo territoryInfo, ResourceInfo resourceInfo, DiplomacyInfo diplomacyInfo) {
       this.territoryInfo = territoryInfo;
       this.resourceInfo = resourceInfo;
       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(territoryInfo, resourceInfo, 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{territoryInfo=null, resourceInfo=ResourceInfo{info='ספרד יש 100 זהב, לרוסיה 80 זהב, לצרפת יש 90 זהב'}, diplomacyInfo=DiplomacyInfo{info='צרפת במלחמה עם רוסיה, ספרד נקטה בעמדה ניטרלית'}} עם זאת, קיבלנו תשובה לשאלה איזה ערך יוקצה לשדה חולף . מוקצה לו ערך ברירת המחדל. עבור אובייקטים, זה ריק . ניתן לקרוא פרק מצוין בנושא זה בספר 'ראש בראש ג'אווה', שימו לב :)
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION