التسلسل في جافا – تسلسل جافا

تسلسل في جافا تم إدخاله في JDK 1.1 وهو أحد الميزات المهمة في جافا الأساسية.

التسلسل في جافا

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

  1. Serializable في جافا
  2. إعادة تصميم الفئة مع التسلسل و serialVersionUID
  3. واجهة Java Externalizable
  4. طرق تسلسل جافا
  5. التسلسل مع التوريث
  6. نمط تسلسل الوكيل

قابلية التسلسل في جافا

إذا كنت تريد أن يكون كائن الفئة قابلاً للتسلسل، كل ما عليك فعله هو تنفيذ واجهة java.io.Serializable. قابلية التسلسل في جافا هي واجهة علامة وليست لها حقول أو طرق لتنفيذ. إنه عملية Opt-In تمكننا من جعل فئاتنا قابلة للتسلسل. تنفيذ التسلسل في جافا يتم عن طريق ObjectInputStream و ObjectOutputStream، لذا كل ما نحتاجه هو كتابة لهما لحفظه إلى ملف أو إرساله عبر الشبكة. لنرى مثالًا بسيطًا على برنامج تسلسل في جافا.

package com.journaldev.serialization;

import java.io.Serializable;

public class Employee implements Serializable {

//	private static final long serialVersionUID = -6470090944414208496L;
	
	private String name;
	private int id;
	transient private int salary;
//	private String password;
	
	@Override
	public String toString(){
		return "Employee{name="+name+",id="+id+",salary="+salary+"}";
	}
	
	//أساليب getter و setter
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public int getSalary() {
		return salary;
	}

	public void setSalary(int salary) {
		this.salary = salary;
	}

//	public String getPassword() {
//		return password;
//	}
//
//	public void setPassword(String password) {
//		this.password = password;
//	}
	
}

لاحظ أنها شجرة جافا بسيطة مع بعض الخصائص وأساليب getter-setter. إذا كنت تريد أن يكون خصائص الكائن غير مُسلسلة إلى التيار، يمكنك استخدام الكلمة الرئيسية transient مثلما فعلت مع متغير الراتب. الآن فلنفترض أننا نريد كتابة كائناتنا إلى ملف ثم تسلسلها من نفس الملف. لذا نحتاج إلى أساليب مساعدة ستستخدم ObjectInputStream و ObjectOutputStream لأغراض التسلسل.

package com.journaldev.serialization;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
 * A simple class with generic serialize and deserialize method implementations
 * 
 * @author pankaj
 * 
 */
public class SerializationUtil {

	// تعريف الكائن من الملف المعطى
	public static Object deserialize(String fileName) throws IOException,
			ClassNotFoundException {
		FileInputStream fis = new FileInputStream(fileName);
		ObjectInputStream ois = new ObjectInputStream(fis);
		Object obj = ois.readObject();
		ois.close();
		return obj;
	}

	// تسلسل الكائن المعطى وحفظه في ملف
	public static void serialize(Object obj, String fileName)
			throws IOException {
		FileOutputStream fos = new FileOutputStream(fileName);
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(obj);

		fos.close();
	}

}

يرجى ملاحظة أن وسائط الطريقة تعمل مع الكائن الذي هو الفئة الأساسية لأي كائن جافا. تمت كتابته بهذه الطريقة ليكون عامًا بالطبيعة. الآن دعنا نكتب برنامج اختبار لنرى التسلسل في جافا.

package com.journaldev.serialization;

import java.io.IOException;

public class SerializationTest {
	
	public static void main(String[] args) {
		String fileName="employee.ser";
		Employee emp = new Employee();
		emp.setId(100);
		emp.setName("Pankaj");
		emp.setSalary(5000);
		
		//تسلسل إلى ملف
		try {
			SerializationUtil.serialize(emp, fileName);
		} catch (IOException e) {
			e.printStackTrace();
			return;
		}
		
		Employee empNew = null;
		try {
			empNew = (Employee) SerializationUtil.deserialize(fileName);
		} catch (ClassNotFoundException | IOException e) {
			e.printStackTrace();
		}
		
		System.out.println("emp Object::"+emp);
		System.out.println("empNew Object::"+empNew);
	}
}

عند تشغيل البرنامج اختبار التسلسل أعلاه في جافا، نحصل على الإخراج التالي.

