Prednáška č. 10
Lambda výrazy
Príklad: komparátor ako lambda výraz
Pripomeňme si z piatej
prednášky zostupné
triedenie zoznamu celých čísel pomocou metódy
Collections.sort, ktorá ako druhý parameter berie komparátor, čiže
inštanciu nejakej triedy implementujúcej rozhranie Comparator<? super
Integer>.
Videli sme pritom tri možné spôsoby, ako komparátor definovať:
- Vytvoriť „bežnú” triedu implementujúcu rozhranie
Comparator<Integer>a metódusortzavolať pre novovytvorenú inštanciu tejto triedy. - To isté s použitím lokálnej triedy:
import java.util.*;
public class Trieda {
public static void main(String[] args) {
class DualComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
}
List<Integer> a = new ArrayList<>();
a.add(6);
a.add(1);
a.add(3);
a.add(2);
a.add(3);
Collections.sort(a, new DualComparator());
System.out.println(a);
}
}
- Definíciu triedy a vytvorenie jej inštancie spojiť do jediného príkazu s využitím mechanizmu anonymných tried:
import java.util.*;
public class Trieda {
public static void main(String[] args) {
List<Integer> a = new ArrayList<>();
a.add(6);
a.add(1);
a.add(3);
a.add(2);
a.add(3);
Collections.sort(a, new Comparator<>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
System.out.println(a);
}
}
Aj posledný z uvedených spôsobov definície komparátora je však stále
trochu ťažkopádny – jediná pre účely triedenia podstatná informácia je
tu daná riadkom return o2.compareTo(o1);, pričom zvyšok konštrukcie by
bol rovnaký aj pri definícii ľubovoľného iného komparátora.
Rozhranie Comparator<E> je príkladom takzvaného funkcionálneho
rozhrania – čiže rozhrania, ktoré deklaruje jedinú abstraktnú metódu;
v tomto prípade ide o metódu
compare. Toto rozhranie síce definuje niekoľko ďalších statických
metód a metód s modifikátorom default; jedinou metódou, ktorú je
potrebné implementovať kedykoľvek implementujeme rozhranie
Comparator<E>, je ale metóda compare. To okrem iného znamená, že
proces vytvárania tried implementujúcich rozhranie Comparator<E> sa
často môže redukovať iba na implementáciu tejto jednej metódy.
Ako skrátený zápis pre definíciu inštancie anonymnej triedy implementujúcej funkcionálne rozhranie, obsahujúcej iba definíciu jedinej abstraktnej metódy deklarovanej v tomto rozhraní, slúžia v Jave takzvané lambda výrazy. Utriedenie poľa tak možno realizovať pomocou príkazu
Collections.sort(a, (o1, o2) -> {return o2.compareTo(o1);});
alebo pomocou ešte kratšieho príkazu
Collections.sort(a, (o1, o2) -> o2.compareTo(o1));
V zátvorkách pred šípkou sú postupne uvedené identifikátory argumentov
metódy compare a za šípkou nasleduje blok obsahujúci telo metódy
compare, prípadne výraz udávajúci výstupnú hodnotu tejto metódy.
Poznámka: pripomeňme si tiež z piatej prednášky, že uvedený príklad je
čisto ilustračný – rovnako sa správajúci komparátor možno získať aj
volaním statickej generickej metódy
Comparator.<Integer>reverseOrder() resp. skrátene
Comparator.reverseOrder().
Syntax lambda výrazov v Jave
Lambda výraz je teda skráteným zápisom anonymnej triedy implementujúcej funkcionálne rozhranie, ako aj jej inštancie. Alternatívne ich možno považovať aj za reprezentáciu implementácie abstraktnej metódy v tomto rozhraní deklarovanej; preto sa v súvislosti s lambda výrazmi často hovorí aj o anonymných funkciách. Pomenovanie „lambda výraz” odkazuje na lambda kalkul – formalizmus, v ktorom sa podobným spôsobom definujú matematické funkcie (napríklad λx.(x2 + 5) je funkcia f daná predpisom f: x ↦ x2 + 5) a ktorý je okrem iného aj teoretickým základom funkcionálneho programovania.
V prípade, že má jediná abstraktná metóda deklarovaná v implementovanom funkcionálnom rozhraní hlavičku
T f(T1 arg1, T2 arg2, ..., Tn argn);
môže byť lambda výraz reprezentujúci inštanciu anonymnej triedy implementujúcej toto rozhranie tvaru
(arg1, arg2, ..., argn) -> { /* Telo implementacie metody f */ }
Pred argumentmi možno nepovinne uvádzať aj ich typy (buď sa typ uvedie
pri všetkých argumentoch, alebo sa neuvedie pri žiadnom argumente). V
prípade, že je počet argumentov metódy f nulový, píšeme
() -> { /* Telo implementacie metody f */ }
a v prípade, že ide o metódu s jediným argumentom arg, možno vynechať
zátvorky okolo argumentov a písať iba
arg -> { /* Telo implementacie metody f */ }
V prípade, že návratový typ T metódy f nie je void, možno na
pravej strane lambda výrazu namiesto bloku obsahujúceho telo
implementovanej metódy uviesť iba výraz, ktorý sa vyhodnotí na typ T;
v takom prípade bude metóda f počítať tento výraz. V prípade, že
návratovým typom metódy f je void, možno namiesto bloku s telom
implementovanej metódy uviesť volanie jednej metódy s návratovým typom
void (ktoré možno z určitého pohľadu chápať ako „výraz typu void”),
prípadne volanie jednej metódy s iným návratovým typom, pričom sa však
návratová hodnota tejto metódy odignoruje.
Anotácia @FunctionalInterface používaná v nasledujúcich príkladoch je
nepovinná (všetko by rovnako dobre fungovalo aj bez nej), ale odporúčaná
– kompilátor totiž vyhodí chybu kedykoľvek je táto anotácia použitá inde
ako pri funkcionálnom rozhraní.
Príklad 1:
@FunctionalInterface
public interface MyFunctionalInterface {
int f(int a, int b);
}
public class Trieda {
public static void main(String[] args) {
MyFunctionalInterface instance1 = (a, b) -> {
if (a < b) {
return 0;
}
return a + b;
};
MyFunctionalInterface instance2 = (a, b) -> a * b;
System.out.println(instance1.f(2, 3));
System.out.println(instance2.f(2, 3));
}
}
Príklad 2:
@FunctionalInterface
public interface MyFunctionalInterface {
int f(int a);
}
public class Trieda {
public static void main(String[] args) {
MyFunctionalInterface instance1 = (a) -> {return a + 1;};
MyFunctionalInterface instance2 = a -> {return a - 1;};
MyFunctionalInterface instance3 = x -> x + 3;
System.out.println(instance1.f(2));
System.out.println(instance2.f(2));
System.out.println(instance3.f(2));
}
}
Príklad 3:
@FunctionalInterface
public interface MyFunctionalInterface {
void f();
}
public class Trieda {
public static void main(String[] args) {
MyFunctionalInterface instance1 = () -> {System.out.println("Hello, lambda!");};
MyFunctionalInterface instance2 = () -> System.out.println("Hello, lambda!");
instance1.f();
instance2.f();
}
}
Rovnako ako z lokálnych a anonymných tried, možno aj z definícií lambda výrazov pristupovať k premenným inštancií, či k finálnym alebo „v podstate finálnym” lokálnym premenným metód, ktorých sú súčasťou.
Ďalší príklad použitia lambda výrazov
Rozhranie
Iterable<T>
deklaruje okrem iného aj metódu
forEach,
ktorá postupne pre všetky prvky vracané iterátorom vykoná
danú akciu a predstavuje tak alternatívu k použitiu cyklu for each.
Akcia, ktorá sa má pre všetky prvky vykonať, je daná inštanciou nejakej
triedy implementujúcej funkcionálne rozhranie
Consumer<T>
z balíka java.util.function. V ňom je deklarovaná jediná abstraktná
metóda
accept
s návratovým typom void a jediným argumentom typu T. Metóda
forEach zavolá túto metódu accept postupne pre všetky prvky vracané
iterátorom.
Príklad: Uvažujme napríklad nasledujúci kód postupne vypisujúci všetky
prvky zoznamu a.
List<Integer> a = new ArrayList<>();
// ...
for (int x : a) {
System.out.print(x + " ");
}
Pomocou metódy forEach a anonymnej triedy môžeme rovnaké vypísanie
prvkov zoznamu napísať aj nasledovne.
List<Integer> a = new ArrayList<>();
// ...
a.forEach(new Consumer<>() {
@Override
public void accept(Integer x) {
System.out.print(x + " ");
}
});
Keďže je rozhranie Consumer funkcionálne, môžeme namiesto anonymnej
triedy použiť lambda výraz.
List<Integer> a = new ArrayList<>();
// ...
a.forEach(x -> System.out.print(x + " "));
Viacero ďalších funkcionálnych rozhraní, ktorých inštancie možno
definovať pomocou lambda výrazov, je definovaných v balíku
java.util.function.
Referencie na metódy
Občas sa stáva, že lambda výraz pozostáva iba z priameho volania nejakej už pomenovanej metódy. Pre takéto lambda výrazy existuje v Jave špeciálna syntax umožňujúca vyjadriť toto správanie o niečo stručnejším spôsobom. Lambda výrazy zadané pomocou tejto syntaxe sa nazývajú aj referenciami na metódy.
Pre triedu Trieda poskytujúcu statickú metódu statickaMetoda(T1 arg1,
..., Tn argn) a nestatickú metódu nestatickaMetoda(U1 arg1, ..., Um
argm) a pre inštanciu instancia triedy Trieda možno písať:
Trieda::statickaMetodanamiesto lambda výrazu(arg1, ..., argn) -> Trieda.statickaMetoda(arg1, ..., argn),instancia::nestatickaMetodanamiesto lambda výrazu(arg1, ..., argm) -> instancia.nestatickaMetoda(arg1, ..., argm),Trieda::nestatickaMetodanamiesto lambda výrazu(arg, arg1, ..., argm) -> arg.nestatickaMetoda(arg1, ..., argm), kdeargje inštancia typuTrieda.
Príklad: Keby sme v príklade uvedenom vyššie prvky x zoznamu a
vypisovali namiesto metódy System.out.print(x + " ") metódou
System.out.println(x), mohli by sme celé vypisovanie zoznamu prepísať
aj nasledovne.
List<Integer> a = new ArrayList<>();
// ...
a.forEach(System.out::println);
Úvod do JavaFX
JavaFX je platforma, ktorú možno využiť na tvorbu aplikácií s grafickým používateľským rozhraním (GUI). Namiesto konzolových aplikácií teda budeme v nasledujúcich niekoľkých prednáškach vytvárať aplikácie grafické (typicky pozostávajúce z jedného alebo niekoľkých okien s ovládacími prvkami, akými sú napríklad tlačidlá, textové polia, a podobne).
- Pokyny k inštalácii JavaFX, ako aj návod na skompilovanie a spustenie prvého programu z IntelliJ IDEA a z príkazového riadku možno nájsť tu.
- Dokumentácia k JavaFX 25 API.
- Ďalšie dokumentácie a tutoriály možno nájsť na stránke projektu.
Vytvorenie aplikácie s jedným grafickým oknom
Minimalistickú JavaFX aplikáciu zobrazujúcu jedno prázdne okno o 300 krát 250 pixeloch s titulkom „Hello, World!” vytvoríme nasledovne:
import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
public class HelloFX extends Application {
@Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
Scene scene = new Scene(pane, 300, 250);
primaryStage.setTitle("Hello, World!");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Uvedený kód si teraz rozoberme:
- Hlavná trieda JavaFX aplikácie (tzn. trieda obsahujúca metódu
main) sa vyznačuje tým, že dedí od abstraktnej triedyApplicationdefinovanej v balíkujavafx.application, ktorý je potrebné importovať. - Každá trieda dediaca od triedy
Applicationmusí implementovať jej abstraktnú metódustart, ktorej argumentom je objektprimaryStagetypuStagereprezentujúci hlavné grafické okno aplikácie (triedaStageje definovaná v balíkujavafx.stage, ktorý je potrebné importovať). V rámci metódystartsa vykonávajú úkony, ktoré sa majú udiať hneď po spustení aplikácie – typicky sa vytvárajú jednotlivé grafické ovládacie prvky aplikácie a špecifikujú sa ich vlastnosti.- V našom prípade je kľúčovým riadkom metódy
startvolanieprimaryStage.show(), ktorým zobrazíme hlavné okno aplikácie. Bez tohto volania by aplikácia bežala „na pozadí”. - Volaním
primaryStage.setTitle("Hello, World!")nastavíme titulok hlavného okna na text „Hello, World!”. - Uvedené dva riadky často stačia na zobrazenie grafického okna s titulkom „Hello, World!” a „náhodne” zvolenou veľkosťou. V závislosti od systému sa však môže stať aj to, že sa nezobrazí nič – grafické okno totiž zatiaľ nič neobsahuje a systém nemá ako „rozumne” vypočítať jeho veľkosť; môže teda túto situáciu vyhodnotiť aj tak, že ešte nie je čo zobraziť.
- Zvyšnými riadkami už len hovoríme, že „obsahom” hlavného okna má
byť prázdna oblasť o veľkosti 300 krát 250 pixelov:
- Kontajnerom pre obsah okna je inštancia triedy
Scene. Ide tu o analógiu s divadelnou terminológiou: okno zodpovedá javisku; na javisku následne možno umiestniť scénu pozostávajúcu z jednotlivých rekvizít. Scénuscenemožno oknuprimaryStagepriradiť volanímprimaryStage.setScene(scene). TriedaSceneje definovaná v balíkujavafx.scene, ktorý je potrebné importovať. - Scéna je daná predovšetkým hierarchickým stromom uzlov
(detaily neskôr), pričom uzlami môžu byť napríklad oblasti,
ale aj ovládacie prvky ako napríklad tlačidlá, či textové
polia. Volaním konštruktora
Scene scene = Scene(pane, 300, 250)vytvoríme scénu o rozmeroch 300 krát 250 pixelov, ktorej koreňovým uzlom je objektpane; ten bude v našom prípade reprezentovať prázdnu oblasť. - Volaním konštruktora
Pane pane = new Pane()vytvoríme novú oblasťpane. Tá môže neskôr slúžiť ako kontajner pre pridávanie rôznych ovládacích prvkov a podobne. TriedaPaneje definovaná v balíkujavafx.scene.layout, ktorý je potrebné importovať.
- Kontajnerom pre obsah okna je inštancia triedy
- V našom prípade je kľúčovým riadkom metódy
- Metóda
mainpri jednoduchých JavaFX aplikáciách typicky pozostáva z jediného riadku, v ktorom sa volá statická metódalaunchtriedyApplication. Tá sa postará o vytvorenie inštancie našej triedyHelloFX, o vytvorenie hlavného grafického okna aplikácie, ako aj o následné zavolanie metódystart, ktorá dostane vytvorené okno ako argument.
Okno s niekoľkými jednoduchými ovládacími prvkami
Podbne ako v príklade vyššie vytvorme aplikáciu pozostávajúcu z jediného grafického okna.
import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
public class Aplikacia extends Application {
@Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
Scene scene = new Scene(pane, 340, 100);
primaryStage.setTitle("Zadávanie textu");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Pridáme teraz do hlavného okna niekoľko ovládacích prvkov tak, ako na
obrázku vpravo. Naším cieľom bude vytvorenie aplikácie umožňujúcej zadať
text, ktorý sa pri kliknutí na tlačidlo OK zjaví v textovom popisku
červenej farby. Ovládacie prvky ako textové pole alebo tlačidlo sú
definované v balíku javafx.scene.control, ktorý je tak nutné
importovať. Podobne na prácu s fontmi budeme potrebovať balík
javafx.scene.text a na prácu s farbami balík javafx.scene.paint.
Začnime s pridaním textového popisku „Zadaj text”. Takéto textové
popisky sú v JavaFX reprezentované triedou
Label,
pričom popisok label1 obsahujúci nami požadovaný text vytvoríme
nasledovne:
import javafx.scene.control.*;
// ...
Label label1 = new Label("Zadaj text:");
Rovnako dobre by sme mohli použiť aj konštruktor bez argumentov, ktorý
je ekvivalentný volaniu konštruktora s argumentom "" – text popisku
label1 možno upraviť aj neskôr volaním label1.setText("Nový text").
Po jeho vytvorení ešte musíme popisok label1 pridať do našej scény –
presnejšie do oblasti pane, ktorá je jej koreňovým uzlom (čo znamená,
že všetky ostatné uzly budú umiestnené v tejto oblasti). Vytvorený
popisok label1 teda pridáme do zoznamu synov oblasti pane
nasledujúcim volaním:
pane.getChildren().add(label1);
Následne môžeme upraviť niektoré vlastnosti vytvoreného popisku, ako napríklad jeho pozíciu a font:
import javafx.scene.text.*; // Kvoli triede Font.
// ...
label1.setFont(Font.font("Tahoma", FontWeight.BOLD, 12));
label1.setLayoutX(20);
label1.setLayoutY(10);
Analogicky vytvoríme aj ostatné komponenty:
import javafx.scene.paint.*; // Kvoli triede Color.
// ...
TextField textField = new TextField();
pane.getChildren().add(textField);
textField.setFont(Font.font("Tahoma", FontWeight.BOLD, 12));
textField.setLayoutX(20);
textField.setLayoutY(30);
textField.setPrefWidth(300);
Label label2 = new Label("(Zatiaľ nebolo zadané nič)");
pane.getChildren().add(label2);
label2.setFont(Font.font("Tahoma", 12));
label2.setTextFill(Color.RED);
label2.setLayoutX(20);
label2.setLayoutY(70);
Button button = new Button("OK");
pane.getChildren().add(button);
button.setFont(Font.font("Tahoma", FontWeight.BOLD, 12));
button.setLayoutX(280);
button.setLayoutY(60);
button.setPrefWidth(40);
button.setPrefHeight(30);
Uvedený spôsob grafického návrhu scény má však hneď dva zásadné nedostatky:
- Môžeme si všimnúť, že takéto pevné rozloženie ovládacích prvkov na
scéne nevyzerá dobre, keď zmeníme veľkosť okna. Provizórne môžeme
tento problém vyriešiť tým, že menenie rozmerov okna jednoducho
zakážeme:
primaryStage.setResizable(false). Takéto riešenie má však ďaleko od ideálneho. Odporúčaným prístupom je využiť namiesto triedyPaneniektorú z jej „inteligentnejších” podtried umožňujúcich (polo)automatické škálovanie scény v závislosti od veľkosti okna. V takom prípade sa absolútne súradnice ovládacích prvkov zvyčajne vôbec nenastavujú. - Formát jednotlivých ovládacích prvkov (ako napríklad font alebo farba) by sa po správnosti nemal nastavovať priamo v zdrojovom kóde. Namiesto toho je odporúčaným prístupom využitie štýlov definovaných v pomocných súboroch JavaFX CSS. Takto je možné meniť formátovanie bez väčších zásahov do zdrojového kódu.
Obidva tieto nedostatky napravíme v rámci nasledujúcej prednášky.
Oživenie ovládacích prvkov (spracovanie udalostí)
Dokončime našu jednoduchú aplikáciu so zadávaním textu pridaním jej
kľúčovej funkcionality: po stlačení tlačidla OK (t.j. button) sa má
do „červeného” popisku prekopírovať text zadaný používateľom do
textového poľa.
Po stlačení tlačidla button je systémom vygenerovaná tzv. udalosť,
ktorá je v tomto prípade typu
ActionEvent.
Udalosť je teda akýsi objekt nesúci informáciu o tom, že bolo stlačené
dané tlačidlo. Každé tlačidlo – objekt typu
Button
– má navyše k dispozícii (zdedenú) metódu
public final void setOnAction(EventHandler<ActionEvent> value)
umožňujúcu „zaregistrovať” pre dané tlačidlo jeho spracúvateľa udalostí
typu ActionEvent. Ním môže byť ľubovoľná trieda implementujúca
rozhranie
EventHandler<ActionEvent>,
ktoré vyžaduje implementáciu jedinej metódy
void handle(ActionEvent event)
Po zaregistrovaní objektu eventHandler ako spracovávateľa udalostí
ActionEvent pre tlačidlo button volaním
button.setOnAction(eventHandler);
sa po každom stlačení tlačidla button vykoná metóda
eventHandler.handle.
Nami požadovanú funkcionalitu tlačidla button tak vieme vyjadriť
napríklad pomocou lokálnej triedy ButtonActionEventHandler.
import javafx.event.*;
// ...
public void start(Stage primaryStage) {
// ...
class ButtonActionEventHandler implements EventHandler<ActionEvent> {
@Override
public void handle(ActionEvent event) {
label2.setText(textField.getText());
}
}
EventHandler<ActionEvent> eventHandler = new ButtonActionEventHandler();
button.setOnAction(eventHandler);
// ...
}
Skrátene môžeme to isté napísať s použitím anonymnej triedy.
public void start(Stage primaryStage) {
// ...
button.setOnAction(new EventHandler<>() {
@Override
public void handle(ActionEvent event) {
label2.setText(textField.getText());
}
});
// ...
}
Ak si navyše uvedomíme, že je rozhraní EventHandler deklarovaná jediná
abstraktná metóda – ide teda o funkcionálne rozhranie – môžeme tento
zápis ešte ďalej zjednodušiť použitím lambda výrazu.
public void start(Stage primaryStage) {
// ...
button.setOnAction(event -> label2.setText(textField.getText()));
// ...
}
Podrobnejšie sa spracúvaním udalostí v JavaFX budeme zaoberať na nasledujúcej prednáške.
Geometrické útvary
Špeciálnym typom uzlov, ktoré možno umiestňovať do scén, sú geometrické
útvary ako napríklad
Circle,
Rectangle,
Arc,
Ellipse,
Line,
Polygon,
atď. Všetky útvary dedia od spoločnej abstraktnej nadtriedy
Shape.
Sú definované v balíku javafx.scene.shape, ktorý je nutné na prácu s
nimi importovať.
Aj keď útvary nevedia vyvolať udalosť typu ActionEvent, môžu vyvolávať
udalosti iných typov. Napríklad kliknutie na útvar myšou vyústi v
udalosť typu
MouseEvent
(definovanú v balíku javafx.scene.input) a spracovávateľa takejto
udalosti možno pre útvar shape zaregistrovať pomocou metódy
shape.setOnMouseClicked.
Nasledujúci kód vykreslí „tabuľku” o 10 krát 10 útvaroch, pričom pre každý sa náhodne určí, či pôjde o štvorec, alebo o kruh. Farba každého z útvarov sa taktiež určí náhodne. Navyše po kliknutí myšou na ktorýkoľvek z útvarov sa jeho farba náhodne zmení.
import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.shape.*;
import javafx.scene.paint.*;
import java.util.*;
public class Aplikacia extends Application {
private Color randomColour(Random random) {
return Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble());
}
@Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
Random random = new Random();
for (int i = 0; i <= 9; i++) {
for (int j = 0; j <= 9; j++) {
Shape shape;
Color colour = randomColour(random);
if (random.nextBoolean()) {
shape = new Rectangle(j * 60 + 5, i * 60 + 5, 50, 50);
} else {
shape = new Circle(j * 60 + 30, i * 60 + 30, 25);
}
shape.setFill(colour);
shape.setOnMouseClicked(event -> shape.setFill(randomColour(random)));
pane.getChildren().add(shape);
}
}
Scene scene = new Scene(pane, 600, 600);
primaryStage.setTitle("Geometrické útvary");
primaryStage.setResizable(false);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}