Czy korzystanie ze składni odwołania do metody zamiast składni lambda w Javie 8 wiąże się z większą wydajnością?

56

Czy odniesienia do metod pomijają obciążenie owijarki lambda? Czy mogą w przyszłości?

Zgodnie z samouczkiem Java na temat metod :

Czasami ... wyrażenie lambda robi tylko wywołanie istniejącej metody. W takich przypadkach często łatwiej jest odnieść się do istniejącej metody według nazwy. Odwołania do metod umożliwiają to; są to zwarte, łatwe do odczytania wyrażenia lambda dla metod, które już mają nazwę.

Wolę składnię lambda od składni odwołania do metody z kilku powodów:

Baranki są wyraźniejsze

Pomimo twierdzeń Oracle, uważam, że składnia lambda jest łatwiejsza do odczytania niż skrót metody obiektowej, ponieważ składnia odwołania do metody jest niejednoznaczna:

Bar::foo

Czy wywołujesz statyczną metodę z jednym argumentem na klasie x i przekazujesz ją x?

x -> Bar.foo(x)

A może wywołujesz metodę wystąpienia z zerowym argumentem na x?

x -> x.foo()

Składnia odwołania do metody może oznaczać jedno lub drugie. Ukrywa to, co faktycznie robi Twój kod.

Jagnięta są bezpieczniejsze

Jeśli odniesiesz się do Bar :: foo jako metody klasy, a Bar później doda metodę instancji o tej samej nazwie (lub odwrotnie), twój kod nie będzie się już kompilował.

Możesz konsekwentnie używać lambdas

Możesz zawinąć dowolną funkcję w lambda - dzięki czemu możesz używać tej samej składni wszędzie. Składnia odwołania do metody nie działa na metodach, które pobierają lub zwracają prymitywne tablice, rzucają sprawdzone wyjątki lub używają tej samej nazwy metody jako instancji i metody statycznej (ponieważ składnia odwołania do metody jest niejednoznaczna co do tego, która metoda zostanie wywołana) . Nie działają, gdy przeciążono metody taką samą liczbą argumentów, ale i tak nie należy tego robić (patrz punkt 41 Josha Blocha), więc nie możemy tego powstrzymać przed odwołaniami do metod.

Wniosek

Jeśli nie ma za to obniżki wydajności, kusi mnie, aby wyłączyć ostrzeżenie w moim IDE i konsekwentnie korzystać ze składni lambda bez rozsypywania okazjonalnych odwołań do metod w moim kodzie.

PS

Ani tu, ani tam, ale w moich snach odwołania do metod obiektowych wyglądają bardziej tak i stosują metodę invoke-dynamic przeciwko metodzie bezpośrednio na obiekcie bez opakowania lambda:

_.foo()
GlenPeterson
źródło
2
To nie odpowiada na twoje pytanie, ale istnieją inne powody, dla których warto używać zamknięć. Moim zdaniem wiele z nich znacznie przewyższa niewielki koszt dodatkowego wywołania funkcji.
9
Myślę, że martwisz się przedwcześnie wydajnością. Moim zdaniem wydajność powinna być kwestią drugorzędną przy stosowaniu odwołania do metody; wszystko polega na wyrażeniu intencji. Jeśli chcesz gdzieś przekazać funkcję, dlaczego po prostu nie przekazać funkcji zamiast jakiejś innej funkcji, która ją deleguje? Wydaje mi się to dziwne; ponieważ nie do końca kupujesz, że funkcje są pierwszorzędne, potrzebują anonimowego opiekuna, aby je przenosić. Ale aby odpowiedzieć bardziej szczegółowo na twoje pytanie, nie zdziwiłbym się, gdyby odwołanie do metody można zaimplementować jako wywołanie statyczne.
Doval
7
.map(_.acceptValue()): Jeśli się nie mylę, wygląda to bardzo podobnie do składni Scali. Może po prostu próbujesz użyć niewłaściwego języka.
Giorgio
2
„Składnia odwołania do metody nie działa na metodach, które zwracają wartość void” - jak to możliwe? Ja przechodząc System.out::printlndo forEach()cały czas ...?
Lukas Eder
3
„Składnia odwołania do metody nie działa na metodach, które pobierają lub zwracają prymitywne tablice, rzucają sprawdzone wyjątki…” To nie jest poprawne. W takich przypadkach można używać odwołań do metod, a także wyrażeń lambda, o ile dany interfejs funkcjonalny na to pozwala.
Stuart Marks

