สวัสดี! ในบทเรียนวันนี้ เราจะพูดถึงซีเรียลไลเซชันและดีซีเรียลไลเซชันใน Java เราจะเริ่มต้นด้วยตัวอย่างง่ายๆ ลองจินตนาการว่าคุณเป็นนักพัฒนาเกมคอมพิวเตอร์ หากคุณเติบโตในยุค 90 และจำเครื่องเล่นเกมในยุคนั้นได้ คุณอาจรู้ว่าพวกเขายังขาดบางสิ่งที่เรามองข้ามไปในปัจจุบัน นั่นคือความสามารถในการบันทึกและโหลดเกม :) ถ้าไม่ ลองนึกภาพดูสิ!
ฉันกลัวว่าเกมที่ไม่มีความสามารถเหล่านี้ในวันนี้จะต้องถึงวาระ! อย่างไรก็ตาม คำว่า 'บันทึก' และ 'โหลด' เกมหมายความว่าอย่างไร เราเข้าใจความหมายในชีวิตประจำวัน: เราต้องการเล่นเกมต่อจากจุดที่เราค้างไว้ ในการทำเช่นนี้ เราสร้าง 'จุดตรวจสอบ' ขึ้นมาเพื่อใช้โหลดเกมในภายหลัง แต่นั่นหมายถึงอะไรสำหรับโปรแกรมเมอร์มากกว่าเกมเมอร์ทั่วไป? คำตอบนั้นง่าย: เราบันทึกสถานะของโปรแกรมของเรา สมมติว่าคุณกำลังเล่นสเปนในเกมกลยุทธ์ เกมของคุณมีสถานะ: ทุกคนมีดินแดนใด, ทุกคนมีทรัพยากรเท่าใด, มีพันธมิตรอะไรบ้างและอยู่กับใคร, ใครอยู่ในสงคราม และอื่นๆ ข้อมูลนี้ซึ่งเป็นสถานะของโปรแกรมของเรา จะต้องได้รับการบันทึกเพื่อกู้คืนข้อมูลและดำเนินเกมต่อไป เมื่อมันเกิดขึ้น การทำให้เป็นอนุกรมใน Javaคือกระบวนการบันทึกสถานะของวัตถุเป็นลำดับของไบต์ การดีซีเรียลไลเซชันใน Javaคือกระบวนการกู้คืนอ็อบเจ็กต์จากไบต์เหล่านี้ อ็อบเจกต์ Java ใดๆ สามารถแปลงเป็นลำดับไบต์ได้ ทำไมเราต้องการสิ่งนี้ เราได้กล่าวซ้ำๆ ว่าโปรแกรมไม่ได้มีอยู่โดยตัวของมันเอง บ่อยครั้งที่พวกเขาโต้ตอบกันแลกเปลี่ยนข้อมูล ฯลฯ รูปแบบไบต์สะดวกและมีประสิทธิภาพสำหรับสิ่งนี้ ตัวอย่างเช่น เราสามารถแปลงวัตถุของเกมที่บันทึกไว้ ของเราคลาสเป็นลำดับของไบต์ ถ่ายโอนไบต์เหล่านี้ผ่านเครือข่ายไปยังคอมพิวเตอร์เครื่องอื่น จากนั้นในคอมพิวเตอร์อีกเครื่องหนึ่งจะแปลงไบต์เหล่านี้กลับเป็นวัตถุ Java! ฟังดูยากใช่มั้ย ดูเหมือนว่ามันจะยากที่จะทำให้สิ่งนี้เกิดขึ้น: / โชคดีที่ไม่เป็นอย่างนั้น! :) ใน Java อินเทอร์เฟซ Serializableมีหน้าที่รับผิดชอบในกระบวนการทำให้เป็นอนุกรม อินเทอร์เฟซนี้เรียบง่ายมาก: คุณไม่จำเป็นต้องใช้วิธีเดียวเพื่อใช้งาน! ดูว่าชั้นเรียนของเราสำหรับการบันทึกเกมนั้นง่ายเพียงใด:
ใน Java ปัญหาประเภทนี้แก้ไขได้ด้วยคีย์เวิร์ดชั่วคราว หากคุณเพิ่มคำหลักนี้ในฟิลด์ของชั้นเรียน ฟิลด์นั้นจะไม่ถูกทำให้เป็นอนุกรม มาลองสร้างหนึ่งในฟิลด์ของคลาสSavedGame ของเรา transientจากนั้นเราจะทำให้เป็นอนุกรมและกู้คืนออบเจกต์เดียว

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) +
'}';
}
}
อาร์เรย์สามตัวรับผิดชอบข้อมูลเกี่ยวกับดินแดน ทรัพยากร และการทูต และอินเทอร์เฟซ Serializable จะบอกเครื่อง Java ว่า: ' ทุกอย่างเรียบร้อยถ้าออบเจกต์ของคลาสนี้สามารถทำให้เป็นอนุกรมได้ ' อินเทอร์เฟซที่ไม่มีอินเทอร์เฟซเดียวดูแปลก: / ทำไมจึงจำเป็น คำตอบสำหรับคำถามนั้นระบุไว้ข้างต้น: จำเป็นต้องให้ข้อมูลที่จำเป็นแก่เครื่อง Java เท่านั้น ในบทเรียนที่ผ่านมา เราได้กล่าวถึงส่วนต่อประสานเครื่องหมายไว้สั้นๆ นี่คืออินเทอร์เฟซข้อมูลพิเศษที่ทำเครื่องหมายคลาสของเราด้วยข้อมูลเพิ่มเติมที่จะเป็นประโยชน์ต่อเครื่อง Java ในอนาคต พวกเขาไม่มีวิธีการใด ๆ ที่คุณต้องใช้ นี่คือSerializable — หนึ่งในอินเทอร์เฟซดังกล่าว นี่เป็นอีกจุดสำคัญ: ทำไมเราถึงต้องการตัวแปร serialVersionUID ยาวสุดท้ายคงที่ส่วนตัวที่เรากำหนดไว้ในคลาส? ฟิลด์นี้มีตัวระบุเวอร์ชันเฉพาะของคลาสที่ต่อเนื่องกัน ทุกคลาสที่ใช้ อินเทอร์เฟซ Serializableมีตัวระบุเวอร์ชัน โดยจะพิจารณาจากเนื้อหาของคลาส — ฟิลด์และลำดับการประกาศ ตลอดจนเมธอดและลำดับการประกาศ และถ้าเราเปลี่ยนประเภทฟิลด์และ/หรือจำนวนฟิลด์ในคลาสของเรา ตัวระบุเวอร์ชันจะเปลี่ยนไปทันที serialVersionUID ยังเขียน เมื่อคลาสถูกทำให้เป็นอนุกรม เมื่อเราพยายาม deserialize เช่น กู้คืนวัตถุจากลำดับไบต์ ค่าของserialVersionUIDจะเทียบกับค่าของserialVersionUIDของชั้นเรียนในโปรแกรมของเรา หากค่าไม่ตรงกัน java.io.InvalidClassException จะถูกส่งออกไป เราจะเห็นตัวอย่างด้านล่างนี้ เพื่อหลีกเลี่ยงสถานการณ์ดังกล่าว เราเพียงตั้งค่าตัวระบุเวอร์ชันสำหรับชั้นเรียนของเราด้วยตนเอง ในกรณีของเรา มันจะเท่ากับ 1 (คุณสามารถใช้หมายเลขอื่นที่คุณชอบก็ได้) ถึงเวลาลองทำให้ อ อบเจก ต์ที่บันทึกไว้ของเราเป็นอนุกรม แล้วดูว่าเกิดอะไรขึ้น!
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();
}
}
อย่าง ที่ คุณเห็น เราได้สร้าง 2 สตรีม: FileOutputStreamและObjectOutputStream คนแรกรู้วิธีเขียนข้อมูลลงในไฟล์ และคนที่สองแปลงวัตถุเป็นไบต์ คุณได้เห็นโครงสร้างที่ซ้อนกันที่คล้ายกันแล้ว เช่นnew BufferedReader(new InputStreamReader(...))ในบทเรียนก่อนหน้านี้ ดังนั้นจึงไม่ควรทำให้คุณตกใจ :) โดยการสร้างห่วงโซ่ของสองสตรีมนี้ เราดำเนินการทั้งสองอย่าง: เราแปลง ออบเจกต์ ที่บันทึกเกมเป็นลำดับของไบต์และบันทึกลงในไฟล์โดยใช้เมธอดwriteObject() และโดยวิธีการที่เราไม่ได้ดูสิ่งที่เราได้รับ! ได้เวลาดูไฟล์แล้ว! *หมายเหตุ: ไม่จำเป็นต้องสร้างไฟล์ล่วงหน้า หากไม่มีไฟล์ที่มีชื่อที่ระบุ ไฟล์นั้นจะถูกสร้างขึ้นโดยอัตโนมัติ* และนี่คือเนื้อหา: ¬н sr SavedGame [DiplomacyInfot [Ljava/lang/String;[ resourceInfoq ~ [ borderInfoq ~ xpur [Ljava.lang. String;¬ТVзй{G xp t pФранция воюет СЃ Россией, Р˜СЃРїР°РЅРёСЏ заняла позицРёСЋ нейтралит етаuq ~ t "РЈ Р˜СЃРїР°РЅРёРё 100 золотаt РЈ Р РѕСЃСЃРёРё 80 Р·РёРё 80 Р·РёР°t !РЈ Франции 90 R · R s R »РѕС‚Р°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 จังหวัด],resourceInfo=[สเปนมี 100 โกลด์ รัสเซียมี 80 โกลด์ ฝรั่งเศสมี 90 โกลด์],DiplomacyInfo=[ฝรั่งเศสกำลังทำสงครามกับรัสเซีย สเปนวางตัวเป็นกลาง]} ยอดเยี่ยม! ก่อนอื่นเราจัดการเพื่อบันทึกสถานะของเกมของเราลงในไฟล์ แล้วจึงกู้คืนจากไฟล์ ตอนนี้ลองทำสิ่งเดียวกัน แต่เราจะลบตัวระบุเวอร์ชันออกจากคลาสที่บันทึกไว้เกม ของเรา เราจะไม่เขียนซ้ำทั้งสองคลาสของเรา รหัสของพวกเขาจะเหมือนกัน เราจะลบserialVersionUID สุดท้ายแบบคงที่ส่วนตัว ออก จากคลาสที่บันทึกไว้ นี่คือวัตถุของเราหลังจากการทำให้เป็นอนุกรม: ¬н sr SavedGameі€MіuOm‰ [DiplomacyInfot [Ljava/lang/String;[ resourceInfoq ~ [ borderInfoq ~ xpur [Ljava.lang.String;¬ТVзй{G xp t pФранция воюет СЃ Р РѕСЃСЃРё] ей, Р˜СЃРїР°РЅРёСЏ заняла позицию нейтралитетаuq ~ t "РЈ Р˜СЃРїР°РЅРёРё 100 Р· รัสเซีย °t РЈ Р РѕСЃСЃРёРё 80 Р·РёРё 80 Р·РёРё 90 Р·РёРё 90 Р·РёРё 6 РїСЂРё 6 РїСЂРё винцийt% Р РѕСЃСЃРёРё 10 провинцийt &РЈ Франции 8 провинций แต่ดูสิว่าจะเกิดอะไรขึ้นเมื่อเราพยายามแยกซีเรียลไลซ์: InvalidClassException : คลาสท้องถิ่นเข้ากันไม่ได้: สตรีม classdesc serialVersionUID = -196410440475012755, คลาสโลคัล serialVersionUID = -6675950253085108747 อย่างไรก็ตาม เราพลาดสิ่งที่สำคัญไป เห็นได้ชัดว่า สตริงและ primitives นั้นถูกซีเรียลไลซ์อย่างง่ายดาย: แน่นอนว่า Java มีกลไกในตัวสำหรับสิ่งนี้ แต่จะเกิดอะไรขึ้นถ้า คลาส ที่ทำให้เป็นอนุกรม ของเรา มีฟิลด์ที่ไม่ใช่แบบดั้งเดิม แต่อ้างอิงถึงวัตถุอื่น ตัวอย่างเช่น เรามาสร้าง คลาส TerritoryInfo , ResourceInfoและDiplomacyInfo แยกกัน เพื่อทำงานกับคลาส ที่บันทึกไว้ของเกม ของเรา
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 + '\'' +
'}';
}
}
และตอนนี้เราเจอคำถาม: คลาสเหล่านี้ทั้งหมดจำเป็นต้องทำให้เป็นอนุกรมได้ หรือไม่ ถ้าเราต้องการทำให้คลาส ที่บันทึกไว้ของเกม เป็นอนุกรม
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 +
'}';
}
}
เอาล่ะ! มาทดสอบกัน! สำหรับตอนนี้ เราจะปล่อยทุกอย่างไว้ตามเดิมและพยายามทำให้ออบเจกต์ที่ บันทึกไว้เป็น อนุกรม:
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 ใช้งานไม่ได้! นี่คือคำตอบสำหรับคำถามของเรา เมื่อวัตถุถูกทำให้เป็นอนุกรม วัตถุทั้งหมดที่อ้างอิงโดยตัวแปรอินสแตนซ์จะถูกทำให้เป็นอนุกรม และถ้าอ็อบเจกต์เหล่านั้นอ้างอิงอ็อบเจกต์อื่นด้วย ก็จะถูกซีเรียลไลซ์ด้วย และตลอดไป คลาสทั้งหมดในสายโซ่นี้ต้องเป็นSerializableมิฉะนั้นจะไม่สามารถทำให้เป็นอนุกรมได้และจะมีข้อยกเว้นเกิดขึ้น โดยวิธีการนี้อาจสร้างปัญหาตามมาได้ ตัวอย่างเช่น เราควรทำอย่างไรหากไม่ต้องการส่วนของคลาสระหว่างการทำให้เป็นอนุกรม หรือจะเป็นอย่างไรถ้าเราได้ คลาส TerritoryInfo 'ผ่านการสืบทอด' เป็นส่วนหนึ่งของไลบรารี และสมมุติต่อไปว่ามันไม่ใช่'ดังนั้น เราจึงไม่สามารถเปลี่ยนแปลงได้ นั่นหมายถึงว่าเราไม่สามารถเพิ่ม ฟิลด์ TerritoryInfo ให้กับคลาส 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' ให้ความสนใจกับมัน :)
GO TO FULL VERSION