เกิดอะไรขึ้นกับข้อมูลทั่วไปของ Java? [ปิด]


49

ฉันเคยเห็นหลายครั้งในเว็บไซต์นี้โพสต์ที่ประณามการใช้งานจาวาทั่วไป ตอนนี้ฉันสามารถพูดได้อย่างตรงไปตรงมาว่าฉันไม่ได้มีปัญหาใด ๆ กับการใช้พวกเขา อย่างไรก็ตามฉันไม่ได้พยายามที่จะสร้างชั้นเรียนตัวเอง ดังนั้นปัญหาของคุณกับการสนับสนุนทั่วไปของ Java คืออะไร




ตามที่ Clinton Begin ("iBatis guy") กล่าวว่า"พวกเขาไม่ทำงาน ... "
gnat

คำตอบ:


54

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


4
เพียงเพื่อเพิ่มเพราะลบออกประเภท, การดึงข้อมูลทั่วไปที่รันไทม์เป็นไปไม่ได้เว้นแต่คุณจะใช้สิ่งที่ต้องการTypeLiteral google-guice.googlecode.com/svn/trunk/javadoc/com/google/inject/...
เจเรมี Heiler

43
ความหมายนั้นnew ArrayList<String>.getClass() == new ArrayList<Integer>.getClass()
หมายเหตุตนเอง - คิดชื่อ

9
@ ไม่มีงานมันไม่ได้ และ C ++ ใช้วิธีการที่แตกต่างกันอย่างสมบูรณ์ในการ metaprogramming ที่เรียกว่าแม่แบบ มันใช้การพิมพ์แบบเป็ดและคุณสามารถทำสิ่งที่มีประโยชน์เช่นtemplate<T> T add(T v1, T v2) { return v1->add(v2); }นั้นและคุณมีวิธีการทั่วไปในการสร้างฟังก์ชั่นที่addสองสิ่งไม่ว่าสิ่งนั้นจะเป็นอะไรพวกเขาเพียงแค่ต้องมีวิธีการตั้งชื่อadd()ด้วยพารามิเตอร์เดียว
ตรินิแดด

7
@ งานมันสร้างรหัสแน่นอน เทมเพลตนั้นมีวัตถุประสงค์เพื่อแทนที่ตัวประมวลผลล่วงหน้า C และเพื่อที่จะทำเช่นนั้นพวกเขาควรจะสามารถใช้กลอุบายที่ฉลาดมากและเป็นภาษาทัวริงที่สมบูรณ์ด้วยตัวเอง Java generics เป็นเพียงน้ำตาลสำหรับภาชนะบรรจุที่ปลอดภัยต่อการพิมพ์และ C generics ดีกว่า แต่ยังเป็นเทมเพลต C ++ ของชายยากจน
ตรินิแดด

3
@Job AFAIK, Java generics ไม่ได้สร้างคลาสสำหรับประเภททั่วไปใหม่แต่ละประเภทเพียงแค่เพิ่ม typecasts ให้กับรหัสที่ใช้วิธีการทั่วไปดังนั้นจึงไม่ใช่ IMO การเขียนโปรแกรมเมตา แม่แบบ C # สร้างรหัสใหม่สำหรับประเภททั่วไปแต่ละประเภทนั่นคือถ้าคุณใช้ List <int> และ List <double> ในโลกของ C # จะสร้างรหัสสำหรับแต่ละประเภท ในการเปรียบเทียบกับเทมเพลต C # generics ยังคงต้องการทราบว่าคุณสามารถป้อนมันประเภทใดได้คุณไม่สามารถใช้Addตัวอย่างง่ายๆที่ฉันให้มาได้ไม่มีทางโดยไม่ทราบล่วงหน้าว่าจะใช้คลาสใดซึ่งเป็นข้อเสียเปรียบ
ตรินิแดด

26

สังเกตว่าคำตอบที่ให้ไว้แล้วนั้นเน้นไปที่การผสมผสานระหว่าง Java-the-language, JVM และ Java Class Library

ไม่มีอะไรผิดปกติกับ generics ของ Java เท่าที่เกี่ยวกับ Java-the-language ตามที่อธิบายไว้ในC # VS generics Java , ยาชื่อสามัญของ Java จะสวยมากดีในระดับภาษาที่1

