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() {
// ...
}
}
-
Dále předpokládejme, že jsou uživatelé sociální sítě uložení v seznamu
List<Person>
.
Krok 1: Primitivní řešení
-
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();
}
}
}
-
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();
}
}
}
-
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);
}
public static void printPersons(List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
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
interface CheckPerson {
boolean test(Person p);
}
public static void printPersons(List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
List<Person> roster = ...
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
-
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);
}
-
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();
}
}
}
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
...
-
Všimněte si, že rozhraní
Predicate
používá typT
pouze na zjištění typu vstupního argumentu metody. Jenže typ objektu, který metodě posíláme, může být (a je) rozpoznán za běhu. Takže tato informace je v podstatě nedůležitá. -
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 interpret za běhu.
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()
.
public static void processPersons(
List<Person> roster,
Predicate<Person> tester,
Consumer<Person> block)
{
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}
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
.
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);
}
}
}
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));
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.
-
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.