Úvod
Tento výukový program je průvodce na různé funkční rozhraní v Java 8, stejně jako jejich obecné případy použití a použití ve standardní JDK knihovny.
Další čtení:
Iterable, aby Proud v Javě
jak používat logiku if / else v proudech Java 8
Lambdas v Javě 8
Java 8 přinesla silné nové syntaktické zlepšení ve formě lambda výrazů. Lambda je anonymní funkce, kterou můžeme zvládnout jako občan prvotřídního jazyka. Můžeme ji například předat nebo vrátit z metody.
před Javou 8 bychom obvykle vytvořili třídu pro každý případ, kdy jsme potřebovali zapouzdřit jeden kus funkčnosti. To znamenalo spoustu zbytečného kódu boilerplate k definování něčeho, co sloužilo jako primitivní reprezentace funkcí.
článku „Lambda Výrazy a Funkční Rozhraní: Tipy a Osvědčené Postupy,“ popisuje podrobněji funkční rozhraní a osvědčené postupy práce s lambdy. Tato příručka se zaměřuje na některá konkrétní funkční rozhraní, která jsou přítomna v Javě.util.funkční balíček.
funkční rozhraní
doporučuje se, aby všechna funkční rozhraní měla informativní anotaci @FunctionalInterface. To jasně komunikuje účel rozhraní a také umožňuje kompilátoru generovat chybu, pokud anotované rozhraní nesplňuje podmínky.
jakékoli rozhraní se SAM (Single Abstract Method) je funkční rozhraní a jeho implementace může být považována za lambda výrazy.
Všimněte si, že výchozí metody Java 8 nejsou abstraktní a nepočítají se; funkční rozhraní může mít stále více výchozích metod. Můžeme to pozorovat při pohledu na dokumentaci funkce.
Funkce
nejvíce jednoduchý a obecný případ lambda je funkční rozhraní s metodou, která obdrží jednu hodnotu a vrátí další. Tato funkce jednoho argumentu je zastoupena Funkce rozhraní, které je parametrické podle typů její argument a návratová hodnota:
public interface Function<T, R> { … }
Jedno použití Funkce typu v standardní knihovna je Mapa.metoda computeIfAbsent. Tato metoda vrací hodnotu z mapy podle klíče, ale vypočítá hodnotu, pokud klíč již není v mapě přítomen. Pro výpočet hodnoty, používá prošel implementace Funkce:
Map<String, Integer> nameMap = new HashMap<>();Integer value = nameMap.computeIfAbsent("John", s -> s.length());
V tomto případě budeme počítat hodnotu použitím funkce na klíč, dát dovnitř mapě, a také se vrátil z volání metody. Lambda můžeme nahradit referencí metody, která odpovídá předaným a vráceným typům hodnot.
nezapomeňte, že objekt, na který metodu vyvoláme, je ve skutečnosti implicitním prvním argumentem metody. To nám umožňuje obsadit odkaz na délku metody instance na funkční rozhraní:
Integer value = nameMap.computeIfAbsent("John", String::length);
Funkce rozhraní má také výchozí skládat metoda, která nám umožňuje spojit několik funkcí do jednoho a provádět je postupně:
Function<Integer, String> intToString = Object::toString;Function<String, String> quote = s -> "'" + s + "'";Function<Integer, String> quoteIntToString = quote.compose(intToString);assertEquals("'5'", quoteIntToString.apply(5));
quoteIntToString funkce je kombinací citovat funkce aplikované na výsledek intToString funkce.
Primitivní Funkci Specializace
Vzhledem k tomu, primitivní typ nemůže být obecný typ argumentu, tam jsou verze Funkce rozhraní pro nejvíce používané primitivní typy double, int, dlouhé, a jejich kombinace v argumenty a návratové typy:
- 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: s oběma argumenty a návratový typ definován jako primitivní typy, podle jejich jména
Jako příklad, tam je žádné out-of-the-box funkční rozhraní pro funkci, která trvá krátkou a vrací byte, ale nic nám brání psát naše vlastní:
@FunctionalInterfacepublic interface ShortToByteFunction { byte applyAsByte(short s);}
Teď můžeme napsat metodu, která transformuje pole zkrat na pole bajtů pomocí pravidla definována 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;}
Zde je, jak bychom ji mohli použít k transformaci pole šortky na pole bajtů vynásobí 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);
Dvě Funkce Arity Specializace
definovat lambdy se dvěma argumenty, musíme použít další rozhraní, které obsahují „Bi“ klíčové slovo v jejich názvech: BiFunction, ToDoubleBiFunction, ToIntBiFunction, a ToLongBiFunction.
BiFunction má generované Argumenty i návratový typ, zatímco ToDoubleBiFunction a další nám umožňují vrátit primitivní hodnotu.
jedním z typických příkladů použití tohoto rozhraní ve standardním API je mapa.metoda replaceAll, která umožňuje nahradit všechny hodnoty v mapě nějakou vypočítanou hodnotou.
použijme implementaci Bifunkce, která obdrží klíč a starou hodnotu pro výpočet nové hodnoty platu a jeho vrácení.
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);
dodavatelé
dodavatelské funkční rozhraní je další specializací funkcí, která nebere žádné argumenty. Obvykle jej používáme pro líné generování hodnot. Například, pojďme definovat funkci, která čtverce dvojitou hodnotu. Neobdrží hodnotu samotnou, ale dodavatele této hodnoty:
public double squareLazy(Supplier<Double> lazyValue) { return Math.pow(lazyValue.get(), 2);}
To nám umožňuje líně generovat argument pro vyvolání této funkce pomocí Dodavatele realizace. To může být užitečné, pokud generování argumentu trvá značné množství času. Budeme simulovat, že pomocí metody sleepUninterruptibly Guava:
Supplier<Double> lazyValue = () -> { Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS); return 9d;};Double valueSquared = squareLazy(lazyValue);
dalším případem použití dodavatele je definování logiky pro generování sekvencí. Abychom to demonstrovali, použijeme Statický proud.generovat metodu pro vytvoření proudu Fibonacciho čísel:
int fibs = {0, 1};Stream<Integer> fibonacci = Stream.generate(() -> { int result = fibs; int fib3 = fibs + fibs; fibs = fibs; fibs = fib3; return result;});
funkce, kterou předáme proudu.metoda generování implementuje funkční rozhraní dodavatele. Všimněte si, že být užitečný jako generátor, dodavatel obvykle potřebuje nějaký vnější stav. V tomto případě jeho stav zahrnuje poslední dvě Fibonacciho sekvenční čísla.
pro implementaci tohoto stavu používáme pole namísto několika proměnných, protože všechny externí proměnné použité uvnitř lambda musí být efektivně konečné.
Další specializace Dodavatele funkční rozhraní patří BooleanSupplier, DoubleSupplier, LongSupplier a IntSupplier, jejichž návratové typy jsou odpovídající primitiv.
spotřebitelé
Na rozdíl od dodavatele spotřebitel akceptuje zobecněný argument a nic nevrací. Je to funkce, která představuje vedlejší účinky.
například pozdravme všechny v seznamu jmen vytištěním pozdravu v konzole. Lambda přešla na seznam.forEach metoda implementuje Spotřebitele funkční rozhraní:
List<String> names = Arrays.asList("John", "Freddy", "Samuel");names.forEach(name -> System.out.println("Hello, " + name));
Existují také specializované verze Spotřebitel — DoubleConsumer, IntConsumer a LongConsumer—, které dostávají primitivní hodnoty jako argumenty. Zajímavější je rozhraní BiConsumer. Jeden z jeho případů užití je iterace přes položky 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"));
Další sadu specializovaných BiConsumer verze se skládá z ObjDoubleConsumer, ObjIntConsumer, a ObjLongConsumer, které přijímají dva argumenty; jedním z argumentů je generified, a druhá je primitivní typ.
predikáty
v matematické logice je predikát funkcí, která přijímá hodnotu a vrací booleovskou hodnotu.
predikátové funkční rozhraní je specializací funkce, která přijímá zobecněnou hodnotu a vrací boolean. Typický případ použití Predikátu lambda je filtrovat sbírku hodnoty:
List<String> names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");List<String> namesWithA = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList());
V kódu výše, jsme filtrovat seznam pomocí Stream API a ponechat pouze názvy, které začínají písmenem „A“. Implementace predikátu zapouzdřuje logiku filtrování.
stejně jako ve všech předchozích příkladech existují verze IntPredicate, DoublePredicate a LongPredicate této funkce, které přijímají primitivní hodnoty.
Operátoři
Operátor rozhraní jsou zvláštní případy funkce, která zobrazí a vrátí stejnou hodnotu typu. Rozhraní UnaryOperator obdrží jediný argument. Jeden z jeho použití případy, ve Sbírkách API je nahradit všechny hodnoty v seznamu s některými vypočtených hodnot stejného typu:
List<String> names = Arrays.asList("bob", "josh", "megan");names.replaceAll(name -> name.toUpperCase());
Seznam.funkce replaceAll vrací neplatné, protože nahrazuje hodnoty na místě. Aby odpovídala účelu, musí lambda použitá k transformaci hodnot seznamu vrátit stejný typ výsledku, jaký obdrží. Proto je zde užitečný UnaryOperator.
samozřejmě místo názvu – > name.toUpperCase (), můžeme jednoduše použít odkaz na metodu:
names.replaceAll(String::toUpperCase);
jedním z nejzajímavějších případů použití Binaryoperátoru je operace redukce. Předpokládejme, že chceme agregovat sbírku celých čísel v součtu všech hodnot. S Stream API, můžeme to udělat pomocí kolektoru, ale více obecný způsob, jak to udělat, by bylo použít snížení metoda:
List<Integer> values = Arrays.asList(3, 5, 8, 9, 12);int sum = values.stream() .reduce(0, (i1, i2) -> i1 + i2);
snížení metoda obdrží počáteční akumulátoru hodnotu a BinaryOperator funkce. Argumenty této funkce jsou dvojice hodnot stejného typu; samotná funkce také obsahuje logiku pro jejich spojení do jedné hodnoty stejného typu. Prošel funkcí musí být asociativní, což znamená, že pořadí hodnota agregace nezáleží, tj. tato podmínka by měla držet:
op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)
asociativní vlastnost BinaryOperator operátor funkce nám umožňuje snadno paralelizovat proces redukce.
samozřejmě, tam jsou také obory UnaryOperator a BinaryOperator, které mohou být použity s primitivní hodnoty, a to DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator, a LongBinaryOperator.
starší funkční rozhraní
ne všechna funkční rozhraní se objevila v Javě 8. Mnoho rozhraní z předchozích verzí Java splňovat omezení FunctionalInterface, a můžeme je použít jako lambdy. Mezi významné příklady patří Runnable a Callable rozhraní, které se používají v API souběžnosti. V Javě 8 jsou tato rozhraní také označena anotací @FunctionalInterface. To nám umožňuje výrazně zjednodušit kód souběžnosti:
Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));thread.start();