emp Object::Employee{name=Pankaj,id=100,salary=5000}
empNew Object::Employee{name=Pankaj,id=100,salary=0}

نظرًا لأن الراتب هو متغير عابر، فإن قيمته لم تُحفظ في الملف وبالتالي لم تُسترجع في الكائن الجديد. بالمثل، لا تُسلسل قيم المتغيرات الثابتة static لأنها تنتمي إلى الفئة وليست الكائن.

تحسين الفئة باستخدام التسلسل وserialVersionUID

يسمح التسلسل في جافا ببعض التغييرات في فئة جافا إذا كان بإمكان تجاهلها. بعض التغييرات في الفئة التي لن تؤثر على عملية فك التسلسل هي:

  • إضافة متغيرات جديدة إلى الفئة
  • تغيير المتغيرات من عابرة إلى غير عابرة، للتسلسل فهو مثل إضافة حقل جديد.
  • تغيير المتغير من ثابت إلى غير ثابت، للتسلسل فهو مثل إضافة حقل جديد.

لكن لكي تعمل جميع هذه التغييرات، يجب أن يكون لديك serialVersionUID معرفة في الفئة. لنكتب فئة اختبارية فقط لفك التسلسل للملف المتسلسل بالفعل من الفئة اختبارية سابقة.

package com.journaldev.serialization;

import java.io.IOException;

public class DeserializationTest {

	public static void main(String[] args) {

		String fileName="employee.ser";
		Employee empNew = null;
		
		try {
			empNew = (Employee) SerializationUtil.deserialize(fileName);
		} catch (ClassNotFoundException | IOException e) {
			e.printStackTrace();
		}
		
		System.out.println("empNew Object::"+empNew);
		
	}
}

الآن ألغ تعليق متغير “كلمة المرور” وطرقها المحددة والمعينة من فئة الموظف وقم بتشغيلها. ستحصل على استثناء أدناه؛

java.io.InvalidClassException: com.journaldev.serialization.Employee; local class incompatible: stream classdesc serialVersionUID = -6470090944414208496, local class serialVersionUID = -6234198221249432383
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:604)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1601)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1514)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1750)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1347)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:369)
	at com.journaldev.serialization.SerializationUtil.deserialize(SerializationUtil.java:22)
	at com.journaldev.serialization.DeserializationTest.main(DeserializationTest.java:13)
empNew Object::null

السبب واضح أن serialVersionUID للفئة السابقة والفئة الجديدة مختلفة. في الواقع، إذا لم تعرف الفئة serialVersionUID، فإنه سيتم حسابه تلقائيًا وتعيينه للفئة. يستخدم Java متغيرات الفئة، والطرق، واسم الفئة، والحزمة وما إلى ذلك لتوليد هذا الرقم الطويل الفريد. إذا كنت تعمل مع أي بيئة تطوير متكاملة، فستحصل تلقائيًا على تحذير يفيد بأن “الفئة القابلة للتسلسل Employee لا تعلن عن حقل serialVersionUID نهائيًا وثابتًا من النوع long”. يمكننا استخدام الأداة الفعالة للغة Java “serialver” لتوليد serialVersionUID للفئة، يمكننا تشغيلها لفئة Employee بالأمر أدناه.

SerializationExample/bin$serialver -classpath . com.journaldev.serialization.Employee

لاحظ أنه ليس من الضروري أن يتم إنشاء إصدار التسلسل الفردي من هذا البرنامج نفسه، يمكننا تعيين قيمة نريدها. يجب أن تكون هناك فقط لتمكين عملية فك التسلسل من التعرف على أن الفئة الجديدة هي الإصدار الجديد لنفس الفئة ويجب أن يتم فك تسلسلها إذا كان ذلك ممكنًا. على سبيل المثال، قم بإلغاء تعليق حقل serialVersionUID فقط من فئة Employee وشغل برنامج SerializationTest. الآن ألغ تعليق حقل كلمة المرور من فئة الموظف وقم بتشغيل برنامج DeserializationTest وسترى أن تدفق الكائن يتم فك تسلسله بنجاح لأن التغيير في فئة الموظف متوافق مع عملية التسلسل.

واجهة Java Externalizable

إذا لاحظت عملية التسلسل في جافا، يتم ذلك تلقائيًا. في بعض الأحيان نريد إخفاء بيانات الكائن للحفاظ على سلامتها. يمكننا القيام بذلك عن طريق تنفيذ واجهة java.io.Externalizable وتوفير تنفيذ لطرق writeExternal() و readExternal() لاستخدامها في عملية التسلسل.

