Java 8 lambdas, Function.identity () lub t-> t

240

Mam pytanie dotyczące zastosowania Function.identity()metody.

Wyobraź sobie następujący kod:

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.

Czy jest jakiś powód, dla którego powinieneś używać Function.identity()zamiast str->str(lub odwrotnie). Myślę, że druga opcja jest bardziej czytelna (oczywiście kwestia gustu). Ale czy istnieje jakiś „prawdziwy” powód, dla którego należy być preferowanym?

Przemysław Głębocki
źródło
6
Ostatecznie nie, to nie robi różnicy.
fge
50
Albo jedno jest w porządku. Idź z tym, co uważasz za bardziej czytelne. (Nie martw się, bądź szczęśliwy.)
Brian Goetz,
3
Wolałbym t -> tpo prostu dlatego, że jest bardziej zwięzły.
David Conrad,
3
Pytanie nieco niezwiązane, ale czy ktokolwiek wie, dlaczego projektanci języka sprawiają, że identity () zwraca instancję Function zamiast parametru typu T i zwraca go, aby metoda mogła być używana z referencjami metod?
Kirill Rakhman
Twierdziłbym, że znajomość słowa „tożsamość” jest pożyteczna, ponieważ ma on istotne znaczenie w innych obszarach programowania funkcjonalnego.
orbfish

Odpowiedzi:

312

Począwszy od bieżącej implementacji JRE, Function.identity()zawsze zwróci tę samą instancję, a każde wystąpienie identifier -> identifierspowoduje nie tylko utworzenie własnej instancji, ale nawet odrębną klasę implementacji. Aby uzyskać więcej informacji, zobacz tutaj .

Powodem jest to, że kompilator generuje syntetyczną metodę z trywialnym ciałem tego wyrażenia lambda (w przypadku x->xrównoważnika return identifier;) i informuje środowisko wykonawcze, aby utworzyło implementację funkcjonalnego interfejsu wywołującego tę metodę. Dlatego środowisko wykonawcze widzi tylko różne metody docelowe, a bieżąca implementacja nie analizuje metod w celu ustalenia, czy niektóre metody są równoważne.

Więc użycie Function.identity()zamiast x -> xmoże zaoszczędzić trochę pamięci, ale nie powinno to decydować o twojej decyzji, jeśli naprawdę uważasz, że x -> xjest to bardziej czytelne niż Function.identity().

Możesz również wziąć pod uwagę, że podczas kompilacji z włączonymi informacjami debugowania metoda syntetyczna będzie miała atrybut debugowania linii wskazujący na linie kodu źródłowego zawierające wyrażenie lambda, dlatego masz szansę znaleźć źródło konkretnej Functioninstancji podczas debugowania . Natomiast podczas napotkania instancji zwróconej Function.identity()podczas debugowania operacji nie wiadomo, kto wywołał tę metodę i przekazał instancję do operacji.

Holger
źródło
5
Niezła odpowiedź. Mam wątpliwości dotyczące debugowania. Jak to może być przydatne? Bardzo mało prawdopodobne jest uzyskanie śledzenia stosu wyjątku dotyczącego x -> xramki. Czy sugerujesz ustawić punkt przerwania na tej lambda? Zwykle nie jest tak łatwo umieścić punkt przerwania w lambda o jednym wyrażeniu (przynajmniej w Eclipse) ...
Tagir Valeev
14
@Tagir Valeev: możesz debugować kod, który odbiera dowolną funkcję, i przejść do metody zastosowania tej funkcji. Wtedy możesz skończyć na kodzie źródłowym wyrażenia lambda. W przypadku jawnego wyrażenia lambda będziesz wiedział, skąd pochodzi funkcja i będziesz miał szansę rozpoznać, w którym miejscu podjęta została decyzja o przejściu funkcji tożsamości. Podczas korzystania z Function.identity()tych informacji zostanie utracone. Następnie łańcuch połączeń może pomóc w prostych przypadkach, ale pomyśl o np. Ocenie wielowątkowej, w której oryginalny inicjator nie znajduje się w stosie…
Holger
2
Ciekawe w tym kontekście: blog.codefx.org/java/instances-non-capturing-lambdas
Wim Deblauwe
13
@Wim Deblauwe: Interesujące, ale zawsze widziałbym to na odwrót: jeśli metoda fabryczna nie stwierdza wprost w swojej dokumentacji, że zwróci nową instancję przy każdym wywołaniu, nie możesz zakładać, że tak będzie. Więc nie powinno być zaskoczeniem, jeśli tak nie jest. W końcu jest to jeden z głównych powodów, dla których warto stosować metody fabryczne new. new Foo(…)gwarantuje utworzenie nowej instancji dokładnie tego samego typu Foo, podczas gdy Foo.getInstance(…)może zwrócić istniejącą instancję (podtyp) Foo
Holger
93

