+5

Sử dụng bounded wildcard hiệu quả trong java

Kiểu Parameterized là kiểu bất biến, bất cứ hai kiểu khác nhau Type1Type2, List<Type1> không thể là subtype củaList<Type2>. Trong khi điều đó thực sự trái ngược List<String> không phải là một subtype của List<Oject>. Bạn có thể thêm bất kỳ object vào trong List<Object>, nhưng bạn chỉ có thể thêm các String vào một List<String>.

Đôi khi bạn cần sự linh hoạt hơn là kiểu bất biến có thể cung cấp giả sử bạn có một class stack

public class Stack<E> {
       public Stack();
       public void push(E e);
       public E pop();
       public boolean isEmpty();
}

Bạn muốn thêm một phương thức đẩy một list các elemement vào trong stack.

 public void pushAll(Iterable<E> src) {
      for (E e : src)
push(e);
}

Phương thức này nhìn có vẻ rất ổn nhưng thực sự nó không đạt yêu cầu hoàn toàn. Nếu kiểu của element của Iterable src chính xác với kiểu của stack thì nó sẽ làm việc tốt đẹp nhưng giả sự bạn có một Stack<Number> và bạn gọi push(intVal), intVal là kiểu Integer. Điều này dường như có vẻ đúng về mặt logic bởi vì Integer là subtype của Number.

  Stack<Number> numberStack = new Stack<Number>();
   Iterable<Integer> integers = ... ;
   numberStack.pushAll(integers);

Tuy nhiên nếu bạn thử compile đoạn trên, bạn sẽ gặp một error bởi vì như đã nói trên kiểu parameterized là kiểu bất biến

 StackTest.java:7: pushAll(Iterable<Number>) in Stack<Number>
   cannot be applied to (Iterable<Integer>)
           numberStack.pushAll(integers);
                                  ^

May mắn thay Java cung cấp một loại parameterized tye đặc biệt được gọi là bounded wildcard để sử lý những tình huống như trên. Kiểu của input parameter cho pushAll không nên “Interable of E”“Iterable of some subtype of E", và kiểu wildcard này sẽ chính xác như sau: Iterable<? extends E> và bây giờ phương thức push all sẽ được chỉnh sửa như sau:

pushAll(Iterable<? extends E> src) {
       for (E e : src)
           push(e);
}

Với thay đổi này phương thức pushAll đã có thể hoàn toàn đáp ứng được ví dụ phía trên khi client muốn thêm một list các intVal vào trong một Stack Number.

Bây giờ giả sử bạn muốn viết một phương thức popAll, phương thức đi cùng với phương thức pushAll. Phương thức popAll sẽ pop các element ra khỏi stack và add những element đó vào một collection.

 public void popAll(Collection<E> dst) {
       while (!isEmpty())
           dst.add(pop());
}

Complie thành công và làm việc tốt nếu element type của collection chính xác với stack nhưng lại một lần nữa có không được hoàn hảo. Giả sử bạn có một Stack<Number>và kiểu của CollectionObject. nếu bạn pop một element từ stack và đặt nó vào Collection, khi compile và run liệu nó có gây ra lỗi không ?

Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ... ;
numberStack.popAll(objects);

Nếu bạn có gắng compile đoạn code trên bạn sẽ gặp lỗi tương tự với phiên bản đầu tiên của pushAll: Collection<Object> không phải là subtype của Collection<number>. Một lần nữa wildcard type được sử dụng để sử lý trong tình huống này. Kiểu của input parameter cho popAll không nên “collection of E"“collection of some supertype of E". Kiểu wildcar bây giờ sẽ như sau: Collection<? super E>

popAll(Collection<? super E> dst) {
       while (!isEmpty())
           dst.add(pop());
}

Với những thay đổi này cả Stack và client code đều được compile một cách mượt mà.

Để flexibility cao nhất khi sử dụng wildcard type cho input parameters được thể hiện thông qua producers và consumers, bạn có thể ghi nhớ theo cách sau: PECS stands for producer-extends, consumer-super.

Hay nói một cách khác, nếu một parameterized type thể hiện một T producer, sẽ sử dụng<? extends T>; nếu nó thể hiện một T consumer, sử dụng <? super T>. Trong ví dụ Stack bên trên src parameter của pushAll produces các thể hiện E để sử dụng cho Stack, do vậy kiểu thích hợp cho src là Iterable<? extends E>; dst parameter của popAll consumes các thể hiện E trong Stack, do vậy kiểu thích hợp cho dst là Collection<? super E>. PECS ghi lại các nguyên tắc cơ bản để sử dụng wildcard types.

