JavaRush /จาวาบล็อก /Random-TH /การวิเคราะห์คำถามและคำตอบจากการสัมภาษณ์นักพัฒนา Java ตอนท...
Константин
ระดับ

การวิเคราะห์คำถามและคำตอบจากการสัมภาษณ์นักพัฒนา Java ตอนที่ 15

เผยแพร่ในกลุ่ม
สวัสดี สวัสดี! Java Developer จำเป็นต้องรู้มากแค่ไหน? คุณสามารถโต้เถียงกันเป็นเวลานานในประเด็นนี้ แต่ความจริงก็คือในการสัมภาษณ์คุณจะถูกขับเคลื่อนอย่างเต็มที่ด้วยทฤษฎี แม้แต่ในความรู้เหล่านั้นซึ่งคุณจะไม่มีโอกาสใช้ในการทำงานของคุณ การวิเคราะห์คำถามและคำตอบจากการสัมภาษณ์นักพัฒนา Java  ตอนที่ 15 - 1หากคุณเป็นมือใหม่ ความรู้เชิงทฤษฎีของคุณจะถูกพิจารณาอย่างจริงจัง เนื่องจากยังไม่มีประสบการณ์และความสำเร็จที่ยิ่งใหญ่ สิ่งที่เหลืออยู่คือการตรวจสอบความแข็งแกร่งของฐานความรู้ วันนี้เราจะยังคงเสริมความแข็งแกร่งให้กับฐานนี้ต่อไปโดยพิจารณาคำถามสัมภาษณ์ยอดนิยมสำหรับนักพัฒนา Java บินกันเถอะ!

จาวาคอร์

9. อะไรคือความแตกต่างระหว่างการเชื่อมโยงแบบคงที่และไดนามิกใน Java?

ฉันได้ตอบคำถามนี้แล้วในบทความนี้ในคำถามที่ 18 เกี่ยวกับความหลากหลายแบบคงที่และไดนามิก ฉันแนะนำให้คุณอ่าน

10. เป็นไปได้ไหมที่จะใช้ตัวแปรส่วนตัวหรือตัวแปรที่ได้รับการป้องกันในอินเทอร์เฟซ?

ไม่คุณไม่สามารถ. เนื่องจากเมื่อคุณประกาศอินเทอร์เฟซ คอมไพเลอร์ Java จะเพิ่ม คีย์เวิร์ดสาธารณะและนามธรรม ก่อนวิธีอินเท อร์เฟซและคีย์เวิร์ดสาธารณะคงที่และสุดท้ายก่อนสมาชิกข้อมูลโดย อัตโนมัติ ที่จริงแล้ว หากคุณเพิ่มprivateหรือprotectedจะเกิดข้อขัดแย้งขึ้น และคอมไพลเลอร์จะบ่นเกี่ยวกับตัวแก้ไขการเข้าถึงพร้อมข้อความ: “ตัวแก้ไข '<selected modifier>' ไม่ได้รับอนุญาตที่นี่” เหตุใดคอมไพเลอร์จึงเพิ่มpublic , staticและFinalตัวแปรในอินเทอร์เฟซ? ลองคิดดู:
  • สาธารณะ - อินเทอร์เฟซอนุญาตให้ไคลเอนต์โต้ตอบกับวัตถุ หากตัวแปรไม่เป็นแบบสาธารณะ ลูกค้าจะไม่สามารถเข้าถึงได้
  • คงที่ - ไม่สามารถสร้างอินเทอร์เฟซได้ (หรือค่อนข้างเป็นวัตถุ) ดังนั้นตัวแปรจึงเป็นแบบคงที่
  • สุดท้าย - เนื่องจากอินเทอร์เฟซถูกใช้เพื่อให้ได้นามธรรม 100% ตัวแปรจึงมีรูปแบบสุดท้าย (และจะไม่มีการเปลี่ยนแปลง)

11. Classloader คืออะไรและใช้ทำอะไร?

