Java 8 – Phương thức Generic


Được đăng vào ngày 06/09/2017 | 0 bình luận

Phương thức Generic là các phương thức cho phép sử dụng tham số kiểu dữ liệu tương tự như với kiểu dữ liệu Generic.

Cú pháp

Cú pháp của phương thức generic giống như với phương thức bình thường, khác ở chỗ chúng ta thêm cặp dấu <> và danh sách các tham số kiểu dữ liệu vào giữa cặp dấu đó được đặt trước kiểu dữ liệu trả về.

Ví dụ chúng ta khai báo lớp Util và phương thức compare() theo kiểu Generic như sau:

public class Util {
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}

public class Pair<K, V> {

    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}

Trong đoạn code trên, chúng ta định nghĩa lớp Pair là một lớp generic lưu 2 thuộc tính keyvalue có kiểu dữ liệu bất kì, lớp Util có một phương thức generic là compare() nhận vào 2 đối tượng Pair và so sánh xem 2 đối tượng này có trùng keyvalue hay không.

Cú pháp gọi phương thức generic compare() như sau:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "berry");
boolean same = Util.<Integer, String>compare(p1, p2);

Ở đây chúng ta khai báo tên các kiểu dữ liệu của các đối tượng được truyền vào phương thức compare() ngay trước tên phương thức.

Tuy nhiên chúng ta vẫn có thể không cần khai báo và để Java tự nhận biết các kiểu này:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);

Giới hạn các kiểu dữ liệu được phép truyền

Chúng ta có thể giới hạn chỉ cho phép một số kiểu dữ liệu nào đó được truyền vào khi khai báo kiểu dữ liệu generic hoặc phương thức generic. Để làm việc này thì chúng ta thêm

extends <tên lớp> 

vào sau tham số biến, chẳng hạn <T extends Integer>, và như thế chúng ta sẽ chỉ có thể truyền vào tham số biến là lớp Integer và các lớp con của lớp Integer.

Chẳng hạn chúng ta có đoạn code như sau:

public class Box { 
    public <T> void print(T t) { 
        System.out.println(t + " is a " + t.getClass().getName()); 
    } 

    public static void main(String[] args) { 
        Box box = new Box(); 
        box.<Integer>print(new Integer(10));
    }
}

Trong đoạn code trên chúng ta định nghĩa lớp Box có một phương thức generic nhận vào một tham số, bên trong phương thức này chúng ta in ra giá trị của tham số đó và tên lớp của tham số.

10 is a java.lang.Integer

Đoạn code trên rất đơn giản, nhưng bây giờ chúng ta sửa lại để lớp Box chỉ nhận các đối tượng Number và các lớp con của Number như sau:

public class Box {     
    public <T extends Number> void print(T t) {         
        System.out.println(t + " is a " + t.getClass().getName());     
    } 
    
    public static void main(String[] args) {         
        Box box = new Box();         
        box.<Integer>print(new Integer(10));             // OK
        box.<String>print(new String("Pho Code"));       // error
    }
}

Và như thế là chúng ta chỉ có thể truyền vào lớp Number hoặc các lớp con của lớp Number. Nếu truyền vào các kiểu dữ liệu khác thì Java sẽ báo lỗi.

10 is a java.lang.Integer
Box.java:9: error:  method print in class Box cannot be applied to given types; 
    box.<String>print(new String("Pho Code"));   
     ^  
 required: T  
 found: String  
 reason: explicit type argument String does not conform to declared bound(s) Number  
 where T is a type-variable:    
  T extends Number declared in method <T>print(T)
1 error

Và cũng tương tự chúng ta cũng có thể giới hạn các kiểu dữ liệu được phép dùng cho lớp generic chứ không chỉ có phương thức generic.

Giới hạn nhiều lớp

Nếu muốn giới hạn cho nhiều lớp hơn thì chúng ta ghi tên các lớp cơ sở cách nhau bởi dấu '&':

<T extends Integer & Boolean & Byte>

Sự thừa kế trong các kiểu generic

Như chúng ta đã biết, chúng ta có thể gán các đối tượng có kiểu dữ liệu là kiểu cha cho một đối tượng khác có kiểu dữ liệu là kiểu con. Chẳng hạn như chúng ta có thể gán một đối tượng Object cho một đối tượng IntegerObject là cha của Integer:

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger;   // OK

Đây còn được gọi là “mối quan hệ is a” trong lập trình hướng đối tượng. Khái niệm này cũng được áp dụng cho kiểu dữ liệu generic:

Box<Number> box = new Box<Number>();
box.add(new Integer(10));   // OK
box.add(new Double(10.1));  // OK

Bây giờ chúng ta xem đoạn code sau:

public void print(Box<Number> n) { 
    /* ... */ 
}

Đối với phương thức trên thì chúng ta phải truyền vô một đối tượng có kiểu chính xác là

Box<Number>

Các kiểu khác như Box<Integer> hay Box<Double> đều sẽ bị báo lỗi bởi vì các kiểu Box<Integer> hay Box<Double>… đều không phải là kiểu con của Box<Number> mặc dù Integer hay Double là kiểu con của Number.

Đây là một đặc điểm rất dễ bị nhầm lẫn khi lập trình generic. Để có thể truyền các kiểu Box<Integer> hay Box<Double> thì chỉ có một cách duy nhất là thực hiện việc kế thừa lại các lớp cha.

Được đăng vào ngày 06/09/2017