W twoim przykładzie nie ma dużej różnicy między, str -> stra Function.identity()wewnętrznie jest to po prostu t->t.

Ale czasami nie możemy użyć, Function.identityponieważ nie możemy użyć Function. Spójrz tutaj:

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

to dobrze się skompiluje

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

ale jeśli spróbujesz skompilować

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

pojawi się błąd kompilacji, ponieważ mapToIntoczekuje ToIntFunction, że nie jest to związane z Function. Również ToIntFunctionnie ma identity()metody.

Pshemo
źródło
3
Zobacz stackoverflow.com/q/38034982/14731, aby zobaczyć inny przykład, w którym zastąpienie i -> igo Function.identity()spowoduje błąd kompilatora.
Gili
19
Wolę mapToInt(Integer::intValue).
shmosel
4
@shmosel jest w porządku, ale warto wspomnieć, że oba rozwiązania będą działać podobnie, ponieważ mapToInt(i -> i)jest to uproszczenie mapToInt( (Integer i) -> i.intValue()). Użyj dowolnej wersji, która Twoim zdaniem jest bardziej przejrzysta, dla mnie mapToInt(i -> i)lepiej pokazuje intencje tego kodu.
Pshemo
1
Myślę, że korzystanie z referencji metod może przynieść korzyści w zakresie wydajności, ale w większości są to tylko osobiste preferencje. Uważam to za bardziej opisowe, ponieważ i -> iwygląda jak funkcja tożsamości, czego nie ma w tym przypadku.
shmosel
@shmosel Nie mogę powiedzieć wiele o różnicy w wydajności, więc możesz mieć rację. Ale jeśli wydajność nie jest problemem, pozostanę przy tym, i -> iponieważ moim celem jest zmapowanie liczby całkowitej na int (co mapToIntcałkiem ładnie sugeruje), aby nie wywoływać intValue()metody jawnie . Jak to mapowanie zostanie osiągnięte, tak naprawdę nie jest tak ważne. Więc po prostu zgódźmy się nie zgodzić, ale dzięki za wskazanie możliwej różnicy w wydajności, będę musiał kiedyś przyjrzeć się temu bliżej.
Pshemo
44

Ze źródła JDK :

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

Więc nie, pod warunkiem, że jest poprawny pod względem składniowym.

JasonN
źródło
8
Zastanawiam się, czy to unieważnia powyższą odpowiedź dotyczącą lambda tworzącego obiekt - czy jest to konkretna implementacja.
orbfish
28
@orbfish: to idealnie w linii. Każde wystąpienie t->tw kodzie źródłowym może stworzyć jeden obiekt, a implementacja Function.identity()jest jednym wystąpieniem. Tak więc wszystkie strony wywołujące identity()będą współużytkować ten jeden obiekt, podczas gdy wszystkie witryny jawnie korzystające z wyrażenia lambda t->tutworzą własny obiekt. Metoda Function.identity()ta nie jest w żaden sposób wyjątkowa, ilekroć utworzysz metodę fabryczną, w której zawarte jest powszechnie używane wyrażenie lambda i wywołasz tę metodę zamiast powtarzania wyrażenia lambda, możesz zaoszczędzić trochę pamięci, biorąc pod uwagę bieżącą implementację .
Holger
Zgaduję, że dzieje się tak, ponieważ kompilator optymalizuje tworzenie nowego t->tobiektu za każdym razem, gdy wywoływana jest metoda, i przetwarza ten sam obiekt za każdym razem, gdy wywoływana jest metoda?
Daniel Gray
1
@DanielGray decyzja jest podejmowana w czasie wykonywania. Kompilator wstawia invokedynamicinstrukcję, która zostaje połączona przy pierwszym uruchomieniu przez wykonanie tak zwanej metody bootstrap, która w przypadku wyrażeń lambda znajduje się w pliku LambdaMetafactory. Ta implementacja decyduje o zwróceniu uchwytu do konstruktora, metody fabrycznej lub kodu zawsze zwracającego ten sam obiekt. Może również zdecydować o zwróceniu łącza do już istniejącego uchwytu (co obecnie się nie dzieje).
Holger
@Holger Czy jesteś pewien, że to wezwanie do tożsamości nie zostanie wprowadzone, a następnie potencjalnie zmonorfizowane (i ponownie wprowadzone)?
JasonN