التسلسل في جافا. ليس بهذه البساطة



التسلسل هو عملية تترجم كائنًا إلى تسلسل من البايتات ، ومن ثم يمكن استعادته بالكامل. لماذا هذا مطلوب؟ الحقيقة هي ، أثناء تنفيذ البرنامج العادي ، أن الحد الأقصى لعمر أي كائن معروف - من إطلاق البرنامج إلى نهايته. يسمح لك التسلسل بتوسيع هذا الإطار و "إعطاء الحياة" للكائن بالطريقة نفسها بين إطلاق البرنامج.

مكافأة إضافية لكل شيء هو الحفاظ على منصة مشتركة. بغض النظر عن نظام التشغيل لديك ، فإن الترجمة التسلسلية تترجم الكائن إلى دفق من البايتات ، والتي يمكن استعادتها على أي نظام تشغيل. إذا كنت بحاجة إلى نقل كائن عبر الشبكة ، فيمكنك إجراء تسلسل للكائن وحفظه في ملف ونقله عبر الشبكة إلى المستلم. سيكون قادرًا على استعادة الكائن المستلم. يتيح لك التسلسل أيضًا إمكانية استدعاء أساليب الاتصال عن بُعد (Java RMI) الموجودة على أجهزة مختلفة مع ، ربما ، أنظمة تشغيل مختلفة ، والعمل معهم كما لو كانوا على جهاز عملية استدعاء java.

تنفيذ آلية التسلسل بسيط للغاية. يحتاج فصلك إلى تطبيق واجهة Serializable . هذه الواجهة عبارة عن معرف ليس له طرق ، لكنه يخبر jvm أنه يمكن إجراء تسلسل لكائنات هذه الفئة. نظرًا لأن آلية التسلسل متصلة بنظام الإدخال / الإخراج الأساسي وترجمة الكائن إلى دفق بايت ، لتنفيذه ، يجب عليك إنشاء دفق إخراج OutputStream ، وحزمه في ObjectOutputStream واستدعاء طريقة writeObject (). لاستعادة كائن ، تحتاج إلى حزمة InputStream في ObjectInputStream واستدعاء طريقة readObject ().

أثناء عملية التسلسل ، إلى جانب الكائن القابل للتسلسل ، يتم حفظ الرسم البياني للكائن الخاص به. أي جميع الكائنات المرتبطة بهذا ، سيتم أيضًا تسلسل كائنات الفئات الأخرى معها.

النظر في مثال على التسلسل كائن من شخص الفئة.

import java.io.*; class Home implements Serializable { private String home; public Home(String home) { this.home = home; } public String getHome() { return home; } } public class Person implements Serializable { private String name; private int countOfNiva; private String fatherName; private Home home; public Person(String name, int countOfNiva, String fatherName, Home home) { this.name = name; this.countOfNiva = countOfNiva; this.fatherName = fatherName; this.home = home; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", countOfNiva=" + countOfNiva + ", fatherName='" + fatherName + '\'' + ", home=" + home + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Home home = new Home("Vishnevaia 1"); Person igor = new Person("Igor", 2, "Raphael", home); Person renat = new Person("Renat", 2, "Raphael", home); //      ObjectOutputStream ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("person.out")); objectOutputStream.writeObject(igor); objectOutputStream.writeObject(renat); objectOutputStream.close(); //       ObjectInputStream ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream("person.out")); Person igorRestored = (Person) objectInputStream.readObject(); Person renatRestored = (Person) objectInputStream.readObject(); objectInputStream.close(); //    ByteArrayOutputStream ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream2 = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream2.writeObject(igor); objectOutputStream2.writeObject(renat); objectOutputStream2.flush(); //    ByteArrayInputStream ObjectInputStream objectInputStream2 = new ObjectInputStream( new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); Person igorRestoredFromByte = (Person) objectInputStream2.readObject(); Person renatRestoredFromByte = (Person) objectInputStream2.readObject(); objectInputStream2.close(); System.out.println("Before Serialize: " + "\n" + igor + "\n" + renat); System.out.println("After Restored From Byte: " + "\n" + igorRestoredFromByte + "\n" + renatRestoredFromByte); System.out.println("After Restored: " + "\n" + igorRestored + "\n" + renatRestored); } } 

الخلاصة:

 Before Serialize: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@355da254} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@355da254} After Restored From Byte: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} After Restored: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} 

في هذا المثال ، يتم إنشاء الفئة الرئيسية لتوضيح أنه عند إجراء تسلسل لعنصر الشخص ، يتم إجراء تسلسل للرسم البياني للكائنات به. يجب أن تقوم الفئة الرئيسية أيضًا بتطبيق واجهة Serializable ، وإلا فسيتم إجراء java.io.NotSerializableException . يصف المثال أيضًا التسلسل باستخدام فئة ByteArrayOutputStream .