สิ่งที่ไม่ดีคือ JVM ไม่รองรับยาชื่อสามัญโดยตรงซึ่งมีผลกระทบสำคัญหลายประการ:

  • ส่วนต่าง ๆ ที่เกี่ยวข้องกับการสะท้อนของ Class Library ไม่สามารถเปิดเผยข้อมูลประเภทเต็มรูปแบบที่มีให้ในภาษา Java-the-language
  • การลงโทษประสิทธิภาพบางอย่างเกี่ยวข้อง

ฉันคิดว่าความแตกต่างที่ฉันทำอยู่นั้นเป็นเรื่องอื้อฉาวเพราะ Java-the-language นั้นถูกรวบรวมอย่างแพร่หลายด้วย JVM ซึ่งเป็นเป้าหมายและ Java Class Library เป็นไลบรารีหลัก

1ด้วยข้อยกเว้นที่เป็นไปได้ของ wildcard ซึ่งเชื่อว่าทำให้การอนุมานประเภทไม่สามารถตัดสินใจได้ในกรณีทั่วไป นี่เป็นข้อแตกต่างที่สำคัญระหว่าง C # และ Java generics ที่ไม่ได้กล่าวถึงบ่อยนัก ขอบคุณพลวง


14
JVM ไม่จำเป็นต้องสนับสนุนข้อมูลทั่วไป นี่จะเหมือนกับการบอกว่า C ++ ไม่มีเท็มเพลตเนื่องจากเครื่องจริง ix86 ไม่รองรับเทมเพลตซึ่งเป็นเท็จ ปัญหาคือแนวทางที่คอมไพเลอร์ใช้ในการติดตั้งประเภททั่วไป และอีกครั้งเทมเพลต C ++ ไม่ต้องเสียค่าปรับในเวลาทำงานไม่มีการไตร่ตรองเลยผมคิดว่าเหตุผลเดียวที่จะต้องทำใน Java คือการออกแบบภาษาที่แย่
ตรินิแดด

2
@Trinidad แต่ฉันไม่ได้บอกว่า Java ไม่มี generics ดังนั้นฉันไม่เห็นว่ามันเหมือนกัน และใช่ JVM ไม่จำเป็นต้องสนับสนุนพวกเขาเช่นเดียวกับ C ++ ไม่จำเป็นต้องเพิ่มประสิทธิภาพรหัส แต่การไม่ปรับให้เหมาะสมจะถือว่าเป็น "สิ่งผิดปกติ" อย่างแน่นอน
Roman Starkov

3
สิ่งที่ฉันโต้เถียงคือ JVM ไม่จำเป็นต้องได้รับการสนับสนุนโดยตรงสำหรับข้อมูลทั่วไปสำหรับความสามารถในการใช้การสะท้อนกับพวกเขา คนอื่นทำมาแล้วดังนั้นจึงสามารถทำได้ปัญหาคือ Java ไม่ได้สร้างคลาสใหม่จาก generics สิ่งที่ไม่มีการแก้ไข
ตรินิแดด

4
@Trinidad: ใน C ++ สมมติว่าคอมไพเลอร์มีเวลาและหน่วยความจำเพียงพอคุณสามารถใช้เทมเพลตเพื่อทำอะไรก็ได้และทุกสิ่งที่สามารถทำได้ด้วยข้อมูลที่มีอยู่ในเวลารวบรวม (เนื่องจากการรวบรวมเทมเพลตเป็นทัวริง) แต่มี ไม่มีวิธีสร้างแม่แบบโดยใช้ข้อมูลที่ไม่พร้อมใช้งานก่อนที่โปรแกรมจะทำงาน ใน. net ตรงกันข้ามมันเป็นไปได้ที่โปรแกรมจะสร้างชนิดตามอินพุต; มันจะค่อนข้างง่ายในการเขียนโปรแกรมที่จำนวนของชนิดต่าง ๆ ที่สามารถสร้างได้เกินจำนวนอิเล็กตรอนในจักรวาล
supercat