Chúng ta sẽ áp dụng nguyên tắc PECS cho phương thức reduce dưới đây:

static <E> E reduce(List<E> list, Function<E> f, E initVal) {
       List<E> snapshot;
       synchronized(list) {
           snapshot = new ArrayList<E>(list);
       }
       E result = initVal;
       for (E e : snapshot)
           result = f.apply(result, e);
       return result;
}

Phương thức reduce sử dụng list parameter như một E producer, do vậy định nghĩa của nó nên sử dụng kiểu extend E. parameter f thể hiện một function bao gồm cả consumes và producers E, do vậy wild card type sẽ không không thích hợp cho nó. dưới đây là kết quả sau khi định nghĩa lại phương thức reduce

static <E> E reduce(List<? extends E> list, Function<E> f,
E initVal)

Tiếp tục với một ví dụ khác với union method ở đây là định nghĩa của phương thức này:

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<E>(s1); result.addAll(s2);
return result;
}

Cả hai parameter s1s2 đều là producers, do vậy theo PECS chúng ta nên định nghĩa như sau:

public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)

Chú ý rằng kiểu trả vè vẫn là Set<E> không nên sử dụng wildcard type cho kiểu trả về. Thay vì bổ sung tính felexibility cho user, nó nên được thực hiện sử dụng wildcard type trong client code.

Sử dụng đúng wildcard type sẽ gần như ẩn với user của một class, Các phương thức chỉ chấp nhận các parameter với đúng kiểu đã được định nghĩa. Nếu user của class có suy nghĩ về wildcard type, có khả năng có một vài thứ đang không đúng trong class's API.

Nguyên tắc kiểu tham chiếu khá phức tạp. Hãy xem qua định nghĩa union mà bạn sửa lại, liệu đoạn code dưới đây có thể thực hiện:

Set<Integer> integers = ... ;
 Set<Double> doubles = ... ;
Set<Number> numbers = union(integers, doubles);

Khi compile bạn sẽ gặp lỗi này

Union.java:14: incompatible types
   found : Set<Number & Comparable<? extends Number &
                                             Comparable<?>>>
   required: Set<Number>
           Set<Number> numbers = union(integers, doubles);
                                                              ^

Có một cách giải quyết ngăn cho lỗi này. Nếu compiler thể suy luận kiểu mà bạn muốn, bạn sẽ nói cho nó biết bằng cách sử dụng với một kiểu tường minh. Đây là cách rất ít khi sử dụng, một kiểu tường minh nhìn khá xấu, với việc bổ sung kiểu tường minh sẽ như sau:

Set<Number> numbers = Union.<Number>union(integers, doubles);

Bây giờ chúng chúng ta tiếp tục với một ví dụ về phương thức max:

 public static <T extends Comparable<T>> T max(List<T> list) {
       Iterator<T> i = list.iterator();
       T result = i.next();
       while (i.hasNext()) {
           T t = i.next();
           if (t.compareTo(result) > 0)
        result = t;
        }
       return result;
}

Sau khi phương thức max được viết lại với kiểu wildcard:

public static <T extends Comparable<? super T>> T max( List<? extends T> list)

Để viết lại định nghĩa cho phương thức max chúng ta áp dụng nguyên tắc PECS để chuyển đổi hai lần. Đơn giản nhất là tham số list nó cung cấp một thể hiện T do vậy chúng ta thay đổi từ List<T> thành List<? extends T>, T ban đầu được chỉ định đểextend Comparable<T>, nhưng một comparable của T sẽ consumes T instance, do vậy kiểu parameterized Comparable<T> sẽ được thay thế bằng kiểu bounded wildcard Comarable<? super T>. Comparable luôn luôn là consumers, do vậy bạn luôn luôn sử dụng Comparable<? super T> thay vì Comparable<T>. Điều này đúng với cả Comparator, do đó bạn nên sử dụng Comparator<? super T> thay cho Comparator<T>.

Dưới đây là một ví dụ đơn giản sẽ bị loại trừ đối với phương thức max khi chưa được định nghĩa lại nhưng lại hoàn toàn được cho phép đối với phương thức max chúng ta đã viết lại sử dụng wildcard type:

List<ScheduledFuture<?>> scheduledFutures = ... ;

Lý do bạn không thể áp dụng phương thức max ban đầu cho list này là java.util.concurrent.ScheduledFuture không cài đặt Comparable<ScheduledFuture> thay vào đó nó là một subinterface của Delayed, nó extend Comparable<Delay>. hay nói một cách khác một thể hiện ScheduledFuture không chỉ comparable với các thể hiện ScheduledFuture khác mà còn comparable với bất kỳ thể hiện Delayed nữa, và chính điều này là nguyên nhân gây ra việc từ chối của phương thức max ban đầu.

