سلام! در درس امروز درباره سریال سازی و سریال زدایی در جاوا صحبت می کنیم. با یک مثال ساده شروع می کنیم. فرض کنید یک بازی کامپیوتری ساخته اید. اگر در دهه 90 بزرگ شدهاید و کنسولهای بازی آن دوران را به یاد میآورید، احتمالاً میدانید که آنها فاقد چیزی بودند که ما امروز آن را بدیهی میدانیم - توانایی ذخیره و بارگذاری بازیها :) اگر نه، آن را تصور کنید!
می ترسم امروز یک بازی بدون این توانایی ها محکوم به فنا باشد! به هر حال "ذخیره" و "بارگذاری" یک بازی به چه معناست؟ خوب، ما معنی معمولی را درک می کنیم: می خواهیم بازی را از جایی که متوقف کردیم ادامه دهیم. برای انجام این کار، یک نوع "نقطه چک" ایجاد می کنیم که سپس از آن برای بارگذاری بازی استفاده می کنیم. اما این برای یک برنامه نویس به جای یک گیمر معمولی چه معنایی دارد؟ پاسخ ساده است: ما وضعیت برنامه خود را ذخیره می کنیم . فرض کنید شما به عنوان اسپانیا در Strategium بازی می کنید. بازی شما حالتی دارد: چه کسی مالک چه سرزمینی است، چه کسی چند منبع دارد، چه کسی با چه کسی در اتحاد است، چه کسی با چه کسی در جنگ است و غیره. ما باید به نحوی این اطلاعات، وضعیت برنامه خود را ذخیره کنیم تا در آینده آن را بازیابی کنیم و بازی را ادامه دهیم. زیرا سریال سازی و غیرواقعی سازی دقیقاً برای همین است. سریال سازی فرآیند ذخیره سازی وضعیت یک شی در یک دنباله از بایت ها است. Deserialization فرآیند بازیابی یک شی از این بایت ها است. هر شی جاوا را می توان به دنباله بایت تبدیل کرد. چرا ما به آن نیاز داریم؟ ما بیش از یک بار گفته ایم که برنامه ها به تنهایی وجود ندارند. اغلب، آنها با برنامه های دیگر تعامل دارند، داده ها را مبادله می کنند، و غیره. و یک توالی بایت فرمت مناسب و کارآمدی است. مثلاً میتوانیم
در جاوا مشکلات از این دست با استفاده از