2
@ ตรินิแดด: เห็นได้ชัดว่าไม่มีการดำเนินการของโปรแกรมเดียวที่สามารถสร้างทุกประเภทเหล่านั้น (หรือแน่นอนสิ่งใดเกินกว่าเศษส่วนน้อยของพวกเขา) แต่ประเด็นคือมันไม่จำเป็นต้อง เพียงต้องการสร้างประเภทที่ใช้งานจริงเท่านั้น หนึ่งสามารถมีโปรแกรมที่สามารถรับหมายเลขใด ๆ (เช่น8675309) และจากนั้นสร้างประเภทที่ไม่ซ้ำกับหมายเลขนั้น (เช่นZ8<Z6<Z7<Z5<Z3<Z0<Z9>>>>>>>) ซึ่งจะมีสมาชิกที่แตกต่างจากประเภทอื่น ๆ ใน C ++ ทุกประเภทที่สามารถสร้างจากอินพุตใด ๆ จะต้องถูกสร้างขึ้นในเวลารวบรวม
supercat

13

คำวิจารณ์ปกติคือการขาดการ reification กล่าวคือวัตถุที่รันไทม์ไม่มีข้อมูลเกี่ยวกับข้อโต้แย้งทั่วไป (แม้ว่าข้อมูลจะยังคงปรากฏอยู่ในเขตข้อมูลวิธีการตัวสร้างและคลาสและส่วนต่อขยายที่เพิ่มขึ้น) ซึ่งหมายความว่าคุณสามารถโยนพูดไปArrayList<String> List<File>คอมไพเลอร์จะให้คำเตือน แต่ก็ยังจะเตือนคุณถ้าคุณใช้ArrayList<String>กำหนดไปยังอ้างอิงแล้วโยนมันทิ้งไปObject List<String>อัพไซด์นั้นด้วย generics คุณอาจไม่ควรทำการแคสติ้งประสิทธิภาพจะดีกว่าหากไม่มีข้อมูลที่ไม่จำเป็นและแน่นอนความเข้ากันได้แบบย้อนหลัง

บางคนบ่นว่าคุณไม่สามารถโหลดมากเกินไปบนพื้นฐานของข้อโต้แย้งทั่วไป ( void fn(Set<String>)และvoid fn(Set<File>)) คุณต้องใช้ชื่อวิธีที่ดีกว่าแทน โปรดทราบว่าการโหลดเกินพิกัดนี้ไม่จำเป็นต้องมีการแก้ไขเนื่องจากการโอเวอร์โหลดเป็นปัญหาแบบคงที่และรวบรวมเวลา

ประเภทดั้งเดิมไม่สามารถใช้ได้กับยาชื่อสามัญ

สัญลักษณ์แทนและขอบเขตค่อนข้างซับซ้อน พวกมันมีประโยชน์มาก ถ้า Java ต้องการความไม่เปลี่ยนรูปแบบและอินเทอร์เฟซ tell-Don't-ask ดังนั้นการประกาศด้านแทนที่จะเป็นข้อมูลทั่วไปด้านการใช้จะเหมาะสมกว่า


"โปรดทราบว่าการโหลดเกินพิกัดนี้ไม่จำเป็นต้องมีการแก้ไขเนื่องจากการโอเวอร์โหลดเป็นปัญหาแบบคงที่และรวบรวมเวลา" มันจะไม่ได้? มันจะทำงานยังไงถ้าไม่มีการปรับปรุงใหม่? JVM ไม่จำเป็นต้องรู้ที่รันไทม์คุณมีเซ็ตประเภทใดเพื่อรู้วิธีการโทร?
MatrixFrog

2
@ MatrixFrog ไม่อย่างที่ฉันบอกว่าการโหลดมากเกินไปเป็นปัญหาเวลาคอมไพล์ คอมไพเลอร์ใช้ประเภทสแตติกของนิพจน์เพื่อเลือกโอเวอร์โหลดเฉพาะซึ่งเป็นวิธีการเขียนไปยังไฟล์คลาส หากคุณObject cs = new char[] { 'H', 'i' }; System.out.println(cs);พิมพ์ไร้สาระ เปลี่ยนชนิดของcsการและคุณจะได้รับchar[] Hi
Tom Hawtin - tackline

10

