Lựa chọn chữ ký phương thức cho biểu thức lambda với nhiều loại mục tiêu phù hợp


11

Tôi đã trả lời một câu hỏi và chạy vào một kịch bản mà tôi không thể giải thích. Xem xét mã này:

interface ConsumerOne<T> {
    void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
}

class A {
    private static CustomIterable<A> iterable;
    private static List<A> aList;

    public static void main(String[] args) {
        iterable.forEach(a -> aList.add(a));     //ambiguous
        iterable.forEach(aList::add);            //ambiguous

        iterable.forEach((A a) -> aList.add(a)); //OK
    }
}

Tôi không hiểu tại sao gõ một cách rõ ràng các paramter của lambda (A a) -> aList.add(a)làm cho mã được biên dịch. Ngoài ra, tại sao nó liên kết đến tình trạng quá tải Iterablethay vì một trong CustomIterable?
Có một số giải thích cho điều này hoặc một liên kết đến phần có liên quan của thông số kỹ thuật?

Lưu ý: iterable.forEach((A a) -> aList.add(a));chỉ biên dịch khi CustomIterable<T>mở rộng Iterable<T>(quá tải các phương thức trong CustomIterablekết quả trong lỗi không rõ ràng)


Nhận được điều này trên cả hai:

  • phiên bản openjdk "13.0.2" 2020-01-14
    Trình biên dịch Eclipse
  • phiên bản openjdk "1.8.0_232"
    Trình biên dịch Eclipse

Chỉnh sửa : Đoạn mã trên không thể biên dịch khi xây dựng bằng maven trong khi Eclipse biên dịch dòng mã cuối cùng thành công.


3
Không ai trong số ba trình biên dịch trên Java 8. Bây giờ tôi không chắc đây là lỗi đã được sửa trong phiên bản mới hơn hay lỗi / tính năng đã được giới thiệu ... Có lẽ bạn nên chỉ định phiên bản Java
Sweeper

@Sweeper Ban đầu tôi có cái này bằng jdk-13. Thử nghiệm tiếp theo trong java 8 (jdk8u232) cho thấy các lỗi tương tự. Không chắc chắn tại sao cái cuối cùng không biên dịch trên máy của bạn.
ernest_k

Không thể sao chép trên hai trình biên dịch trực tuyến ( 1 , 2 ). Tôi đang sử dụng 1.8.0_221 trên máy của mình. Điều này đang trở nên kỳ lạ và kỳ lạ hơn ...
Sweeper

1
@ernest_k Eclipse có triển khai trình biên dịch riêng. Đó có thể là thông tin quan trọng cho câu hỏi. Ngoài ra, thực tế là một lỗi xây dựng maven sạch dòng cuối cùng cũng nên được làm nổi bật trong câu hỏi theo ý kiến ​​của tôi. Mặt khác, về câu hỏi được liên kết, giả định rằng OP cũng đang sử dụng Eclipse có thể được làm rõ, vì mã này không thể lặp lại được.
Naman

3
Mặc dù tôi hiểu rằng giá trị trí tuệ của câu hỏi, tôi chỉ có thể khuyên không bao giờ tạo ra các phương thức quá tải vốn chỉ khác nhau về giao diện chức năng và hy vọng rằng nó có thể được gọi một cách an toàn khi vượt qua lambda. Tôi không tin rằng sự kết hợp giữa suy luận và quá tải kiểu lambda là điều mà các lập trình viên trung bình sẽ không bao giờ hiểu được. Đó là một phương trình với rất nhiều biến mà người dùng không kiểm soát. TRÁNH NÀY, làm ơn :)
Stephan Herrmann

Câu trả lời:


8

TL; DR, đây là một lỗi biên dịch.

Không có quy tắc nào được ưu tiên cho một phương thức áp dụng cụ thể khi nó được kế thừa hoặc một phương thức mặc định. Thật thú vị, khi tôi thay đổi mã thành

interface ConsumerOne<T> {
    void accept(T a);
}
interface ConsumerTwo<T> {
  void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
    void forEach(ConsumerTwo<? super T> c); //another overload
}

các iterable.forEach((A a) -> aList.add(a));tuyên bố tạo ra một lỗi trong Eclipse.