Classloader - หรือ Class Loader - ให้การโหลดคลาส Java แม่นยำยิ่งขึ้นการโหลดนั้นรับประกันโดยผู้สืบทอด - ตัวโหลดคลาสเฉพาะเพราะ ClassLoaderนั้นเป็นนามธรรม ทุกครั้งที่โหลดไฟล์ .class ตัวอย่างเช่น หลังจากเรียก Constructor หรือเมธอดสแตติกของคลาสที่เกี่ยวข้อง การดำเนินการนี้จะดำเนินการโดยหนึ่งในลูก หลานของคลาส ClassLoader ทายาทมีสามประเภท:
  1. Bootstrap ClassLoaderเป็นตัวโหลดพื้นฐานที่ใช้งานในระดับ JVM และไม่มีการตอบรับจากสภาพแวดล้อมรันไทม์ เนื่องจากเป็นส่วนหนึ่งของเคอร์เนล JVM และเขียนด้วยโค้ดเนทีฟ ตัวโหลดนี้ทำหน้าที่เป็นพาเรนต์ของอินสแตนซ์ ClassLoader อื่นๆ ทั้งหมด

    รับผิดชอบหลักในการโหลดคลาสภายใน JDK ซึ่งมักจะเป็นrt.jarและไลบรารีหลักอื่นๆ ที่อยู่ใน ไดเร็กทอรี $JAVA_HOME/jre/ lib แพลตฟอร์มที่แตกต่างกันอาจมีการใช้งานที่แตกต่างกันของตัวโหลดคลาสนี้

  2. Extension Classloaderคือตัวโหลดส่วนขยาย ซึ่งเป็นตัวสืบทอดของคลาสตัวโหลดพื้นฐาน ดูแลการโหลดส่วนขยายของคลาสพื้นฐาน Java มาตรฐาน โหลดจากไดเร็กทอรีส่วนขยาย JDK โดยทั่วไปคือ$JAVA_HOME/lib/extหรือไดเร็กทอรีอื่นๆ ที่กล่าวถึงในคุณสมบัติระบบ java.ext.dirs (ตัวเลือกนี้สามารถใช้เพื่อควบคุมการโหลดส่วนขยาย)

  3. System ClassLoaderคือตัวโหลดระบบที่ใช้งานในระดับ JRE ซึ่งดูแลการโหลดคลาสระดับแอปพลิเคชันทั้งหมดลงใน JVM โหลดไฟล์ที่พบในตัวแปรสภาพแวดล้อมคลาส-classpathหรือตัวเลือกบรรทัดคำสั่ง-cp

การวิเคราะห์คำถามและคำตอบจากการสัมภาษณ์นักพัฒนา Java  ตอนที่ 15 - 2ตัวโหลดคลาสเป็นส่วนหนึ่งของรันไทม์ Java ขณะที่ JVM ร้องขอคลาส ตัวโหลดคลาสจะพยายามค้นหาคลาสและโหลดคำจำกัดความของคลาสลงในรันไทม์โดยใช้ชื่อแบบเต็มของคลาส เมธอดjava.lang.ClassLoader.loadClass()มีหน้าที่โหลดคำจำกัดความของคลาสขณะรันไทม์ มันพยายามโหลดคลาสตามชื่อเต็ม หากยังไม่ได้โหลดคลาส คลาสนั้นจะมอบหมายคำขอให้กับตัวโหลดคลาสหลัก กระบวนการนี้เกิดขึ้นซ้ำๆ และมีลักษณะดังนี้:
  1. System Classloader พยายามค้นหาคลาสในแคช

    • 1.1. หากพบคลาส แสดงว่าการโหลดเสร็จสมบูรณ์

    • 1.2. หากไม่พบคลาส การโหลดจะถูกมอบหมายให้กับ Extension Classloader

  2. Extension Classloader พยายามค้นหาคลาสในแคชของตัวเอง

    • 2.1. หากพบคลาสก็จะเสร็จสมบูรณ์

    • 2.2. หากไม่พบคลาส การโหลดจะถูกมอบหมายให้กับ Bootstrap Classloader

  3. Bootstrap Classloader พยายามค้นหาคลาสในแคชของตัวเอง

    • 3.1. หากพบคลาส แสดงว่าการโหลดเสร็จสมบูรณ์

    • 3.2. หากไม่พบคลาส Bootstrap Classloader พื้นฐานจะพยายามโหลดคลาสนั้น

  4. หากกำลังโหลด:

    • 4.1. สำเร็จ - การโหลดชั้นเรียนเสร็จสมบูรณ์

    • 4.2. หากล้มเหลว การควบคุมจะถูกโอนไปยัง Extension Classloader

  5. 5. Extension Classloader พยายามโหลดคลาส และหากกำลังโหลด:

    • 5.1. สำเร็จ - การโหลดชั้นเรียนเสร็จสมบูรณ์

    • 5.2. หากล้มเหลว การควบคุมจะถูกโอนไปยัง System Classloader

  6. 6. System Classloader พยายามโหลดคลาส และหากกำลังโหลด:

    • 6.1. สำเร็จ - การโหลดชั้นเรียนเสร็จสมบูรณ์

    • 6.2. ไม่ผ่านสำเร็จ - มีการสร้างข้อยกเว้น - ClassNotFoundException

