Biểu thức lambda có công dụng nào khác ngoài việc lưu các dòng mã không?


120

Biểu thức lambda có công dụng nào khác ngoài việc lưu các dòng mã không?

Có bất kỳ tính năng đặc biệt nào được cung cấp bởi lambdas giúp giải quyết các vấn đề không dễ giải quyết không? Cách sử dụng điển hình mà tôi đã thấy là thay vì viết thế này:

Comparator<Developer> byName = new Comparator<Developer>() {
  @Override
  public int compare(Developer o1, Developer o2) {
    return o1.getName().compareTo(o2.getName());
  }
};

Chúng ta có thể sử dụng biểu thức lambda để rút ngắn mã:

Comparator<Developer> byName =
(Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName());

27
Lưu ý rằng nó có thể còn ngắn hơn: (o1, o2) -> o1.getName().compareTo(o2.getName())
Thorbjørn Ravn Andersen

88
hoặc vẫn ngắn hơn:Comparator.comparing(Developer::getName)
Thomas Kläger,

31
Tôi sẽ không đánh giá thấp lợi ích đó. Khi mọi thứ dễ dàng hơn nhiều, bạn có nhiều khả năng làm mọi thứ theo cách "đúng đắn". Thường có một số căng thẳng giữa việc viết mã dễ bảo trì hơn trong dài hạn và dễ dàng ghi lại trong ngắn hạn. Lambdas làm giảm sự căng thẳng đó và do đó giúp bạn viết mã sạch hơn.
yshavit

5
Lambdas là các bao đóng, điều này làm cho việc tiết kiệm không gian thậm chí còn lớn hơn, vì bây giờ bạn không phải thêm một hàm tạo tham số, các trường, mã gán, v.v. vào lớp thay thế của mình.
CodesInChaos

Câu trả lời:


116

Biểu thức Lambda không thay đổi tập hợp các vấn đề bạn có thể giải quyết với Java nói chung, nhưng chắc chắn giúp giải quyết một số vấn đề dễ dàng hơn, chỉ vì lý do tương tự như chúng tôi không lập trình bằng hợp ngữ nữa. Loại bỏ các tác vụ thừa khỏi công việc của lập trình viên làm cho cuộc sống dễ dàng hơn và cho phép thực hiện những việc mà bạn thậm chí sẽ không đụng đến, chỉ với số lượng mã bạn sẽ phải tạo (thủ công).

Nhưng các biểu thức lambda không chỉ lưu các dòng mã. Biểu thức Lambda cho phép bạn xác định các hàm , một thứ gì đó mà bạn có thể sử dụng các lớp bên trong ẩn danh như một giải pháp thay thế trước đây, đó là lý do tại sao bạn có thể thay thế các lớp bên trong ẩn danh trong những trường hợp này, nhưng không phải nói chung.

Đáng chú ý nhất, các biểu thức lambda được định nghĩa độc lập với giao diện chức năng mà chúng sẽ được chuyển đổi sang, vì vậy không có thành viên kế thừa nào mà chúng có thể truy cập, hơn nữa, chúng không thể truy cập phiên bản của kiểu thực hiện giao diện chức năng. Trong một biểu thức lambda thissupercó cùng ý nghĩa với ngữ cảnh xung quanh, hãy xem thêm câu trả lời này . Ngoài ra, bạn không thể tạo các biến cục bộ mới che khuất các biến cục bộ của ngữ cảnh xung quanh. Đối với nhiệm vụ dự định là xác định một hàm, điều này loại bỏ rất nhiều nguồn lỗi, nhưng nó cũng ngụ ý rằng đối với các trường hợp sử dụng khác, có thể có các lớp ẩn danh bên trong không thể chuyển đổi thành biểu thức lambda, ngay cả khi triển khai một giao diện chức năng.

