Khi nào sử dụng các phương pháp chung và khi nào sử dụng wild-card?


122

Tôi đang đọc về các phương thức chung từ OracleDocGenericMethod . Tôi khá bối rối về sự so sánh khi nó cho biết khi nào sử dụng wild-card và khi nào sử dụng các phương pháp chung. Trích dẫn từ tài liệu.

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

Chúng tôi có thể đã sử dụng các phương pháp chung ở đây để thay thế:

interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // Hey, type variables can have bounds too!
}

[…] Điều này cho chúng ta biết rằng đối số kiểu đang được sử dụng cho tính đa hình; tác dụng duy nhất của nó là cho phép nhiều kiểu đối số thực tế được sử dụng tại các vị trí gọi khác nhau. Nếu đúng như vậy, người ta nên sử dụng các ký tự đại diện. Các ký tự đại diện được thiết kế để hỗ trợ nhập phụ linh hoạt, đó là những gì chúng tôi đang cố gắng thể hiện ở đây.

Chúng tôi không nghĩ rằng thẻ hoang dã như (Collection<? extends E> c);cũng đang hỗ trợ loại đa hình? Sau đó, tại sao việc sử dụng phương pháp chung được coi là không tốt trong điều này?

Tiếp tục phía trước, nó nói,

Các phương thức chung cho phép các tham số kiểu được sử dụng để thể hiện sự phụ thuộc giữa các kiểu của một hoặc nhiều đối số đối với một phương thức và / hoặc kiểu trả về của nó. Nếu không có sự phụ thuộc như vậy, không nên sử dụng phương pháp chung.

Điều đó có nghĩa là gì?

Họ đã trình bày ví dụ

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

[…]

Chúng tôi có thể viết chữ ký cho phương pháp này theo cách khác, mà không cần sử dụng ký tự đại diện:

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

Tài liệu không khuyến khích khai báo thứ hai và khuyến khích sử dụng cú pháp đầu tiên? Sự khác biệt giữa khai báo thứ nhất và thứ hai là gì? Cả hai dường như đang làm cùng một điều?

Ai đó có thể đặt ánh sáng cho khu vực này.

Câu trả lời:


173

Có một số nơi nhất định, nơi các ký tự đại diện và tham số kiểu cũng làm điều tương tự. Nhưng cũng có những nơi nhất định, nơi bạn phải sử dụng các tham số kiểu.

  1. Nếu bạn muốn thực thi một số mối quan hệ trên các kiểu đối số phương thức khác nhau, bạn không thể làm điều đó với các ký tự đại diện, bạn phải sử dụng các tham số kiểu.

Lấy phương thức của bạn làm ví dụ, giả sử bạn muốn đảm bảo rằng danh sách srcdestdanh sách được truyền đến copy()phương thức phải có cùng kiểu tham số, bạn có thể làm điều đó với các tham số kiểu như sau:

public static <T extends Number> void copy(List<T> dest, List<T> src)

Ở đây, bạn được đảm bảo rằng cả hai destsrccó cùng kiểu tham số cho List. Vì vậy, thật an toàn khi sao chép các phần tử từ srcsang dest.

Nhưng, nếu bạn tiếp tục thay đổi phương pháp để sử dụng ký tự đại diện:

public static void copy(List<? extends Number> dest, List<? extends Number> src)

nó sẽ không hoạt động như mong đợi. Trong trường hợp thứ hai, bạn có thể vượt qua List<Integer>List<Float>như destsrc. Vì vậy, việc di chuyển các phần tử từ srcsang destsẽ không còn an toàn nữa. Nếu bạn không cần loại quan hệ như vậy, thì bạn có thể tự do không sử dụng các tham số kiểu.

Một số khác biệt khác giữa việc sử dụng ký tự đại diện và tham số kiểu là:

  • Nếu bạn chỉ có một đối số kiểu được tham số hóa, thì bạn có thể sử dụng ký tự đại diện, mặc dù tham số kiểu cũng sẽ hoạt động.
  • Tham số loại hỗ trợ nhiều giới hạn, ký tự đại diện thì không.
  • Các ký tự đại diện hỗ trợ cả giới hạn trên và giới hạn dưới, các tham số loại chỉ hỗ trợ giới hạn trên. Vì vậy, nếu bạn muốn xác định một phương thức có Listkiểu Integerhoặc đó là siêu lớp, bạn có thể thực hiện:

    public void print(List<? super Integer> list)  // OK

    nhưng bạn không thể sử dụng tham số kiểu:

     public <T super Integer> void print(List<T> list)  // Won't compile

