JavaRush /مدونة جافا /Random-AR /التسلسل وإلغاء التسلسل في جافا

التسلسل وإلغاء التسلسل في جافا

نشرت في المجموعة
مرحبًا! في محاضرة اليوم سنتحدث عن التسلسل وإلغاء التسلسل في Java. لنبدأ بمثال بسيط. لنفترض أنك منشئ لعبة كمبيوتر. إذا نشأت في التسعينيات وتذكرت وحدات التحكم في الألعاب في تلك الأوقات، فمن المحتمل أنك تعلم أنها تفتقر إلى شيء واضح اليوم - حفظ الألعاب وتحميلها :) إذا لم يكن الأمر كذلك... تخيل! أخشى أن اللعبة التي لا تتاح لها مثل هذه الفرصة اليوم ستكون محكوم عليها بالفشل! وفي الواقع، ما هو "حفظ" و"تحميل" اللعبة؟ حسنًا، بالمعنى المعتاد، نحن نفهم ما هو: نريد مواصلة اللعبة من حيث توقفنا في المرة السابقة. للقيام بذلك، نقوم بإنشاء نوع من "نقطة التفتيش"، والتي نستخدمها بعد ذلك لتحميل اللعبة. ولكن ماذا يعني هذا، ليس بالمعنى اليومي، بل بالمعنى "المبرمج"؟ الجواب بسيط: نحفظ حالة برنامجنا. لنفترض أنك تلعب لعبة إستراتيجية لإسبانيا. لعبتك لها حالة: من يملك أي مناطق، ومن لديه عدد الموارد، ومن يتحالف مع من، ومن، على العكس من ذلك، في حالة حرب، وما إلى ذلك. يجب حفظ هذه المعلومات وحالة برنامجنا بطريقة ما من أجل استعادة البيانات لاحقًا ومتابعة اللعبة. وهذا هو بالضبط ما يتم استخدام آليات التسلسل وإلغاء التسلسل من أجله . التسلسل هو عملية تخزين حالة الكائن في سلسلة من البايتات. إلغاء التسلسل هو عملية إعادة بناء كائن من هذه البايتات. يتم تحويل أي كائن Java إلى سلسلة من البايتات. لما هذا؟ لقد قلنا أكثر من مرة أن البرامج لا توجد من تلقاء نفسها. في أغلب الأحيان يتفاعلون مع بعضهم البعض، ويتبادلون البيانات، وما إلى ذلك. وتنسيق البايت مناسب وفعال لهذا الغرض. يمكننا، على سبيل المثال، تحويل كائن الفئة SavedGame(لعبة محفوظة) إلى سلسلة من البايتات، ونقل تلك البايتات عبر الشبكة إلى كمبيوتر آخر، وعلى الكمبيوتر الثاني تحويل تلك البايتات مرة أخرى إلى كائن Java! من الصعب أن تسمع، أليس كذلك؟ ويبدو أن تنظيم هذه العملية لن يكون سهلاً:/ لحسن الحظ، لا! :) في Java، تعد الواجهة القابلة للتسلسل مسؤولة عن عمليات التسلسل . هذه الواجهة بسيطة للغاية: لا تحتاج إلى تنفيذ طريقة واحدة لاستخدامها! هذه هي الطريقة التي ستبدو بها فئة حفظ اللعبة لدينا:
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) +
               '}';
   }
}
ثلاث مجموعات من البيانات مسؤولة عن معلومات حول المناطق والاقتصاد والدبلوماسية، والواجهة القابلة للتسلسل تخبر جهاز جافا: " كل شيء على ما يرام، إذا كان هناك أي شيء، يمكن إجراء تسلسل للكائنات من هذه الفئة ". الواجهة التي لا تحتوي على أي طرق تبدو غريبة:/ لماذا هي مطلوبة؟ الإجابة على هذا السؤال أعلاه: فقط لتوفير المعلومات الضرورية لجهاز Java. في إحدى المحاضرات السابقة، ذكرنا بإيجاز واجهات العلامات. هذه واجهات إعلامية خاصة تقوم ببساطة بتمييز فئاتنا بمعلومات إضافية ستكون مفيدة لجهاز Java في المستقبل. ليس لديهم أي أساليب تحتاج إلى التنفيذ. لذا، فإن Serializable هي إحدى هذه الواجهات. نقطة أخرى مهمة: المتغير private static final long serialVersionUIDالذي حددناه في الفصل. لماذا هو مطلوب؟ يحتوي هذا الحقل على معرف الإصدار الفريد للفئة المتسلسلة . أي فئة تطبق الواجهة القابلة للتسلسل لها معرف إصدار. يتم حسابه بناءً على محتويات الفصل - الحقول وترتيب الإعلان والطرق. وإذا قمنا بتغيير نوع الحقل و/أو عدد الحقول في فصلنا، فسيتغير معرف الإصدار على الفور. تتم كتابة 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();
   }
}
كما ترون، قمنا بإنشاء موضوعين - FileOutputStreamو ObjectOutputStream. الأول منهما يمكنه كتابة البيانات إلى ملف، والثاني يمكنه تحويل الكائنات إلى بايت. لقد رأيت بالفعل إنشاءات "متداخلة" مماثلة، على سبيل المثال، new BufferedReader(new InputStreamReader(...))في المحاضرات السابقة، لذلك لا ينبغي أن تخيفك :) من خلال إنشاء مثل هذه "السلسلة" من خيطين، نقوم بتنفيذ كلتا المهمتين: نحول الكائن SavedGameإلى مجموعة من البايتات وحفظه في ملف باستخدام الطريقة writeObject(). وبالمناسبة، لم نتحقق حتى مما حصلنا عليه! لقد حان الوقت لإلقاء نظرة على الملف! *ملاحظة: لا يلزم إنشاء الملف مسبقًا. إذا كان الملف بهذا الاسم غير موجود، فسيتم إنشاؤه تلقائيًا* وإليك محتوياته! ¬н sr SavedGame [ DiplomacyInfot [Ljava/lang/String;[ ResourcesInfoq ~ [ الأراضيInfoq ~ xpur [Ljava.lang.String;ТVзй{G xp t pФранцвоюРμС‚ SЃ R РѕСЃСЃРёРμР№, Р˜С Ѓ РїР° РЅРёСЏ Р·Р°Ряла позицию РЅРμйтралитРμта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=[إسبانيا لديها 6 مقاطعات، روسيا لديها 10 مقاطعات، فرنسا لديها 8 مقاطعات], ResourcesInfo=[إسبانيا لديها 100 ذهبية، روسيا لديها 80 ذهبية، فرنسا لديها 90 ذهبية], DiplomacyInfo=[فرنسا في حالة حرب مع روسيا، إسبانيا اتخذت موقف الحياد]} عظيم! تمكنا أولاً من حفظ حالة لعبتنا في ملف، ثم استعادتها من الملف. الآن دعونا نحاول أن نفعل الشيء نفسه، ولكن قم بإزالة SavedGameمعرف الإصدار من فصلنا. لن نقوم بإعادة كتابة كلا الفئتين، فالرمز الموجود فيهما سيكون هو نفسه، وسنقوم فقط SavedGameبإزالة private static final long serialVersionUID. هذا هو هدفنا بعد التسلسل: ¬н sr SavedGamei€MiiuОm‰ [ DiplomacyInfot [Ljava/lang/String;[ ResourcesInfoq ~ [ الأراضيInfoq ~ xpur [Ljava.lang.String;ТВзй{G xp t pФранция РІРѕСЋРμС ‚ СЃ Р РѕСЃСЃРёРμР№, Р˜СЃРїР°РЅРЏР·Р°РЅСЏР»Р° RїРѕР·РёС†РёСЋ РЅРμйтралитРμтаuq ~ t "РЈ Р˜С ЃРї ании 100 Р · олотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Р˜СЃРїР°РЅР ص 6 ر ×СЂРѕРІРёРЅС† РёР№t %РЈ Р РѕСЃСЃРёРё 10 провнинцийt &РЈ РЅС†РёРёС 8 РїС ЂРѕРІРІРёРЅС†РёРЅА وعند محاولة إلغاء تسلسلها هذا ما حدث: InvalidClassException: فئة محلية غير متوافقة: تيار classdesc serialVersionUID = - 196410440475012755, local class serialVersionUID = -6675950253085108747 هذا هو نفس الاستثناء الذي تم ذكره أعلاه، يمكنك قراءة المزيد عن هذا في مقالة أحد طلابنا، بالمناسبة، فقد فاتنا نقطة مهمة، من الواضح أن السلاسل والأوليات يتم تسلسلها بسهولة: من المؤكد أن Java لديها بعض منها، ثم هناك آليات مدمجة لذلك. ولكن ماذا لو كانت فئتنا serializableتحتوي على حقول لا يتم التعبير عنها كأوليات، ولكن كمراجع لكائنات أخرى؟ لنقم، على سبيل المثال، بإنشاء فصول منفصلة 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: فشل DiplomacyInfo! في الواقع، هنا هو الجواب على سؤالنا. عند إجراء تسلسل لكائن ما، يتم إجراء تسلسل لجميع الكائنات التي يشير إليها في متغيرات مثيله. وإذا كانت هذه الكائنات تشير أيضًا إلى كائنات ثالثة، فسيتم إجراء تسلسل لها أيضًا. وهكذا إلى ما لا نهاية. يجب أن تكون جميع الفئات في هذه السلسلة قابلة للتسلسل، وإلا فلن تكون قابلة للتسلسل وسيتم طرح استثناء. وهذا، بالمناسبة، يمكن أن يخلق مشاكل في المستقبل. ماذا يجب أن نفعل، على سبيل المثال، إذا لم نكن بحاجة إلى جزء من الفصل أثناء التسلسل؟ أو، على سبيل المثال، تم "توريث" الفصل الدراسي 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