Czy stosowanie wyrażeń Lambda jest zawsze, gdy jest to możliwe, w dobrej praktyce Java?

52

Niedawno opanowałem wyrażenie Lambda, które zostało wprowadzone w java 8. Odkryłem, że ilekroć używam funkcjonalnego interfejsu, zwykle używam wyrażenia Lambda zamiast tworzyć klasę, która implementuje funkcjonalny interfejs.

Czy jest to uważane za dobrą praktykę? A może ich sytuacje, w których użycie Lambdy jako funkcjonalnego interfejsu jest nieodpowiednie?

Stalowy palec
źródło
122
Na pytanie „Czy stosuje technikę X, gdy tylko jest to możliwe, dobrą praktyką” jedyną prawidłową odpowiedzią jest „nie, używaj techniki X nie zawsze, gdy jest to możliwe, ale kiedy jest to rozsądne” .
Doc Brown
Wybór kiedy użyć lambda (która jest anonimowa) w porównaniu z jakimś innym rodzajem implementacji funkcjonalnej (np. Odniesienie do metody) jest naprawdę interesującym pytaniem. Miałem okazję spotkać Josha Blocha kilka dni temu i to był jeden z punktów, o których mówił. Z tego, co powiedział, rozumiem, że planuje dodać nowy przedmiot do następnej edycji Effective Java zajmującej się tym właśnie pytaniem.
Daniel Pryden
@DocBrown To powinna być odpowiedź, a nie komentarz.
gnasher729
1
@ gnasher729: zdecydowanie nie.
Doc Brown

Odpowiedzi:

116

Istnieje wiele kryteriów, które powinny skłonić Cię do rozważenia niestosowania lambda:

  • Rozmiar Im większa lambda, tym trudniej jest podążać za otaczającą ją logiką.
  • Powtórzenie Lepiej jest utworzyć nazwaną funkcję dla powtarzalnej logiki, chociaż dobrze jest powtarzać bardzo proste lambdy, które są rozłożone.
  • Nazewnictwo Jeśli potrafisz wymyślić świetną nazwę semantyczną, powinieneś jej użyć, ponieważ zapewnia ona dużą przejrzystość kodu. Nie mówię imion jak priceIsOver100. x -> x.price > 100jest tak samo jasne jak ta nazwa. Mam na myśli, isEligibleVoterże takie imiona zastępują długą listę warunków.
  • Zagnieżdżanie Zagnieżdżone lambdy są naprawdę bardzo trudne do odczytania.

Nie idź za burtę. Pamiętaj, że oprogramowanie można łatwo zmienić. W razie wątpliwości napisz to w obie strony i sprawdź, który z nich jest łatwiejszy do odczytania.

Karl Bielefeldt
źródło
1
Ważne jest również, aby wziąć pod uwagę ilość danych przetwarzanych przez lambda, ponieważ lambdas są raczej nieefektywne przy niewielkim przetwarzaniu danych. Naprawdę świecą równolegle do dużych zestawów danych.
CraigR8806
1
@ CraigR8806 Nie sądzę, aby wydajność była tutaj problemem. Korzystanie z anonimowej funkcji lub tworzenie klasy rozszerzającej interfejs funkcjonalny powinno być takie samo pod względem wydajności. Uwagi dotyczące wydajności pojawiają się, gdy mówimy o używaniu interfejsu API strumieni / wyższego rzędu w imperatywnej pętli stylu, ale w tym pytaniu używamy interfejsów funkcjonalnych w obu przypadkach tylko z inną składnią.
puhlen
17
„chociaż w porządku jest powtarzanie bardzo prostych lambd, które są rozłożone” Tylko wtedy, gdy niekoniecznie zmieniają się razem. Jeśli wszystkie muszą mieć taką samą logikę, aby Twój kod był poprawny, powinieneś uczynić je w rzeczywistości tą samą metodą, aby zmiany musiały być dokonane tylko w jednym miejscu.
jpmc26
Ogólnie jest to naprawdę dobra odpowiedź. Wzmianka o zastosowaniu odwołania do metody jako alternatywy dla lambda spowodowałaby załamanie tylko dziury, którą widzę w tej odpowiedzi.
Morgen
3
W razie wątpliwości pisz w obie strony i zapytaj kogoś, kto zachowa łatwiejszy do odczytania kod.
corsiKa
14