Hơn nữa, cấu trúc new Type() { … }đảm bảo tạo ra một thể hiện riêng biệt mới (như newmọi khi). Các cá thể lớp bên trong ẩn danh luôn giữ một tham chiếu đến cá thể bên ngoài của chúng nếu được tạo trong một staticngữ cảnh không phải . Ngược lại, biểu thức lambda chỉ nắm bắt một tham chiếu đến thiskhi cần thiết, tức là nếu chúng truy cập thishoặc không phải là staticthành viên. Và chúng tạo ra các trường hợp của danh tính không xác định có chủ ý, cho phép việc triển khai quyết định trong thời gian chạy có sử dụng lại các trường hợp hiện có hay không (xem thêm “ Biểu thức lambda có tạo một đối tượng trên heap mỗi khi nó được thực thi không? ”).

Những khác biệt này áp dụng cho ví dụ của bạn. Cấu trúc lớp bên trong ẩn danh của bạn sẽ luôn tạo ra một phiên bản mới, nó cũng có thể nắm bắt một tham chiếu đến phiên bản bên ngoài, trong khi của bạn (Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName())là một biểu thức lambda không bắt giữ sẽ đánh giá thành một singleton trong các triển khai điển hình. Hơn nữa, nó không tạo .classtệp trên ổ cứng của bạn.

Tất nhiên, do sự khác biệt về cả ngữ nghĩa và hiệu suất, biểu thức lambda có thể thay đổi cách các lập trình viên giải quyết các vấn đề nhất định trong tương lai, cũng do các API mới bao gồm các ý tưởng về lập trình chức năng sử dụng các tính năng ngôn ngữ mới. Xem thêm biểu thức lambda Java 8 và các giá trị hạng nhất .


1
Về cơ bản, biểu thức lambda là một kiểu lập trình hàm. Cho rằng bất kỳ nhiệm vụ nào cũng có thể được thực hiện với lập trình chức năng hoặc lập trình mệnh lệnh, không có lợi thế nào ngoại trừ khả năng đọc và khả năng bảo trì , điều này có thể lớn hơn các mối quan tâm khác.
Draco18s không còn tin tưởng SE nữa

1
@Trilarion: bạn có thể điền vào toàn bộ cuốn sách với định nghĩa của hàm . Bạn cũng sẽ phải quyết định xem nên tập trung vào mục tiêu hay vào các khía cạnh được hỗ trợ bởi các biểu thức lambda của Java. Liên kết cuối cùng của câu trả lời của tôi (liên kết này ) đã thảo luận về một số khía cạnh này trong ngữ cảnh của Java. Nhưng để hiểu câu trả lời của tôi, thì cũng đủ hiểu rằng các hàm là một khái niệm khác với các lớp. Để biết thêm chi tiết, đã có các tài nguyên hiện có và Hỏi & Đáp…
Holger

1
@Trilarion: cả hai, các hàm không cho phép giải quyết nhiều vấn đề hơn, trừ khi bạn coi số lượng mã / độ phức tạp hoặc cách giải quyết cần thiết cho các giải pháp khác là không thể chấp nhận được (như đã nói trong đoạn đầu tiên) và đã có một cách để xác định các hàm , như đã nói, các lớp bên trong có thể được sử dụng như một giải pháp thay thế cho việc không có cách tốt hơn để định nghĩa các hàm (nhưng các lớp bên trong không phải là hàm, vì chúng có thể phục vụ nhiều mục đích hơn). Nó cũng giống như với OOP — nó không cho phép làm bất cứ điều gì bạn không thể làm với assembly, nhưng bạn vẫn có thể hình dung một ứng dụng như Eclipse được viết trong assembly?
Holger

3
@EricDuminil: với tôi, độ hoàn chỉnh của Turing là quá lý thuyết. Ví dụ, cho dù Java là Turing hoàn chỉnh hay không, bạn không thể viết trình điều khiển thiết bị Windows bằng Java. (Hoặc trong PostScript, cũng đã hoàn thành Turing) Việc thêm các tính năng vào Java cho phép thực hiện điều đó, sẽ thay đổi tập hợp các vấn đề bạn có thể giải quyết với nó, tuy nhiên, điều đó sẽ không xảy ra. Vì vậy, tôi thích điểm Assembly hơn; vì mọi thứ bạn có thể làm đều được thực hiện trong các lệnh CPU thực thi, nên không có gì bạn không thể làm trong Assembly hỗ trợ mọi lệnh CPU (cũng dễ hiểu hơn là chủ nghĩa hình thức máy Turing).
Holger

