Java generics super vs extends

Trong bài viết này, chúng ta sẽ xem xét một vài cách sử dụng advanced của wildcards (wildcards là dấu "?" trong các từ khóa "? extends E" hay "? super T" ...). Giả sử chúng ta có một cấu trúc dữ liệu chuyên dùng để ghi dữ liệu, ở đây là interface Sink:

interface Sink<T> {
    flush(T t);
}

Cách sử dụng nó được mô tả bằng một đoạn mã dưới đây. Method writeAll() được thiết kế để ghi dữ liệu từ một collection tới Sink và trả về element cuối cùng được ghi.

public static <T> T writeAll(Collection<T> coll, Sink<T> snk) {
    T last;
    for (T t : coll) {
        last = t;
        snk.flush(last);
    }
    return last;
}

Nếu chúng ta muốn chạy chương trình bằng các tham số như dưới đây:

Sink<Object> s;
Collection<String> cs;
String str = writeAll(cs, s); // Lỗi.

Khi compile chương trình sẽ bị lỗi vì truyền sai kiểu argument do cả Object và String khác kiểu nhau nên không phải là kiểu phù hợp của T. Chúng ta có thể fix lỗi này bằng cách chỉnh sửa chữ ký của method writeAll() như bên dưới:

public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...}
...
// Call is OK, but wrong return type.
String str = writeAll(cs, s);

Lời gọi method writeAll() bây giờ là hợp lệ do T có kiểu là Object, tuy nhiên giá trị trả về lại không phù hợp với kiểu String nên chương trình vẫn lỗi.

Để giải quyết vấn đề vừa gọi được method và giá trị trả ra đúng chúng ta sẽ dùng wildcard với super:

public static <T> T writeAll(Collection<T> coll, Sink<? super T> snk) {
    ...
}
String str = writeAll(cs, s); // Yes!

Dấu hỏi chấm trong cú pháp "? super T" có nghĩa là một tham số chưa biết cụ thể là kiểu gì, nhưng nó là kiểu cha của T. Bằng cách thay đổi chữ ký method như ở trên T sẽ có kiểu là String, do Sink<? super T> có thể được gán bằng Sink<String>.

Hãy lấy một ví dụ khác thực tế hơn

java.util.TreeSet<E> biểu diễn một cấu trúc cây mà ở đó mỗi element có kiểu là E và được sắp xếp theo thứ tự. Một cách để khởi tạo TreeSet là truyền nó một Comparator đối tượng. Comparator này sẽ được sử dụng để sắp xếp các thành phần của TreeSet như kết quả mong muốn:

TreeSet(Comparator<E> c)

interface Comparator<T> {
    int compare(T fst, T snd);
}

Giả sử chúng ta muốn tạo một TreeSet<String> và truyền cho TreeSet một Coparator phù hợp, chúng ta cần phải truyền cho nó một Comparator có thể so sánh các string với nhau. Điều này có thể thực hiện được bằng một Comparator<String>, tuy nhiên chúng ta cũng muốn có thể truyền vào một Comparator<Object>. Wildcard cận dưới sẽ được dùng để chúng ta có thể truyền argument một cách mềm dẻo như ý chúng ta muốn:

TreeSet(Comparator<? super E> c)

Đoạn code này cho phép cả 2 comparator trên được sử dụng.

Kết Luận: Bằng cách sử dụng hợp lý wildcard với super và extends trong Java sẽ làm cho một đoạn mã hay chương trình trở nên mềm dẻo hơn.