CodeGym /จาวาบล็อก /สุ่ม /การทำให้เป็นซีเรียลไลเซชันและดีซีเรียลไลเซชันใน Java
John Squirrels
ระดับ
San Francisco

การทำให้เป็นซีเรียลไลเซชันและดีซีเรียลไลเซชันใน Java

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

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ของเรา ได้ เพราะจากนั้น คลาสที่ บันทึก ไว้เกมทั้งหมด จะไม่ได้รับการซีเรียลไลซ์! นั่นเป็นปัญหา: / การทำให้เป็นอนุกรมและดีซีเรียลไลเซชันใน Java - 2ใน Java ปัญหาประเภทนี้แก้ไขได้ด้วยคีย์เวิร์ดชั่วคราว หากคุณเพิ่มคำหลักนี้ในฟิลด์ของชั้นเรียน ฟิลด์นั้นจะไม่ถูกทำให้เป็นอนุกรม มาลองสร้างหนึ่งในฟิลด์ของคลาสSavedGame ของเรา transientจากนั้นเราจะทำให้เป็นอนุกรมและกู้คืนออบเจกต์เดียว

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' ให้ความสนใจกับมัน :)
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION