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

ความแตกต่างระหว่างการทำให้เป็นซีเรียลไลเซชันและดีซีเรียลไลเซชันใน Java คืออะไร

เผยแพร่ในกลุ่ม
สวัสดี! ในบทเรียนวันนี้ เราพูดถึงการทำให้เป็นซีเรียลไลเซชันและดีซีเรียลไลเซชันใน Java เราจะเริ่มต้นด้วยตัวอย่างง่ายๆ สมมติว่าคุณได้สร้างเกมคอมพิวเตอร์ หากคุณเติบโตในยุค 90 และจำเครื่องเล่นเกมในยุคนั้นได้ คุณอาจรู้ว่าพวกเขายังขาดบางสิ่งที่เรามองข้ามไปในปัจจุบัน นั่นคือความสามารถในการบันทึกและโหลดเกม :) ถ้าไม่ ลองนึกภาพดูสิ! ความแตกต่างระหว่างการทำให้เป็นซีเรียลไลเซชันและดีซีเรียลไลเซชันใน Java คืออะไร  - 1 ฉันกลัวว่าวันนี้เกมที่ไม่มีความสามารถเหล่านี้จะถึงวาระ! การ "บันทึก" และ "โหลด" เกมหมายความว่าอย่างไร เราเข้าใจความหมายทั่วไป: เราต้องการเล่นเกมต่อจากจุดที่เราค้างไว้ ในการทำเช่นนี้ เราสร้าง "จุดตรวจสอบ" ซึ่งเราจะใช้ในการโหลดเกม แต่สิ่งนี้มีความหมายอย่างไรกับโปรแกรมเมอร์มากกว่าเกมเมอร์ทั่วไป คำตอบนั้นง่าย:. สมมติว่าคุณกำลังเล่นเป็นสเปนใน 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ฟิลด์ให้กับเราได้SavedGameclass เพราะการทำเช่นนั้นจะทำให้ทั้งSavedGameclass ไม่สามารถจัดลำดับได้! นั่นเป็นปัญหา :/ ความแตกต่างระหว่างการทำให้เป็นซีเรียลไลเซชันและดีซีเรียลไลเซชันใน Java คืออะไร  - 2ใน 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" ยังมีบทเกี่ยวกับหัวข้อนี้อีกด้วย ให้ความสนใจหน่อย :)
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION