Có một lợi ích hiệu năng nào khi sử dụng cú pháp tham chiếu phương thức thay vì cú pháp lambda trong Java 8 không?


56

Các tham chiếu phương thức có bỏ qua phần trên của trình bao bọc lambda không? Họ có thể trong tương lai?

Theo Hướng dẫn Java về Tài liệu tham khảo phương pháp :

Đôi khi ... một biểu thức lambda không làm gì ngoài việc gọi một phương thức hiện có. Trong những trường hợp đó, thường rõ ràng hơn khi đề cập đến phương thức hiện có theo tên. Tài liệu tham khảo phương pháp cho phép bạn làm điều này; chúng là các biểu thức lambda nhỏ gọn, dễ đọc cho các phương thức đã có tên.

Tôi thích cú pháp lambda hơn cú pháp tham chiếu phương thức vì nhiều lý do:

Lambdas rõ ràng hơn

Bất chấp tuyên bố của Oracle, tôi thấy cú pháp lambda dễ đọc hơn so với cú pháp tham chiếu phương thức đối tượng vì cú pháp tham chiếu phương thức không rõ ràng:

Bar::foo

Bạn có đang gọi một phương thức một đối số tĩnh trên lớp của x và truyền nó x không?

x -> Bar.foo(x)

Hoặc bạn đang gọi một phương thức đối số zero trên x?

x -> x.foo()

Cú pháp tham chiếu phương thức có thể thay thế cho một trong hai. Nó ẩn những gì mã của bạn đang thực sự làm.

Lambdas an toàn hơn

Nếu bạn tham chiếu Bar :: foo như một phương thức lớp và Bar sau đó thêm một phương thức cá thể có cùng tên (hoặc ngược lại), mã của bạn sẽ không còn được biên dịch nữa.

Bạn có thể sử dụng lambdas một cách nhất quán

Bạn có thể gói bất kỳ chức năng nào trong lambda - vì vậy bạn có thể sử dụng cùng một cú pháp một cách nhất quán ở mọi nơi. Cú pháp tham chiếu phương thức sẽ không hoạt động trên các phương thức lấy hoặc trả về mảng nguyên thủy, ném ngoại lệ được kiểm tra hoặc có cùng tên phương thức được sử dụng làm thể hiện và phương thức tĩnh (vì cú pháp tham chiếu phương thức không rõ ràng về phương thức nào sẽ được gọi) . Chúng không hoạt động khi bạn có các phương thức quá tải với cùng số lượng đối số, nhưng dù sao bạn cũng không nên làm điều đó (xem mục 41 của Josh Bloch) vì vậy chúng tôi không thể chống lại các tham chiếu phương thức.

Phần kết luận

Nếu không có hình phạt về hiệu năng khi làm như vậy, tôi muốn tắt cảnh báo trong IDE của mình và sử dụng cú pháp lambda một cách nhất quán mà không rắc tham chiếu phương thức không thường xuyên vào mã của tôi.

PS

Không có ở đây cũng không có, nhưng trong giấc mơ của tôi, các tham chiếu phương thức đối tượng trông giống như thế này và áp dụng invoke-Dynamic đối với phương thức trực tiếp trên đối tượng mà không có trình bao bọc lambda:

_.foo()

2
Điều này không trả lời câu hỏi của bạn, nhưng có những lý do khác để sử dụng đóng cửa. Theo tôi, nhiều người trong số họ vượt xa chi phí nhỏ của một cuộc gọi chức năng bổ sung.

9
Tôi nghĩ rằng bạn đang lo lắng về hiệu suất sớm. Theo tôi hiệu suất nên là một xem xét thứ cấp cho việc sử dụng một tham chiếu phương pháp; tất cả là về việc thể hiện ý định của bạn. Nếu bạn cần truyền một hàm ở đâu đó, tại sao không chỉ truyền hàm thay vì một số hàm khác ủy nhiệm cho nó? Nó có vẻ xa lạ với tôi; giống như bạn không hoàn toàn mua các chức năng đó là những thứ hạng nhất, chúng cần một người đi kèm ẩn danh để di chuyển chúng. Nhưng để giải quyết câu hỏi của bạn trực tiếp hơn, tôi sẽ không ngạc nhiên nếu tham chiếu phương thức có thể được thực hiện như một cuộc gọi tĩnh.
Doval

7
.map(_.acceptValue()): Nếu tôi không nhầm, nó trông rất giống cú pháp Scala. Có lẽ bạn chỉ đang cố sử dụng ngôn ngữ sai.
Giorgio

2
"Cú pháp tham chiếu phương thức sẽ không hoạt động trên các phương thức trả về void" - Làm thế nào? Anh đang đi System.out::printlnđến forEach()tất cả các thời gian ...?
Lukas Eder

3
"Cú pháp tham chiếu phương thức sẽ không hoạt động trên các phương thức lấy hoặc trả về mảng nguyên thủy, đưa ra các ngoại lệ được kiểm tra ...." Điều đó không chính xác. Bạn có thể sử dụng các tham chiếu phương thức cũng như các biểu thức lambda cho các trường hợp như vậy, miễn là giao diện chức năng được đề cập cho phép.
Stuart Marks