Có một vấn đề nhỏ với định nghĩa được viết lại của hàm max: nó ngăn chặn phương thức khi compile. dưới đây là định nghĩa của hàm max.

 public static <T extends Comparable<? super T>> T max(
           List<? extends T> list) {
       Iterator<T> i = list.iterator();
       T result = i.next();
       while (i.hasNext()) {
           T t = i.next();
           if (t.compareTo(result) > 0)
        result = t;
        }
       return result;
}

Ở đây khi bạn compile sẽ xuất hiện một lỗi:

Max.java:7: incompatible types
   found   : Iterator<capture#591 of ? extends T>
   required: Iterator<T>
           Iterator<T> i = list.iterator();
                                          ^

Error này có nghĩa rằng list không phải là một List<T>, do đó interator không thể trả về một Interator<T>. nó trả vê một subtype của T, đó đó chúng ta sẽ thay đổi định nghĩa của interator sử dụng wildcard type:

Iterator<? extends T> i = list.iterator();

Có sự khác biệt giữa kiểu parameter và wildcard, nhiều phương thức có thể định nghĩa sử dụng một trong hai. ví dụ ở đây có hai định nghĩa cho phương thức chuyển đổi vị trí của hai item trong một list. Phương thức đầu tiên sử dụng kiểu unbounded parameter và phương thức thứ 2 sử dụng kiểu unbounded wildcard:

public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);

Có một trong hai khai báo thích hợp hơn đó là khai báo thứ hai bởi vì nó đơn giản hơn. Bạn truyền bất kỳ list nào vào và chuyển đổi vị trị của các element và sẽ không cần lo lắng về kiểu của tham số. Có một nguyên tắc: nếu kiểu parameter chỉ xuất hiện một lần trong khai báo phương thức, thì nên thay thế nó bằng một wildcard. Nếu nó là một unbounded type thì nên thay thế nó bằng một bounded wildcard.

Sẽ có một lỗi với kiểu khai báo thứ hai của phương thức swap, nó sử dụng một wildcard thay vì một kiểu parameter và đơn giản việc cài đặt này sẽ không thể compile:

public static void swap(List<?> list, int i, int j) {
       list.set(i, list.set(j, list.get(i)));
}

Thử compile nó sẽ đưa ra một thông báo lỗi :

Swap.java:5: set(int,capture#282 of ?) in List<capture#282 of ?> cannot be applied to (int,Object)
           list.set(i, list.set(j, list.get(i)));
                              ^

Dường như chúng ta không thể đẩy một element ngược trở lại list mà chúng ta chỉ có thể lấy nó ra. Vấn đề này là do kiểu của list là List<?>, và bạn không thể put bất kỳ dữ liệu ngoại trừ null vào trong một List<?>. May mắn thay có một cách cài đặt phương thức này mà không cần phải sử dụng cast hoặc sử dụng kiểu raw, đó là viết một phương thức private helper để giữ kiểu wildcard. Phương thức helper này là một generic method để có thể dữ được kiểu truyền vào:

public static void swap(List<?> list, int i, int j) {
       swapHelper(list, i, j);
}
 
private static <E> void swapHelper(List<E> list, int i, int j) { 
    list.set(i, list.set(j, list.get(i)));
}

Phương thức swapHelper biết rằng list là một List<E>. Do đó, nó biết rằng bất kỳ dữ liệu nó lấy ra của list thì đều là kiểu E, và nó đảm bảo đẩy bất kỳ dữ liệu kiểu E nào vào trong list. Việc thực hiện này hơi phức tạp một chút nhưng nó cho phép chúng ta export một khai báo wildcard đẹp dành cho phương thức swap trong khi vẫn lợi dụng được lợi thế của một generic method bên trong. Client của phương thức swap không cần phải biết sự phức tạp của phương thức swapHelper nhưng chúng lại hoàn toàn được hưởng lợi từ swapHelper.

Tóm lại, sử dụng wildcar type trong các API của bạn một cách tinh tế sẽ giúp cho các API flexible hơn. Nếu bạn viết một library được sử dụng rộng rãi, thì việc sử dụng thích hợp kiểu wildcard nên được coi là bắt buộc. Hãy nhớ nguyên tắc cơ bản: producer-extends, consumer-super (PECS) và tất cả các comparable và comparator đều là consumers.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.