Vì không có thuộc tính của forEach(Consumer<? super T) c)phương thức từIterable<T> giao diện thay đổi khi khai báo một tình trạng quá tải khác, nên quyết định chọn phương thức này của Eclipse không thể (nhất quán) dựa trên bất kỳ thuộc tính nào của phương thức. Đây vẫn là phương thức được kế thừa duy nhất, vẫn là defaultphương thức duy nhất , vẫn là phương thức JDK duy nhất, v.v. Cả hai thuộc tính này sẽ không ảnh hưởng đến việc lựa chọn phương thức nào.

Lưu ý rằng thay đổi khai báo thành

interface CustomIterable<T> {
    void forEach(ConsumerOne<? super T> c);
    default void forEach(ConsumerTwo<? super T> c) {}
}

cũng tạo ra một lỗi mập mờ mơ hồ, do đó, số lượng phương thức quá tải có thể áp dụng cũng không thành vấn đề, ngay cả khi chỉ có hai ứng cử viên, không có ưu tiên chung nào đối với default các phương thức.

Cho đến nay, vấn đề dường như xuất hiện khi có hai phương pháp áp dụng và default phương pháp và mối quan hệ thừa kế có liên quan, nhưng đây không phải là nơi thích hợp để đào sâu hơn.


Nhưng có thể hiểu rằng các cấu trúc của ví dụ của bạn có thể được xử lý bằng các mã triển khai khác nhau trong trình biên dịch, một cấu trúc thể hiện một lỗi trong khi cái kia thì không.
a -> aList.add(a)là một biểu thức lambda được gõ ngầm , không thể được sử dụng cho độ phân giải quá tải. Ngược lại, (A a) -> aList.add(a)là một biểu thức lambda được gõ rõ ràng có thể được sử dụng để chọn một phương thức khớp từ các phương thức bị quá tải, nhưng nó không giúp ích ở đây (không nên trợ giúp ở đây), vì tất cả các phương thức đều có các loại tham số có cùng chữ ký chức năng .

Như một ví dụ ngược lại, với

static void forEach(Consumer<String> c) {}
static void forEach(Predicate<String> c) {}
{
  forEach(s -> s.isEmpty());
  forEach((String s) -> s.isEmpty());
}

các chữ ký chức năng khác nhau và sử dụng biểu thức lambda loại rõ ràng thực sự có thể giúp chọn đúng phương thức trong khi biểu thức lambda được gõ ngầm không giúp ích gì, vì vậy forEach(s -> s.isEmpty()) tạo ra lỗi trình biên dịch. Và tất cả các trình biên dịch Java đều đồng ý về điều đó.

Lưu ý rằng đó aList::addlà một tham chiếu phương thức mơ hồ, vì addphương thức cũng bị quá tải, do đó, nó cũng không thể giúp chọn một phương thức, nhưng dù sao các tham chiếu phương thức có thể được xử lý bởi các mã khác nhau. Chuyển sang một cách rõ ràng aList::containshoặc thay đổi Listthành Collection, để addrõ ràng, không thay đổi kết quả trong cài đặt Eclipse của tôi (tôi đã sử dụng 2019-06).


1
@howlger bình luận của bạn không có ý nghĩa. Phương thức mặc định phương thức được kế thừa và phương thức bị quá tải. Không có phương pháp nào khác. Thực tế là phương thức được kế thừa là một defaultphương thức chỉ là một điểm bổ sung. Câu trả lời của tôi đã chỉ ra một ví dụ trong đó Eclipse không ưu tiên cho phương thức mặc định.
Holger

1
@howlger có một sự khác biệt cơ bản trong hành vi của chúng ta. Bạn chỉ phát hiện ra rằng loại bỏ defaultthay đổi kết quả và ngay lập tức giả định để tìm ra lý do cho hành vi được quan sát. Bạn quá tự tin về nó đến nỗi bạn gọi sai câu trả lời khác, mặc dù thực tế là chúng thậm chí không mâu thuẫn. Bởi vì bạn đang phóng chiếu hành vi của chính mình lên người khác nên tôi không bao giờ tuyên bố quyền thừa kế là lý do . Tôi đã chứng minh rằng nó không phải. Tôi đã chứng minh rằng hành vi này không nhất quán, vì Eclipse chọn phương thức cụ thể trong một kịch bản nhưng không phải trong một kịch bản khác, nơi tồn tại ba tình trạng quá tải.
Holger