package com.journaldev.externalization;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Person implements Externalizable{

	private int id;
	private String name;
	private String gender;
	
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		out.writeInt(id);
		out.writeObject(name+"xyz");
		out.writeObject("abc"+gender);
	}

	@Override
	public void readExternal(ObjectInput in) throws IOException,
			ClassNotFoundException {
		id=in.readInt();
		// اقرأ بنفس الترتيب الذي تمت الكتابة به
		name=(String) in.readObject();
		if(!name.endsWith("xyz")) throw new IOException("corrupted data");
		name=name.substring(0, name.length()-3);
		gender=(String) in.readObject();
		if(!gender.startsWith("abc")) throw new IOException("corrupted data");
		gender=gender.substring(3);
	}

	@Override
	public String toString(){
		return "Person{id="+id+",name="+name+",gender="+gender+"}";
	}
	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getGender() {
		return gender;
	}

	public void setGender(String gender) {
		this.gender = gender;
	}

}

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

package com.journaldev.externalization;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class ExternalizationTest {

	public static void main(String[] args) {
		
		String fileName = "person.ser";
		Person person = new Person();
		person.setId(1);
		person.setName("Pankaj");
		person.setGender("Male");
		
		try {
			FileOutputStream fos = new FileOutputStream(fileName);
			ObjectOutputStream oos = new ObjectOutputStream(fos);
		    oos.writeObject(person);
		    oos.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		FileInputStream fis;
		try {
			fis = new FileInputStream(fileName);
			ObjectInputStream ois = new ObjectInputStream(fis);
		    Person p = (Person)ois.readObject();
		    ois.close();
		    System.out.println("Person Object Read="+p);
		} catch (IOException | ClassNotFoundException e) {
			e.printStackTrace();
		}
	    
	}
}

عند تشغيل البرنامج أعلاه، نحصل على الناتج التالي.

Person Object Read=Person{id=1,name=Pankaj,gender=Male}

لذلك، أيهما أفضل للاستخدام في التسلسل في جافا. في الواقع، من الأفضل استخدام واجهة Serializable وبحلول نهاية المقال، ستعرف لماذا.

طرق تسلسل جافا

لقد رأينا أن التسلسل في جافا يكون تلقائيًا وكل ما نحتاجه هو تنفيذ واجهة Serializable. التنفيذ موجود في فئتي ObjectInputStream وObjectOutputStream. ولكن ماذا لو أردنا تغيير الطريقة التي نقوم بها فيها بحفظ البيانات، على سبيل المثال لدينا بعض المعلومات الحساسة في الكائن ونرغب في تشفيرها/فك تشفيرها قبل الحفظ/استرجاعها. لهذا السبب هناك أربع طرق يمكننا توفيرها في الفئة لتغيير سلوك التسلسل. إذا كانت هذه الطرق موجودة في الفئة، فإنها تستخدم لأغراض التسلسل.

  1. readObject(ObjectInputStream ois): إذا كانت هذه الطريقة موجودة في الفئة، ستستخدم فئة ObjectInputStream الطريقة readObject() لقراءة الكائن من التدفق.

  2. writeObject(ObjectOutputStream oos): إذا كانت هذه الطريقة موجودة في الفئة، ستستخدم فئة ObjectOutputStream الطريقة writeObject() لكتابة الكائن إلى التدفق. أحد الاستخدامات الشائعة هو إعتام متغيرات الكائن للحفاظ على تمامية البيانات.

  3. Object writeReplace(): إذا كانت هذه الطريقة موجودة، سيتم استدعاؤها بعد عملية التسلسل والكائن الذي تُرجعه يتم تسلسله إلى التدفق.

  4. Object readResolve(): إذا كانت هذه الطريقة موجودة، سيتم استدعاؤها بعد عملية إلغاء التسلسل، وستُرجع هذه الطريقة الكائن النهائي إلى برنامج الcaller. إحدى الاستخدامات الشائعة لهذه الطريقة هي تنفيذ نمط Singleton مع فئات Serialized. اقرأ المزيد في التسلسل ونمط الSingleton.

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

التسلسل مع التوريث

أحيانًا نحتاج إلى تمديد فئة لا تنفذ واجهة Serializable. إذا اعتمدنا على سلوك التسلسل التلقائي وكان للفئة الأساسية بعض الحالة، فلن يتم تحويلها إلى تيار وبالتالي لن يتم استرجاعها لاحقًا. هذا هو مكان واحد، حيث تساعد الأساليب readObject() و writeObject() حقًا. من خلال توفير تنفيذهما، يمكننا حفظ حالة الفئة الأساسية إلى التيار ومن ثم استرجاعها لاحقًا. دعنا نرى هذا في العمل.

package com.journaldev.serialization.inheritance;

public class SuperClass {

	private int id;
	private String value;
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getValue() {
		return value;
	}
	public void setValue(String value) {
		this.value = value;
	}	
}

الفئة الأساسية هي فولدر جافا بسيط ولكنها لا تنفذ واجهة Serializable.

package com.journaldev.serialization.inheritance;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectInputValidation;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class SubClass extends SuperClass implements Serializable, ObjectInputValidation{

	private static final long serialVersionUID = -1322322139926390329L;

	private String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	
	@Override
	public String toString(){
		return "SubClass{id="+getId()+",value="+getValue()+",name="+getName()+"}";
	}
	
	//إضافة طريقة مساعدة للتسلسل لحفظ/تهيئة حالة الفئة الأساسية
	private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
		ois.defaultReadObject();
		
		//لاحظ أن ترتيب القراءة والكتابة يجب أن يكون متماثلًا
		setId(ois.readInt());
		setValue((String) ois.readObject());	
	}
	
	private void writeObject(ObjectOutputStream oos) throws IOException{
		oos.defaultWriteObject();
		
		oos.writeInt(getId());
		oos.writeObject(getValue());
	}

	@Override
	public void validateObject() throws InvalidObjectException {
		//التحقق من الكائن هنا
		if(name == null || "".equals(name)) throw new InvalidObjectException("name can't be null or empty");
		if(getId() <=0) throw new InvalidObjectException("ID can't be negative or zero");
	}	
}