2
@abelito nó phổ biến đối với tất cả các cấu trúc lập trình cấp cao hơn, cung cấp ít “khả năng hiển thị hơn về những gì đang thực sự diễn ra”, vì đó là tất cả những gì nó nói, ít chi tiết hơn, tập trung nhiều hơn vào vấn đề lập trình thực tế. Bạn có thể sử dụng nó để làm cho mã tốt hơn hoặc tệ hơn; nó chỉ là một công cụ. Giống như tính năng bình chọn; đó là một công cụ bạn có thể sử dụng theo cách dự định, để đưa ra đánh giá có cơ sở về chất lượng thực tế của một bài đăng. Hoặc bạn sử dụng nó để phản đối một câu trả lời chi tiết và hợp lý, chỉ vì bạn ghét lambdas.
Holger

52

Ngôn ngữ lập trình không dành cho máy móc thực thi.

Chúng dành cho các lập trình viên để suy nghĩ .

Ngôn ngữ là một cuộc trò chuyện với trình biên dịch để biến suy nghĩ của chúng ta thành thứ mà máy có thể thực thi. Một trong những phàn nàn chính về Java từ những người sử dụng nó từ các ngôn ngữ khác (hoặc đểcho các ngôn ngữ khác) là nó buộc một mô hình tinh thần nhất định đối với lập trình viên (tức là mọi thứ đều là một lớp).

Tôi sẽ không cân nhắc điều đó tốt hay xấu: mọi thứ đều là sự đánh đổi. Nhưng các lambdas Java 8 cho phép các lập trình viên suy nghĩ về các chức năng , đây là điều mà trước đây bạn không thể làm trong Java.

Nó cũng giống như một lập trình viên thủ tục học cách suy nghĩ về các lớp khi họ đến với Java: bạn thấy chúng dần dần chuyển từ các lớp có cấu trúc được tôn vinh và có các lớp 'trợ giúp' với một loạt các phương thức tĩnh và chuyển sang một thứ gì đó gần giống với thiết kế OO hợp lý (đường cong uốn khúc).

Nếu bạn chỉ nghĩ về chúng như một cách ngắn gọn hơn để thể hiện các lớp ẩn danh bên trong thì có lẽ bạn sẽ không thấy chúng rất ấn tượng theo cách mà người lập trình thủ tục ở trên có thể không nghĩ rằng các lớp là bất kỳ cải tiến lớn nào.


5
Tuy nhiên, hãy cẩn thận với điều đó, tôi đã từng nghĩ rằng OOP là tất cả, và sau đó tôi đã bị nhầm lẫn kinh khủng với các kiến ​​trúc hệ thống-thành phần-thực thể (whaddaya có nghĩa là bạn không có một lớp cho từng loại đối tượng trò chơi?! Và từng đối tượng trò chơi không phải là một đối tượng OOP ?!)
user253751

3
Nói cách khác, suy nghĩ trong OOP, thay vì chỉ nghĩ các lớp * o f * như một công cụ khác, y đã hạn chế suy nghĩ của tôi theo hướng xấu.
user253751

2
@immibis: có rất nhiều cách khác nhau để “suy nghĩ trong OOP”, vì vậy bên cạnh sai lầm phổ biến là nhầm lẫn giữa “suy nghĩ trong OOP” với “suy nghĩ trong lớp học”, có rất nhiều cách để suy nghĩ theo cách phản hiệu quả. Thật không may, điều đó xảy ra quá thường xuyên ngay từ đầu, vì ngay cả sách và tài liệu hướng dẫn dạy những người kém cỏi, cũng chỉ ra những khuôn mẫu phản đối trong các ví dụ và truyền bá tư duy đó. Nhu cầu giả định là "có một lớp cho mỗi loại" bất cứ điều gì, chỉ là một ví dụ, mâu thuẫn với khái niệm trừu tượng, tương tự như vậy, dường như có huyền thoại "OOP có nghĩa là viết càng nhiều bản soạn sẵn càng tốt", v.v.
Holger

