Sự khác biệt giữa <? mở rộng Base> và <T mở rộng Base>?


29

Trong ví dụ này:

import java.util.*;

public class Example {
    static void doesntCompile(Map<Integer, List<? extends Number>> map) {}
    static <T extends Number> void compiles(Map<Integer, List<T>> map) {}

    static void function(List<? extends Number> outer)
    {
        doesntCompile(new HashMap<Integer, List<Integer>>());
        compiles(new HashMap<Integer, List<Integer>>());
    }
}

doesntCompile() không thể biên dịch với:

Example.java:9: error: incompatible types: HashMap<Integer,List<Integer>> cannot be converted to Map<Integer,List<? extends Number>>
        doesntCompile(new HashMap<Integer, List<Integer>>());
                      ^

trong khi compiles()được chấp nhận bởi trình biên dịch.

Câu trả lời này giải thích rằng sự khác biệt duy nhất là không giống như <? ...>, <T ...>cho phép bạn tham khảo loại sau, dường như không phải là trường hợp.

Sự khác biệt giữa <? extends Number><T extends Number>trong trường hợp này là gì và tại sao không biên dịch đầu tiên?


Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Samuel Liew

[1] Java Chung loại: sự khác biệt giữa Danh sách <? mở rộng Số> và Danh sách <T kéo dài Số> dường như đang hỏi cùng một câu hỏi, nhưng trong khi nó có thể được quan tâm thì nó không thực sự là một bản sao. [2] Mặc dù đây là một câu hỏi hay, tiêu đề không phản ánh đúng câu hỏi cụ thể được hỏi trong câu cuối cùng.
skomisa

Điều này đã được trả lời ở đây Danh sách chung Java <Danh sách <? gia hạn số >>
Mạnh mẽ Nguyễn

Làm thế nào về lời giải thích ở đây ?
jrook

Câu trả lời:


14

Bằng cách xác định phương thức với chữ ký sau:

static <T extends Number> void compiles(Map<Integer, List<T>> map) {}

và gọi nó như:

compiles(new HashMap<Integer, List<Integer>>());

Trong các lọ §8.1.2 chúng tôi tìm thấy, phần đó (phần thú vị được in đậm bởi tôi):

Một khai báo lớp chung định nghĩa một tập hợp các kiểu tham số (§4.5), một kiểu cho mỗi lần gọi có thể của phần tham số kiểu theo đối số kiểu . Tất cả các loại tham số này chia sẻ cùng một lớp trong thời gian chạy.

Nói cách khác, loại Tđược khớp với loại đầu vào và được chỉ định Integer. Chữ ký sẽ có hiệu quả trở thành static void compiles(Map<Integer, List<Integer>> map).

Khi nói đến doesntCompilephương thức, jls định nghĩa các quy tắc của phân nhóm ( §4.5.1 , được in đậm bởi tôi):

Một đối số loại T1 được cho là chứa một đối số loại khác là T2, được viết là <= T1, nếu tập hợp các loại được biểu thị bằng T2 có thể là một tập hợp con của tập hợp các loại được biểu thị bởi T1 theo quy tắc đóng phản xạ và chuyển tiếp của các quy tắc sau ( trong đó <: biểu thị phân nhóm (§4.10)):

  • ? kéo dài T <=? kéo dài S nếu T <: S

  • ? kéo dài T <=?

  • ? siêu T <=? siêu S nếu S <: T

  • ? siêu T <=?

  • ? siêu T <=? mở rộng đối tượng

  • T <= T

  • T <=? kéo dài T

  • T <=? siêu T

Điều này có nghĩa, ? extends Numberthực sự có chứa Integerhoặc thậm chí List<? extends Number>chứa List<Integer>, nhưng nó không phải là trường hợp cho Map<Integer, List<? extends Number>>Map<Integer, List<Integer>>. Thêm về chủ đề đó có thể được tìm thấy trong chủ đề SO này . Bạn vẫn có thể làm cho phiên bản có ?ký tự đại diện hoạt động bằng cách khai báo rằng bạn mong đợi một kiểu con của List<? extends Number>:

public class Example {
    // now it compiles
    static void doesntCompile(Map<Integer, ? extends List<? extends Number>> map) {}
    static <T extends Number> void compiles(Map<Integer, List<T>> map) {}

    public static void main(String[] args) {
        doesntCompile(new HashMap<Integer, List<Integer>>());
        compiles(new HashMap<Integer, List<Integer>>());
    }
}

