Lambdas Java 8, Function.identity () hoặc t-> t


240

Tôi có một câu hỏi liên quan đến việc sử dụng Function.identity()phương pháp.

Hãy tưởng tượng đoạn mã sau:

Arrays.asList("a", "b", "c")
          .stream()
          .map(Function.identity()) // <- This,
          .map(str -> str)          // <- is the same as this.
          .collect(Collectors.toMap(
                       Function.identity(), // <-- And this,
                       str -> str));        // <-- is the same as this.

Có bất kỳ lý do tại sao bạn nên sử dụng Function.identity()thay vì str->str(hoặc ngược lại). Tôi nghĩ rằng tùy chọn thứ hai dễ đọc hơn (tất nhiên là vấn đề về hương vị). Nhưng, có bất kỳ lý do "thực sự" tại sao một người nên được ưa thích?


6
Cuối cùng, không, điều này sẽ không tạo ra sự khác biệt.
fge

50
Hoặc là tốt. Đi với bất cứ điều gì bạn nghĩ là dễ đọc hơn. (Đừng lo lắng, hãy hạnh phúc.)
Brian Goetz

3
Tôi thích t -> tđơn giản hơn vì nó ngắn gọn hơn.
David Conrad

3
Câu hỏi hơi không liên quan, nhưng có ai biết tại sao các nhà thiết kế ngôn ngữ tạo danh tính () trả về một thể hiện của Hàm thay vì có một tham số của loại T và trả về nó để phương thức có thể được sử dụng với các tham chiếu phương thức không?
Kirill Rakhman

Tôi sẽ tranh luận rằng có một cách sử dụng để nói chuyện với từ "danh tính", vì nó có một ý nghĩa quan trọng trong các lĩnh vực khác của lập trình chức năng.
orbfish

Câu trả lời:


312

Kể từ khi triển khai JRE hiện tại, Function.identity()sẽ luôn trả về cùng một thể hiện trong khi mỗi lần xuất hiện identifier -> identifiersẽ không chỉ tạo ra cá thể của riêng nó mà thậm chí còn có một lớp triển khai riêng biệt. Để biết thêm chi tiết, xem tại đây .

Lý do là trình biên dịch tạo ra một phương thức tổng hợp giữ phần thân tầm thường của biểu thức lambda đó (trong trường hợp x->xtương đương return identifier;) và báo cho bộ thực thi để tạo ra một triển khai giao diện chức năng gọi phương thức này. Vì vậy, thời gian chạy chỉ thấy các phương thức đích khác nhau và việc triển khai hiện tại không phân tích các phương thức để tìm hiểu xem các phương thức nhất định có tương đương hay không.

Vì vậy, sử dụng Function.identity()thay vì x -> xcó thể tiết kiệm một số bộ nhớ nhưng điều đó sẽ không dẫn đến quyết định của bạn nếu bạn thực sự nghĩ rằng x -> xnó dễ đọc hơn Function.identity().

Bạn cũng có thể xem xét rằng khi biên dịch với thông tin gỡ lỗi được bật, phương thức tổng hợp sẽ có thuộc tính gỡ lỗi dòng chỉ vào (các) dòng mã nguồn giữ biểu thức lambda, do đó bạn có cơ hội tìm thấy nguồn của một Functioncá thể cụ thể trong khi gỡ lỗi . Ngược lại, khi gặp trường hợp được trả về bởi Function.identity()trong quá trình gỡ lỗi một thao tác, bạn sẽ không biết ai đã gọi phương thức đó và chuyển thể hiện đó vào thao tác.


5
Câu trả lời tốt đẹp. Tôi có một số nghi ngờ về gỡ lỗi. Làm thế nào nó có thể hữu ích? Rất khó có thể có được dấu vết ngăn xếp ngoại lệ liên quan đến x -> xkhung. Bạn có đề nghị đặt điểm dừng cho lambda này không? Thông thường, không dễ để đặt điểm dừng vào lambda biểu thức đơn (ít nhất là trong Eclipse) ...
Tagir Valeev

14
@Tagir Valeev: bạn có thể gỡ lỗi mã nhận hàm tùy ý và bước vào phương thức áp dụng của hàm đó. Sau đó, bạn có thể kết thúc tại mã nguồn của biểu thức lambda. Trong trường hợp biểu thức lambda rõ ràng, bạn sẽ biết chức năng đến từ đâu và có cơ hội nhận ra quyết định thông qua nơi nào được đưa ra mặc dù chức năng nhận dạng đã được thực hiện. Khi sử dụng Function.identity()thông tin đó bị mất. Sau đó, chuỗi cuộc gọi có thể giúp ích trong các trường hợp đơn giản nhưng hãy nghĩ đến, ví dụ: đánh giá đa luồng trong đó trình khởi tạo ban đầu không có trong ngăn xếp dấu vết
Holger