To zależy. Ilekroć znajdziesz tę samą lambdę w różnych miejscach, powinieneś rozważyć wdrożenie klasy, która implementuje interfejs. Ale gdybyś użył anonimowej klasy wewnętrznej, inaczej uważam, że lambda jest znacznie lepsza.

Tohnmeister
źródło
6
Nie zapomnij o możliwości użycia odwołania do metody! Z tego właśnie powodu Oracle dodało (i dodaje jeszcze więcej w Javie 9) metody statyczne i metody domyślne.
Jörg W Mittag
13

Popieram odpowiedź Karla Bielefeldta, ale chcę przedstawić krótkie uzupełnienie.

  • Debugowanie Niektóre IDE zmagają się z zasięgiem w lambdzie i próbują wyświetlać zmienne składowe w kontekście lambda. Chociaż mam nadzieję, że ta sytuacja ulegnie zmianie, może być denerwujące utrzymywanie czyjegoś kodu, gdy jest on wypełniony lambdami.
Jolleyboy
źródło
Zdecydowanie prawda o .NET. Niekoniecznie sprawia, że ​​unikam jagniąt, ale z pewnością nie czuję się winny, jeśli zamiast tego robię coś innego.
user1172763
3
W takim przypadku używasz niewłaściwego IDE. Zarówno IntelliJ, jak i Netbeans radzą sobie całkiem dobrze w tym konkretnym obszarze.
David Foerster
1
@ user1172763 W programie Visual Studio, jeśli chcesz wyświetlić zmienne składowe, odkryłem, że możesz podróżować w górę stosu wywołań, dopóki nie przejdziesz do kontekstu lambda.
Zev Spitz
6

Dostęp do zmiennych lokalnych obejmującego zakres

Przyjęta odpowiedź Karla Bielefeldta jest poprawna. Mogę dodać jeszcze jedno rozróżnienie:

  • Zakres

Kod lambda zagnieżdżony w metodzie wewnątrz klasy może uzyskiwać dostęp do dowolnie efektywnych zmiennych końcowych znalezionych w tej metodzie i klasie.

Utworzenie klasy, która implementuje funkcjonalny interfejs, nie daje takiego bezpośredniego dostępu do stanu kodu wywołującego.

Aby zacytować Java Tutorial (podkreślenie moje):

Podobnie jak klasy lokalne i anonimowe, wyrażenia lambda mogą przechwytywać zmienne; mają taki sam dostęp do zmiennych lokalnych obejmującego zakres . Jednak w przeciwieństwie do klas lokalnych i anonimowych, wyrażenia lambda nie mają żadnych problemów z cieniowaniem (więcej informacji można znaleźć w części Cieniowanie). Wyrażenia lambda mają zasięg leksykalny. Oznacza to, że nie dziedziczą żadnych nazw z nadtypu ani nie wprowadzają nowego poziomu zakresu. Deklaracje w wyrażeniu lambda są interpretowane tak, jak w otaczającym środowisku.

Chociaż wyciąganie długiego kodu i nazywanie go ma wiele zalet, należy to porównać z prostotą bezpośredniego dostępu do stanu otaczającej metody i klasy.

Widzieć:

Basil Bourque
źródło
4

Może to być wybieranie nitów, ale do wszystkich innych doskonałych punktów zawartych w innych odpowiedziach dodałbym:

Preferuj referencje metod, jeśli to możliwe. Porównać:

employees.stream()
         .map(Employee::getName)
         .forEach(System.out::println);

przeciw

employees.stream()
         .map(employee -> employee.getName())
         .forEach(employeeName -> System.out.println(employeeName));

Użycie odwołania do metody pozwala uniknąć konieczności nazwania argumentów lambdy, które zwykle są zbędne i / lub prowadzą do leniwych nazw, takich jak elub x.

Matt McHenry
źródło
0

Opierając się na odpowiedzi Matta McHenry'ego, może istnieć związek między lambda a referencjami metod, przy czym lambda może być przepisana jako seria referencji metod. Na przykład:

employees.stream()
         .forEach(employeeName -> System.out.println(employee.getName()));

vs

employees.stream()
         .map(Employee::getName)
         .forEach(System.out::println);
LaFayette
źródło