[1] Tôi nghĩ bạn có ý ? extends Numberchứ không phải ? extends Numeric. [2] Khẳng định của bạn rằng "không phải là trường hợp của Danh sách <? Mở rộng Số> và Danh sách <Số nguyên>" là không chính xác. Như @VinceEmigh đã chỉ ra, bạn có thể tạo một phương thức static void demo(List<? extends Number> lst) { }và gọi nó như thế này demo(new ArrayList<Integer>());hoặc thế này demo(new ArrayList<Float>());, và mã biên dịch và chạy OK. Hoặc có lẽ tôi đã đọc sai hoặc hiểu sai những gì bạn đã nêu?
skomisa

@ Bạn đúng trong cả hai trường hợp. Khi nói đến điểm thứ hai của bạn, tôi đã viết nó một cách sai lệch. Tôi có nghĩa List<? extends Number>là một tham số loại của toàn bản đồ, không phải chính nó. Cảm ơn bạn rất nhiều về lời nhận xét.
Andronicus

@skomisa từ cùng một lý do List<Number>không chứa List<Integer>. Giả sử bạn có một chức năng static void check(List<Number> numbers) {}. Khi gọi với check(new ArrayList<Integer>());nó không biên dịch, bạn phải định nghĩa phương thức là static void check(List<? extends Number> numbers) {}. Với bản đồ, nó giống nhau nhưng có nhiều lồng nhau hơn.
Andronicus

1
@skomisa giống như Numberlà một tham số kiểu của danh sách và bạn cần thêm ? extendsđể biến nó thành hiệp phương sai, List<? extends Number>là một tham số kiểu Mapvà cũng cần ? extendscho hiệp phương sai.
Andronicus

1
ĐỒNG Ý. Vì bạn đã cung cấp giải pháp cho ký tự đại diện đa cấp (còn gọi là "ký tự đại diện lồng nhau" ?) Và được liên kết với tham chiếu JLS có liên quan, nên có tiền thưởng.
skomisa

6

Trong cuộc gọi:

compiles(new HashMap<Integer, List<Integer>>());

T được khớp với Integer, vì vậy loại đối số là a Map<Integer,List<Integer>>. Đây không phải là trường hợp của phương thức doesntCompile: kiểu của đối số nằm trong Map<Integer, List<? extends Number>>bất kỳ đối số thực tế nào trong cuộc gọi; và đó là không thể chuyển nhượng từ HashMap<Integer, List<Integer>>.

CẬP NHẬT

Trong doesntCompilephương thức, không có gì ngăn cản bạn làm điều gì đó như thế này:

static void doesntCompile(Map<Integer, List<? extends Number>> map) {
    map.put(1, new ArrayList<Double>());
}

Vì vậy, rõ ràng, nó không thể chấp nhận HashMap<Integer, List<Integer>>như là một đối số.


Sau đó, một cuộc gọi hợp lệ doesntCompilesẽ như thế nào? Chỉ tò mò về điều đó.
Xtreme Biker

1
@XtremeBiker doesntCompile(new HashMap<Integer, List<? extends Number>>());sẽ hoạt động như bình thường doesntCompile(new HashMap<>());.
skomisa

@XtremeBiker, thậm chí điều này sẽ hoạt động, Bản đồ <Số nguyên, Danh sách <? mở rộng Số >> map = new HashMap <Integer, List <? mở rộng Số >> (); map.put (null, ArrayList mới <Integer> ()); doesntCompile (bản đồ);
MOnkey

"không thể gán được từ HashMap<Integer, List<Integer>>" bạn có thể giải thích rõ tại sao nó không được gán từ nó không?
Dev Null

@DevNull xem cập nhật của tôi ở trên
Maurice Perry

2

Ví dụ đơn giản về trình diễn. Ví dụ tương tự có thể được hình dung như dưới đây.

static void demo(List<Pair<? extends Number>> lst) {} // doesn't work
static void demo(List<? extends Pair<? extends Number>> lst) {} // works
demo(new ArrayList<Pair<Integer>()); // works
demo(new ArrayList<SubPair<Integer>()); // works for subtype too

public static class Pair<T> {}
public static class SubPair<T> extends Pair<T> {}

List<Pair<? extends Number>>là loại ký tự đại diện đa cấp trong khi đó List<? extends Number>là loại ký tự đại diện tiêu chuẩn.

