Generics
ปกติเราสามารถประกาศคลาส List ดังนี้
คลาส List โดยปกติจะรับค่า objects ได้หลายประเภท เราสามารถส่งค่า objects ประเภทต่างๆเข้าไปได้เช่น
ส่วนการดึงค่าออกมาสามารถทำได้โดย
generic เป็นการประกาศตัวแปรภายในเครื่องหมายวงเล็บมุม <angle brackets> และตามด้วยชื่อคลาส
การใส่ generic type ไปยังคลาส List ทำให้คอมไพเลอร์จะตรวจสอบเฉพาะตัวแปรที่กำหนด เช่น String เท่านัั้นที่จะเพิ่มไปใน List
ดังนั้นต่อไปหากจะดึงค่าจาก List ออกมาก็ไม่จำเป็นต้องทำ casting
Generics จะทำงานกับ object ดังนั้นหากระบุคำสั่งแบบด้านล่างนี้ ระบบจะคอมไพล์ไม่ผ่าน
1 | List list = new ArrayList(); |
คลาส List โดยปกติจะรับค่า objects ได้หลายประเภท เราสามารถส่งค่า objects ประเภทต่างๆเข้าไปได้เช่น
1 2 3 | list.add("a"); list.add(new Integer(1)); list.add(Boolean.TRUE); |
ส่วนการดึงค่าออกมาสามารถทำได้โดย
1 | String s = (String) list.get(0); |
generic เป็นการประกาศตัวแปรภายในเครื่องหมายวงเล็บมุม <angle brackets> และตามด้วยชื่อคลาส
1 | List<String> list = new ArrayList<String>(); |
การใส่ generic type ไปยังคลาส List ทำให้คอมไพเลอร์จะตรวจสอบเฉพาะตัวแปรที่กำหนด เช่น String เท่านัั้นที่จะเพิ่มไปใน List
1 2 3 | list.add("a"); //OK list.add(new Integer(1)); // Compile-time error list.add(Boolean.TRUE); // Compile-time error |
ดังนั้นต่อไปหากจะดึงค่าจาก List ออกมาก็ไม่จำเป็นต้องทำ casting
1 | String s = list.get(0); |
Generics จะทำงานกับ object ดังนั้นหากระบุคำสั่งแบบด้านล่างนี้ ระบบจะคอมไพล์ไม่ผ่าน
1 | List<int> list = new ArrayList<int>(); |
คลาสที่รองรับ generics แต่ไม่ได้ระบุ Object ไว้จะเรียกว่า Raw type
1 2 3 4 | // Raw type List raw = new ArrayList(); // Generic type List<String> generic = new ArrayList<String>(); |
The Diamond Operator
ก่อน Java7 เราจะต้องระบุ generics type ทั้งสองข้างของคำสั่ง เช่น
1 | List<List<String>> generic = new ArrayList<List<String>>(); |
หลังจาก Java7 เราสามารถเขียนคำสั่งการสร้าง object ได้ง่ายขึ้นโดยใช้คำสั่ง
1 | List<List<String>> generic = new ArrayList<>(); |
ฝั่งขวาของคำสั่งข้างบนคล้ายรูปเพชร ดังนั้นจึงเรียกว่า diamond operator
ใน Java8 เราสามารถใช้ diamond operator อ้างอิงกับ parameter ได้ ตามตัวอย่างนี้
1 2 3 4 5 6 | void testGenericParam(List<String> list) { } void test() { // In Java 7, this line generates a compile error // In Java 8, this line compiles fine testGenericParam(new ArrayList<>()); } |
Generic Classes
เรามาดูความยืดหยุ่นของ generic class จากตัวอย่างสองตัวอย่างนี้
1 2 3 4 5 6 7 8 9 10 11 12 | class Holder { private String s; public Holder(String s) { this.s = s; } public String getObject() { return s; } public void printObject() { System.out.println(s); } } |
คลาสนี้คอมไพล์ผ่านปกติ และจะรับ object ประเภท String เท่านั้น และถ้าหากเราจะสร้างคลาสนี้ให้รองรับ Integer เราจะต้องเขียนโปรแกรมตามนี้
1 2 3 4 5 6 7 8 9 10 11 12 | class IntegerHolder { private Integer s; public Holder(Integer s) { this.s = s; } public Integer getObject() { return s; } public void printObject() { System.out.println(s); } } |
Generics ช่วยจัดการในกรณีข้างต้น เริ่มจากการประกาศตัวแปรดังนี้
1 2 3 | class Holder<T> { // ... } |
ตัวแปร T เป็น generic type ที่ใช้ในคลาสนี้
1 2 3 4 5 6 7 8 9 10 11 12 | class Holder<T> { private T t; public Holder(T t) { this.t = t; } public T getObject() { return t; } public void printObject() { System.out.println(t); } } |
เมื่อสร้าง instance ของคลาสแล้ว เราจึงทำการระบุประเภทของคลาส T
1 2 3 | Holder<String> h1 = new Holder<>("Hi"); Holder<Integer> h2 = new Holder<>(1); String s = h1.getObject(); |
แต่ถ้าหากไม่ระบุประเภทของคลาส T เราก็จะใช้ raw type แทน (ซึ่งใช้ Object type)
1 2 | Holder h3 = new Holder("Hi again"); Object o = h3.getObject(); |
จากตัวอย่างข้างต้น เราสามารถสร้าง parameter เป็นสองค่าได้ดังนี้
1 2 3 | class Holder<T, U> { // ... } |
Generic Methods
ดูตัวอย่างการสร้าง parameter ในคลาสนี้
1 2 3 4 5 | class Utils { public static <T> void print(T t) { System.out.println(t); } } |
คลาสนี้กำหนดเมทอดที่รับ argument ของคลาส T
และดูสองตัวอย่างนี้ในการกำหนด generic methods
1 2 3 4 5 | <T> void genericMethod1(List<T> list) { } <T, U> T genericMethod2(U u) { T t = null; return t; } |
เมื่อเมทอดกำหนด generic type จะต้องกำหนดไว้ก่อน return type
ส่วนถ้าคลาสกำหนด generic type จะกำหนดไว้หลังชื่อของคลาส
การเรียกใช้เมทอดของตัวอย่างแรกนั้น ใช้คำสั่งนี้
1 | Utils().print(10); |
หรือจะกำหนดประเภทของ object ระหว่าง dot กับชื่อเมทอดก็ได้
1 | Utils().<Integer>print(10); |
Wildcards
Generics ใช้ได้ในหลายกรณี แต่จะมีปัญหาในสองเรื่องนี้
เรื่องแรกเราอาจเข้าใจผิดได้ในกรณีที่เรารู้ว่า ArrayList สืบทอดมาจาก List และ String ก็เป็น subclass ของ Object โค้ดต่อไปนี้น่าจะถูกต้อง
1 | List<Object> list = new ArrayList<String>(); |
แต่ code นี้คอมไพล์ไม่ผ่านเพราะว่าในเรื่องของ generics เราไม่สามารถกำหนดคลาสลูกไปยังคลาสแม่ได้ และคลาสที่ระบุต้องเหมือนกันทั้งสองข้าง
เราสามารถเขียนโค้ดใหม่โดยใช้พารามิเตอร์ Wildcards (<?>)
1 | List<?> list = new ArrayList<String>(); |
โค้ดนี้คอมไพล์ผ่าน และพารามิเตอร์ Wildcards (<?>) บอกว่าประเภทของ List นั้นไม่ทราบแต่สามารถ match กับคลาสใดก็ได้
หรือจะมองว่า List<?> เป็น superclass ของ List ก็ได้เพราะว่าสามารถกำหนดประเภทใดๆให้แก่ List ได้
1 2 3 4 5 6 7 8 9 | List<String> stringList = new ArrayList<>(); List<Integer> intList = new ArrayList<>(); // No problem List<?> unknownTypeList = stringList; // No problem either List<?> unknownTypeList = intList; for(Object o : unknownTypeList) { // Object? System.out.println(o); } |
เนื่องจาก Compiler ไม่รู้ประเภทของ List<?> เราจึงใช้คลาส Object เพื่อไม่ให้เกิด error ในตอน run-time
แต่ควรระวังว่า List<?> กับ List<Object> นั้นต่างกันการประกาศอย่างหลังจะทำให้ code ข้างต้นคอมไพล์ไม่ผ่าน
ตัวอย่างด้านล่างนี้ ก็ทำให้ code คอมไพล์ไม่ผ่านเช่นกัน เพราะคอมไพเลอร์ไม่สามารถระบุชนิดของ object ได้
1 2 | List<?> list = new ArrayList<String>(); list.add("Hi"); // Compile-time error |
วิธีการป้องกันความสับสนของชนิด object นั้น อาจใช้วิธีของ "bounded wildcards" เรามาดูตัวอย่างที่เกิดความสับสนและทำให้คอมไพล์ไม่ผ่านจาก code นี้
1 2 3 4 5 6 | class Printer<T> { public void print(T t) { System.out.println(t.toUpperCase());// Error // What if T doesn't represent a String? } } |
สูตรของ bounded wildcards มีสองข้อนี้
- ? extends T (Upper-bounded wildcard)
- ? super T (Lower-bounded wildcard)
1 2 3 4 5 | class Printer<T extends String> { public void print(T t) { System.out.println(t.toUpperCase());//OK! } } |
<T extends String> หมายถึงคลาสที่ extends มาจาก String เมื่อนำไปใช้สามารถแทน T ด้วย String ได้
1 2 3 | Printer<String> p1 = new Printer<>(); // OK // Error, Byte is not a String Printer<Byte> p2 = new Printer<>(); |
Upper-bounded wildcard ยังนำมาแก้ปัญหา code ต่อไปนี้
1 2 | List<Object> list = new ArrayList<String>(); // Error List<? extends Object> list2 = new ArrayList<String>(); // OK! |
แต่เราไม่สามารถเพิ่ม list โดยคำสั่งด้านล่างนี้เพราะคอมไพเลอร์ไม่แน่ใจว่า list ใช้เก็บ object ประเภทอะไร
1 | list2.add("Hi"); // Compile-time error |
การกำหนด List<Number> จะเป็นการเฉพาะเจาะจงมากกว่า List<? extends Number> โดยการกำหนดแบบแรกจะรับค่าที่กำหนดไว้เป็น List<Number> เท่านั้น แต่การกำหนดอย่างหลังจะรับค่าที่กำหนดไว้เป็น List<Integer>, List<Float> และอื่นๆได้
1 2 3 4 5 6 7 8 9 10 11 12 | List<Integer> listInteger = new ArrayList<>(); List<Float> listFloat = new ArrayList<>(); List<Number> listNumber = new ArrayList<>(); listNumber.add(new Integer(1)); // OK listNumber.add(new Float(1.0F)); // OK listNumber = listInteger; // Error listNumber = listFloat; // Error List<? extends Number> listExtendsNum = new ArrayList<>(); // This would cause an error // listExtendsNum.add(new Integer(1)); listExtendsNum = listInteger; // OK listExtendsNum = listFloat; // OK |
ต่อมา เรามาดู lower-bounded wildcard จากตัวอย่างนี้
1 | List<? super Integer> list = new ArrayList<>(); |
จากตัวอย่างนี้ list สามารถเป็นประเภท Integer หรือ super type ของ Interger ก็ได้
1 2 3 | List<? super Integer> list = new ArrayList<>(); list.add(1); // OK! list.add(2); // OK! |
จากที่กล่าวมา เราต้องไม่สับสนระหว่างการ add กับการ assign เนื่องจากการ add เราสามารถเพิ่ม subclass ของ T ได้เพราะมันคงเป็นคลาส T เช่นเดิม
1 2 3 4 5 6 7 | List<Integer> listInteger = new ArrayList<>(); List<Object> listObject = new ArrayList<>(); List<? super Number> listSuperNum = new ArrayList<>(); listSuperNum.add(new Integer(1)); // OK listSuperNum.add(new Float(1.0F)); // OK listSuperNum = listInteger; // Error! listSuperNum = listObject; // OK |
Generic limitations
บทส่งท้ายเรามาดูข้อจำกัดของ generic กัน
Generics ไม่สามารถใช้ได้กับ primitive types
1 2 | // Use Wrappers instead List<int> list = new ArrayList<>(); |
เราไม่สามารถสร้าง instance ของตัวแปรพารามิเตอร์
1 2 3 4 | class Test<T> { T var = new T(); // You don't know the type's constructors } |
เราไม่สามารถประกาศตัวแปรพารามิเตอร์เป็น static ได้
1 2 3 4 5 6 | class Test<T> { // If a static member is shared by many instances, // and each instance can declare a different type, // what is the actual type of var? static T var; } |
generic ไม่สามารถใช้ instanceof ได้
1 2 | if(obj instanceof List<Integer>) { // Error } |
1 2 3 4 | if (obj instanceof List<?>) { // It only works with the unbounded // wildcard to verify that obj is a List } |
generic ไม่สามารถสร้าง array ได้
1 2 3 4 5 | class Test<T> { T[] array; // OK T[] array1 = new T[100]; // Error List<String>[] array2 = new List<String>[10]; // Error } |
เราไม่สามารถ catch หรือ throw generic ได้
1 2 3 4 5 6 7 8 | class GenericException<T> extends Exception { } // Error <T extends Exception> void method() { try { // ... } catch(T e) { // Error } } |
แต่เราสามารถใช้ generic ใน throw clause ได้
1 2 3 | class Test<T extends Exception> { public void method() throws T { } // OK } |

No comments:
Post a Comment