يمكن استخلاص استنتاج مثير للاهتمام من نتائج تنفيذ البرنامج: عند استعادة الكائنات التي كانت تشير إلى نفس الكائن قبل إجراء التسلسل ، سيتم استعادة هذا الكائن مرة واحدة فقط . يمكن ملاحظة ذلك على نفس الارتباطات في الكائنات بعد الاسترداد:

 After Restored From Byte: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} After Restored: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} 

ومع ذلك ، يُرى أيضًا أنه عند التسجيل باستخدام دفق إخراج اثنين (لدينا ObjectInputStream و ByteArrayOutputStream ) ، سيتم إعادة إنشاء الكائن الرئيسي ، على الرغم من أنه تم إنشاؤه بالفعل من قبل في أحد التدفقات. نرى هذا في عناوين مختلفة من الكائنات المنزلية التي وردت في اثنين من التدفقات. اتضح أنه إذا قمت بالتسلسل باستخدام دفق إخراج واحد ، ثم استعادة الكائن ، فلدينا ضمان باستعادة شبكة كاملة من الكائنات دون التكرارات غير الضرورية. بالطبع ، أثناء تنفيذ البرنامج ، قد تتغير حالة الكائنات ، ولكن هذا على ضمير المبرمج.

المشكلة

يوضح المثال أيضًا أنه عند استعادة كائن ، قد يحدث ClassNotFoundException . ما هو السبب في ذلك؟ الحقيقة هي أنه يمكننا بسهولة إجراء تسلسل لكائن من فئة الشخص إلى ملف ، ونقله عبر الشبكة إلى صديقنا ، الذي يمكنه استعادة الكائن عن طريق تطبيق آخر لا توجد فيه فئة الشخص بكل بساطة.

في التسلسل. كيف تصنع؟

ماذا لو كنت تريد إدارة التسلسل بنفسك؟ على سبيل المثال ، يقوم الكائن الخاص بك بتخزين اسم المستخدم وكلمة المرور للمستخدمين. تحتاج إلى إجراء تسلسل له لمزيد من الإرسال عبر الشبكة. تمرير كلمة مرور في هذه الحالة غير موثوق بها للغاية. كيفية حل هذه المشكلة؟ هناك طريقتان. أولاً ، استخدم الكلمة الأساسية المؤقتة . ثانياً ، بدلاً من تحقيق مصلحة Serializable ، استخدم امتدادها - الواجهة Externalizable . النظر في أمثلة الطرق الأولى والثانية لمقارنتها.

الطريقة الأولى - التسلسل باستخدام عابرة

 import java.io.*; public class Logon implements Serializable { private String login; private transient String password; public Logon(String login, String password) { this.login = login; this.password = password; } @Override public String toString() { return "Logon{" + "login='" + login + '\'' + ", password='" + password + '\'' + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Logon igor = new Logon("IgorIvanovich", "Khoziain"); Logon renat = new Logon("Renat", "2500RUB"); System.out.println("Before: \n" + igor); System.out.println(renat); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Externals.out")); out.writeObject(igor); out.writeObject(renat); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Externals.out")); igor = (Logon) in.readObject(); renat = (Logon) in.readObject(); System.out.println("After: \n" + igor); System.out.println(renat); } } 

الخلاصة:

 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} After: Logon{login='IgorIvanovich', password='null'} Logon{login='Renat', password='null'} 

الطريقة الثانية - التسلسل مع تنفيذ واجهة Externalizable

 import java.io.*; public class Logon implements Externalizable { private String login; private String password; public Logon() { } public Logon(String login, String password) { this.login = login; this.password = password; } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(login); } @Override public String toString() { return "Logon{" + "login='" + login + '\'' + ", password='" + password + '\'' + '}'; } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { login = (String) in.readObject(); } public static void main(String[] args) throws IOException, ClassNotFoundException { Logon igor = new Logon("IgorIvanovich", "Khoziain"); Logon renat = new Logon("Renat", "2500RUB"); System.out.println("Before: \n" + igor); System.out.println(renat); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Externals.out")); out.writeObject(igor); out.writeObject(renat); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Externals.out")); igor = (Logon) in.readObject(); renat = (Logon) in.readObject(); System.out.println("After: \n" + igor); System.out.println(renat); } } 

الخلاصة:

 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} After: Logon{login='IgorIvanovich', password='null'} Logon{login='Renat', password='null'} 

