Czy wyrażenia lambda mają inne zastosowanie niż zapisywanie wierszy kodu?

120

Czy wyrażenia lambda mają inne zastosowanie niż zapisywanie wierszy kodu?

Czy są jakieś specjalne funkcje zapewniane przez lambdy, które rozwiązały problemy, które nie były łatwe do rozwiązania? Typowe użycie, które widziałem, jest takie, że zamiast pisać to:

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

Możemy użyć wyrażenia lambda, aby skrócić kod:

Comparator<Developer> byName =
(Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName());
Vikash
źródło
27
Zauważ, że może być jeszcze krótszy: (o1, o2) -> o1.getName().compareTo(o2.getName())
Thorbjørn Ravn Andersen
88
lub jeszcze krócej:Comparator.comparing(Developer::getName)
Thomas Kläger
31
Nie lekceważę tej korzyści. Kiedy sprawy są o wiele łatwiejsze, jest bardziej prawdopodobne, że zrobisz coś „we właściwy” sposób. Często występuje napięcie między pisaniem kodu, który jest łatwiejszy do utrzymania w dłuższej perspektywie, a łatwiejszym do zapisania w krótkim okresie. Lambdy zmniejszają to napięcie i pomagają w pisaniu czystszego kodu.
yshavit
5
Lambdy są zamknięciami, co sprawia, że ​​oszczędność miejsca jest jeszcze większa, ponieważ teraz nie musisz dodawać sparametryzowanego konstruktora, pól, kodu przypisania itp. Do klasy zastępczej.
CodesInChaos

Odpowiedzi:

116

Wyrażenia lambda nie zmieniają ogólnie zestawu problemów, które można rozwiązać za pomocą Javy, ale zdecydowanie ułatwiają rozwiązywanie pewnych problemów, z tego samego powodu, dla którego nie programujemy już w języku asemblerowym. Usunięcie zbędnych zadań z pracy programisty ułatwia życie i pozwala robić rzeczy, których inaczej byśmy nawet nie tknęli, tylko dla ilości kodu, który musiałbyś wyprodukować (ręcznie).

Ale wyrażenia lambda to nie tylko zapisywanie wierszy kodu. Wyrażenia lambda umożliwiają definiowanie funkcji , w przypadku których wcześniej można było użyć anonimowych klas wewnętrznych jako obejścia, dlatego w takich przypadkach można zastąpić anonimowe klasy wewnętrzne, ale nie ogólnie.

Przede wszystkim wyrażenia lambda są definiowane niezależnie od interfejsu funkcjonalnego, na który będą konwertowane, więc nie ma dziedziczonych elementów, do których mogliby uzyskać dostęp, a ponadto nie mogą uzyskać dostępu do wystąpienia typu implementującego interfejs funkcjonalny. W wyrażeniu lambda thisi supermają takie samo znaczenie jak w otaczającym kontekście, zobacz także tę odpowiedź . Nie można także tworzyć nowych zmiennych lokalnych, które przesłaniają zmienne lokalne otaczającego kontekstu. W przypadku zamierzonego zadania definiowania funkcji usuwa to wiele źródeł błędów, ale oznacza to również, że w innych przypadkach użycia mogą istnieć anonimowe klasy wewnętrzne, których nie można przekonwertować na wyrażenie lambda, nawet jeśli implementuje interfejs funkcjonalny.

Ponadto konstrukcja new Type() { … }gwarantuje utworzenie nowej odrębnej instancji (jak newzawsze). Anonimowe instancje klas wewnętrznych zawsze zachowują odniesienie do ich wystąpienia zewnętrznego, jeśli zostały utworzone w innym statickontekście. W przeciwieństwie do tego wyrażenia lambda przechwytują odniesienie tylko thiswtedy, gdy jest to potrzebne, tj. Jeśli mają dostęp thislub nie są staticczłonkami. Tworzą instancje celowo nieokreślonej tożsamości, co pozwala implementacji zdecydować w czasie wykonywania, czy ponownie wykorzystać istniejące instancje (zobacz także „ Czy wyrażenie lambda tworzy obiekt na stercie za każdym razem, gdy jest wykonywane? ”).

