CodeGym /مدونة جافا /Random-AR /التسلسل وإلغاء التسلسل في جافا
John Squirrels
مستوى
San Francisco

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

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