Java generics ดูดเนื่องจากคุณไม่สามารถทำสิ่งต่อไปนี้:

public class AsyncAdapter<Parser,Adapter> extends AsyncTask<String,Integer,Adapter> {
    proptected Adapter doInBackground(String... keywords) {
      Parser p = new Parser(keywords[0]); // this is an error
      /* some more stuff I was hoping for but couldn't do because
         the compiler wouldn't let me
      */
    }
}

คุณไม่จำเป็นต้องเขียงทุกชั้นในรหัสข้างต้นเพื่อให้มันทำงานได้ตามที่ควรถ้า generics เป็นพารามิเตอร์ระดับสามัญจริง ๆ


คุณไม่จำเป็นต้องฆ่าคนทุกชนชั้น คุณเพียงแค่ต้องเพิ่มคลาสของโรงงานที่เหมาะสมและฉีดเข้าไปใน AsyncAdapter (และโดยพื้นฐานแล้วสิ่งนี้ไม่ได้เกี่ยวกับยาชื่อสามัญ แต่ความจริงที่ว่าผู้สร้างไม่ได้รับมรดกใน Java)
Peter Taylor

13
@PeterTaylor: ระดับโรงงานที่เหมาะสมเพียงแค่ผลักแผ่นเหล็กทั้งหมดให้เป็นระเบียบและไม่สามารถแก้ปัญหาได้ ตอนนี้ทุกครั้งที่ฉันต้องการยกตัวอย่างอินสแตนซ์ใหม่ของParserฉันจะต้องทำให้รหัสโรงงานที่ซับซ้อนยิ่งขึ้น ในขณะที่ถ้า "ทั่วไป" มีความหมายอย่างแท้จริงโดยทั่วไปแล้วก็ไม่จำเป็นต้องมีโรงงานและเรื่องไร้สาระอื่น ๆ ทั้งหมดที่เกิดจากชื่อของรูปแบบการออกแบบ มันเป็นหนึ่งในหลาย ๆ เหตุผลที่ฉันชอบทำงานในภาษาแบบไดนามิก
davidk01

2
บางครั้งคล้ายกันใน C ++ ซึ่งคุณไม่สามารถผ่านที่อยู่ของนวกรรมิกเมื่อใช้เทมเพลต + เสมือน - คุณสามารถใช้ functor จากโรงงานแทน
Mark K Cowan

Java ดูดเพราะคุณไม่สามารถเขียน "proptected" ก่อนที่ชื่อของวิธีการหรือไม่ สงสารคุณ. ยึดติดกับภาษาแบบไดนามิก และระวัง Haskell เป็นพิเศษ
fdreger

5
  • คำเตือนคอมไพเลอร์สำหรับพารามิเตอร์ทั่วไปที่ขาดหายไปซึ่งไม่มีจุดประสงค์ทำให้ภาษาไม่มีความหมายเช่น public String getName(Class<?> klazz){ return klazz.getName();}

  • ยาสามัญไม่เล่นดีกับอาร์เรย์

  • ข้อมูลประเภทที่หายไปทำให้การสะท้อนของการคัดเลือกนักแสดงและเทปพันสายไฟ


ฉันจะได้รับรำคาญเมื่อฉันได้รับการเตือนสำหรับการใช้แทนHashMap HashMap<String, String>
Michael K

1
@Michael แต่จริง ๆ แล้วควรใช้กับ generics ...
ทางเลือก

ปัญหาอาร์เรย์ไปที่การปรับแก้ได้ ดูคำอธิบายเกี่ยวกับหนังสือ Java Generics (Alligator)
ncmathsadist

5

ฉันคิดว่าคำตอบอื่น ๆ ได้กล่าวถึงเรื่องนี้ในระดับหนึ่ง แต่ไม่ชัดเจนมาก ปัญหาอย่างหนึ่งของยาชื่อสามัญก็คือการสูญเสียประเภทสามัญในระหว่างกระบวนการสะท้อนกลับ ตัวอย่างเช่น:

List<String> arr = new ArrayList<String>();
assertTrue( ArrayList.class, arr.getClass() );
TypeVarible[] types = arr.getClass().getTypedVariables();