Te różnice dotyczą twojego przykładu. Twoja anonimowa konstrukcja klasy wewnętrznej zawsze będzie tworzyła nową instancję, może również przechwytywać odwołanie do instancji zewnętrznej, podczas gdy twoje (Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName())jest nieprzechwytywanym wyrażeniem lambda, które w typowych implementacjach zostanie oszacowane na singleton. Co więcej, nie tworzy .classpliku na dysku twardym.

Biorąc pod uwagę różnice zarówno semantyczne, jak i wydajnościowe, wyrażenia lambda mogą zmienić sposób, w jaki programiści będą rozwiązywać pewne problemy w przyszłości, oczywiście również z powodu nowych interfejsów API obejmujących koncepcje programowania funkcjonalnego z wykorzystaniem nowych funkcji języka. Zobacz także wyrażenie lambda Java 8 i wartości pierwszej klasy .

Holger
źródło
1
Zasadniczo wyrażenia lambda są rodzajem programowania funkcjonalnego. Biorąc pod uwagę, że każde zadanie można wykonać za pomocą programowania funkcjonalnego lub programowania imperatywnego, nie ma żadnych korzyści poza czytelnością i łatwością konserwacji , które mogą przeważać nad innymi obawami.
Draco18s nie ufa już SE
1
@Trilarion: możesz wypełnić całe książki definicją funkcji . Musisz także zdecydować, czy skupić się na celu, czy na aspektach obsługiwanych przez wyrażenia lambda języka Java. Ostatni link mojej odpowiedzi ( ten ) omawia już niektóre z tych aspektów w kontekście Javy. Ale aby zrozumieć moją odpowiedź, wystarczy zrozumieć, że funkcje to inne pojęcie niż klasy. Aby uzyskać więcej informacji, istnieją już istniejące zasoby i pytania i odpowiedzi…
Holger
1
@Trilarion: obie funkcje nie pozwalają na rozwiązanie większej liczby problemów, chyba że uznasz wielkość kodu / złożoność lub obejścia wymagane przez inne rozwiązania za niedopuszczalne (jak wspomniano w pierwszym akapicie), a był już sposób na zdefiniowanie funkcji , jak już powiedziano, klasy wewnętrzne mogą być używane jako obejście braku lepszego sposobu definiowania funkcji (ale klasy wewnętrzne nie są funkcjami, ponieważ mogą służyć o wiele więcej celów). Jest tak samo jak w przypadku OOP - nie pozwala na zrobienie niczego, czego nie można by zrobić w asemblerze, ale czy możesz wyobrazić sobie aplikację taką jak Eclipse napisaną w asemblerze?
Holger,
3
@EricDuminil: dla mnie kompletność Turinga jest zbyt teoretyczna. Np. Bez względu na to, czy Java jest kompletna, czy nie, nie możesz napisać sterownika urządzenia Windows w Javie. (Lub w PostScript, który jest również kompletny w Turingu) Dodanie funkcji do Javy, które to umożliwiają, zmieniłoby zestaw problemów, które można za jego pomocą rozwiązać, jednak tak się nie stanie. Więc wolę punkt zbiórki; ponieważ wszystko, co możesz zrobić, jest zaimplementowane w wykonywalnych instrukcjach procesora, nie ma niczego, czego nie można by zrobić w asemblerze, który obsługuje każdą instrukcję procesora (również łatwiejszy do zrozumienia niż formalizm maszyny Turinga).
Holger
2
@abelito jest wspólne dla wszystkich konstrukcji programowania wyższego poziomu, aby zapewnić mniej „wglądu w to, co się naprawdę dzieje”, ponieważ o to w tym wszystkim chodzi, mniej szczegółów, więcej skupienia się na rzeczywistym problemie programistycznym. Możesz go użyć, aby ulepszyć lub pogorszyć kod; to tylko narzędzie. Podobnie jak funkcja głosowania; jest to narzędzie, którego można używać w zamierzony sposób, aby zapewnić rzetelną ocenę faktycznej jakości postu. Lub używasz go, aby przegłosować wyszukaną, rozsądną odpowiedź, tylko dlatego, że nienawidzisz lambd.
Holger
52

Języki programowania nie są przeznaczone do wykonywania przez maszyny.