13
@Wim Deblauwe: Thú vị, nhưng tôi sẽ luôn thấy nó theo cách khác: nếu một phương thức nhà máy không nêu rõ trong tài liệu của mình rằng nó sẽ trả về một thể hiện mới trên mỗi lời gọi, bạn không thể cho rằng nó sẽ như vậy. Vì vậy, nó không đáng ngạc nhiên nếu nó không. Rốt cuộc, đó là một lý do lớn để sử dụng các phương pháp nhà máy thay vì new. new Foo(…)guaranties để tạo ra một thể hiện mới của các loại chính xác Foo, trong khi đó, Foo.getInstance(…)có thể trở về một thể hiện của (một subtype của) Foo...
Holger

93

Trong ví dụ của bạn không có sự khác biệt lớn giữa str -> strFunction.identity()vì bên trong nó chỉ đơn giản là vậy t->t.

Nhưng đôi khi chúng ta không thể sử dụng Function.identityvì chúng ta không thể sử dụng a Function. Hãy xem ở đây:

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);

cái này sẽ biên dịch tốt

int[] arrayOK = list.stream().mapToInt(i -> i).toArray();

nhưng nếu bạn cố gắng biên dịch

int[] arrayProblem = list.stream().mapToInt(Function.identity()).toArray();

bạn sẽ nhận được lỗi biên dịch vì mapToIntmong đợi ToIntFunction, không liên quan đến Function. Cũng ToIntFunctionkhông có identity()phương pháp.


3
Xem stackoverflow.com/q/38034982/14731 để biết ví dụ khác trong đó thay thế i -> ibằng Function.identity()sẽ dẫn đến lỗi trình biên dịch.
Gili

19
Tôi thích mapToInt(Integer::intValue).
shmosel

4
@shmosel là OK nhưng điều đáng nói là cả hai giải pháp sẽ hoạt động tương tự nhau vì mapToInt(i -> i)đơn giản hóa mapToInt( (Integer i) -> i.intValue()). Sử dụng bất kỳ phiên bản nào bạn nghĩ là rõ ràng hơn, đối với tôi mapToInt(i -> i)tốt hơn cho thấy ý định của mã này.
Pshemo

1
Tôi nghĩ rằng có thể có lợi ích hiệu suất trong việc sử dụng tham chiếu phương thức, nhưng chủ yếu chỉ là sở thích cá nhân. Tôi thấy nó mô tả nhiều hơn, bởi vì nó i -> itrông giống như một chức năng nhận dạng, không có trong trường hợp này.
shmosel

@shmosel Tôi không thể nói nhiều về sự khác biệt hiệu suất để bạn có thể đúng. Nhưng nếu hiệu suất không phải là vấn đề tôi sẽ ở lại i -> ivì mục tiêu của tôi là ánh xạ Integer sang int ( mapToIntgợi ý khá độc đáo) không gọi intValue()phương thức rõ ràng . Làm thế nào bản đồ này sẽ đạt được không thực sự quan trọng. Vì vậy, hãy đồng ý không đồng ý nhưng cảm ơn vì đã chỉ ra sự khác biệt hiệu suất có thể xảy ra, tôi sẽ cần xem xét kỹ hơn vào một ngày nào đó.
Pshemo

44

Từ nguồn JDK :

static <T> Function<T, T> identity() {
    return t -> t;
}

Vì vậy, không, miễn là nó đúng về mặt cú pháp.


8
Tôi tự hỏi nếu điều này làm mất hiệu lực câu trả lời ở trên liên quan đến lambda tạo ra một đối tượng - hoặc nếu đây là một triển khai cụ thể.
orbfish

28
@orbfish: đó là hoàn toàn phù hợp. Mỗi lần xuất hiện t->ttrong mã nguồn có thể tạo ra một đối tượng và việc thực hiện Function.identity()một lần xuất hiện. Vì vậy, tất cả các trang web gọi identity()sẽ chia sẻ một đối tượng đó trong khi tất cả các trang web sử dụng biểu thức lambda rõ ràng t->tsẽ tạo ra đối tượng của riêng chúng. Phương thức Function.identity()này không đặc biệt theo bất kỳ cách nào, bất cứ khi nào bạn tạo một phương thức nhà máy đóng gói một biểu thức lambda thường được sử dụng và gọi phương thức đó thay vì lặp lại biểu thức lambda, bạn có thể lưu một số bộ nhớ, khi thực hiện hiện tại .
Holger

Tôi đoán rằng điều này là do trình biên dịch tối ưu hóa việc tạo ra một t->tđối tượng mới mỗi khi phương thức được gọi và tái chế cùng một đối tượng bất cứ khi nào phương thức được gọi?
Daniel Gray

1
@DanielGray quyết định được đưa ra trong thời gian chạy. Trình biên dịch chèn một invokedynamiclệnh được liên kết trong lần thực thi đầu tiên của nó bằng cách thực hiện một phương thức được gọi là phương thức bootstrap, trong trường hợp các biểu thức lambda được đặt trong LambdaMetafactory. Việc thực hiện này quyết định trả về một điều khiển cho một hàm tạo, một phương thức xuất xưởng hoặc mã luôn trả về cùng một đối tượng. Nó cũng có thể quyết định trả lại một liên kết đến một điều khiển đã tồn tại (hiện không xảy ra).
Holger

@Holger Bạn có chắc rằng lệnh gọi nhận dạng này sẽ không được nội tuyến sau đó có khả năng được đơn hình hóa (và được nội tuyến lại)?
JasonN
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.