Odpowiedzi:

12

W wielu scenariuszach myślę, że lambda i odwołanie do metody są równoważne. Ale lambda zawinie cel wywołania według deklarującego typu interfejsu.

Na przykład

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

Zobaczysz, jak konsola generuje plik stacktrace.

W lambda()metodzie wywoływanej target()jest metoda lambda$lambda$0(InvokeTest.java:20), która ma identyfikowalne informacje o linii. Oczywiście, to jest lambda, którą piszesz, kompilator generuje dla ciebie anonimową metodę. A następnie obiekt wywołujący metodę lambda jest czymś podobnym InvokeTest$$Lambda$2/1617791695.run(Unknown Source), to jest invokedynamicwywołanie w JVM, oznacza to, że wywołanie jest powiązane z wygenerowaną metodą .

W methodReference(), wywołanie metody target()jest bezpośrednio InvokeTest$$Lambda$1/758529971.run(Unknown Source), oznacza to, że wywołanie jest bezpośrednio powiązane z InvokeTest::targetmetodą .

Wniosek

Przede wszystkim, w porównaniu z referencją do metody, użycie wyrażenia lambda spowoduje tylko jedno wywołanie metody do metody generowania z lambda.

JasonMing
źródło
1
Poniższa odpowiedź na temat metafabryki jest nieco lepsza. Dodałem kilka przydatnych komentarzy, które są z nim związane (mianowicie, w jaki sposób implementacje takie jak Oracle / OpenJDK używają ASM do generowania obiektów klasy opakowania potrzebnych do tworzenia instancji obsługi lambda / metody.
Ajax
29

Chodzi o metafactory

Po pierwsze, większość odniesień do metod nie wymaga rozpracowywania przez metafabrykację lambda, są one po prostu stosowane jako metoda referencyjna. W sekcji „Lambda body sugaring” artykułu o tłumaczeniu wyrażeń lambda („TLE”):

Wszystkie rzeczy są równe, metody prywatne są lepsze niż nieprywatne, metody statyczne są lepsze niż metody instancji, najlepiej, jeśli ciała lambda są zaprojektowane w najbardziej wewnętrznej klasie, w której pojawia się wyrażenie lambda, podpisy powinny pasować do podpisu ciała lambda, dodatkowo argumenty powinny być poprzedzone na początku listy argumentów dla przechwyconych wartości i w ogóle nie zaprzeczałyby odwołaniom do metod. Istnieją jednak wyjątki, w których możemy odejść od tej podstawowej strategii.

Jest to dodatkowo podkreślone w dalszej części „The Lambda Metafactory”:

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

implArgument wskazuje metodę lambda, albo odcukrzona ciało lambda lub metody o nazwie w odniesieniu metody.

Odwołania do Integer::summetody static ( ) lub niezwiązanej z instancją ( Integer::intValue) są „najprostsze” lub najbardziej „wygodne”, w tym sensie, że można je optymalnie obsłużyć za pomocą wariantu „szybkiej ścieżki” bez rozłączania . Ta przewaga jest pomocna w „Warunkach metafabryki” TLE:

Eliminując argumenty tam, gdzie nie są potrzebne, pliki klas stają się mniejsze. A opcja szybkiej ścieżki obniża poprzeczkę dla maszyny wirtualnej do zindywidualizowania operacji konwersji lambda, umożliwiając jej traktowanie jako operacji „boksowania” i ułatwianie optymalizacji rozpakowywania.

Oczywiście referencja do metody przechwytywania instancji ( obj::myMethod) musi zawierać ograniczoną instancję jako argument do uchwytu metody w celu wywołania, co może oznaczać potrzebę odłączenia przy użyciu metod „pomostowych”.

Wniosek

Nie jestem do końca pewien, na czym polega owijanie lambda, o czym sugerujesz, ale nawet jeśli ostateczny wynik użycia zdefiniowanych przez ciebie lambd lub referencji metod jest taki sam, sposób , w jaki został osiągnięty, wydaje się zupełnie inny, i może być inaczej w przyszłości, jeśli teraz tak nie jest. Dlatego przypuszczam, że bardziej prawdopodobne jest, że odniesienia do metod mogą być obsługiwane przez metafactory w bardziej optymalny sposób.

hjk
źródło
1
Jest to najbardziej odpowiednia odpowiedź, ponieważ w rzeczywistości dotyczy wewnętrznej maszyny potrzebnej do stworzenia lambda. Jako ktoś, kto faktycznie debugował proces tworzenia lambda (aby dowiedzieć się, jak wygenerować stabilne identyfikatory dla danego odwołania lambda / metody, aby można je było usunąć z map), zaskoczyło mnie, jak wiele pracy włożono w stworzenie wystąpienie lambda. Oracle / OpenJDK używa ASM do dynamicznego generowania klas, które w razie potrzeby zamykają dowolną zmienną, do której odwołuje się twoja lambda (lub kwalifikator instancji odwołania do metody) ...
Ajax
1
... Oprócz zamykania zmiennych, lambdas w szczególności tworzą również metodę statyczną, która jest wstrzykiwana do klasy deklarującej, dzięki czemu utworzona lambda ma coś do wywołania w celu odwzorowania funkcji, które chcesz wywołać. Odwołanie do metody z kwalifikatorem instancji zajmie tę instancję jako parametr tej metody; Nie sprawdziłem, czy odwołanie do metody :: :: w klasie prywatnej pomija to zamknięcie, ale przetestowałem, że metody statyczne faktycznie pomijają metodę pośrednią (i po prostu tworzą obiekt zgodny z celem wywołania w strona połączenia).
Ajax
1
Prawdopodobnie metoda prywatna również nie będzie wymagała wirtualnej wysyłki, podczas gdy wszystko bardziej publiczne będzie musiało zamknąć instancję (za pomocą wygenerowanej metody statycznej), aby mogła wywoływać rzeczywistą instancję w celu upewnienia się, że nadpisuje. TL; DR: Odwołania do metod zamykają się przy mniejszej liczbie argumentów, więc mniej wyciekują i wymagają mniej dynamicznego generowania kodu.
Ajax
3
Ostatni spamerski komentarz ... Dynamiczne generowanie klas jest dość ciężkie. Prawdopodobnie jest to lepsze niż ładowanie anonimowej klasy do modułu ładującego klasy (ponieważ najcięższe części są wykonywane tylko raz), ale byłbyś naprawdę zaskoczony, ile pracy zajmuje stworzenie instancji lambda. Interesujące byłoby zobaczenie prawdziwych punktów odniesienia dla lambdów w porównaniu do referencji metod w porównaniu do klas anonimowych. Zauważ, że dwa lambda lub odwołania do metod, które odnoszą się do tych samych rzeczy, ale w różnych miejscach tworzą różne klasy w czasie wykonywania (nawet w tej samej metodzie, zaraz po sobie).
Ajax
0

Istnieje jedna dość poważna konsekwencja przy użyciu wyrażeń lambda, które mogą wpływać na wydajność.

Kiedy deklarujesz wyrażenie lambda, tworzysz zamknięcie w zakresie lokalnym.

Co to oznacza i jak wpływa na wydajność?

Cieszę się, że zapytałeś. Oznacza to, że każde z tych małych wyrażeń lambda jest małą anonimową klasą wewnętrzną, co oznacza, że ​​zawiera odniesienie do wszystkich zmiennych, które są w tym samym zakresie wyrażenia lambda.

Oznacza to także thisodwołanie do instancji obiektu i wszystkich jego pól. W zależności od czasu rzeczywistego wywołania lambda może to oznaczać dość znaczny wyciek zasobów, ponieważ śmieciarz nie jest w stanie puścić tych odniesień, dopóki obiekt trzymający lambda wciąż żyje ...

Roland Tepp
źródło
To samo dotyczy niestatycznych odniesień do metod.
Ajax
12
Jagnięta Java nie są ściśle zamknięciami. Nie są też anonimowymi klasami wewnętrznymi. NIE zawierają one odniesienia do wszystkich zmiennych w zakresie, w którym zostały zadeklarowane. Niosą TYLKO odniesienie do zmiennych, do których faktycznie się odnoszą. Dotyczy to również thisobiektu, do którego można się odwoływać. Dlatego lambda nie przecieka zasobów, po prostu przez ich pole manewru; trzyma tylko przedmioty, których potrzebuje. Z drugiej strony anonimowa klasa wewnętrzna może przeciekać zasoby, ale nie zawsze to robi. Zobacz ten kod jako przykład: a.blmq.us/2mmrL6v
squid314
Ta odpowiedź jest po prostu błędna
Stefan Reich
Dziękuję @StefanReich, to było bardzo pomocne!
Roland Tepp