Programovanie (1) v C/C++
1-INF-127, ZS 2024/25
Letný semester, prednáška č. 10
Oznamy
- Tretiu domácu úlohu je potrebné odovzdať do stredy 13. apríla, 13:10 (čiže do začiatku najbližších cvičení).
- Dnes po prednáške bude zverejnené zadanie štvrtej domácej úlohy, ktorú bude potrebné odovzdať do stredy 27. apríla, 13:10.
- Počas cvičení v stredu 20. apríla od 13:10 do 14:40 (čiže hneď po Veľkej noci) bude prebiehať štvrtý test zameraný látku z prvých ôsmich prednášok. Po dobu riešenia testu je potrebná účasť na cvičeniach.
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ódu sort zavolať 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::statickaMetoda namiesto lambda výrazu (arg1, ..., argn) -> Trieda.statickaMetoda(arg1, ..., argn),
- instancia::nestatickaMetoda namiesto lambda výrazu (arg1, ..., argm) -> instancia.nestatickaMetoda(arg1, ..., argm),
- Trieda::nestatickaMetoda namiesto lambda výrazu (arg, arg1, ..., argm) -> arg.nestatickaMetoda(arg1, ..., argm), kde arg je inštancia typu Trieda.
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 15 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 triedy Application definovanej v balíku javafx.application, ktorý je potrebné importovať.
- Každá trieda dediaca od triedy Application musí implementovať jej abstraktnú metódu start, ktorej argumentom je objekt primaryStage typu Stage reprezentujúci hlavné grafické okno aplikácie (trieda Stage je definovaná v balíku javafx.stage, ktorý je potrebné importovať). Metóda start sa vykoná hneď po spustení aplikácie. V rámci metódy start sa typicky vytvárajú jednotlivé ovládacie prvky aplikácie a špecifikujú sa ich vlastnosti.
- V našom prípade je kľúčovým riadkom metódy start volanie primaryStage.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énu scene možno oknu primaryStage priradiť volaním primaryStage.setScene(scene). Trieda Scene je definovaná v balíku javafx.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 objekt pane; 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. Trieda Pane je definovaná v balíku javafx.scene.layout, ktorý je potrebné importovať.
- Metóda main JavaFX aplikácie typicky pozostáva z jediného riadku, v ktorom sa volá statická metóda launch triedy Application. Tá sa postará o vytvorenie inštancie našej triedy HelloFX, o vytvorenie hlavného grafického okna aplikácie, ako aj o následné zavolanie metódy start, 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 s 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 triedy Pane niektorú 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 spracová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);
}
}