ไม่ได้เขียนบล๊อกเกี่ยวกับ Programming มาพักใหญ่ๆ และ วันนี้ขอจัดหน่อยละกัน
Java และภาษาตระกูล C ส่วนใหญ่จะมีลักษณะการประกาศตัวแปรแบบ Type-sensitive หรือต้องฟิกไปเลยตั้งแต่สร้างตัวแปรว่ามันจะเก็บข้อมูลแบบไหน เช่น
int x, y; double d; String str;
แต่ถ้าเป็นพวกภาษา Dynamic-type การสร้างตัวแปรพวกนี้จะง่ายกว่า เช่น var x ใน JavaScript หรือ $x ใน PHP ซึ่งสร้างอะไรขึ้นมาก็เก็บได้ตั้งแต่ตัวเลขยัน object
ซึ่งบางครั้งเราใช้ก็อาจจะเกิดเหตุขัดใจนิดหน่อยกับภาษาพวก Type-sensitive เพราะ
บางครั้ง เราก็ไม่รู้ Type ของตัวแปรนะ
มาถึงตรงนี้อาจจะมีคนสงสัย ว่าเราเขียนโปรแกรมเอง แล้วเราจะไม่รู้ Type ของโค้ดเราได้ยังไง?
เหตุการณ์นี้มักจะเกิดขึ้นเมื่อเราเขียนโค้ดพวก Library เช่น ถ้าเราจะสร้าง LinkedList ซึ่งมี Node เป็นตัวเก็บข้อมูล (ใครยังไม่รู้จัก LinkedList ลองไปอ่านเรื่อง Data Structure ดูนะ)
เราก็สร้าง Node ขึ้นมาแบบนี้ละกัน
class Node{ int data; Node next }
สมมุติว่าเราอยากได้ LinkedList ที่เก็บ integer เราก็เลยสร้าง Node ที่เก็บ data เป็น int ... ก็ได้นี่นา แล้วปัญหาคืออะไรงั้นเหรอ
คำถาม: แล้วถ้าเราต้องการ LinkedList ที่เก็บ String ได้ล่ะ
อ้อ ก็สร้าง Node ตัวใหม่มาไงล่ะ
class NodeString{ String data; Node next; }
แบบนี้ไง
อืม... ลองมาคิดกันเล่นๆ นะ ถ้าทำแบบนี้ แล้วในอนาคตต้องการ LinkedList ที่เก็บข้อมูลชนิดอื่นได้จะทำไงล่ะ จะสร้างคลาส NodeXXX เพิ่มขึ้นมาแบบนี้ไปทุกครั้งเลยเหรอ ไม่ดีมั้ง?
ทางแก้ก็คือเอา Generic มาช่วยล่ะ
Generic / มองtypeเป็นตัวแปรซะ
มาดูวิธีแก้ปัญหาการเขียนโค้ดเมื่อกี้แบบใหม่นะ
ในเมื่อเอาจริงๆ แล้ว คลาส Node ของเรามันก็ไม่จำเป็นต้องมีหลายตัวนี่นา แต่แค่ตรง data เราไม่รู้จะใช่typeอะไรเข้าไปเท่านั้นเอง
ถ้าไม่รู้ type ก็ใช้ Object แทนก็ได้นี่?
class Node{
Object data;
Node next
}
ในเมื่อคลาส Object เป็นพ่อทุกสถาบันของคลาสในโลก Java (ทุกคลาส extends มาจากมันหมด) ดังนั้นมันจะเก็บอะไรก็ได้
แต่การใช้ Object ก็มีปัญหาเล็กน้อยนั่นคือเวลาเอาข้อมูลออกมาใช้ จำเป็นต้อง Casting ชนิดข้อมูลก่อน ซึ่งมันมีโอกาสพังสูง
Node n1 = new Node();
Node n2 = new Node();
n1.data = 1;
n2.data = "stringยังไงล่ะ";
int x = (int) n1.data;
int y = (int) n2.data;
เมื่อ Object เก็บได้ทุกอย่าง เวลาใส่เราก็สบายใจได้ว่าจะtypeอะไรก็โยนให้มันได้
แต่ตอนเรียกใช้เท่านั้นแหละ เราต้องทำการ Cast มันกลับเป็น type เดิมของมันซึ่งตรงนี้ไม่ว่าคุณจะแคสแบบไหนมันคอมไพล์ผ่านหมดเลยนะ!! ความซวยจึงบังเกิดถ้าคุณดันแคสมันผิดtype เช่นในตัวอย่าง n2.data นั้นเก็บค่า String อยู่ พอสั่งมันแคสเป็น int ก็เตรียมตัวเจอ ClassCastException เด้งขึ้นมาได้เลยนะ
ในเมื่อมันไม่เวิร์ค งั้นเอาใหม่!
class Node{
X data;
Node next;
}
ในเมื่อเราไม่รู้ว่าตกลงแล้ว data จะเป็นtypeอะไร ก็ใช้เป็นตัวแปรแทนละกัน (ฮา)
โอเค ยังไม่ต้องแย้งว่า Java จะมอง X เป็นชื่อคลาสซึ่งมันยังไม่มีและจะขึ้นตัวแดง เราแค่บอกเพิ่มว่า X ตัวนี้น่ะมันเป็นtypeนะ แบบนี้
class Node<X>{ X data; Node next; }
เขียนแบบนี้แปลว่า "เวลาเรา new Node ขึ้นมาน่ะ อยากให้คนใช้ส่งคลาสเข้ามาให้ด้วย" ใครเคยใช้คลาสพวก ArrayList<คลาส> น่าจะนึกภาพออก
//จากเดิม Node n = new Node(); //กลายเป็น Node<String> n1 = new Node<String>(); Node<Integer> n2 = new Node<Integer>(); Node<MyOwnClass> n3 = new Node<MyOwnClass>();
ในกรณีนี้ ทั้ง n1, n2, n3 จะสร้างขึ้นมาจากคลาส Node ตัวเดียวกันเลย แต่ .data ของพวกมันจะต่างกันแล้ว เพราะคลาสที่เราส่งเข้าไปตอนสร้างเป็นคนละตัวกัน
แล้ว <E> หรือ <T> มันคืออะไร
ไม่มีอะไรหรอก คุณสามารถใช้ตัวแปรอะไรแทนtypeของคุณก็ได้ จะใช้ X แบบตัวอย่างเมื่อกี้ก็ได้ แค่ส่วนใหญ่ในโลกโปรแกรมมิ่งเขามักจะมีเทรนด์การตั้งชื่ออยู่ แบบเวลาเราวนลูป ก็มักจะใช้ i=0 แบบนั้นใช่มั้ยล่ะ
โดยหลักๆ เขาจะใช้พวกนี้กัน
- E - Element (ตัวมาตราฐานที่ใช้ใน Java Collections Framework)
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - ชนิดข้อมูลตัวที่ 2, 3, 4, ...
รับมากกว่า 1 Type ก็ได้
หากโค้ดของเราต้องการ Generic Type มากกว่า1ตัวก็แค่ใส่ , คั่นไปแบบนี้
class Node<E, T, S>{ E data1; T data2; S data3; }
แต่คำเตือนคืออย่าใช้เยอะโดนไม่จำเป็น บางเคสเลี่ยงไปใช้ interface หรือ abstract class แทนก็ได้นะ มันอ่านเข้าใจง่ายกว่า
ปะ Generic ที่ method ก็ได้นะ
ถ้าการปะ Generic ที่ตัวคลาสยังไม่ตอบโจทย์พอ (เพราะมีบางเคสที่เราต้อง new Object ก่อน แล้วมารู้typeตอนกำลังจะเรียก method) ก็ไปปะที่ method แทนได้นะ
class Node<E>{ E data; <T> void method1(T t){ //TODO } <T> T method2(T t){ //TODO return t; } } //แล้วก็เรียกใช้ Node n = new Node(); n.method1("อะไรก็ได้เลย"); int x = n.method2(123); String str = n.method2("ใส่สตริงลงไปก็จะรีเทิร์นสตริงไงล่ะ");
โดยให้วาง Generic ไว้หน้า return_type แต่แต่หลัง access modifier และ static (แบบนี้ public static <T> T method(T t) นะ) วิธีใช้อาจจะเข้าใจยากกว่าการปะ Generic ที่ตัวคลาสไปสักหน่อย คือครั้งนี้เวลาเราเรียกใช้เราไม่ต้องส่งชื่อคลาสไปแล้ว แต่ตัว Generic จะดูtypeจากค่าที่เราส่งเข้าไปเอง เช่น
- n.method2(123) - ก็จะมองว่า T เป็น int
- n.method2("ใส่สตริงลงไปก็จะรีเทิร์นสตริงไงล่ะ") - ก็จะมองว่า T เป็นสตริงนะ
เรื่องสุดท้าย ขอปิดด้วย
การ filter ให้ใส่ type เฉพาะที่เราต้องการด้วยคำสั่ง "extends"
ปัญหาสุดท้ายของการใช้ Generic คือบางครั้งเราไม่ได้ต้องการให้คนที่เรียกใช้ใส่คลาสมั่วๆ อะไรมาก็ได้ เพราะมันจะเกิดเคสแบบนี้
class Node<E>{
E data;
Node next;
int compateTo(Node other){
return data ????? other.data;
}
}
จะเขียน method: compateTo แต่ทีนี้ เนื่องจากเราไม่รู้ว่า E เป็นคลาสอะไรกันแน่ (อย่าเข้าใจผิดว่าทุกคลาสจะต้องมี compareTo ให้เรียกใช้นะ) แล้วเราจะเปรียบเทียบ data ทั้ง2ตัวยังไงล่ะ
ก็แปลว่าจริงๆ เราอยากจะให้เขาเรียกใช้โดยใส่คลาสอะไรก็ได้เหมือนเดิมนั่นแหละ ขอแค่คลาสนั้นจะต้องมี compareTo ให้เรียกใช้
Note: เมธอด int compareTo(Object other) เป็นเมธอดบังคับในอินเตอร์เฟซ Comparable ซึ่งมีมาให้ใน Java Libraryตัวหลักอยู่แล้วนะ บอกให้รู้ไว้ก่อน
class Node<E extends Comparable>{ E data; Node next; int compateTo(Node other){ return data.compareTo(other.data); } }
แต่ถึงมันจะเป็น interface แต่พออยู่ใน Generic แล้วเราจะใช้ extends
แทนนะ
โอเค เท่านี้ E ของเราก็จะเป็นคลาสอะไรก็ได้ แต่มี compateTo
อย่างแน่นอน เรียกได้อย่างสบายใจและล่ะ
...
จากที่เขียนไป คงเห็นตัวอย่างการใช้ Generic ไปพอสมควรแล้วนะ จริงๆ ก็ยังมีอีกมาก เช่นการใช้ Nested-Generic หรือ Genericซ้อนGeneric เช่น <E<T>> แต่มันไม่ค่อยเจอ
สำหรับtipการใช้ Java วันนี้ก็จบแค่นี้ก่อนละกัน ^__^
1 Response
[…] แต่เนื่องจาก List นั้นไม่ใช่ array แท้ๆ (มันเป็น class ที่สร้างขึ้นมาเอง) เราเลยต้องมีการบอกด้วยว่าข้อมูลในลิสต์นี้เป็นชนิดอะไรด้วยการใส่ generic ลงไป (generic คือไอ้ <…> ที่อยู่ข้างหลังนะ อ่านเรื่อง generic เพิ่มเติมได้ที่นี่) […]