จาวาโปรแกรมมิ่ง - Generics



Generics

ปกติเราสามารถประกาศคลาส List ดังนี้

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)
เริ่มจากการใช้ Upper-bounded wildcard มาแก้ปัญหา code ด้านบนได้ตามนี้
    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