الفرق الأول بين الخيارين اللذين يلفت انتباهك هو حجم الرمز. عند تطبيق الواجهة Externalizable ، نحتاج إلى تجاوز طريقتين: writeExternal () و readExternal () . في الأسلوب WriteExternal () ، نشير إلى الحقول التي سيتم إجراء تسلسل لها ، وكيفية قراءتها في readExternal () . عند استخدام كلمة عابرة ، نشير صراحةً إلى الحقل أو الحقول التي لا تحتاج إلى إجراء تسلسل. نلاحظ أيضًا أنه في الطريقة الثانية ، أنشأنا صراحة مُنشئًا افتراضيًا ، علاوة على ذلك ، منشئًا عامًا. لماذا يتم ذلك؟ دعنا نحاول تشغيل الكود بدون هذا المنشئ. وإلقاء نظرة على الإخراج:

 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} Exception in thread "main" java.io.InvalidClassException: Logon; no valid constructor at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:169) at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:874) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2043) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431) at Logon.main(Logon.java:45) 

حصلنا على استثناء java.io.InvalidClassException . ما هو السبب في ذلك؟ إذا تابعت تتبع المكدس ، يمكنك معرفة وجود خطوط في مُنشئ فئة ObjectStreamClass :

  if (externalizable) { cons = getExternalizableConstructor(cl); } else { cons = getSerializableConstructor(cl); 

بالنسبة للواجهة الخارجية ، سيتم استدعاء طريقة منشئ getExternalizableConstructor () ، والتي من خلالها من خلال Reflection سنحاول الحصول على المُنشئ الافتراضي للفئة التي نقوم باستعادة الكائن من أجلها. إذا لم نتمكن من العثور عليه ، أو أنه ليس عامًا ، فسنحصل على استثناء. يمكنك التغلب على هذا الموقف على النحو التالي: لا تقم بإنشاء أي مُنشئ بشكل صريح في الفصل وملء الحقول باستخدام المستوطنين والحصول على القيمة بحرف. ثم ، عند تجميع الفئة ، سيتم إنشاء مُنشئ افتراضي ، والذي سيكون متاحًا لـ getExternalizableConstructor () . بالنسبة إلى Serializable ، فإن الأسلوب getSerializableConstructor () يحصل على مُنشئ فئة الكائن ويبحث عن الفئة المطلوبة منه ، وإذا لم يعثر عليها ، فسنحصل على استثناء ClassNotFoundException . اتضح أن الاختلاف الرئيسي بين Serializable و Externalizable هو أن الأول لا يحتاج إلى مُنشئ لإنشاء استعادة كائن. وسوف تسترد ببساطة تماما من بايت. للمرة الثانية ، أثناء الاستعادة ، سيتم أولاً إنشاء كائن باستخدام المُنشئ عند نقطة الإعلان ، ثم سيتم كتابة قيم حقوله من البايتات المستلمة أثناء التسلسل. أنا شخصياً أفضل الطريقة الأولى ، فهي أبسط بكثير. علاوة على ذلك ، حتى إذا كنا لا نزال بحاجة إلى تعيين سلوك التسلسل ، لا يمكننا استخدام Externalizable ، وكذلك تنفيذ Serializable عن طريق إضافة (دون تجاوز ) أساليب WriteObject () و readObject () إليها. لكن لكي "يشتغلوا" ، يجب أن يتم توقيعهم بدقة.

 import java.io.*; public class Talda implements Serializable { private String name; private String description; public Talda(String name, String description) { this.name = name; this.description = description; } private void writeObject(ObjectOutputStream stream) throws IOException { stream.defaultWriteObject(); System.out.println("Our writeObject"); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); System.out.println("Our readObject"); } @Override public String toString() { return "Talda{" + "name='" + name + '\'' + ", description='" + description + '\'' + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Talda partizanka = new Talda("Partizanka", "Viiiski"); System.out.println("Before: \n" + partizanka); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Talda.out")); out.writeObject(partizanka); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Talda.out")); partizanka = (Talda) in.readObject(); System.out.println("After: \n" + partizanka); } } 

الخلاصة:

 Before: Talda{name='Partizanka', description='Viiiski'} Our writeObject Our readObject After: Talda{name='Partizanka', description='Viiiski'} 

داخل أساليبنا المضافة ، يتم استدعاء defaultWriteObject () و defaultReadObject () . إنهم مسؤولون عن التسلسل الافتراضي ، كما لو كان يعمل بدون الطرق التي أضفناها.

في الواقع ، هذه ليست سوى قمة جبل الجليد ، إذا واصلت الخوض في آلية التسلسل ، فبفضل درجة عالية من الاحتمالات ، يمكنك العثور على مزيد من الفروق الدقيقة ، ونجد ما نقوله: "التسلسل ... ليس بهذه البساطة".

Source: https://habr.com/ru/post/ar431524/


All Articles