- wprowadzenie
- Czytaj dalej:
- Iterable to Stream w Javie
- jak używać logiki if/else w strumieniach Java 8
- Lambda w Javie 8
- Interfejsy funkcjonalne
- funkcje
- specjalizacje funkcji prymitywnych
- specjalizacje funkcji dwuargumentowych
- dostawcy
- konsumenci
- predykaty
- operatory
- starsze Interfejsy funkcjonalne
- wniosek
wprowadzenie
Ten poradnik jest przewodnikiem po różnych funkcjonalnych interfejsach obecnych w Java 8, a także ich ogólnych przypadkach użycia i użycia w standardowej bibliotece JDK.
Czytaj dalej:
Iterable to Stream w Javie
jak używać logiki if/else w strumieniach Java 8
Lambda w Javie 8
Java 8 przyniosła potężne nowe ulepszenie składniowe w postaci wyrażeń lambda. Lambda jest anonimową funkcją, którą możemy obsłużyć jako obywatel języka pierwszej klasy. Na przykład, możemy go przekazać lub zwrócić z metody.
przed Java 8, Zwykle tworzyliśmy klasę dla każdego przypadku, w którym potrzebowaliśmy zamknąć jeden element funkcjonalności. To implikowało wiele niepotrzebnego kodu boilerplate do zdefiniowania czegoś, co służyło jako prymitywna reprezentacja funkcji.
artykuł „Lambda Expressions and Functional Interfaces: Tips and Best Practices” opisuje bardziej szczegółowo funkcjonalne interfejsy i najlepsze praktyki pracy z lambda. Ten przewodnik koncentruje się na niektórych szczególnych interfejsach funkcjonalnych, które są obecne w Javie.util.pakiet funkcji.
Interfejsy funkcjonalne
zaleca się, aby wszystkie interfejsy funkcjonalne miały informacyjną adnotację @FunctionalInterface. To wyraźnie komunikuje cel interfejsu, a także pozwala kompilatorowi generować błąd, jeśli interfejs z adnotacjami nie spełnia warunków.
każdy interfejs z SAM(Single Abstract Method) jest interfejsem funkcjonalnym, a jego implementacja może być traktowana jako wyrażenia lambda.
zauważ, że domyślne metody Javy 8 nie są abstrakcyjne i nie liczą się; funkcjonalny interfejs może nadal mieć wiele domyślnych metod. Możemy to zaobserwować, przeglądając dokumentację funkcji.
funkcje
najprostszym i najbardziej ogólnym przypadkiem lambda jest funkcjonalny interfejs z metodą, która odbiera jedną wartość i zwraca drugą. Ta funkcja pojedynczego argumentu jest reprezentowana przez interfejs funkcji, który jest parametryzowany przez typy jej argumentu i wartość zwracaną:
public interface Function<T, R> { … }
jednym z zastosowań typu funkcji w bibliotece standardowej jest Map.metoda computeIfAbsent. Ta metoda zwraca wartość z mapy za pomocą klucza, ale oblicza wartość, jeśli klucz nie jest jeszcze obecny na mapie. Aby obliczyć wartość, używa przekazanej implementacji funkcji:
Map<String, Integer> nameMap = new HashMap<>();Integer value = nameMap.computeIfAbsent("John", s -> s.length());
w tym przypadku obliczymy wartość, stosując funkcję do klucza, umieszczając ją wewnątrz mapy, a także zwracając ją z wywołania metody. Możemy zastąpić lambda referencją metody, która pasuje do przekazywanych i zwracanych typów wartości.
pamiętaj, że obiekt, na którym wywołujemy metodę, jest w rzeczywistości pierwszym domyślnym argumentem metody. Pozwala nam to na rzucenie odwołania długości metody instancji do interfejsu funkcji:
Integer value = nameMap.computeIfAbsent("John", String::length);
interfejs funkcji posiada również domyślną metodę compose, która pozwala nam połączyć kilka funkcji w jedną i wykonać je kolejno:
Function<Integer, String> intToString = Object::toString;Function<String, String> quote = s -> "'" + s + "'";Function<Integer, String> quoteIntToString = quote.compose(intToString);assertEquals("'5'", quoteIntToString.apply(5));
funkcja quoteIntToString jest kombinacją funkcji quote zastosowanej do wyniku funkcji intToString.
specjalizacje funkcji prymitywnych
ponieważ Typ prymitywny nie może być argumentem typu ogólnego, istnieją wersje interfejsu funkcji dla najczęściej używanych typów prymitywnych double, int, long i ich kombinacji w typach argumentów i zwracanych:
- IntFunction, LongFunction, DoubleFunction: arguments are of specified type, return type is parameterized
- ToIntFunction, ToLongFunction, ToDoubleFunction: return type is of specified type, arguments are parameterized
- DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToIntFunction, LongToDoubleFunction: mając zarówno argument, jak i typ zwracany zdefiniowane jako typy prymitywne, określone przez ich nazwy
jako przykład, nie ma gotowego interfejsu funkcjonalnego dla funkcji, która bierze skrót i zwraca bajt, ale nic nie powstrzymuje nas przed napisaniem naszego własnego:
@FunctionalInterfacepublic interface ShortToByteFunction { byte applyAsByte(short s);}
teraz możemy napisać metodę, która przekształca tablicę skrótu do tablicy bajtów za pomocą reguły zdefiniowanej przez ShortToByteFunction:
public byte transformArray(short array, ShortToByteFunction function) { byte transformedArray = new byte; for (int i = 0; i < array.length; i++) { transformedArray = function.applyAsByte(array); } return transformedArray;}
oto jak możemy go użyć do przekształcenia tablicy krótkich w tablicę bajtów pomnożoną przez 2:
short array = {(short) 1, (short) 2, (short) 3};byte transformedArray = transformArray(array, s -> (byte) (s * 2));byte expectedArray = {(byte) 2, (byte) 4, (byte) 6};assertArrayEquals(expectedArray, transformedArray);
specjalizacje funkcji dwuargumentowych
aby zdefiniować lambda z dwoma argumentami, musimy użyć dodatkowych interfejsów, które zawierają słowo kluczowe „Bi” w nazwach: BiFunction, ToDoubleBiFunction, Tointbifunction i ToLongBiFunction.
BiFunction ma zarówno argumenty, jak i typ zwracany uogólniony, podczas gdy ToDoubleBiFunction i inne pozwalają nam zwrócić pierwotną wartość.
jednym z typowych przykładów użycia tego interfejsu w standardowym API jest mapa.metoda replaceAll, która pozwala na zamianę wszystkich wartości na mapie na jakąś obliczoną wartość.
użyjmy implementacji dwufunkcyjnej, która otrzymuje klucz i starą wartość, aby obliczyć nową wartość wynagrodzenia i ją zwrócić.
Map<String, Integer> salaries = new HashMap<>();salaries.put("John", 40000);salaries.put("Freddy", 30000);salaries.put("Samuel", 50000);salaries.replaceAll((name, oldValue) -> name.equals("Freddy") ? oldValue : oldValue + 10000);
dostawcy
interfejs funkcjonalny dostawcy to kolejna specjalizacja funkcji, która nie przyjmuje żadnych argumentów. Zazwyczaj używamy go do leniwego generowania wartości. Na przykład, zdefiniujmy funkcję, która kwadratuje podwójną wartość. Sam nie otrzyma wartości, ale dostawca tej wartości:
public double squareLazy(Supplier<Double> lazyValue) { return Math.pow(lazyValue.get(), 2);}
pozwala to na leniwe wygenerowanie argumentu wywołania tej funkcji przy użyciu implementacji dostawcy. Może to być użyteczne, jeśli generowanie argumentu zajmuje dużo czasu. Zasymulujemy, że za pomocą metody sleepUninterruptibly Guava:
Supplier<Double> lazyValue = () -> { Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS); return 9d;};Double valueSquared = squareLazy(lazyValue);
innym przypadkiem użycia dla Dostawcy jest zdefiniowanie logiki generowania sekwencji. Aby to zademonstrować, użyjmy strumienia statycznego.generowanie metody tworzenia strumienia liczb Fibonacciego:
int fibs = {0, 1};Stream<Integer> fibonacci = Stream.generate(() -> { int result = fibs; int fib3 = fibs + fibs; fibs = fibs; fibs = fib3; return result;});
funkcja, którą przekazujemy do strumienia.metoda generowania implementuje interfejs funkcjonalny dostawcy. Zauważ, że aby być użytecznym jako generator, dostawca zwykle potrzebuje jakiegoś zewnętrznego stanu. W tym przypadku jego stan składa się z dwóch ostatnich liczb ciągu Fibonacciego.
aby zaimplementować ten stan, używamy tablicy zamiast kilku zmiennych, ponieważ wszystkie zewnętrzne zmienne użyte wewnątrz lambda muszą być ostatecznie ostateczne.
Inne specjalizacje interfejsu funkcjonalnego dostawcy obejmują BooleanSupplier, DoubleSupplier,LongSupplier i IntSupplier, których typy zwracane są odpowiadającymi im pierwotnymi.
konsumenci
w przeciwieństwie do dostawcy konsument przyjmuje uogólniony argument i nic nie zwraca. Jest to funkcja, która reprezentuje skutki uboczne.
na przykład, powitajmy wszystkich na liście nazwisk, drukując powitanie w konsoli. Lambda przeszła na Listę.metoda forEach implementuje interfejs funkcjonalny konsumenta:
List<String> names = Arrays.asList("John", "Freddy", "Samuel");names.forEach(name -> System.out.println("Hello, " + name));
istnieją również wyspecjalizowane wersje konsumenta — DoubleConsumer, IntConsumer i LongConsumer — które otrzymują prymitywne wartości jako argumenty. Bardziej interesujący jest interfejs BiConsumer. Jednym z jego przypadków użycia jest iteracja poprzez wpisy mapy:
Map<String, Integer> ages = new HashMap<>();ages.put("John", 25);ages.put("Freddy", 24);ages.put("Samuel", 30);ages.forEach((name, age) -> System.out.println(name + " is " + age + " years old"));
inny zestaw wyspecjalizowanych wersji BiConsumer składa się z ObjDoubleConsumer, ObjIntConsumer i ObjLongConsumer, które otrzymują dwa argumenty; jeden z nich jest uogólniony, a drugi jest typu prymitywnego.
predykaty
w logice matematycznej predykat jest funkcją, która otrzymuje wartość i zwraca wartość logiczną.
interfejs funkcyjny predykatu jest specjalizacją funkcji, która otrzymuje uogólnioną wartość i zwraca wartość logiczną. Typowym przypadkiem użycia predykatu lambda jest filtrowanie zbioru wartości:
List<String> names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");List<String> namesWithA = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList());
w powyższym kodzie filtrujemy listę używając Stream API i zachowujemy tylko Nazwy zaczynające się na literę „a”. Implementacja predykatu zawiera logikę filtrowania.
podobnie jak we wszystkich poprzednich przykładach, istnieją wersje IntPredicate, DoublePredicate i LongPredicate tej funkcji, które otrzymują prymitywne wartości.
operatory
interfejsy operatora są szczególnymi przypadkami funkcji, która odbiera i zwraca ten sam typ wartości. Interfejs Uniaryoperator otrzymuje jeden argument. Jednym z jego przypadków użycia w API kolekcji jest zastąpienie wszystkich wartości na liście niektórymi obliczonymi wartościami tego samego typu:
List<String> names = Arrays.asList("bob", "josh", "megan");names.replaceAll(name -> name.toUpperCase());
lista.funkcja replaceAll zwraca void, ponieważ zastępuje wartości w miejscu. Aby spełnić ten cel, lambda użyta do przekształcenia wartości listy musi zwracać ten sam typ wyniku, jaki otrzymuje. Z tego powodu przydatny jest tutaj Uniaryoperator.
oczywiście zamiast nazwy -> nazwa.toUpperCase (), możemy po prostu użyć referencji metody:
names.replaceAll(String::toUpperCase);
jednym z najciekawszych przypadków użycia BinaryOperator jest operacja redukcji. Załóżmy, że chcemy zsumować zbiór liczb całkowitych w sumie wszystkich wartości. W Stream API moglibyśmy to zrobić używając kolektora, ale bardziej ogólnym sposobem byłoby użycie metody reduce:
List<Integer> values = Arrays.asList(3, 5, 8, 9, 12);int sum = values.stream() .reduce(0, (i1, i2) -> i1 + i2);
metoda reduce otrzymuje początkową wartość akumulatora i funkcję BinaryOperator. Argumenty tej funkcji są parą wartości tego samego typu; sama funkcja zawiera również logikę łączenia ich w jedną wartość tego samego typu. Przekazywana funkcja musi być asocjacyjna, co oznacza, że kolejność agregacji wartości nie ma znaczenia, tzn. powinien być spełniony następujący warunek:
op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)
asocjacyjna właściwość funkcji operatora BinaryOperator pozwala na łatwe równoległe dopasowanie procesu redukcji.
oczywiście istnieją również specjalizacje UnaryOperator i BinaryOperator, które mogą być używane z prymitywnymi wartościami, a mianowicie DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator i LongBinaryOperator.
starsze Interfejsy funkcjonalne
nie wszystkie interfejsy funkcjonalne pojawiły się w Javie 8. Wiele interfejsów z poprzednich wersji Javy jest zgodnych z ograniczeniami interfejsu funkcyjnego i możemy ich używać jako lambda. Wyróżniającymi się przykładami są uruchamialne i Wywoływalne interfejsy używane w interfejsach API współbieżności. W Javie 8 interfejsy te są również oznaczone adnotacją @ FunctionalInterface. Pozwala nam to znacznie uprościć kod współbieżności:
Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));thread.start();