Są dla programistów do myślenia .

Języki to rozmowa z kompilatorem, mająca na celu przekształcenie naszych myśli w coś, co maszyna może wykonać. Jedną z głównych skarg na Javę ze strony osób, które przychodzą do niej z innych języków (lub zostawiajądla innych języków) było to, że wymusza ona pewien model mentalny na programiście (tj. Wszystko jest klasą).

Nie będę się zastanawiać, czy to dobrze, czy źle: wszystko to kompromisy. Ale lambdy Java 8 pozwalają programistom myśleć w kategoriach funkcji , czego wcześniej nie można było robić w Javie.

To to samo, co programista proceduralny, który uczy się myśleć w kategoriach klas, gdy przychodzą do języka Java: widzisz, jak stopniowo odchodzą od klas, które są gloryfikowanymi strukturami i mają klasy `` pomocnicze '' z zestawem metod statycznych i przechodzą do czegoś, co bardziej przypomina racjonalny projekt OO (mea culpa).

Jeśli pomyślisz o nich jako o krótszym sposobie wyrażania anonimowych klas wewnętrznych, to prawdopodobnie nie uznasz ich za bardzo imponujące w taki sam sposób, w jaki programista proceduralny powyżej prawdopodobnie nie sądził, że klasy były znacznym ulepszeniem.

Jared Smith
źródło
5
Uważaj jednak, kiedyś myślałem, że OOP to wszystko, a potem strasznie pomyliłem się z architekturą jednostka-komponent-system (co znaczy, że nie masz klasy dla każdego rodzaju obiektu gry ?! i każdego obiektu gry) nie jest obiektem OOP ?!)
user253751
3
Innymi słowy, myślenie w OOP, zamiast po prostu myśleć o * o f * klasach jako o innym narzędziu, ograniczyło moje myślenie w zły sposób.
user253751
2
@immibis: istnieje wiele różnych sposobów „myślenia w trybie OOP”, więc oprócz powszechnego błędu polegającego na myleniu „myślenia w trybie OOP” z „myśleniem na zajęciach”, istnieje wiele sposobów myślenia w sposób nieproduktywny. Niestety zdarza się to zbyt często od samego początku, ponieważ nawet książki i samouczki uczą gorszych, pokazują anty-wzorce w przykładach i rozpowszechniają to myślenie. Zakładana potrzeba „posiadania klasy dla każdego rodzaju” czegokolwiek, to tylko jeden przykład, zaprzeczający koncepcji abstrakcji, podobnie wydaje się, że istnieje mit „OOP oznacza pisanie jak największej liczby schematów” itp.
Holger,
@immibis, chociaż w tamtym przypadku to ci nie pomogło, ta anegdota w rzeczywistości potwierdza ten punkt: język i paradygmat zapewniają ramy dla strukturyzowania twoich myśli i przekształcania ich w projekt. W twoim przypadku natknąłeś się na krawędzie ramy, ale w innym kontekście mogłoby to pomóc ci wejść w wielką kulę błota i stworzyć brzydki projekt. Dlatego zakładam, że „wszystko jest kompromisem”. Utrzymanie tej drugiej korzyści przy jednoczesnym uniknięciu pierwszej kwestii jest kluczową kwestią przy podejmowaniu decyzji o tym, jak „duży” będzie język.
Leushenko
@immibis Dlatego ważne jest, aby dowiedzieć się kilka paradygmatu dobrze : Każda dotrze do Ciebie o niedoskonałości innych.
Jared Smith
36

Zapisywanie wierszy kodu może być postrzegane jako nowa funkcja, jeśli umożliwia napisanie znacznej części logiki w krótszy i wyraźniejszy sposób, co zajmuje innym osobom mniej czasu na przeczytanie i zrozumienie.

Bez wyrażeń lambda (i / lub odwołań do metod) Stream potoki byłyby znacznie mniej czytelne.

Pomyśl na przykład, jak Streamwyglądałby poniższy potok, gdybyś zastąpił każde wyrażenie lambda anonimową instancją klasy.

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

To byłby:

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());

Jest to dużo trudniejsze do napisania niż wersja z wyrażeniami lambda i dużo bardziej podatne na błędy. Trudniej też to zrozumieć.