Câu trả lời:


12

Trong nhiều kịch bản, tôi nghĩ lambda và tham chiếu phương thức là tương đương. Nhưng lambda sẽ bao bọc mục tiêu gọi bằng kiểu giao diện khai báo.

Ví dụ

public class InvokeTest {

    private static void invoke(final Runnable r) {
        r.run();
    }

    private static void target() {
        new Exception().printStackTrace();
    }

    @Test
    public void lambda() throws Exception {
        invoke(() -> target());
    }

    @Test
    public void methodReference() throws Exception {
        invoke(InvokeTest::target);
    }
}

Bạn sẽ thấy giao diện điều khiển xuất stacktrace.

Trong lambda(), phương thức gọi target()lambda$lambda$0(InvokeTest.java:20), trong đó có thông tin dòng có thể theo dõi. Rõ ràng, đó là lambda bạn viết, trình biên dịch tạo ra một phương thức ẩn danh cho bạn. Và sau đó, người gọi của phương thức lambda giống như InvokeTest$$Lambda$2/1617791695.run(Unknown Source), đó là invokedynamiccuộc gọi trong JVM, nó có nghĩa là cuộc gọi được liên kết với phương thức được tạo .

Trong methodReference(), gọi phương thức target()là trực tiếp InvokeTest$$Lambda$1/758529971.run(Unknown Source), nó có nghĩa là cuộc gọi được liên kết trực tiếp với InvokeTest::targetphương thức .

Phần kết luận

Trên tất cả, so sánh với tham chiếu phương thức, sử dụng biểu thức lambda sẽ chỉ gây ra thêm một lệnh gọi phương thức đến phương thức tạo từ lambda.