@immibis mặc dù trong trường hợp đó nó không giúp được gì cho bạn, nhưng giai thoại đó thực sự ủng hộ quan điểm: ngôn ngữ và mô hình cung cấp một khuôn khổ để cấu trúc suy nghĩ của bạn và biến chúng thành một thiết kế. Trong trường hợp của bạn, bạn đã chạy ngược lại các cạnh của khuôn khổ, nhưng trong một bối cảnh khác, việc làm đó có thể giúp bạn khỏi lạc vào một cục bùn lớn và tạo ra một thiết kế xấu xí. Do đó, tôi cho rằng "mọi thứ đều phải đánh đổi". Giữ lợi ích sau trong khi tránh vấn đề trước là một câu hỏi quan trọng khi quyết định mức độ "lớn" của việc tạo ra một ngôn ngữ.
Leushenko

@immibis rằng lý do tại sao điều quan trọng là bạn phải học một loạt các mô hình tốt : mỗi đạt bạn về những bất cập của những người khác.
Jared Smith

36

Lưu dòng mã có thể được xem như một tính năng mới, nếu nó cho phép bạn viết một đoạn logic đáng kể theo cách ngắn gọn và rõ ràng hơn, khiến người khác mất ít thời gian hơn để đọc và hiểu.

Nếu không có biểu thức lambda (và / hoặc tham chiếu phương thức) thì Streamđường ống sẽ khó đọc hơn nhiều.

Ví dụ, hãy nghĩ xem Streamđường ống sau sẽ trông như thế nào nếu bạn thay thế mỗi biểu thức lambda bằng một cá thể lớp ẩn danh.

List<String> names =
    people.stream()
          .filter(p -> p.getAge() > 21)
          .map(p -> p.getName())
          .sorted((n1,n2) -> n1.compareToIgnoreCase(n2))
          .collect(Collectors.toList());

Nó sẽ là:

List<String> names =
    people.stream()
          .filter(new Predicate<Person>() {
              @Override
              public boolean test(Person p) {
                  return p.getAge() > 21;
              }
          })
          .map(new Function<Person,String>() {
              @Override
              public String apply(Person p) {
                  return p.getName();
              }
          })
          .sorted(new Comparator<String>() {
              @Override
              public int compare(String n1, String n2) {
                  return n1.compareToIgnoreCase(n2);
              }
          })
          .collect(Collectors.toList());

Điều này khó viết hơn nhiều so với phiên bản có biểu thức lambda và nó dễ bị lỗi hơn nhiều. Nó cũng khó hiểu hơn.

Và đây là một đường ống tương đối ngắn.

Để làm cho điều này có thể đọc được mà không có biểu thức lambda và tham chiếu phương thức, bạn sẽ phải xác định các biến chứa các thể hiện giao diện chức năng khác nhau đang được sử dụng ở đây, điều này sẽ phân chia logic của đường ống, khiến nó khó hiểu hơn.


17
Tôi nghĩ rằng đây là một cái nhìn sâu sắc quan trọng. Có thể lập luận rằng một điều gì đó trên thực tế là không thể thực hiện được trong một ngôn ngữ lập trình nếu tổng chi phí về cú pháp và nhận thức quá lớn để hữu ích, do đó các phương pháp tiếp cận khác sẽ vượt trội hơn. Do đó, bằng cách giảm chi phí cú pháp và nhận thức (như lambdas làm so với các lớp ẩn danh), các đường ống dẫn như ở trên trở nên khả thi. Nói dễ hiểu hơn, nếu việc viết một đoạn mã khiến bạn bị sa thải khỏi công việc, thì có thể nói rằng điều đó là không thể.
Sebastian Redl

Nitpick: Bạn không thực sự cần Comparatorfor sortedvì nó so sánh theo thứ tự tự nhiên. Có thể thay đổi nó thành so sánh độ dài của các chuỗi hoặc tương tự, để làm cho ví dụ tốt hơn?
tobias_k