Các khởi tạo cụ thể hợp lệ của loại thẻ hoang dã List<? extends Number>bao gồm Numbervà bất kỳ kiểu con nào Numbertrong khi đó trong trường hợp List<Pair<? extends Number>>đó là một đối số kiểu của đối số loại và chính nó có một khởi tạo cụ thể của loại chung.

Generics là bất biến nên Pair<? extends Number>loại thẻ hoang dã chỉ có thể chấp nhận Pair<? extends Number>>. Kiểu bên trong ? extends Numberđã covariant. Bạn phải tạo kiểu bao quanh là hiệp phương sai để cho phép hiệp phương sai.


Làm thế nào mà <Pair<Integer>>nó không hoạt động <Pair<? extends Number>>nhưng làm việc với <T extends Number> <Pair<T>>?
jaco0646

@ jaco0646 Về cơ bản, bạn đang hỏi cùng một câu hỏi với OP và câu trả lời từ Andronicus đã được chấp nhận. Xem ví dụ mã trong câu trả lời đó.
skomisa

@skomisa, vâng, tôi đang hỏi cùng một câu hỏi vì một vài lý do: một là câu trả lời này dường như không thực sự giải quyết câu hỏi của OP; nhưng hai là tôi thấy câu trả lời này dễ hiểu hơn. Tôi không thể làm theo câu trả lời từ Andronicus theo bất kỳ cách nào dẫn tôi đến việc hiểu các khái niệm lồng nhau và không lồng nhau, hoặc thậm chí Tso với ?. Một phần của vấn đề là khi Andronicus đạt đến điểm quan trọng trong lời giải thích của mình, anh ta đã chuyển sang một chủ đề khác chỉ sử dụng các ví dụ tầm thường. Tôi đã hy vọng có được một câu trả lời rõ ràng và đầy đủ hơn ở đây.
jaco0646

1
@ jaco0646 OK. Tài liệu "Câu hỏi thường gặp về Java Generics - Kiểu đối số" của Angelika Langer có Câu hỏi thường gặp có tiêu đề Ký tự đại diện đa cấp (nghĩa là lồng nhau) nghĩa là gì? . Đó là nguồn tốt nhất mà tôi biết để giải thích các vấn đề được nêu ra trong câu hỏi của OP. Các quy tắc cho các ký tự đại diện lồng nhau không đơn giản và trực quan.
skomisa

1

Tôi khuyên bạn nên xem tài liệu về các ký tự đại diện chung, đặc biệt là các hướng dẫn sử dụng ký tự đại diện

Thẳng thắn nói phương pháp của bạn #doesntCompile

static void doesntCompile(Map<Integer, List<? extends Number>> map) {}

và gọi như

doesntCompile(new HashMap<Integer, List<Integer>>());

Về cơ bản là không chính xác

Hãy thêm thực thi pháp lý :

    static void doesntCompile(Map<Integer, List<? extends Number>> map) {
        List<Double> list = new ArrayList<>();
        list.add(0.);
        map.put(0, list);
    }

Nó thực sự tốt, bởi vì Double kéo dài Số, vì vậy đặt List<Double>là hoàn toàn tốt cũng như List<Integer>, phải không?

Tuy nhiên, bạn vẫn cho rằng việc chuyển qua đây từ ví dụ của bạn là hợp phápnew HashMap<Integer, List<Integer>>() ?

Trình biên dịch không nghĩ như vậy và đang cố gắng hết sức để tránh những tình huống như vậy.

Cố gắng thực hiện tương tự với phương thức #compile và trình biên dịch rõ ràng sẽ không cho phép bạn đưa danh sách nhân đôi vào bản đồ.

    static <T extends Number> void compiles(Map<Integer, List<T>> map) {
        List<Double> list = new ArrayList<>();
        list.add(10.);
        map.put(10, list); // does not compile
    }

Về cơ bản bạn có thể đặt gì, nhưng List<T>mà lý do tại sao nó an toàn để gọi phương thức đó với new HashMap<Integer, List<Integer>>()hoặc new HashMap<Integer, List<Double>>()hoặc new HashMap<Integer, List<Long>>()hoặc new HashMap<Integer, List<Number>>().

Vì vậy, tóm lại, bạn đang cố gắng gian lận với trình biên dịch và nó khá bảo vệ chống lại sự gian lận đó.

NB: câu trả lời được đăng bởi Maurice Perry là hoàn toàn chính xác. Tôi chỉ không chắc là nó đủ rõ ràng, vì vậy đã cố gắng (thực sự hy vọng tôi quản lý) để thêm bài viết rộng rãi hơn.

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.