1
@howlger bên cạnh đó, tôi đã đặt tên cho một kịch bản khác ở cuối bình luận này , tạo một giao diện, không kế thừa, hai phương thức, một phương thức defaultkhác abstract, với hai đối số như người tiêu dùng và thử nó. Eclipse nói chính xác rằng nó không rõ ràng, mặc dù một là một defaultphương thức. Rõ ràng sự kế thừa vẫn có liên quan đến lỗi Eclipse này, nhưng không giống như bạn, tôi sẽ không phát điên và gọi các câu trả lời khác là sai, chỉ vì họ không phân tích toàn bộ lỗi này. Đó không phải là công việc của chúng tôi ở đây.
Holger

1
@howlger không, vấn đề là đây là một lỗi. Hầu hết độc giả thậm chí không quan tâm đến các chi tiết. Eclipse có thể tung xúc xắc mỗi khi nó chọn một phương thức, nó không thành vấn đề. Eclipse không nên chọn một phương thức khi nó mơ hồ, vì vậy nó không quan trọng tại sao nó chọn một phương thức. Câu trả lời này chứng minh rằng hành vi không nhất quán, điều này đã đủ để chỉ ra rằng đó là một lỗi. Không cần thiết phải chỉ ra dòng trong mã nguồn của Eclipse khi có sự cố xảy ra. Đó không phải là mục đích của Stackoverflow. Có lẽ, bạn đang nhầm lẫn Stackoverflow với trình theo dõi lỗi của Eclipse.
Holger

1
@howlger bạn lại tuyên bố không chính xác rằng tôi đã đưa ra một tuyên bố (sai) về lý do tại sao Eclipse đưa ra lựa chọn sai đó. Một lần nữa, tôi đã không, vì nhật thực không nên đưa ra lựa chọn nào cả. Phương pháp còn mơ hồ. Điểm. Lý do tại sao tôi sử dụng thuật ngữ di truyền trực tuyến , bởi vì việc phân biệt các phương thức có tên giống hệt nhau là cần thiết. Thay vào đó, tôi có thể nói phương thức mặc định của Viking, mà không thay đổi logic. Chính xác hơn, tôi nên sử dụng cụm từ , chính phương thức Eclipse được chọn không chính xác vì bất kỳ lý do gì . Bạn có thể sử dụng một trong ba cụm từ thay thế cho nhau và logic không thay đổi.
Holger

2

Trình biên dịch Eclipse giải quyết chính xác defaultphương thức , vì đây là phương thức cụ thể nhất theo Đặc tả ngôn ngữ Java 15.12.2.5 :

Nếu chính xác một trong những phương thức cụ thể tối đa là cụ thể (nghĩa là không abstracthoặc mặc định), thì đó là phương pháp cụ thể nhất.

javac(được sử dụng bởi Maven và IntelliJ theo mặc định) cho biết cuộc gọi phương thức không rõ ràng ở đây. Nhưng theo Đặc tả ngôn ngữ Java thì không mơ hồ vì một trong hai phương thức là phương thức cụ thể nhất ở đây.

Các biểu thức lambda được gõ ngầm được xử lý khác với các biểu thức lambda được gõ rõ ràng trong Java. Được gõ hoàn toàn trái ngược với các biểu thức lambda được gõ rõ ràng rơi vào giai đoạn đầu tiên để xác định các phương thức gọi nghiêm ngặt (xem Đặc tả ngôn ngữ Java jls-15.12.2.2 , điểm đầu tiên). Do đó, cách gọi phương thức ở đây không rõ ràng đối với các biểu thức lambda được gõ ngầm .

Trong trường hợp của bạn, cách giải quyết cho javaclỗi này là chỉ định loại giao diện chức năng thay vì sử dụng biểu thức lambda được gõ rõ ràng như sau:

iterable.forEach((ConsumerOne<A>) aList::add);

hoặc là

iterable.forEach((Consumer<A>) aList::add);

Dưới đây là ví dụ của bạn được giảm thiểu để thử nghiệm:

class A {

    interface FunctionA { void f(A a); }
    interface FunctionB { void f(A a); }

    interface FooA {
        default void foo(FunctionA functionA) {}
    }

    interface FooAB extends FooA {
        void foo(FunctionB functionB);
    }