@tobias_k không sao. Tôi đã thay đổi nó để compareToIgnoreCaselàm cho nó hoạt động khác hơn sorted().
Eran

1
compareToIgnoreCasevẫn là một ví dụ xấu, vì bạn có thể chỉ cần sử dụng bộ String.CASE_INSENSITIVE_ORDERso sánh hiện có . Đề xuất của @ tobias_k về việc so sánh theo độ dài (hoặc bất kỳ thuộc tính nào khác không có bộ so sánh tích hợp) phù hợp hơn. Đánh giá bởi ví dụ mã SO Q & A, lambdas gây ra rất nhiều hiện thực so sánh không cần thiết ...
Holger

Tôi có phải là người duy nhất nghĩ rằng ví dụ thứ hai dễ hiểu hơn không?
Trận chiến Clark

8

Lặp lại nội bộ

Khi lặp lại Bộ sưu tập Java, hầu hết các nhà phát triển có xu hướng lấy một phần tử và sau đó xử lý nó. Đó là, lấy mục đó ra và sau đó sử dụng hoặc lắp lại nó, v.v. Với các phiên bản Java trước 8, bạn có thể triển khai một lớp bên trong và thực hiện một số việc như:

numbers.forEach(new Consumer<Integer>() {
    public void accept(Integer value) {
        System.out.println(value);
    }
});

Bây giờ với Java 8, bạn có thể làm tốt hơn và ít dài dòng hơn với:

numbers.forEach((Integer value) -> System.out.println(value));

hoặc tốt hơn

numbers.forEach(System.out::println);

Hành vi như lập luận

Đoán trường hợp sau:

public int sumAllEven(List<Integer> numbers) {
    int total = 0;

    for (int number : numbers) {
        if (number % 2 == 0) {
            total += number;
        }
    } 
    return total;
}

Với giao diện Predicate của Java 8, bạn có thể làm tốt hơn như vậy:

public int sumAll(List<Integer> numbers, Predicate<Integer> p) {
    int total = 0;

    for (int number : numbers) {
        if (p.test(number)) {
            total += number;
        }
    }
    return total;
}

Gọi nó như:

sumAll(numbers, n -> n % 2 == 0);

Nguồn: DZone - Tại sao chúng ta cần biểu thức Lambda trong Java


Có lẽ là trường hợp đầu tiên là một cách để tiết kiệm một số dòng mã, nhưng nó làm cho mã dễ đọc
alseether

4
Lặp lại nội bộ là một tính năng lambda như thế nào? Vấn đề đơn giản của thực tế là về mặt kỹ thuật, lambdas không cho phép bạn làm bất cứ điều gì mà bạn không thể làm trước java-8. Đó chỉ là một cách ngắn gọn hơn để chuyển mã xung quanh.
Ousmane D.

@ Aominè Trong trường hợp này numbers.forEach((Integer value) -> System.out.println(value));các biểu thức lambda được làm bằng hai phần một trong những ngày còn lại của biểu tượng mũi tên (->) niêm yết của các thông số và một ở ngay chứa của nó cơ thể . Trình biên dịch tự động tìm ra rằng biểu thức lambda có cùng chữ ký của phương thức không được triển khai duy nhất của giao diện Người tiêu dùng.
cùng nhau

5
Tôi không hỏi biểu thức lambda là gì, tôi chỉ nói rằng việc lặp nội bộ không liên quan gì đến biểu thức lambda.
Ousmane D.

1
@ Aominè tôi thấy quan điểm của bạn, nó không có bất cứ điều gì với lambdas cho mỗi gia nhập , nhưng khi bạn chỉ ra, nó giống như một cách sạch của văn bản. Tôi nghĩ về mặt hiệu quả cũng tốt như các phiên bản trước 8
cùng lúc

4