A to jest stosunkowo krótki rurociąg.

Aby uczynić to czytelnym bez wyrażeń lambda i odniesień do metod, należałoby zdefiniować zmienne, które przechowują różne używane tutaj wystąpienia interfejsu funkcjonalnego, co spowodowałoby podzielenie logiki potoku, utrudniając zrozumienie.

Eran
źródło
17
Myślę, że to kluczowy spostrzeżenie. Można argumentować, że coś jest praktycznie niemożliwe do zrobienia w języku programowania, jeśli narzut składniowy i poznawczy jest zbyt duży, aby był użyteczny, więc inne podejścia będą lepsze. Dlatego poprzez zmniejszenie narzutu składniowego i poznawczego (tak jak robią to lambdy w porównaniu z klasami anonimowymi), potoki takie jak powyższe stają się możliwe. Bardziej z przymrużeniem oka, jeśli napisanie fragmentu kodu spowoduje zwolnienie z pracy, można powiedzieć, że nie jest to możliwe.
Sebastian Redl
Nitpick: W rzeczywistości nie potrzebujesz Comparatorfor, sortedponieważ i tak porównuje się w naturalnej kolejności. Może zmienić to na porównanie długości sznurków lub podobnych, żeby przykład był jeszcze lepszy?
tobias_k
@tobias_k nie ma problemu. Zmieniłem to na, compareToIgnoreCaseżeby zachowywał się inaczej niż sorted().
Eran
1
compareToIgnoreCasenadal jest złym przykładem, ponieważ możesz po prostu użyć istniejącego String.CASE_INSENSITIVE_ORDERkomparatora. Sugestia @ tobias_k dotycząca porównywania według długości (lub jakiejkolwiek innej właściwości bez wbudowanego komparatora) pasuje lepiej. Sądząc po przykładach kodu SO Q&A, lambdy spowodowały wiele niepotrzebnych implementacji komparatorów…
Holger
Czy tylko ja uważam, że drugi przykład jest łatwiejszy do zrozumienia?
Clark Battle,
8

Iteracja wewnętrzna

Podczas iteracji kolekcji Java większość programistów zwykle pobiera element, a następnie go przetwarza . To znaczy, wyjmij ten element, a następnie użyj go lub włóż ponownie, itp. W przypadku wersji Java starszych niż 8 można zaimplementować klasę wewnętrzną i zrobić coś takiego:

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

Teraz z Javą 8 możesz robić lepiej i mniej rozwlekle z:

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

albo lepiej

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

Zachowania jako argumenty

Zgadnij następujący przypadek:

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

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

Dzięki interfejsowi Java 8 Predicate możesz zrobić tak lepiej:

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

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

Nazywając to tak:

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

Źródło: DZone - Why We Need Lambda Expressions in Java

alseether
źródło
Prawdopodobnie pierwszy przypadek to sposób na zaoszczędzenie kilku wierszy kodu, ale sprawia, że ​​kod jest łatwiejszy do odczytania
podobnie jak
4
W jaki sposób wewnętrzna iteracja jest funkcją lambda? Prosta sprawa jest taka, że ​​z technicznego punktu widzenia lambdy nie pozwalają ci zrobić niczego, czego nie mógłbyś zrobić przed Java-8. To po prostu bardziej zwięzły sposób przekazywania kodu.
Ousmane D.
@ Aominè W tym przypadku numbers.forEach((Integer value) -> System.out.println(value));wyrażenie lambda składa się z dwóch części: jednej po lewej stronie symbolu strzałki (->) zawierającej parametry, a po prawej zawierającej jego treść . Kompilator automatycznie ustala, że ​​wyrażenie lambda ma taką samą sygnaturę, jak jedyna niezaimplementowana metoda interfejsu konsumenta.
również
5
Nie pytałem, czym jest wyrażenie lambda, mówię tylko, że wewnętrzna iteracja nie ma nic wspólnego z wyrażeniami lambda.
Ousmane D.
1
@ Aominè Rozumiem twój punkt widzenia, to nie ma nic z lambdas per se , ale jak to podkreślić, że bardziej przypomina sposób czystszego wpisywania. Myślę, że jeśli chodzi o wydajność, jest tak dobra, jak wersje przed 8
sumie
4