1
Câu trả lời dưới đây về metafactory tốt hơn một chút. Tôi đã thêm một số nhận xét hữu ích có liên quan đến nó (cụ thể là cách triển khai như Oracle / OpenJDK sử dụng ASM để tạo các đối tượng lớp trình bao bọc cần thiết để tạo các phiên bản xử lý lambda / phương thức.
Ajax

29

Đó là tất cả về siêu dữ liệu

Đầu tiên, hầu hết các tham chiếu phương thức không cần giải mã bằng siêu dữ liệu lambda, chúng chỉ đơn giản được sử dụng làm phương thức tham chiếu. Trong phần "Lambda body sugaring" của bài viết Bản dịch của Lambda Expressions ("TLE"):

Tất cả mọi thứ đều bằng nhau, các phương thức riêng tư thích hơn các phương thức tĩnh, các phương thức tĩnh thích hợp hơn các phương thức cá thể, tốt nhất là nếu các cơ thể lambda được đưa vào lớp trong cùng, trong đó biểu thức lambda xuất hiện, chữ ký phải khớp với chữ ký cơ thể của lambda, thêm các đối số nên được đặt trước ở phía trước danh sách đối số cho các giá trị được bắt giữ và hoàn toàn không tham chiếu phương thức. Tuy nhiên, có những trường hợp ngoại lệ mà chúng ta có thể phải đi chệch khỏi chiến lược cơ bản này.

Điều này được nhấn mạnh hơn nữa trong "The Lambda Metafactory" của TLE:

metaFactory(MethodHandles.Lookup caller, // provided by VM
            String invokedName,          // provided by VM
            MethodType invokedType,      // provided by VM
            MethodHandle descriptor,     // lambda descriptor
            MethodHandle impl)           // lambda body

Đối implsố xác định phương thức lambda, hoặc là phần thân lambda đã tách rời hoặc phương thức có tên trong tham chiếu phương thức.

Các tham chiếu Integer::sumphương thức tĩnh ( ) hoặc không giới hạn ( Integer::intValue) là 'đơn giản nhất' hoặc 'thuận tiện nhất', theo nghĩa là chúng có thể được xử lý tối ưu bởi một biến thể siêu dữ liệu 'đường dẫn nhanh' mà không cần giải mã . Ưu điểm này được chỉ ra một cách hữu ích trong "Các biến thể Metafactory" của TLE:

Bằng cách loại bỏ các đối số khi không cần thiết, các tệp lớp trở nên nhỏ hơn. Và tùy chọn đường dẫn nhanh làm giảm thanh cho VM để thực hiện thao tác chuyển đổi lambda, cho phép nó được coi là một hoạt động "quyền anh" và tối ưu hóa việc mở hộp thư.

Đương nhiên, một tham chiếu phương thức bắt cá thể ( obj::myMethod) cần cung cấp cá thể bị ràng buộc làm đối số cho xử lý phương thức để gọi, điều này có thể có nghĩa là cần phải giải mã bằng cách sử dụng các phương thức 'cầu'.

Phần kết luận

Tôi không chắc chắn chính xác về 'trình bao bọc' lambda mà bạn đang gợi ý là gì, nhưng mặc dù kết quả cuối cùng của việc sử dụng lambdas hoặc tham chiếu phương thức do người dùng định nghĩa là giống nhau, cách tiếp cận có vẻ khá khác nhau và có thể khác trong tương lai nếu bây giờ không phải vậy. Do đó, tôi cho rằng nhiều khả năng các tham chiếu phương thức có thể được xử lý theo cách tối ưu hơn bởi siêu dữ liệu.


1
Đây là câu trả lời phù hợp nhất, vì nó thực sự chạm vào các máy móc nội bộ cần thiết để tạo ra lambda. Là một người thực sự gỡ lỗi quá trình tạo lambda (để tìm ra cách tạo id ổn định cho tham chiếu phương thức / lambda đã cho để họ có thể xóa khỏi bản đồ), tôi thấy khá ngạc nhiên khi chỉ cần thực hiện bao nhiêu công việc để tạo ra một ví dụ của lambda. Oracle / OpenJDK sử dụng ASM để tạo động các lớp, nếu cần, đóng trên bất kỳ biến nào được tham chiếu trong lambda của bạn (hoặc vòng loại cá thể của tham chiếu phương thức) ...
Ajax

1
... Ngoài việc đóng các biến, đặc biệt lambdas còn tạo ra một phương thức tĩnh được đưa vào lớp khai báo để lambda được tạo có một cái gì đó để gọi để ánh xạ vào (các) hàm bạn muốn gọi. Một tham chiếu phương thức với một vòng loại cá thể sẽ lấy cá thể đó làm tham số cho phương thức này; Tôi đã không kiểm tra nếu tham chiếu phương thức :: này trong lớp riêng bỏ qua việc đóng này, nhưng thực tế tôi đã kiểm tra rằng các phương thức tĩnh thực sự bỏ qua phương thức trung gian (và chỉ tạo một đối tượng để tuân thủ mục tiêu gọi tại gọi trang web).
Ajax

1
Có lẽ một phương thức riêng tư cũng sẽ không cần gửi ảo, trong khi mọi thứ công khai hơn sẽ cần phải đóng lại đối tượng (thông qua một phương thức tĩnh được tạo) để nó có thể bất biến trên ví dụ thực tế để đảm bảo rằng nó thông báo ghi đè. TL; DR: Tham chiếu phương thức gần với ít đối số hơn, do đó chúng rò rỉ ít hơn và yêu cầu tạo mã ít động hơn.
Ajax

3
Nhận xét spam cuối cùng ... Thế hệ lớp năng động là khá nặng. Vẫn có thể tốt hơn là tải một lớp ẩn danh trong trình nạp lớp (vì các phần nặng nhất chỉ được thực hiện một lần), nhưng bạn sẽ thực sự ngạc nhiên khi biết có bao nhiêu công việc tạo ra một thể hiện lambda. Sẽ rất thú vị khi thấy điểm chuẩn thực sự của lambdas so với tham chiếu phương thức so với các lớp ẩn danh. Lưu ý rằng hai lambdas hoặc tham chiếu phương thức tham chiếu cùng một thứ, nhưng tại các vị trí khác nhau tạo ra các lớp khác nhau trong thời gian chạy (ngay cả trong cùng một phương thức, ngay sau nhau).
Ajax


0

Có một hậu quả khá nghiêm trọng khi sử dụng biểu thức lambda có thể ảnh hưởng đến hiệu suất.

Khi bạn khai báo một biểu thức lambda, bạn đang tạo một bao đóng trên phạm vi cục bộ.

Điều này có nghĩa là gì và nó ảnh hưởng đến hiệu suất như thế nào?

Vâng, tôi rất vui vì bạn đã hỏi. Điều đó có nghĩa là mỗi biểu thức lambda nhỏ đó là một lớp bên trong ẩn danh nhỏ và điều đó có nghĩa là nó mang theo nó một tham chiếu đến tất cả các biến có cùng phạm vi của biểu thức lambda.

Điều này cũng có nghĩa là thistham chiếu của thể hiện đối tượng và tất cả các trường của nó. Tùy thuộc vào thời điểm gọi lambda thực tế, điều này có thể dẫn đến rò rỉ tài nguyên khá đáng kể vì người thu gom rác không thể từ bỏ các tài liệu tham khảo này miễn là đối tượng đang giữ lambda vẫn còn sống ...


Tương tự với các tham chiếu phương thức không tĩnh.
Ajax

12
Java lambdas không hoàn toàn đóng cửa. Họ cũng không phải là lớp bên trong vô danh. Chúng KHÔNG mang tham chiếu đến tất cả các biến trong phạm vi chúng được khai báo. Họ CHỈ mang một tham chiếu đến các biến họ thực sự tham chiếu. Điều này cũng áp dụng cho các thisđối tượng có thể được tham chiếu. Do đó, lambda không rò rỉ tài nguyên chỉ bằng cách có phạm vi cho chúng; nó chỉ giữ cho các đối tượng nó cần. Mặt khác, một lớp bên trong ẩn danh có thể rò rỉ tài nguyên, nhưng không phải lúc nào cũng làm như vậy. Xem mã này để biết ví dụ: a.blmq.us/2mmrL6v
squid314

Câu trả lời đó đơn giản là sai
Stefan Reich

Cảm ơn bạn @StefanReich, Điều đó rất hữu ích!
Roland Tepp
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.