น่าเสียดายที่ประเภทที่ส่งคืนไม่สามารถบอกคุณได้ว่าประเภททั่วไปของ arr คือ String มันแตกต่างกันเล็กน้อย แต่มันสำคัญ เนื่องจาก arr ถูกสร้างขึ้นที่ runtime ชนิดทั่วไปจะถูกลบใน runtime ดังนั้นคุณไม่สามารถหาได้ ในขณะที่บางคนกล่าวว่าArrayList<Integer>มีลักษณะเหมือนกันArrayList<String>จากมุมมองการสะท้อน

สิ่งนี้อาจไม่สำคัญสำหรับผู้ใช้ Generics แต่สมมุติว่าเราต้องการสร้างกรอบงานแฟนซีที่ใช้การสะท้อนเพื่อหาสิ่งแฟนซีเกี่ยวกับวิธีที่ผู้ใช้ประกาศประเภททั่วไปที่เป็นรูปธรรมของอินสแตนซ์

Factory<MySpecialObject> factory = new Factory<MySpecialObject>();
MySpecialObject obj = factory.create();

สมมติว่าเราต้องการให้โรงงานทั่วไปสร้างอินสแตนซ์MySpecialObjectเนื่องจากเป็นประเภททั่วไปที่เป็นรูปธรรมที่เราประกาศสำหรับอินสแตนซ์นี้ คลาสของโรงงานไม่สามารถซักถามตัวเองเพื่อค้นหาประเภทที่เป็นรูปธรรมที่ประกาศไว้สำหรับอินสแตนซ์นี้ได้เนื่องจาก Java ลบพวกเขา

ใน generics ของ. Net คุณสามารถทำสิ่งนี้ได้เพราะ ณ รันไทม์วัตถุจะรู้ว่ามันเป็นประเภททั่วไปเพราะคอมไพเลอร์ปฏิบัติตามมันเป็นไบนารี ด้วยการลบ Java ไม่สามารถทำได้


1

ฉันสามารถพูดสิ่งที่ดีเกี่ยวกับยาชื่อสามัญได้ แต่นั่นไม่ใช่คำถาม ฉันสามารถบ่นว่าพวกเขาไม่สามารถใช้งานได้ในเวลาทำงานและไม่ทำงานกับอาร์เรย์ แต่ถูกกล่าวถึง

ความรำคาญใจอย่างใหญ่หลวง: บางครั้งฉันก็เข้าสู่สถานการณ์ที่ไม่สามารถใช้ยาชื่อสามัญได้ (อาร์เรย์เป็นตัวอย่างที่ง่ายที่สุด) และฉันไม่สามารถเข้าใจได้ว่ายาชื่อสามัญไม่สามารถทำงานได้หรือว่าฉันแค่โง่ ฉันเกลียดที่ ยาชื่อสามัญควรทำงานทุกครั้ง ทุกครั้งที่ฉันไม่สามารถทำสิ่งที่ฉันต้องการโดยใช้ Java-the-language ฉันรู้ว่าปัญหาคือฉันและฉันรู้ว่าถ้าฉันยังคงผลักดันฉันจะไปที่นั่นในที่สุด ด้วยยาชื่อสามัญถ้าฉันขัดขืนเกินไปฉันจะเสียเวลามาก

แต่ปัญหาที่แท้จริงคือยาชื่อสามัญเพิ่มความซับซ้อนมากเกินไปเพื่อผลประโยชน์ที่น้อยเกินไป ในกรณีที่ง่ายที่สุดมันสามารถหยุดฉันจากการเพิ่มแอปเปิ้ลในรายการที่มีรถยนต์ ละเอียด. แต่หากไม่มีข้อมูลทั่วไปข้อผิดพลาดนี้จะทำให้ ClassCastException รวดเร็วมากในขณะใช้งานโดยเสียเวลาเพียงเล็กน้อย หากฉันเพิ่มรถที่มีที่นั่งสำหรับเด็กที่มีเด็กอยู่ในนั้นฉันต้องมีการเตือนเวลารวบรวมว่ารายการนั้นมีไว้สำหรับรถยนต์ที่มีที่นั่งสำหรับเด็กที่มีลิงชิมแปนซีอยู่หรือไม่ รายการอินสแตนซ์ของวัตถุธรรมดาเริ่มดูเหมือนความคิดที่ดี