Istnieje wiele korzyści z używania lambd zamiast klas wewnętrznych, jak poniżej:

  • Uczyń kod bardziej zwartym i wyrazistym bez wprowadzania większej semantyki składni języka. podałeś już przykład w swoim pytaniu.

  • Korzystając z lambd, z przyjemnością programujesz z operacjami w stylu funkcjonalnym na strumieniach elementów, takich jak przekształcenia map-redukuj w kolekcjach. zobacz dokumentację pakietów java.util.function i java.util.stream .

  • Nie ma pliku klas fizycznych generowanych przez kompilator dla lambd. W ten sposób sprawia, że ​​dostarczane aplikacje są mniejsze. Jak pamięć przypisuje lambda?

  • Kompilator zoptymalizuje tworzenie lambda, jeśli lambda nie uzyskuje dostępu do zmiennych poza swoim zakresem, co oznacza, że ​​instancja lambda zostanie utworzona tylko raz przez JVM. Aby uzyskać więcej informacji, zobacz odpowiedź @ Holger na pytanie Czy buforowanie odwołań do metod to dobry pomysł w Javie 8? .

  • Lambdy mogą implementować interfejsy z wieloma markerami oprócz interfejsu funkcjonalnego, ale anonimowe klasy wewnętrzne nie mogą implementować więcej interfejsów, na przykład:

    //                 v--- create the lambda locally.
    Consumer<Integer> action = (Consumer<Integer> & Serializable) it -> {/*TODO*/};
holi-java
źródło
2

Lambdy to po prostu cukier syntaktyczny dla anonimowych zajęć.

Przed lambdami można użyć anonimowych klas, aby osiągnąć to samo. Każde wyrażenie lambda można przekonwertować na klasę anonimową.

Jeśli używasz IntelliJ IDEA, może wykonać konwersję za Ciebie:

  • Umieść kursor w lambdzie
  • Naciśnij alt / opcja + enter

wprowadź opis obrazu tutaj

Zamiatarka
źródło
3
... Myślałem, że @StuartMarks już wyjaśnił składnię cukrową (sp? Gr?) Lambd? ( stackoverflow.com/a/15241404/3475600 , softwareengineering.stackexchange.com/a/181743 )
srborlongan
2

Odpowiadając na twoje pytanie, faktem jest, że lambdy nie pozwalają ci zrobić niczego, czego nie mogłeś zrobić przed Java-8, a raczej pozwalają ci napisać bardziej zwięzły kod. Zaletą tego jest to, że Twój kod będzie bardziej przejrzysty i bardziej elastyczny.

Ousmane D.
źródło
Najwyraźniej @Holger rozwiał wszelkie wątpliwości dotyczące wydajności lambda. To nie tylko sposób na uczynienie kodu czystszym, ale jeśli lambda nie przechwytuje żadnej wartości, będzie to klasa Singleton (wielokrotnego użytku)
alseether
Jeśli zagłębisz się głęboko, każda funkcja językowa ma swoją różnicę. Pytanie jest na wysokim poziomie, dlatego swoją odpowiedź napisałem na wysokim poziomie.
Ousmane D.
2

Jedną rzeczą, o której jeszcze nie wspomniałem, jest to, że lambda pozwala zdefiniować funkcjonalność tam, gdzie jest używana .

Więc jeśli masz jakąś prostą funkcję selekcyjną, nie musisz umieszczać jej w osobnym miejscu z kilkoma szablonami, po prostu piszesz lambdę, która jest zwięzła i odpowiednia lokalnie.

Carlos
źródło
1

Tak, jest wiele zalet.

  • Nie ma potrzeby definiowania całej klasy, możemy przekazać jej implementację jako odniesienie.
    • Wewnętrzne tworzenie klasy spowoduje utworzenie pliku .class, natomiast jeśli używasz lambdy, kompilator unika tworzenia klasy, ponieważ w lambdzie zamiast klasy przekazujesz implementację funkcji.
  • Ponowne wykorzystanie kodu jest wyższe niż wcześniej
  • I jak powiedziałeś, kod jest krótszy niż normalna implementacja.
Sunil Kanzar
źródło