สวัสดี! ในบทเรียนวันนี้ เราพูดถึงการทำให้เป็นซีเรียลไลเซชันและดีซีเรียลไลเซชันใน Java เราจะเริ่มต้นด้วยตัวอย่างง่ายๆ สมมติว่าคุณได้สร้างเกมคอมพิวเตอร์ หากคุณเติบโตในยุค 90 และจำเครื่องเล่นเกมในยุคนั้นได้ คุณอาจรู้ว่าพวกเขายังขาดบางสิ่งที่เรามองข้ามไปในปัจจุบัน นั่นคือความสามารถในการบันทึกและโหลดเกม :) ถ้าไม่ ลองนึกภาพดูสิ! ฉันกลัวว่าวันนี้เกมที่ไม่มีความสามารถเหล่านี้จะถึงวาระ! การ "บันทึก" และ "โหลด" เกมหมายความว่าอย่างไร เราเข้าใจความหมายทั่วไป: เราต้องการเล่นเกมต่อจากจุดที่เราค้างไว้ ในการทำเช่นนี้ เราสร้าง "จุดตรวจสอบ" ซึ่งเราจะใช้ในการโหลดเกม แต่สิ่งนี้มีความหมายอย่างไรกับโปรแกรมเมอร์มากกว่าเกมเมอร์ทั่วไป คำตอบนั้นง่าย:. สมมติว่าคุณกำลังเล่นเป็นสเปนใน Strategium เกมของคุณมีสถานะ: ใครเป็นเจ้าของดินแดนใด, ใครมีทรัพยากรเท่าไร, ใครเป็นพันธมิตรกับใคร, ใครทำสงครามกับใคร, และอื่นๆ เราต้องบันทึกข้อมูลนี้ สถานะของโปรแกรมของเรา เพื่อกู้คืนในอนาคตและดำเนินเกมต่อไป สำหรับสิ่งนี้คือสิ่งที่ต้องทำให้ เป็นซีเรียลไลเซชันและ ดี ซีเรียลไลเซชัน การทำให้เป็นอนุกรมเป็นกระบวนการจัดเก็บสถานะของวัตถุในลำดับของไบต์ ดีซีเรียลไลเซชันเป็นกระบวนการกู้คืนวัตถุจากไบต์เหล่านี้ อ็อบเจกต์ Java ใดๆ สามารถแปลงเป็นลำดับไบต์ได้ ทำไมเราต้องการสิ่งนั้น? เราได้กล่าวไปแล้วหลายครั้งว่าโปรแกรมไม่ได้มีอยู่โดยตัวของมันเอง บ่อยครั้งที่พวกเขาโต้ตอบกับโปรแกรมอื่น แลกเปลี่ยนข้อมูล ฯลฯ และลำดับไบต์ก็เป็นรูปแบบที่สะดวกและมีประสิทธิภาพ ตัวอย่างเช่น เราสามารถเปลี่ยน
SavedGame
อ็อบเจกต์ของเราเป็นลำดับของไบต์ ส่งไบต์เหล่านี้ผ่านเครือข่ายไปยังคอมพิวเตอร์เครื่องอื่น จากนั้นในคอมพิวเตอร์เครื่องที่สองก็เปลี่ยนไบต์เหล่านี้กลับเป็นอ็อบเจกต์ Java! ฟังดูยากใช่ไหม? และการดำเนินการตามขั้นตอนนี้ดูเหมือนจะเจ็บปวด :/ อย่างมีความสุข ไม่เป็นเช่นนั้น! :) ใน Java, theSerializable
อินเทอร์เฟซมีหน้าที่รับผิดชอบในกระบวนการทำให้เป็นอนุกรม อินเทอร์เฟซนี้เรียบง่ายมาก: คุณไม่จำเป็นต้องใช้วิธีใดวิธีหนึ่งเพื่อใช้งาน! นี่คือหน้าตาของคลาสเซฟเกมของเราที่เรียบง่าย:
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 บอกเครื่องเสมือน Java: " ทุกอย่างเรียบร้อย — ถ้าจำเป็น อ็อบเจ็กต์ของคลาสนี้สามารถทำให้เป็นอนุกรมได้ " อินเทอร์เฟซที่ไม่มีอินเทอร์เฟซเดียวดูแปลก: / ทำไมจึงจำเป็น คำตอบสำหรับคำถามนี้สามารถดูได้ด้านบน: ทำหน้าที่ให้ข้อมูลที่จำเป็นแก่เครื่องเสมือน Java เท่านั้น ในบทเรียนก่อนหน้านี้ เราได้กล่าวถึงส่วนต่อประสานเครื่องหมายไว้ สั้น ๆ นี่คืออินเทอร์เฟซข้อมูลพิเศษที่ทำเครื่องหมายคลาสของเราด้วยข้อมูลเพิ่มเติมที่จะเป็นประโยชน์ต่อเครื่อง Java ในอนาคต พวกเขาไม่มีวิธีการใด ๆ ที่คุณต้องใช้Serializable
เป็นหนึ่งในอินเทอร์เฟซเหล่านั้น ประเด็นสำคัญอีกประการ: ทำไมเราถึงต้องการprivate static final long serialVersionUID
ตัวแปรที่เรากำหนดไว้ในคลาส? ทำไมถึงจำเป็น? ฟิลด์นี้มีตัวระบุเฉพาะสำหรับเวอร์ชันของคลาสซีเรียลไลซ์ คลาสใด ๆ ที่ใช้Serializable
อินเทอร์เฟซมีversion
ตัวระบุ มันถูกคำนวณตามเนื้อหาของคลาส: ฟิลด์, ลำดับที่ประกาศ, เมธอด ฯลฯ หากเราเปลี่ยนประเภทของฟิลด์และ/หรือจำนวนฟิลด์ในคลาสของเรา ตัวระบุเวอร์ชันก็จะเปลี่ยนไปทันที . serialVersionUID
ยังเขียนเมื่อคลาสถูกทำให้เป็นอนุกรม เมื่อเราพยายาม deserialize นั่นคือ กู้คืนอ็อบเจกต์จากชุดของไบต์ ค่าที่เกี่ยวข้อง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 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 провинций
อ๊ะ :( ดูเหมือนว่าโปรแกรมของเราใช้งานไม่ได้ :( อันที่จริง มันใช้งานได้ คุณจำได้ว่าเราส่งชุดของไบต์ ไม่ใช่แค่วัตถุหรือข้อความ ไปยังไฟล์ใช่ไหม นี่คือสิ่งที่ ชุดของไบต์มีลักษณะดังนี้ :) นี่คือเกมที่เราบันทึกไว้ หากเราต้องการคืนค่าออบเจกต์ดั้งเดิมของเรา เช่น เริ่มและเล่นเกมต่อจากที่เราค้างไว้ จากนั้นเราจำเป็นต้องมีกระบวนการย้อนกลับ: ดีซีเรียลไลเซชัน นี่คือลักษณะที่ปรากฏในเกมของเรา กรณี:
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
นี่เป็นข้อยกเว้นที่เรากล่าวถึงข้างต้น อย่างไรก็ตาม เราพลาดสิ่งที่สำคัญไป มันสมเหตุสมผลแล้วที่ Strings และ primitives สามารถทำให้เป็นอนุกรมได้อย่างง่ายดาย: Java อาจมีกลไกในตัวที่จะทำสิ่งนี้ แต่จะเกิดอะไรขึ้นถ้า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
class เพราะการทำเช่นนั้นจะทำให้ทั้งSavedGame
class ไม่สามารถจัดลำดับได้! นั่นเป็นปัญหา :/ ใน Java ปัญหาประเภทนี้สามารถแก้ไขได้โดยใช้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
อินเทอร์เฟซซึ่งเราจะพูดถึงในบทเรียนถัดไป นอกจากนี้ หนังสือ "Head-First Java" ยังมีบทเกี่ยวกับหัวข้อนี้อีกด้วย ให้ความสนใจหน่อย :)
GO TO FULL VERSION