Người giới thiệu:


1
Đây là câu trả lời kỳ lạ. Nó không giải thích lý do tại sao bạn cần phải sử dụng ?. Bạn có thể viết lại nó thành `public static <T1 expand Number, T2 expand Number> void copy (List <T1> dest, List <T2> src) và trong trường hợp này, nó trở nên rõ ràng những gì đang xảy ra.
kan

@kan. Đó là vấn đề thực sự. Bạn có thể sử dụng tham số kiểu để thực thi cùng một kiểu, nhưng bạn không thể làm điều đó với các ký tự đại diện. Sử dụng hai kiểu khác nhau cho tham số kiểu là một điều khác nhau.
Rohit Jain

1
@benz. Bạn không thể xác định giới hạn thấp hơn trong Listtham số loại using. List<T super Integer>không hợp lệ và sẽ không biên dịch.
Rohit Jain

2
@benz. Bạn được chào đón :) Tôi thực sự khuyên bạn nên đi qua liên kết tôi đã đăng ở cuối. Đó là tài nguyên tốt nhất về Generics mà bạn có được.
Rohit Jain

3
@ jorgen.ringen <T extends X & Y>-> nhiều giới hạn.
Rohit Jain

12

Hãy xem xét ví dụ sau từ Lập trình Java của James Gosling ấn bản thứ 4 bên dưới, nơi chúng tôi muốn hợp nhất 2 SinglyLinkQueue:

public static <T1, T2 extends T1> void merge(SinglyLinkQueue<T1> d, SinglyLinkQueue<T2> s){
    // merge s element into d
}

public static <T> void merge(SinglyLinkQueue<T> d, SinglyLinkQueue<? extends T> s){
        // merge s element into d
}

Cả hai phương pháp trên đều có chức năng giống nhau. Vì vậy, cái nào là tốt hơn? Câu trả lời là câu thứ hai. Theo lời của chính tác giả:

"Quy tắc chung là sử dụng ký tự đại diện khi bạn có thể vì mã có ký tự đại diện thường dễ đọc hơn mã có nhiều tham số kiểu. Khi quyết định xem bạn có cần một biến kiểu hay không, hãy tự hỏi xem biến kiểu đó có được sử dụng để liên quan hai hay nhiều tham số hay không, hoặc để liên kết kiểu tham số với kiểu trả về. Nếu câu trả lời là không, thì ký tự đại diện là đủ. "

Lưu ý: Trong sách chỉ đưa ra phương thức thứ hai và tên tham số kiểu là S thay vì 'T'. Phương pháp đầu tiên không có trong sách.


i bình chọn cho các trích dẫn của một cuốn sách, đó là trực tiếp và ngắn gọn
Kurapika

9

Trong câu hỏi đầu tiên của bạn: Có nghĩa là nếu có mối quan hệ giữa kiểu của tham số và kiểu trả về của phương thức thì hãy sử dụng kiểu chung.

Ví dụ:

public <T> T giveMeMaximum(Collection<T> items);
public <T> Collection<T> applyFilter(Collection<T> items);

Ở đây bạn đang trích xuất một số chữ T theo một tiêu chí nhất định. Nếu T là Longcác phương thức của bạn sẽ trả về LongCollection<Long> ; kiểu trả về thực sự phụ thuộc vào kiểu tham số, do đó nó hữu ích và được khuyên là nên sử dụng kiểu chung.

Khi trường hợp này không xảy ra, bạn có thể sử dụng các loại thẻ đại diện:

public int count(Collection<?> items);
public boolean containsDuplicate(Collection<?> items);

Trong hai ví dụ này, bất kể loại mục nào trong bộ sưu tập, kiểu trả về sẽ là intboolean .

Trong các ví dụ của bạn:

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

hai hàm đó sẽ trả về một boolean bất kể loại mục nào trong bộ sưu tập. Trong trường hợp thứ hai, nó được giới hạn trong các trường hợp của lớp con E.

Câu hỏi thứ hai:

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

Đoạn mã đầu tiên này cho phép bạn chuyển List<? extends T> srcmột tham số không đồng nhất . Danh sách này có thể chứa nhiều phần tử của các lớp khác nhau miễn là tất cả chúng đều mở rộng lớp cơ sở T.

nếu bạn có:

interface Fruit{}

class Apple implements Fruit{}
class Pear implements Fruit{}
class Tomato implements Fruit{}

bạn có thể làm

List<? extends Fruit> basket = new ArrayList<? extends Fruit>();
basket.add(new Apple());
basket.add(new Pear());
basket.add(new Tomato());
List<Fruit> fridge = new ArrayList<Fruit>(); 

Collections.copy(fridge, basket);// works 

Mặt khác

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

ràng buộc List<S> srcthuộc về một lớp cụ thể S là lớp con của T. Danh sách chỉ có thể chứa các phần tử của một lớp (trong trường hợp này là S) và không có lớp nào khác, ngay cả khi chúng cũng triển khai T. Bạn sẽ không thể sử dụng ví dụ trước của tôi nhưng bạn có thể làm:

List<Apple> basket = new ArrayList<Apple>();
basket.add(new Apple());
basket.add(new Apple());
basket.add(new Apple());
List<Fruit> fridge = new ArrayList<Fruit>();

Collections.copy(fridge, basket); /* works since the basket is defined as a List of apples and not a list of some fruits. */

1
List<? extends Fruit> basket = new ArrayList<? extends Fruit>();Không phải là một cú pháp hợp lệ. Bạn phải khởi tạo ArrayList không giới hạn.
Arnold Pistorius

Không thể thêm Apple vào giỏ trong ví dụ trên vì giỏ có thể là danh sách các quả lê '. Ví dụ không chính xác AFAIK. Và cũng không biên dịch.
Khanna111

1
@ArnoldPistorius Điều đó làm tôi bối rối. Tôi đã kiểm tra tài liệu API của ArrayList và nó có một hàm tạo đã ký ArrayList(Collection<? extends E> c). Bạn có thể giải thích cho tôi lý do tại sao bạn đã nói như vậy?
Kurapika

@Kurapika Có thể là tôi đang sử dụng phiên bản Java cũ không? Bình luận đã được đăng gần 3 năm trước.
Arnold Pistorius

2

Phương thức ký tự đại diện cũng chung chung - bạn có thể gọi nó với một số loại kiểu.

Các <T>cú pháp định nghĩa một tên kiểu dữ liệu. Nếu một biến kiểu có bất kỳ công dụng nào (ví dụ như trong việc triển khai phương thức hoặc như một ràng buộc cho kiểu khác), thì bạn nên đặt tên cho nó, nếu không bạn có thể sử dụng ?, làm biến ẩn danh. Vì vậy, có vẻ như chỉ là một con đường tắt.

Hơn nữa, ?không thể tránh cú pháp khi bạn khai báo một trường:

class NumberContainer
{
 Set<? extends Number> numbers;
}

3
Đây không phải là một bình luận?
Buhake Sindi

@BuhakeSindi Xin lỗi, điều gì không rõ ràng? Tại sao -1? Tôi nghĩ nó trả lời câu hỏi.
kan

2

Tôi sẽ thử và trả lời câu hỏi của bạn, từng câu một.

Chúng tôi không nghĩ rằng thẻ hoang dã như (Collection<? extends E> c);cũng đang hỗ trợ loại đa hình?

Không. Lý do là ký tự đại diện bị giới hạn không có loại tham số xác định. Đó là một ẩn số. Tất cả những gì nó "biết" là "vật chứa" thuộc loạiE (bất cứ điều gì được định nghĩa). Vì vậy, nó không thể xác minh và xác minh xem giá trị được cung cấp có khớp với loại bị giới hạn hay không.

Vì vậy, không hợp lý khi có các hành vi đa hình trên các ký tự đại diện.

Tài liệu không khuyến khích khai báo thứ hai và khuyến khích sử dụng cú pháp đầu tiên? Sự khác biệt giữa khai báo thứ nhất và thứ hai là gì? Cả hai dường như đang làm cùng một điều?

Tùy chọn đầu tiên tốt hơn trong trường hợp này vì Tluôn bị giới hạn và sourcechắc chắn sẽ có các giá trị (của ẩn số) là các lớp con T.

Vì vậy, giả sử rằng bạn muốn sao chép tất cả danh sách các số, tùy chọn đầu tiên sẽ là

Collections.copy(List<Number> dest, List<? extends Number> src);

src, về cơ bản, có thể chấp nhận List<Double>, List<Float>v.v. vì có giới hạn trên đối với kiểu tham số hóa được tìm thấy trong dest.

Tùy chọn thứ 2 sẽ buộc bạn phải ràng buộc Sđối với mọi loại bạn muốn sao chép, như vậy

//For double 
Collections.copy(List<Number> dest, List<Double> src); //Double extends Number.

//For int
Collections.copy(List<Number> dest, List<Integer> src); //Integer extends Number.

Như Slà một kiểu tham số hóa cần ràng buộc.

Tôi hi vọng cái này giúp được.


Bạn có thể giải thích những gì bạn có nghĩa là ở đoạn cuối cùng của bạn
benz

Một trong đó nêu ra tùy chọn thứ 2 sẽ buộc bạn phải ràng buộc một cái ...... bạn có thể giải thích kỹ hơn không
benz

<S extends T>nói rằng đó Slà một kiểu tham số hóa là một lớp con của T, vì vậy nó yêu cầu một kiểu tham số hóa (không có ký tự đại diện) là một lớp con của T.
Buhake Sindi

2

Một sự khác biệt khác không được liệt kê ở đây.

static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : a) {
        c.add(o); // correct
    }
}

Nhưng sau đây sẽ dẫn đến lỗi thời gian biên dịch.

static <T> void fromArrayToCollection(T[] a, Collection<?> c) {
    for (T o : a) {
        c.add(o); // compile time error
    }
}

0

Theo như tôi hiểu, chỉ có một trường hợp sử dụng khi ký tự đại diện là thực sự cần thiết (nghĩa là có thể biểu thị một cái gì đó mà bạn không thể diễn đạt bằng cách sử dụng các tham số kiểu rõ ràng). Đây là lúc bạn cần chỉ định giới hạn dưới.

Ngoài ra, các ký tự đại diện còn dùng để viết mã ngắn gọn hơn, như được mô tả bằng các câu lệnh sau trong tài liệu bạn đề cập:

Các phương thức chung cho phép các tham số kiểu được sử dụng để thể hiện sự phụ thuộc giữa các kiểu của một hoặc nhiều đối số đối với một phương thức và / hoặc kiểu trả về của nó. Nếu không có sự phụ thuộc như vậy, không nên sử dụng phương pháp chung.

[...]

Sử dụng ký tự đại diện rõ ràng và ngắn gọn hơn so với khai báo các tham số kiểu rõ ràng, và do đó nên được ưu tiên hơn bất cứ khi nào có thể.

[...]

Các ký tự đại diện cũng có lợi thế là chúng có thể được sử dụng bên ngoài các chữ ký của phương thức, như các loại trường, biến cục bộ và mảng.


0

Chủ yếu -> Các ký tự đại diện thực thi các thông số chung ở cấp tham số / đối số của một phương thức Không chung chung. Ghi chú. Nó cũng có thể được thực hiện trong genericMethod theo mặc định, nhưng ở đây thay vì? chúng ta có thể sử dụng chính T.

gói thuốc chung;

public class DemoWildCard {


    public static void main(String[] args) {
        DemoWildCard obj = new DemoWildCard();

        obj.display(new Person<Integer>());
        obj.display(new Person<String>());

    }

    void display(Person<?> person) {
        //allows person of Integer,String or anything
        //This cannnot be done if we use T, because in that case we have to make this method itself generic
        System.out.println(person);
    }

}

class Person<T>{

}

SO ký tự đại diện có các cách sử dụng cụ thể của nó như thế này.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.