O co jde?
-
Java byla navržena jako čistě objektový jazyk.
-
Lambda výrazy do tohoto jazyka přináší prvky funkcionálního programování.
-
Jak mohou tyto dva různé světy koexistovat?
-
Proč a jak je funkcionální syntaxe integrována do objektového API Javy?
Motivační příklad
Založeno na Java Tutorial |
-
Předpokládejme, že chceme vytvořit sociální síť. Administrátorům chceme umožnit provádění různých akcí jako například zasílání zpráv těm uživatelům sociální sítě, kteří splňují nějaká kritéria.
-
Předpokládejme, že uživatelé jsou reprezentování následující třídou:
public class Person {
public enum Sex {
MALE, FEMALE
}
private String name;
private LocalDate birthday;
private Sex gender;
private String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
Krok 1: Primitivní řešení
-
Dále předpokládejme, že jsou uživatelé sociální sítě uložení v seznamu
List<Person>
. -
Základní implementace vyhledávání lidí podle kritéria může vypadat následovně:
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
Otázky
- Q
-
Co když chceme přidat další operaci, např. výpis lidí mladších než nějaký věk?
- A
-
Musí se přidat nová metoda, nebo se existující metoda musí pojmout víc obecněji.
Krok 2: Zobecněné vyhledávání
public static void printPersonsWithinAgeRange(List<Person> roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}
Otázky
- Q
-
Co když chceme vypisovat uživatele specifického pohlaví, nebo dokonce kombinace věk a pohlaví?
- Q
-
Co když se rozhodneme změnit třídu
Person
a přidat do ní atributy, např. vzájemné vztahy nebo geografická lokace? - A
-
Přestože je tato metoda obecnější než předchozí
printPersonsOlderThan
, snaha vytvořit specifickou metodu pro každý možný vyhledávací dotaz je neudržitelná. - Řešení
-
Oddělit kód, který specifikuje vyhledávací kritéria, od samotného vyhledávání.
Jména metod musí být výstižná a popisná. Proto byla metoda přejmenována. |
Krok 3: Použití vlastního rozhraní [1/2]
-
Definujeme rozhraní pro vyhledávací a vytvoříme implementaci:
interface CheckPerson {
boolean test(Person p);
}
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}
Ve stejnou chvíli můžeme mít definováno několik vyhledávacích kritérií (tříd implementujících rozhraní). |
Krok 3: Použití vlastního rozhraní [2/2]
-
Vyhledávací metoda se změní následovně:
public static void printPersons(List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
A volá se takto:
List<Person> roster = ...
printPersons(roster, new CheckPersonEligibleForSelectiveService());
- Q
-
Je nutné definovat
CheckPersonEligibleForSelectiveService
ve speciální třídě? - A
-
Není. Můžeme použít anonymní třídy a redukovat tak kód.
Krok 4: Použití anonymní třídy
interface CheckPerson {
boolean test(Person p);
}
// separate iteration and tester
public static void printPersons(List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
Provedení filtrace
// do the filtering
List<Person> roster = ...
printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);
-
Všimněte si, že
CheckPerson
je funkcionální - obsahuje jedinou metodu. Proto je ale název metody nepodstatný.- Q
-
Mohli bychom název metody nějak vynechat?
- A
-
Ano, pokud použijeme lambda výraz, který se dá chápat jako definice anonymní metody implementující nějaké funkcionální rozhraní.
Krok 5: Použití lambda výrazu
List<Person> roster = ...
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
Vysvětlení lambda syntaxe →
-
Levá část (před šipkou) obsahuje vstupní parametry, pravá strana pak kód případně výstupní hodnotu.
-
Všimněte si, že rozhraní
CheckPerson
se teď v kódu objevuje pouze typ argumentu v metoděprintPerson()
. -
Ale název typu (ani metody, jak už víme) není důležitý.
- Q
-
Mohli bychom vynechat z kódu i typ
CheckPerson
? - A
-
Ano. Java nabízí pro takové případy vlastní generická předdefinovaná rozhraní.
Krok 6: Použití existujících funkcionálních rozhraní [1/3]
-
Zamysleme se nad původní definicí našeho rozhraní:
interface CheckPerson {
boolean test(Person p);
}
Zobecnění pomocí generických typů
-
Jeho význam můžeme zobecnit pomocí generických typů podobně, jako je tomu u rozhraní
java.util.functions.Predicate
:
interface Predicate<T> {
boolean test(T t);
}
Krok 6: Použití existujících funkcionálních rozhraní [2/3]
-
CheckPerson
tedy již nadále nepotřebujeme. Můžeme místo něj použítPredicate<T>
:
public static void printPersons(List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
Vlastní filtrace zůstává
List<Person> roster = ...
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
Krok 6: Použití existujících funkcionálních rozhraní [3/3]
-
Zamysleme se nad rozhraním
Predicate<T>
a jeho použitím ještě jednou:
interface Predicate<T> {
boolean test(T t);
}
...
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
Typování Predicate<T>
-
Všimněte si, že rozhraní
Predicate
používá typT
pouze na zjištění typu vstupního argumentu metody. -
V řadě případů lze typ argumentu odvodit z kontextu pomocí type inference, pak je tato informace nadbytečná, ale nevadí.
- Q
-
Mohli bychom deklaraci typu při volání vynechat?
- A
-
Ano. Následující fragment kódu je rovněž správně. Že
T
odpovídáPerson
zjistí Java při překladu.
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
Jaká máme funkcionální rozhraní v Javě?
-
Rozhraní v
java.util.function
package, například- Predicate<T>
-
s jednou metodou
boolean test(T t)
, - Supplier<T>
-
s jednou metodou
void get(T t)
, - Consumer<T>
-
s jednou metodou
void accept(T t)
.
-
Další rozhraní, která sice mohou definovat více metod, ale jen jedna zni je nestatická, například
-
Comparator<T>
zjava.util
, -
Iterable<T>
zjava.lang
.
-
Příklad použití dalších funkcionálních rozhraní
-
Naše současná implementace vyhledávací metody vypisuje informace o osobách splňujících predikát:
public static void printPersons(List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
-
Co když ale s osobami, které splňují predikát daný parametrem
tester
, chceme dělat něco jiného, než jen vypisovat informace o nich?
Funkcionální rozhraní Consumer
-
Funkcionální rozhraní
Consumer<T>
s metodouvoid accept(T t)
nabízí obecnou operaci na zpracování objektu. -
Jako implementaci rozhraní
Consumer<T>
lze použít jakýkoliv kód, který na daném objektu něco vykoná, ale nic nevrací. -
Velmi často se prostě zavolá bezparametrická metoda definovaná na daném objektu, v našem případě
p.printPerson()
.
Příklad definice zpracování s accept
public static void processPersons(
List<Person> roster,
Predicate<Person> tester,
Consumer<Person> block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}
Příklad zpracování
processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);
Funkcionální rozhraní Function
-
Co když nám nestačí zpracovat původní objekty
Person
, ale rádi bychom z nich vytáhli nějaké informace, například e-mail, a teprve tyto informace zpracovali? -
K tomu potřebuje rozhraní, které by vracelo nějakou hodnotu.
-
Funkcionální rozhraní
Function<T,R>
nabízí metoduR apply(T t)
, která slouží k "transformaci" dat typuT
na data typuR
.
Příklad definice zpracování s apply
a accept
public static void processPersons(
List<Person> roster,
Predicate<Person> tester,
Function<Person, String> mapper,
Consumer<String> block) {
for (Person p : roster) {
if (tester.test(p)) {
String data = mapper.apply(p);
block.accept(data);
}
}
}
Příklad zpracování
processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
Extenzivní využití generických typů [1/2]
-
Metoda
processPersons
sice pracuje pouze se dvěma typy (Person
andString
), ale její smysl se dá zobecnit takto: procházej objekty, vyber, které splňují predikát, vezmi z nich nějaká data, a tato data zpracuj. -
Tohoto zobecnění lze dosáhnout zobecněním typů za použití generik:
public static <X, Y> void processElements(
Iterable<X> source,
Predicate<X> tester,
Function <X, Y> mapper,
Consumer<Y> block) {
for (X p : source) {
if (tester.test(p)) {
Y data = mapper.apply(p);
block.accept(data);
}
}
}
Extenzivní využití generických typů [2/2]
-
Vypsání e-mailových adres lidí pak lze vypsat stejně jako předtím:
processElements(roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
-
Co se ale změnilo je to, že lze takto zpracovat i jiné třídy/objekty, než jen
Person
!
Datové proudy (streams)
-
Rozhraní
java.util.stream.Stream<T>
používá nastíněné principy a nabízí jednoduché metody, které mohou být použité pro proudové zpracování dat. Náš kód lze přepsat takto:
roster.stream()
.filter(
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25)
.map(p -> p.getEmailAddress())
.forEach(email -> System.out.println(email));
Shrnutí: Principy proudového zpracování dat
-
Java Core API nabízí jednoduchá funkcionální rozhraní vhodná pro proudové zpracování dat.
-
Díky generickým typům jsou tato rozhraní jsou nezávislá na tom, jaká data jsou v proudu uložena.
-
Rozhraní
Stream
poskytuje metody, které využívají funkcionální rozhraní pro definici jednoduchých operací nad proudovými daty. -
Operace je aplikována na všechny objekty proudu.
-
Většina těchto metod vrací "výsledný" proud jako výstupní hodnotu, takže lze operace snadno řetězit.
Shrnutí/2
-
Vývojáři mohou využít anonymní třídy pro snadnou definici proudových operací (implementaci požadovaných rozhraní).
-
Protože se ale jedná o funkcionální rozhraní, lze navíc použít kompaktní zápis pomocí lambda výrazů.
-
Resumé: Vývojáři mohou implementovat proudově-orientované zpracování dat s použitím přístupu podobného funkcionálním jazykům.