รหัสทั่วไปมีคำและตัวอักษรจำนวนมากและใช้พื้นที่มากและใช้เวลาอ่านนานกว่า ฉันสามารถใช้เวลามากมายในการรับรหัสพิเศษทั้งหมดเพื่อให้ทำงานได้อย่างถูกต้อง เมื่อใดฉันจะรู้สึกฉลาดอย่างชาญฉลาด ฉันเสียเวลาไปหลายชั่วโมงหรือมากกว่านั้นและฉันก็ต้องสงสัยว่ามีใครอีกไหมที่จะสามารถหารหัสได้ ฉันต้องการที่จะมอบให้เพื่อการบำรุงรักษาให้กับผู้ที่มีความฉลาดน้อยกว่าตัวฉันเองหรือผู้ที่มีเวลาน้อยที่จะเสีย

ในทางกลับกัน (ฉันได้รับบาดเจ็บเล็กน้อยและรู้สึกว่าจำเป็นต้องให้สมดุล) มันดีเมื่อใช้คอลเลกชันที่เรียบง่ายและแผนที่ที่มีการเพิ่มใส่และได้รับการตรวจสอบเมื่อเขียนและมักจะไม่เพิ่มมาก ซับซ้อนรหัส( ถ้า มีคน อื่น เขียนคอลเลกชันหรือแผนที่) และ Java ดีกว่านี้กว่า C # ดูเหมือนว่าไม่มีคอลเลกชัน C # ที่ฉันต้องการใช้ในการจัดการข้อมูลทั่วไป (ฉันยอมรับว่าฉันมีรสนิยมแปลก ๆ ในคอลเลกชัน)


พูดจาโผงผางดี อย่างไรก็ตามมีหลายไลบรารี / กรอบงานที่สามารถมีอยู่ได้โดยไม่ใช้ชื่อสามัญเช่น Guice, Gson, Hibernate และยาชื่อสามัญก็ไม่ยากเมื่อคุณคุ้นเคย การพิมพ์และอ่านอาร์กิวเมนต์ชนิดเป็น PITA จริง ที่นี่วาลช่วยถ้าคุณสามารถใช้
maaartinus

1
@maaartinus: เขียนสิ่งนั้นสักครู่แล้ว OP ก็ขอ "ปัญหา" ด้วย ฉันพบว่ายาชื่อสามัญมีประโยชน์อย่างยิ่งและชอบพวกเขามาก - มากกว่าตอนนี้ อย่างไรก็ตามหากคุณกำลังเขียนคอลเลกชันของคุณเองพวกเขาจะยากที่จะเรียนรู้ และประโยชน์ของมันจะจางหายไปเมื่อประเภทคอลเลกชันของคุณถูกกำหนดในเวลาทำงาน - เมื่อคอลเลกชันมีวิธีที่บอกคลาสของรายการ ณ จุดนี้ข้อมูลทั่วไปไม่ทำงานและ IDE ของคุณจะสร้างคำเตือนที่ไม่มีความหมายหลายร้อยรายการ การเขียนโปรแกรม Java 99.99% ไม่เกี่ยวข้องกับเรื่องนี้ แต่ฉันเพิ่งมีปัญหามากเมื่อฉันเขียนข้างต้น
RalphChapin

เป็นทั้งผู้พัฒนา Java และ C # ฉันสงสัยว่าทำไมคุณถึงชอบคอลเลกชัน Java?
Ivaylo Slavov

Fine. But without generics this error would throw a ClassCastException really quick at run time with little time wasted.ขึ้นอยู่กับว่าต้องใช้เวลารันนานเท่าใดระหว่างการเริ่มต้นโปรแกรมและการกดบรรทัดของรหัสที่เป็นปัญหา หากผู้ใช้รายงานข้อผิดพลาดและใช้เวลาหลายนาที (หรือกรณีที่เลวร้ายที่สุดหลายชั่วโมงหรือหลายวัน) ในการทำซ้ำการตรวจสอบเวลาที่คอมไพล์เริ่มดูดีขึ้นเรื่อย ๆ ...
Mason Wheeler
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.