SavedGame
شیء خود را به دنبالهای از بایت تبدیل کنیم، این بایتها را از طریق شبکه به رایانه دیگری بفرستیم و سپس در رایانه دوم این بایتها را دوباره به یک شی جاوا تبدیل کنیم! سخت به نظر می رسد، درست است؟ و اجرای این پروسه دردناک به نظر می رسد :/ خوشبختانه اینطور نیست! :) در جاوا، Serializable
رابط مسئول فرآیند سریال سازی است. این رابط بسیار ساده است: برای استفاده از آن نیازی به پیاده سازی یک روش واحد ندارید! کلاس ذخیره بازی ما به این شکل ساده به نظر می رسد:
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) +
'}';
}
}
این سه آرایه مسئول اطلاعات مربوط به مناطق، منابع و دیپلماسی هستند. رابط Serializable به ماشین مجازی جاوا می گوید: " همه چیز درست است - در صورت لزوم، اشیاء این کلاس را می توان سریال کرد ". یک رابط بدون رابط واحد عجیب به نظر می رسد :/ چرا لازم است؟ پاسخ این سوال را می توان در بالا مشاهده کرد: این فقط در خدمت ارائه اطلاعات لازم به ماشین مجازی جاوا است. در یکی از درس های قبلی به طور خلاصه به رابط های نشانگر اشاره کردیم . اینها رابط های اطلاعاتی ویژه ای هستند که به سادگی کلاس های ما را با اطلاعات اضافی مشخص می کنند که در آینده برای ماشین جاوا مفید خواهد بود. آنها هیچ روشی ندارند که شما باید پیاده سازی کنید. Serializable
یکی از این رابط ها است. private static final long serialVersionUID
نکته مهم دیگر: چرا به متغیری که در کلاس تعریف کردیم نیاز داریم ؟ چرا نیاز است؟ این فیلد حاوی یک شناسه منحصر به فرد برای نسخه کلاس سریال شده است . هر کلاسی که Serializable
اینترفیس را پیاده سازی می کند یک شناسه دارد version
. بر اساس محتویات کلاس محاسبه می شود: فیلدهای آن، ترتیب اعلان آنها، روش ها و غیره. اگر نوع فیلد و/یا تعداد فیلدهای کلاس خود را تغییر دهیم، شناسه نسخه بلافاصله تغییر می کند. . serialVersionUID
زمانی که کلاس سریالی می شود نیز نوشته می شود. هنگامی که ما سعی می کنیم یک شی را از مجموعه ای از بایت ها بازیابی کنیم، مقدار مربوطه serialVersionUID
با مقدار serialVersionUID
for کلاس در برنامه ما مقایسه می شود. اگر مقادیر مطابقت ندارند، یک 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 neutral position"};
SavedGame savedGame = new SavedGame(territoryInfo, resourcesInfo, 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 free resources
objectOutputStream.close();
}
}
همانطور که می بینید، ما 2 جریان ایجاد کردیم: FileOutputStream
و ObjectOutputStream
. اولی می تواند داده ها را در یک فایل بنویسد و دومی اشیا را به بایت تبدیل می کند. شما قبلاً ساختارهای مشابه "تودرتو" را دیده اید، به عنوان مثال new BufferedReader(new InputStreamReader(...))
، در درس های قبلی، بنابراین آنها نباید شما را بترسانند :) با ایجاد چنین "زنجیره ای" از دو جریان، ما هر دو کار را انجام می دهیم: ما شی را SavedGame
به یک مجموعه تبدیل می کنیم. بایت و با استفاده از writeObject()
روش آن را در یک فایل ذخیره کنید. و اتفاقاً ما حتی به چیزی که به دست آوردیم نگاه نکردیم! وقت آن است که به پرونده نگاه کنید! *توجه: لازم نیست فایل را از قبل ایجاد کنید. اگر فایلی با آن نام وجود نداشته باشد، به صورت خودکار ایجاد می شود* و اینم محتویات آن!
¬н sr SavedGame [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territoriesInfoq ~ xpur [Ljava.lang.String;ТVзй{G xp t pФранция воюет СЃ Россией, Рспания заняла позицию нейтралитетаuq ~ t "РЈ Рспании 100 золотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Рспании 6 провинцийt %РЈ Р РѕСЃСЃРёРё 10 провинцийt &РЈ Франции 8 провинций
اوه اوه :( به نظر میاد برنامه ما کار نکرده :( در واقع کار کرده مجموعه ای از بایت ها به نظر می رسد :) این بازی ذخیره شده ما است!اگر بخواهیم شی اصلی خود را بازیابی کنیم، یعنی بازی را از جایی که متوقف کردیم شروع کنیم و ادامه دهیم، پس به روند معکوس نیاز داریم: deserialization. در اینجا به چه شکل خواهد بود مورد:
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=["Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces], resourcesInfo=[Spain has 100 gold, Russia has 80 gold, France has 90 gold], diplomacyInfo=[France is at war with Russia, Spain has taken a neutral position]}
عالی! ما موفق شدیم ابتدا وضعیت بازی خود را در یک فایل ذخیره کنیم و سپس آن را از فایل بازیابی کنیم. حالا بیایید سعی کنیم همین کار را انجام دهیم، اما بدون شناسه نسخه برای SavedGame
کلاس ما. ما هر دو کلاس خود را بازنویسی نمی کنیم. کد آنها ثابت می ماند، اما ما private static final long serialVersionUID
از SavedGame
کلاس حذف می کنیم. در اینجا هدف ما پس از سریال سازی است:
¬н sr SavedGameі€MіuОm‰ [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territoriesInfoq ~ xpur [Ljava.lang.String;ТVзй{G xp t pФранция воюет СЃ Россией, Рспания заняла позицию нейтралитетаuq ~ t "РЈ Рспании 100 золотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Рспании 6 провинцийt %РЈ Р РѕСЃСЃРёРё 10 провинцийt &РЈ Франции 8 провинций
اما ببینید چه اتفاقی میافتد وقتی میخواهیم آن را غیرواقعی کنیم:
InvalidClassException: local class incompatible: stream classdesc serialVersionUID = -196410440475012755, local class serialVersionUID = -6675950253085108747
این همان استثنایی است که در بالا به آن اشاره کردیم. اتفاقاً ما یک چیز مهم را از دست دادیم. منطقی است که رشته ها و primitives را می توان به راحتی سریال کرد: جاوا احتمالاً نوعی مکانیزم داخلی برای انجام این کار دارد. اما اگر serializable
کلاس ما فیلدهایی داشته باشد که اولیه نیستند، بلکه به اشیاء دیگر ارجاع می دهند، چه؟ به عنوان مثال، بیایید کلاس های جداگانه TerritoriesInfo
و کلاس ResourcesInfo
هایی DiplomacyInfo
برای کار با SavedGame
کلاس خود ایجاد کنیم.
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 + '\'' +
'}';
}
}
و اکنون یک سوال مطرح می شود: آیا اگر بخواهیم کلاس تغییر یافته خود را سریال کنیم ، آیا همه این کلاس ها باید باشند ؟Serializable
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
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(territoriesInfo, resourcesInfo, diplomacyInfo);
FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(savedGame);
objectOutputStream.close();
}
}
نتیجه:
Exception in thread "main" java.io.NotSerializableException: DiplomacyInfo
کار نکرد! اساساً این پاسخ سؤال ماست. هنگامی که یک شی سریالی می شود، تمام اشیایی که توسط متغیرهای نمونه آن ارجاع می شوند، سریال می شوند. و اگر آن اشیاء به اشیاء دیگر نیز ارجاع دهند، آنها نیز سریالی هستند. و غیره تا بی نهایت. تمام کلاس های این زنجیره باید باشندSerializable
، در غیر این صورت سریال کردن آنها غیرممکن خواهد بود و استثنایی ایجاد می شود. به هر حال، این می تواند مشکلاتی را در مسیر ایجاد کند. اگر مثلاً هنگام سریال سازی به قسمتی از کلاس نیازی نداشته باشیم چه باید بکنیم؟ یا، برای مثال، اگر TerritoryInfo
کلاس به عنوان بخشی از کتابخانه شخص ثالث برای ما بیاید، چه میشود. و فرض کنید که اینطور نیست Serializable
و بر این اساس، ما نمی توانیم آن را تغییر دهیم. معلوم میشود که نمیتوانیم TerritoryInfo
فیلدی را به کلاس خود اضافه کنیم SavedGame
، زیرا انجام این کار کل SavedGame
کلاس را غیرقابل سریالسازی میکند! این یک مشکل است :/ 
transient
کلمه کلیدی حل می شود. اگر این کلمه کلیدی را به فیلدی از کلاس خود اضافه کنید، آن قسمت سریالی نمی شود. بیایید سعی کنیم یکی از SavedGame
فیلدهای نمونه کلاس را گذرا کنیم. سپس یک شی را سریال سازی و بازیابی می کنیم.
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
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(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='Spain has 100 gold, Russia has 80 gold, France has 90 gold'}, diplomacyInfo=DiplomacyInfo{info='France is at war with Russia, Spain has taken a neutral position'}}
علاوه بر این، ما پاسخی به سؤال خود در مورد اینکه چه مقداری به یک transient
فیلد اختصاص داده می شود، دریافت کردیم. به آن مقدار پیش فرض اختصاص داده می شود. برای اشیا، این است null
. وقتی چند دقیقه وقت دارید می توانید این مقاله عالی در مورد سریال سازی را بخوانید.
همچنین Externalizable
رابط کاربری را ذکر می کند که در درس بعدی در مورد آن صحبت خواهیم کرد. علاوه بر این، کتاب "جاوا سر اول" فصلی در این زمینه دارد. کمی بهش توجه کن :)
GO TO FULL VERSION