หัวข้อของคลาสโหลดเดอร์นั้นกว้างใหญ่และไม่ควรละเลย หากต้องการทำความคุ้นเคยกับรายละเอียดเพิ่มเติม ฉันแนะนำให้คุณอ่านบทความนี้และเราจะไม่อ้อยอิ่งและเดินหน้าต่อไป

12. พื้นที่ข้อมูลรันไทม์คืออะไร?

Ares ข้อมูลรันไทม์ - พื้นที่ข้อมูลรันไทม์ JVM JVM กำหนดพื้นที่ข้อมูลรันไทม์บางส่วนที่จำเป็นระหว่างการทำงานของโปรแกรม บางส่วนถูกสร้างขึ้นเมื่อ JVM เริ่มทำงาน อื่นๆ เป็นเธรดภายในเครื่องและถูกสร้างขึ้นเมื่อมีการสร้างเธรดเท่านั้น (และถูกทำลายเมื่อเธรดถูกทำลาย) พื้นที่ข้อมูลรันไทม์ JVM มีลักษณะดังนี้: การวิเคราะห์คำถามและคำตอบจากการสัมภาษณ์นักพัฒนา Java  ตอนที่ 15 - 3
  • PC Register อยู่ในเครื่องของแต่ละเธรดและมีที่อยู่ของคำสั่ง JVM ที่เธรดกำลังดำเนินการอยู่ในปัจจุบัน

  • JVM Stack คือพื้นที่หน่วยความจำที่ใช้เป็นที่จัดเก็บสำหรับตัวแปรภายในเครื่องและผลลัพธ์ชั่วคราว แต่ละเธรดมีสแต็กแยกเป็นของตัวเอง: ทันทีที่เธรดยุติ สแต็กนี้จะถูกทำลายไปด้วย เป็นที่น่าสังเกตว่าข้อดีของสแต็กเหนือฮีปคือประสิทธิภาพ ในขณะที่ฮีปมีข้อได้เปรียบในระดับการจัดเก็บข้อมูลอย่างแน่นอน

  • Native Method Stack - พื้นที่ข้อมูลต่อเธรดที่เก็บองค์ประกอบข้อมูล คล้ายกับสแต็ก JVM สำหรับการเรียกใช้เมธอดเนทิฟ (ไม่ใช่ Java)

  • ฮีป - ถูกใช้โดยเธรดทั้งหมดเป็นร้านค้าที่มีอ็อบเจ็กต์ ข้อมูลเมตาของคลาส อาร์เรย์ ฯลฯ ซึ่งสร้างขึ้นขณะรันไทม์ พื้นที่นี้ถูกสร้างขึ้นเมื่อ JVM เริ่มทำงานและถูกทำลายเมื่อปิดระบบ

  • พื้นที่วิธีการ - พื้นที่รันไทม์นี้ใช้ร่วมกับเธรดทั้งหมด และถูกสร้างขึ้นเมื่อ JVM เริ่มทำงาน มันจัดเก็บโครงสร้างสำหรับแต่ละคลาส เช่น Runtime Constant Pool, โค้ดสำหรับตัวสร้างและวิธีการ, ข้อมูลวิธีการ ฯลฯ

13. วัตถุที่ไม่เปลี่ยนรูปคืออะไร?

ในส่วนนี้ของบทความในคำถามที่ 14 และ 15 มีคำตอบสำหรับคำถามนี้อยู่แล้ว ดังนั้นลองดูโดยไม่เสียเวลา

14. คลาส String มีความพิเศษอย่างไร?