Có nhiều lợi ích khi sử dụng lambdas thay vì lớp bên trong như sau:

  • Làm cho mã gọn gàng và biểu cảm hơn mà không cần giới thiệu thêm ngữ nghĩa cú pháp ngôn ngữ. bạn đã đưa ra một ví dụ trong câu hỏi của bạn.

  • Bằng cách sử dụng lambdas, bạn hài lòng với việc lập trình với các hoạt động kiểu hàm trên các luồng phần tử, chẳng hạn như các phép biến đổi thu nhỏ bản đồ trên các tập hợp. xem tài liệu gói java.util . Chức năng & java.util.stream .

  • Không có tệp lớp vật lý nào được tạo cho lambdas bằng trình biên dịch. Do đó, nó làm cho các ứng dụng được giao của bạn nhỏ hơn. Làm thế nào Bộ nhớ gán cho lambda?

  • Trình biên dịch sẽ tối ưu hóa việc tạo lambda nếu lambda không truy cập các biến ngoài phạm vi của nó, có nghĩa là cá thể lambda chỉ tạo một lần bởi JVM. để biết thêm chi tiết, bạn có thể xem câu trả lời của @ Holger cho câu hỏi Bộ đệm tham chiếu phương thức có phải là một ý tưởng hay trong Java 8 không? .

  • Lambdas có thể triển khai nhiều giao diện đánh dấu bên cạnh giao diện chức năng, nhưng các lớp ẩn danh bên trong không thể triển khai nhiều giao diện hơn, ví dụ:

    //                 v--- create the lambda locally.
    Consumer<Integer> action = (Consumer<Integer> & Serializable) it -> {/*TODO*/};

2

Lambdas chỉ là đường cú pháp cho các lớp ẩn danh.

Trước lambdas, các lớp ẩn danh có thể được sử dụng để đạt được điều tương tự. Mọi biểu thức lambda có thể được chuyển đổi thành một lớp ẩn danh.

Nếu bạn đang sử dụng IntelliJ IDEA, nó có thể thực hiện chuyển đổi cho bạn:

  • Đặt con trỏ vào lambda
  • Nhấn alt / option + enter

nhập mô tả hình ảnh ở đây


3
... Tôi nghĩ @StuartMarks đã làm rõ cú pháp sugar-ity (sp? Gr?) Của lambdas? ( Stackoverflow.com/a/15241404/3475600 , softwareengineering.stackexchange.com/a/181743 )
srborlongan

2

Để trả lời câu hỏi của bạn, thực tế là lambdas không cho phép bạn làm bất cứ điều gì mà bạn không thể làm trước java-8, thay vào đó nó cho phép bạn viết mã ngắn gọn hơn . Lợi ích của việc này là mã của bạn sẽ rõ ràng và linh hoạt hơn.


Rõ ràng @Holger đã đánh tan mọi nghi ngờ về hiệu quả lambda. Đó không chỉ là một cách để làm cho mã sạch hơn, nhưng nếu lambda không nắm bắt được bất kỳ giá trị nào, nó sẽ là một lớp Singleton (có thể tái sử dụng)
cùng lúc

Nếu bạn tìm hiểu sâu, mọi đặc điểm ngôn ngữ đều có sự khác biệt. Câu hỏi trong tầm tay ở mức cao nên tôi đã viết câu trả lời của mình ở mức cao.
Ousmane D.

2

Một điều tôi chưa thấy đề cập là lambda cho phép bạn xác định chức năng mà nó được sử dụng .

Vì vậy, nếu bạn có một số chức năng lựa chọn đơn giản, bạn không cần phải đặt nó ở một nơi riêng biệt với một loạt bảng soạn sẵn, bạn chỉ cần viết một lambda ngắn gọn và phù hợp với địa phương.


1

Có rất nhiều lợi thế ở đó.

  • Không cần phải định nghĩa toàn bộ lớp, chúng ta có thể chuyển việc thực hiện chức năng tự nó làm tham chiếu.
    • Việc tạo lớp bên trong sẽ tạo ra tệp .class trong khi nếu bạn sử dụng lambda thì việc tạo lớp sẽ bị trình biên dịch tránh bởi vì trong lambda bạn đang truyền thực thi hàm thay vì lớp.
  • Khả năng tái sử dụng mã cao hơn trước đây
  • Và như bạn đã nói mã ngắn hơn sau đó thực hiện bình thường.
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.