لاحظ أن ترتيب كتابة وقراءة البيانات الإضافية إلى التسلسل يجب أن يكون متطابقًا. يمكننا وضع بعض البرمجيات في قراءة وكتابة البيانات لجعلها آمنة. كما لاحظ أن الفئة تقوم بتنفيذ واجهة ObjectInputValidation. من خلال تنفيذ الطريقة validateObject()، يمكننا وضع بعض التحققات التجارية للتأكد من أن سلامة البيانات لم يتم التأثير عليها. لنكتب فئة اختبار ونرى ما إذا كان بإمكاننا استرداد حالة الفئة الأعلى من البيانات المتسلسلة أم لا.

package com.journaldev.serialization.inheritance;

import java.io.IOException;

import com.journaldev.serialization.SerializationUtil;

public class InheritanceSerializationTest {

	public static void main(String[] args) {
		String fileName = "subclass.ser";
		
		SubClass subClass = new SubClass();
		subClass.setId(10);
		subClass.setValue("Data");
		subClass.setName("Pankaj");
		
		try {
			SerializationUtil.serialize(subClass, fileName);
		} catch (IOException e) {
			e.printStackTrace();
			return;
		}
		
		try {
			SubClass subNew = (SubClass) SerializationUtil.deserialize(fileName);
			System.out.println("SubClass read = "+subNew);
		} catch (ClassNotFoundException | IOException e) {
			e.printStackTrace();
		}
	}
}

عند تشغيل الفئة أعلاه، نحصل على الناتج التالي.

SubClass read = SubClass{id=10,value=Data,name=Pankaj}

وبهذه الطريقة، يمكننا تسلسل حالة الفئة الأعلى حتى لو لم يتم تنفيذ واجهة Serializable. تأتي هذه الاستراتيجية في المساعدة عندما تكون الفئة الأعلى فئة من جهة ثالثة لا يمكننا تغييرها.

نمط الوكيل للتسلسل

يأتي التسلسل في جافا مع بعض المخاطر الجادة مثل؛

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

بنية التفضيل التسلسلي لجافا هي وسيلة لتحقيق أمان أكبر مع التسلسل. في هذه البنية، يُستخدم فئة داخلية خاصة وثابتة كبروكسي لأغراض التسلسل. تم تصميم هذه الفئة بطريقة تحتفظ بحالة الفئة الرئيسية. يتم تنفيذ هذه البنية عن طريق تنفيذ بشكل صحيح الأساليب readResolve() و writeReplace(). دعونا نبدأ أولاً بكتابة فئة تنفيذ بنية التفضيل التسلسلي ومن ثم سنحللها لفهم أفضل.