ในช่วงต้นของการวิเคราะห์ เราได้พูดคุยซ้ำแล้วซ้ำเล่าเกี่ยวกับคุณสมบัติบางอย่างของ String (มีส่วนแยกต่างหากสำหรับเรื่องนี้) ตอนนี้เรามาสรุปคุณสมบัติของString :
  1. เป็นวัตถุที่ได้รับความนิยมมากที่สุดใน Java และใช้เพื่อวัตถุประสงค์ที่หลากหลาย ในแง่ของความถี่ในการใช้งานก็ไม่น้อยหน้าแม้แต่กับประเภทดั้งเดิม

  2. วัตถุของคลาสนี้สามารถสร้างได้โดยไม่ต้องใช้คีย์เวิร์ดใหม่ - โดยตรงผ่านเครื่องหมายคำพูดString str = “string”; .

  3. สตริงเป็น คลาส ที่ไม่เปลี่ยนรูป : เมื่อสร้างอ็อบเจ็กต์ของคลาสนี้ ข้อมูลจะไม่สามารถเปลี่ยนแปลงได้ (เมื่อคุณเพิ่ม + “สตริงอื่น” ให้กับสตริงบางตัว ดังนั้นคุณจะได้รับสตริงใหม่ที่สาม) ความไม่เปลี่ยนรูปของคลาส String ทำให้เธรดปลอดภัย

  4. คลาสStringได้รับการสรุป (มี ตัวแก้ไข ขั้นสุดท้าย ) ดังนั้นจึงไม่สามารถสืบทอดได้

  5. สตริงมีพูลสตริงของตัวเองซึ่งเป็นพื้นที่หน่วยความจำในฮีปที่แคชค่าสตริงที่สร้างขึ้น ในส่วนนี้ของซีรีส์ในคำถามที่ 62 ฉันอธิบายพูลสตริง

  6. Java มีแอนะล็อกของ Stringซึ่งออกแบบมาเพื่อทำงานกับสตริง - StringBuilderและStringBufferแต่มีความแตกต่างที่ไม่แน่นอน คุณสามารถอ่านเพิ่มเติมเกี่ยวกับพวกเขาได้ในบทความนี้

การวิเคราะห์คำถามและคำตอบจากการสัมภาษณ์นักพัฒนา Java  ตอนที่ 15 - 4

15. ความแปรปรวนร่วมประเภทคืออะไร?

เพื่อทำความเข้าใจความแปรปรวนร่วม เราจะดูตัวอย่าง สมมติว่าเรามีคลาสสัตว์:
public class Animal {
 void voice() {
   System.out.println("*тишина*");
 }
}
และ คลาส Dog บางคลาสก็ขยายออกไป :
public class Dog extends Animal {