    public static void main(String[] args) {
        FooAB foo = new FooAB() {
            @Override public void foo(FunctionA functionA) {
                System.out.println("FooA::foo");
            }
            @Override public void foo(FunctionB functionB) {
                System.out.println("FooAB::foo");
            }
        };
        java.util.List<A> list = new java.util.ArrayList<A>();

        foo.foo(a -> list.add(a));      // ambiguous
        foo.foo(list::add);             // ambiguous

        foo.foo((A a) -> list.add(a));  // not ambiguous (since FooA::foo is default; javac bug)
    }

}

4
Bạn đã bỏ lỡ điều kiện tiên quyết ngay trước câu được trích dẫn: Từ Nếu tất cả các phương thức cụ thể tối đa có chữ ký tương đương ghi đè Tất nhiên, hai phương thức có đối số là giao diện hoàn toàn không liên quan không có chữ ký tương đương ghi đè. Ngoài ra, điều này sẽ không giải thích tại sao Eclipse dừng chọn defaultphương thức khi có ba phương thức ứng cử viên hoặc khi cả hai phương thức được khai báo trong cùng một giao diện.
Holger

1
@Holger Câu trả lời của bạn tuyên bố, "Không có quy tắc nào được ưu tiên cho một phương thức áp dụng cụ thể khi nó được kế thừa hoặc một phương thức mặc định." Tôi có hiểu chính xác bạn rằng bạn đang nói rằng điều kiện tiên quyết cho quy tắc không tồn tại này không áp dụng ở đây không? Xin lưu ý, tham số ở đây là giao diện chức năng (xem JLS 9.8).
howlger

1
Bạn xé một câu ra khỏi bối cảnh. Câu này mô tả việc lựa chọn các phương thức tương đương ghi đè , nói cách khác, lựa chọn giữa các khai báo mà tất cả sẽ gọi cùng một phương thức trong thời gian chạy, bởi vì sẽ chỉ có một phương thức cụ thể trong lớp cụ thể. Điều này không liên quan đến trường hợp các phương thức riêng biệt như forEach(Consumer)forEach(Consumer2)không bao giờ có thể kết thúc ở cùng một phương thức thực hiện.
Holger

2
@StephanHerrmann Tôi không biết một JEP hoặc JSR, nhưng vẻ bề ngoài thay đổi như một sửa chữa cho phù hợp với ý nghĩa của “bê tông”, tức là so sánh với JLS§9.4 : “ phương pháp mặc định là khác biệt với phương pháp bê tông (§8.4. 3.1), được khai báo trong các lớp. Mùi, không bao giờ thay đổi.
Holger

2
@StephanHerrmann có, có vẻ như việc chọn một ứng cử viên từ các phương pháp tương đương ghi đè đã trở nên phức tạp hơn và thật thú vị khi biết lý do đằng sau nó, nhưng nó không liên quan đến câu hỏi trong tay. Cần có một tài liệu khác giải thích những thay đổi và động lực. Trước đây, đã có, nhưng với chính sách "một phiên bản mới mỗi năm" này, việc giữ chất lượng dường như là không thể ...
Holger

2

Mã nơi Eclipse triển khai JLS §15.12.2.5 không tìm thấy phương thức nào cụ thể hơn phương thức kia, ngay cả đối với trường hợp lambda được gõ rõ ràng.

Vì vậy, lý tưởng Eclipse sẽ dừng lại ở đây và báo cáo sự mơ hồ. Thật không may, việc thực hiện giải quyết quá tải có mã không tầm thường ngoài việc thực hiện JLS. Theo hiểu biết của tôi, mã này (bắt đầu từ thời điểm Java 5 mới) phải được giữ lại để điền vào một số khoảng trống trong JLS.

Tôi đã nộp https://bugs.eclipse.org/562538 để theo dõi điều này.

Không phụ thuộc vào lỗi đặc biệt này, tôi chỉ có thể khuyên mạnh mẽ chống lại kiểu mã này. Quá tải là tốt cho một số lượng lớn các bất ngờ trong Java, được nhân với suy luận kiểu lambda, độ phức tạp khá vượt trội so với mức tăng nhận thức được.


Cảm ơn bạn. Đã đăng nhập bug.eclipse.org/bugs/show_orms.cgi?id=562507 , có thể bạn có thể giúp liên kết chúng hoặc đóng một bản sao ...
ernest_k
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.