package com.journaldev.serialization.proxy;

import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Data implements Serializable{

	private static final long serialVersionUID = 2087368867376448459L;

	private String data;
	
	public Data(String d){
		this.data=d;
	}

	public String getData() {
		return data;
	}

	public void setData(String data) {
		this.data = data;
	}
	
	@Override
	public String toString(){
		return "Data{data="+data+"}";
	}
	
	// فئة بروكسي التسلسل
	private static class DataProxy implements Serializable{
	
		private static final long serialVersionUID = 8333905273185436744L;
		
		private String dataProxy;
		private static final String PREFIX = "ABC";
		private static final String SUFFIX = "DEFG";
		
		public DataProxy(Data d){
			// تحجيم البيانات لأغراض الأمان
			this.dataProxy = PREFIX + d.data + SUFFIX;
		}
		
		private Object readResolve() throws InvalidObjectException {
			if(dataProxy.startsWith(PREFIX) && dataProxy.endsWith(SUFFIX)){
			return new Data(dataProxy.substring(3, dataProxy.length() -4));
			}else throw new InvalidObjectException("data corrupted");
		}
		
	}
	
	// استبدال كائن مسلسل بكائن DataProxy
	private Object writeReplace(){
		return new DataProxy(this);
	}
	
	private void readObject(ObjectInputStream ois) throws InvalidObjectException{
		throw new InvalidObjectException("Proxy is not used, something fishy");
	}
}
  • يجب أن تقوم الفئتان Data و DataProxy بتنفيذ واجهة Serializable.
  • يجب على DataProxy أن يكون قادرًا على الحفاظ على حالة كائن Data.
  • DataProxy هي فئة داخلية خاصة وثابتة، بحيث لا يمكن للفئات الأخرى الوصول إليها.
  • يجب على DataProxy أن يحتوي على مُنشئ واحد فقط يأخذ Data كوسيط.
  • يجب أن توفر فئة Data الأساسية الأسلوب writeReplace() الذي يعيد نموذج DataProxy. لذا، عند تسلسل كائن Data، يكون التيار المُرجعي هو فئة DataProxy. ومع ذلك، لا يمكن استخدام فئة DataProxy مباشرة لأنها ليست مرئية خارجياً.
  • يجب أن تقوم فئة DataProxy بتنفيذ الأسلوب readResolve() الذي يعيد كائن Data. لذا، عند فك تسلسل كائن Data، يتم فك تسلسل DataProxy داخليًا وعند استدعاء أسلوب readResolve()، نحصل على كائن Data.
  • أخيرًا، قم بتنفيذ الطريقة readObject() في فئة Data وقم برمي استثناء InvalidObjectException لتجنب هجمات القراصنة الذين يحاولون تزييف متداول Data وتحليله.

لنكتب اختبارًا صغيرًا للتحقق مما إذا كان التنفيذ يعمل أم لا.

package com.journaldev.serialization.proxy;

import java.io.IOException;

import com.journaldev.serialization.SerializationUtil;

public class SerializationProxyTest {

	public static void main(String[] args) {
		String fileName = "data.ser";
		
		Data data = new Data("Pankaj");
		
		try {
			SerializationUtil.serialize(data, fileName);
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		try {
			Data newData = (Data) SerializationUtil.deserialize(fileName);
			System.out.println(newData);
		} catch (ClassNotFoundException | IOException e) {
			e.printStackTrace();
		}
	}

}

عند تشغيل الفئة أعلاه، نحصل على الإخراج التالي في وحدة التحكم.

Data{data=Pankaj}

إذا قمت بفتح ملف data.ser، يمكنك رؤية أن كائن DataProxy يتم حفظه كتدفق في الملف.

قم بتنزيل مشروع تسلسل Java

هذا كل شيء بخصوص التسلسل في Java، يبدو أمرًا بسيطًا ولكن يجب علينا استخدامه بحذر ومن الأفضل عدم الاعتماد دائمًا على التنفيذ الافتراضي. قم بتنزيل المشروع من الرابط أعلاه وتفاعل معه لتعلم المزيد.

Source:
https://www.digitalocean.com/community/tutorials/serialization-in-java