 @Override
 public void voice() {
   System.out.println("Гав, гав, гав!!!");
 }
}
ดังที่เราจำได้ เราสามารถกำหนดออบเจ็กต์ประเภททายาทให้กับประเภทพาเรนต์ได้อย่างง่ายดาย:
Animal animal = new Dog();
นี่จะไม่มีอะไรมากไปกว่าความหลากหลาย สะดวก คล่องตัวใช่หรือไม่? แล้วรายชื่อสัตว์ล่ะ? เราสามารถให้รายการที่มีวัตถุDog ร่วมกับ Animal ทั่วไปได้ หรือไม่
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs;
ในกรณีนี้ เส้นสำหรับกำหนดรายชื่อสุนัขให้กับรายชื่อสัตว์จะถูกขีดเส้นใต้ด้วยสีแดง เช่น คอมไพเลอร์จะไม่ส่งรหัสนี้ แม้ว่าที่จริงแล้วการมอบหมายนี้ดูเหมือนจะค่อนข้างสมเหตุสมผล (ท้ายที่สุดแล้วเราสามารถกำหนด วัตถุ Dog ให้กับตัวแปรประเภท Animalได้) แต่ก็ไม่สามารถทำได้ เนื่องจากหากได้รับอนุญาตเราจะสามารถใส่ วัตถุ Animal ลงในรายการที่เดิมตั้งใจให้เป็น สุนัข ได้ ในขณะ ที่คิดว่าเรามีเฉพาะสุนัข อยู่ในรายการ จากนั้น ยกตัวอย่าง เราจะใช้ เมธอด get() เพื่อนำ อ็อบเจ็กต์จาก รายการ Dogsโดยคิดว่าเป็นสุนัข และเรียกเมธอดบางอย่างของ อ็อบเจ็กต์ Dogซึ่งAnimalไม่มี และตามที่คุณเข้าใจนี่เป็นไปไม่ได้ - ข้อผิดพลาดจะเกิดขึ้น แต่โชคดีที่คอมไพเลอร์ไม่พลาดข้อผิดพลาดเชิงตรรกะนี้ด้วยการกำหนดรายชื่อผู้สืบทอดให้กับรายชื่อผู้ปกครอง (และในทางกลับกัน) ใน Java คุณสามารถกำหนดวัตถุรายการให้กับรายการตัวแปรที่มีข้อมูลทั่วไปที่ตรงกันเท่านั้น สิ่งนี้เรียกว่าการแปรปรวน หากพวกเขาสามารถทำเช่นนี้ได้ มันจะถูกเรียกและเรียกว่าความแปรปรวนร่วม นั่นคือความแปรปรวนร่วมคือถ้าเราสามารถตั้งค่าวัตถุประเภทArrayList<Dog> ให้เป็นตัวแปร ประเภทList<Animal> ปรากฎว่า Java ไม่รองรับความแปรปรวนร่วมใช่ไหม ไม่ว่ายังไงก็ตาม! แต่สิ่งนี้ทำในลักษณะพิเศษของมันเอง การออกแบบใช้ทำอะไร? ขยายสัตว์ มันถูกวางไว้พร้อมกับตัวแปรทั่วไปที่เราต้องการตั้งค่าออบเจ็กต์รายการ โดยมีตัวแปรทั่วไปของการสืบทอด โครงสร้างทั่วไปนี้หมายความว่าประเภทใดๆ ที่สืบทอดมาจากประเภทAnimal จะทำ (และประเภทAnimalก็ตกอยู่ภายใต้ลักษณะทั่วไปนี้ด้วย) ในทางกลับกันAnimalไม่เพียงแต่เป็นคลาสเท่านั้น แต่ยังเป็นอินเทอร์เฟซด้วย (อย่าหลงกลด้วย คีย์เวิร์ด ขยาย ) เราสามารถทำงานที่ได้รับมอบหมายก่อนหน้านี้ได้ดังนี้: การวิเคราะห์คำถามและคำตอบจากการสัมภาษณ์นักพัฒนา Java  ตอนที่ 15 - 5
List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs;
ด้วยเหตุนี้คุณจะเห็นใน IDE ว่าคอมไพเลอร์จะไม่บ่นเกี่ยวกับโครงสร้างนี้ เรามาตรวจสอบฟังก์ชันการทำงานของการออกแบบนี้กันดีกว่า สมมติว่าเรามีวิธีการที่ให้สัตว์ทุกตัวส่งเสียงเข้ามา:
public static void animalsVoice(List<? extends Animal> animals) {
 for (Animal animal : animals) {
   animal.voice();
 }
}
ให้รายชื่อสุนัขแก่เขา:
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
dogs.add(new Dog());
animalsVoice(dogs);
ในคอนโซลเราจะเห็นผลลัพธ์ต่อไปนี้:
วูฟ วูฟ วูฟ!!! วูฟ วูฟ วูฟ!!! วูฟ วูฟ วูฟ!!!
ซึ่งหมายความว่าแนวทางความแปรปรวนร่วมนี้ใช้ได้ผลสำเร็จ โปรดทราบว่าข้อมูลทั่วไปนี้รวมอยู่ในรายการ? ขยาย Animalเราไม่สามารถแทรกข้อมูลใหม่ประเภทใด ๆ ได้: ทั้งDog type หรือแม้แต่Animal type :
List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs;
animals.add(new Dog());
dogs.add(new Animal());
จริงๆ แล้ว ในสองบรรทัดสุดท้าย คอมไพเลอร์จะเน้นการแทรกวัตถุด้วยสีแดง นี่เป็นเพราะความจริงที่ว่าเราไม่สามารถแน่ใจได้ร้อยเปอร์เซ็นต์ว่ารายการของออบเจ็กต์ประเภทใดที่จะถูกกำหนดให้กับรายการด้วยข้อมูลโดย<? ขยายสัตว์> . การวิเคราะห์คำถามและคำตอบจากการสัมภาษณ์นักพัฒนา Java  ตอนที่ 15 - 6ฉันอยากจะพูดเกี่ยวกับความแปรปรวนเนื่องจากโดยปกติแล้วแนวคิดนี้จะไปพร้อมกับความแปรปรวนร่วมเสมอ และตามกฎแล้ว พวกเขาจะถูกถามเกี่ยวกับสิ่งเหล่านั้นร่วมกัน แนวคิดนี้ค่อนข้างตรงกันข้ามกับความแปรปรวนร่วม เนื่องจากโครงสร้างนี้ใช้ประเภททายาท สมมติว่าเราต้องการรายการที่สามารถกำหนดรายการประเภทอ็อบเจ็กต์ที่ไม่ใช่บรรพบุรุษของ อ็อบเจ็กต์ Dogได้ อย่างไรก็ตามเราไม่ทราบล่วงหน้าว่ามันจะเป็นประเภทใดโดยเฉพาะ ในกรณีนี้คือการสร้างแบบฟอร์ม? super Dogซึ่งทุกประเภทมีความเหมาะสม - ต้นกำเนิดของ คลาส Dog :
List<Animal> animals = new ArrayList<>();
List<? super Dog> dogs = animals;
dogs.add(new Dog());
dogs.add(new Dog());
เราสามารถเพิ่มอ็อบเจ็กต์ประเภทDog ลงในรายการได้อย่างปลอดภัยด้วยคำสั่งทั่วไป เพราะไม่ว่าในกรณีใดก็ตาม มันมีวิธีการนำไปใช้ทั้งหมดของบรรพบุรุษของมัน แต่เราจะไม่สามารถเพิ่มวัตถุประเภทAnimalได้ เนื่องจากไม่มีความแน่นอนว่าจะมีวัตถุประเภทนี้อยู่ข้างในและไม่ใช่ ตัวอย่างเช่นDog ท้ายที่สุดแล้ว เราสามารถขอวิธีการของคลาส Dogจากองค์ประกอบของรายการนี้ได้ ซึ่ง Animalจะไม่มี ในกรณีนี้จะเกิดข้อผิดพลาดในการคอมไพล์ นอกจากนี้ หากเราต้องการใช้วิธีก่อนหน้านี้ แต่ด้วยวิธีทั่วไป:
public static void animalsVoice(List<? super Dog> dogs) {
 for (Dog dog : dogs) {
   dog.voice();
 }
}
เราจะได้รับข้อผิดพลาดในการคอมไพล์ในfor loop เนื่องจากเราไม่สามารถแน่ใจได้ว่ารายการที่ส่งคืนนั้นมีอ็อบเจ็กต์ประเภทDogและมีอิสระที่จะใช้วิธีการของมัน หากเราเรียกเมธอด Dogs.get(0)ในรายการนี้ - เราจะได้วัตถุประเภทObject นั่นคือ เพื่อให้ เมธอด AnimalsVoice() ทำงาน อย่างน้อยที่สุดเราจำเป็นต้องเพิ่มการปรับแต่งเล็กๆ น้อยๆ โดยจำกัดข้อมูลประเภทให้แคบลง:
public static void animalsVoice(List<? super Dog> dogs) {
 for (Object obj : dogs) {
   if (obj instanceof Dog) {
     Dog dog = (Dog) obj;
     dog.voice();
   }
 }
}
การวิเคราะห์คำถามและคำตอบจากการสัมภาษณ์นักพัฒนา Java  ตอนที่ 15 - 7

16. Object class มีวิธีการอย่างไรบ้าง?

ในส่วนนี้ของซีรีส์นี้ ในย่อหน้าที่ 11 ฉันได้ตอบคำถามนี้แล้ว ดังนั้นฉันขอแนะนำให้คุณอ่านหากคุณยังไม่ได้ตอบ นั่นคือจุดที่เราจะสิ้นสุดในวันนี้ พบกันใหม่ในตอนต่อไป! การวิเคราะห์คำถามและคำตอบจากการสัมภาษณ์นักพัฒนา Java  ตอนที่ 15 - 8
วัสดุอื่นๆ ในชุด:
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION