This is an old revision of the document!
Table of Contents
Programování v jazyce JAVA: vlastnosti a koncepce jazyka. Principy objektového programování.
B0B36PJV Webové stránky předmětu
- Vývojové prostředí – JDK, JVM, kompilace a běh programu, správa paměti, GC, profilování a optimalizace.
- Objekty, třídy a jejich vztahy – princip abstrakce a zapouzdření, modifikátory přístupu. Interface a abstraktní třída. Dědičnost a kompozice, polymorfismus, dynamická vazba.
- Výčtové typy – práce s kolekcemi, vzor iterátor, generické typy.
- Vnitřní a anonymní třídy – imutabilita, vzor singleton. Proměnné a metody třídy vs. instance.
- Mechanismus výjimek – typy a jejich ošetření, vlastní výjimky. Práce se soubory – přístup k souboru, textové vs. binární, proudy, ukládání dat. Sokety – typy soketů, typy spojení, síťová komunikace.
- Paralelismus – vícevláknové aplikace, problém souběhu a zastavení. Tvorba vláken a jejich ukončení, threadpool, synchronizace, volatilita.
1. Vývojové prostředí – JDK, JVM, kompilace a běh programu, správa paměti, GC, profilování a optimalizace
JDK, JRE, JVM
Většinu programovacích jazyků je možné rozdělit do dvou kategorií:
- kompilované - program je zkompilovaný do spustitelného souboru, skládá se z nativních procesorových instrukcí, které přímo vykonává procesor a musí být tedy kompilován pro konkrétní architekturu a systém. Mezi tyto jazyky patří například C/C++
- interpretované - program není zkompilovaný, pro spuštění je nutný jiný program (interpreter), který program při spuštění převádí do nativních procesorových instrukcí, které může počítač vykonávat. Typický interpretovaný jazyk je třeba Python.
Javu je možné zařadit do obou kategorií - program se první zkompiluje do bytecode, který je obecný - není specifický pro danou platformu. Pro tento proces je potřeba JDK (java development kit).
Tento bytecode je možné následně pustit s JRE (java runtime environment- subset JDK), který bytecode za běhu interpretuje.
Proces kompilace a spuštění v Javě:
- Zdrojový kód se píše do souborů s příponou `.java`.
- Překladač `javac` přeloží `.java` do `.class` souboru s bytecode.
- Bytecode spouští JVM pomocí příkazu `java NázevTřídy`.
Java tedy umožňuje program zkompilovat pouze jednou do bytecode a následně spustit na jekékoliv platformě s JRE (i když v praxi to obecně tak jednoduché není - compile once, debug everywhere). Tento přístup vede k rychlosti blížící se kompilovaným jazykům bez nutnosti kompilovat pro každou platformu zvlášť.
JVM (java virtual machine) je obsažen v JRE i JDK a je zodpovědný za překlad bytecode do nativních procesorových instrukcí.
JAR
JAR (Java ARchive) je archivní formát pro sdružení více tříd (*.class*) a dalších souborů do jednoho balíčku. Používá se k distribuci a spouštění Java aplikací.
Součástí může být také soubor `MANIFEST.MF`, ve kterém lze určit hlavní třídu programu:
- např. `Main-Class: com.example.Main`
- Umožňuje poté spustit aplikaci jednoduše: `java -jar program.jar`
Správa paměti
Stack
Malý paměťový prostor, ve kterém se typicky ukládají lokální proměnné (což obsahuje i reference, atd). Funguje na principu last-in first-out. Při zavolání funkce se na stacku alokuje místo pro všechny lokální proměnné dané funkce. Implementováno přes stack-pointer, který ukládá adresu konce stacku.
Pro každé volání metody vzniká tzv. aktivní záznam, který obsahuje:
- návratovou adresu,
- parametry metody,
- lokální proměnné.
Po ukončení metody se tento záznam odstraní.
Heap
Velký paměťový prostor, dynamicky alokovaná paměť. Typicky se zde ukládají objekty a větší datové struktury. Alokována pomocí new. O dealokaci se nestará programátor, ale Garbage Collector (GC).
Heap je sdílený mezi všemi vlákny programu. Objekty zde zůstávají tak dlouho, dokud na ně existuje reference – poté je odstraní GC.
GC
Garbage Collector běží na pozadí JVM. Běží ve vlastním vlákně (nebo i více vláknech), takže neblokuje běh samotného programu (asynchronní). Drží si seznam všech referencí na data na heap a jakmile zjistí, že už žádné reference neexistují, tak data uvolní (dealokuje).
e.printStackTrace(); // ošetření výjimky log(ex); throw ex; // volitelné předání dál
} finally {
cleanup(); // provede se vždy
} </code> * `catch` může být více, podle typu výjimky. * `finally` se vždy vykoná – i když dojde k výjimce nebo `return`. ==== Try-with-resources ==== Od Javy 7 existuje `try-with-resources`, který automaticky zavře prostředky (např. soubory, streamy) implementující `AutoCloseable`.
try (BufferedReader br = Files.newBufferedReader(Path.of("data.txt"))) { return br.readLine(); }
==== Vlastní výjimky ==== Vlastní výjimky se vytvářejí děděním ze třídy `Exception` nebo `RuntimeException`.
public class MyException extends Exception { public MyException() {} public MyException(String msg) { super(msg); } public MyException(String msg, Throwable cause) { super(msg, cause); } } }
* Kdy se hodí? Když stávající výjimky nedostačují a potřebuješ popsat specifickou situaci.
* Rozhodni se, zda má být checked (nutí volající k ošetření) nebo unchecked (dědí z RuntimeException).
* Pokud dědíme z `Exception`, jedná se o checked výjimku – musí být ošetřena.
* Pokud dědíme z `RuntimeException`, jedná se o unchecked výjimku – nemusí být ošetřena.
==== Best practices ====
* Zachytávej co nejkonkrétnější typy výjimek, až poté obecné (`Exception`, `Throwable`).
* Nepoužívej prázdné `catch` bloky – vždy loguj nebo informuj uživatele.
* Uvolňuj prostředky pomocí `finally` nebo `try-with-resources`.
* Přidávej popisné chybové zprávy (`new IOException(“Soubor nenalezen”)`).
* Vlastní výjimky používej pro specifické, smysluplné chyby v aplikaci.
==== Shrnutí podle typu výjimky ====
* `Throwable` – kořenová třída, zachytitelná, ale neměla by se běžně používat.
* `Exception` – standardní ošetřitelné výjimky.
* `IOException`, `SQLException`, `ParseException` – checked výjimky.
* `RuntimeException`, `NullPointerException`, `IndexOutOfBoundsException` – unchecked.
* `Error` – závažné chyby – většinou se nezachytávají (`OutOfMemoryError`, `StackOverflowError`).
===== Práce se soubory (java.io) =====
Soubor je množina údajů uložená ve vnější paměti. Přístup k němu probíhá pomocí proudů (streamů), které umožňují čtení a zápis dat sekvenčně nebo náhodně. Dělíme je podle typu dat na textové a binární.
==== Klíčové třídy ====
* File – reprezentuje cestu k souboru nebo adresáři. Lze ověřit existenci, práva, velikost, vytvořit složky/soubory.
* InputStream / OutputStream – bajtové proudy (pro binární data).
* Dekorátory: Buffered* (vyrovnávací paměť), Data* (primitiva), Object* (serializace), GZIP* (komprese)… * **Reader / Writer** – znakové proudy (pro textová data). * Často používané: BufferedReader, InputStreamReader, FileWriter. * **RandomAccessFile** – umožňuje číst/zapisovat na libovolnou pozici (náhodný přístup). * Rozhraní: Closeable, Flushable, Serializable – sjednocují chování při zavírání, flushování a serializaci objektů.
==== Otevření binárních proudů ====
try (InputStream in = new FileInputStream("logo.png"); OutputStream out = new FileOutputStream("copy.png")) { byte[] buf = new byte[8192]; int n; while ((n = in.read(buf)) != -1) { out.write(buf, 0, n); } } // oba proudy se zavřou automaticky (try-with-resources)
==== Otevření a čteni textového souboru (java.io) ==== Nejčastější (a dodnes plně dostačující) kombinací je dvojice ``FileReader`` + ``BufferedReader``:
try (BufferedReader br = new BufferedReader(new FileReader("soubor.txt"))) { String line; while ((line = br.readLine()) != null) { System.out.println(line); } } // try-with-resources zajistí zavření souboru
* ``FileReader`` čte znaky (automaticky použije defaultní kódování platformy, nebo explicitně zadej např. ``StandardCharsets.UTF_8``). * ``BufferedReader`` obalí čtení do větších bloků → méně systémových volání, vyšší výkon. * Konstrukce try-with-resources (Java 7+) zavře reader i v případě výjimky. ==== Textové vs. binární soubory ==== * Textové – čitelné pro člověka, pracujeme se znaky pomocí Reader/Writer. Nutné správné kódování (UTF-8, ASCII…). * Binární – rychlejší, nečitelné, používáme InputStream/OutputStream. Vhodné pro obrázky, audio, objekty apod. ==== Serializace objektů ====
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("state.bin"))) { oos.writeObject(gameState); }
* Třída musí implementovat Serializable.
* Lze uložit i více objektů. Pozor na kompatibilitu verzí tříd (pole serialVersionUID).
==== Přístup k datům ====
* Sekvenční – čtení/zápis po sobě jdoucích bajtů/řádků (většina proudů).
* Náhodný (Random Access) – přímý přístup k určitému místu v souboru (RandomAccessFile).
==== Cesty k souborům ====
* Absolutní – začíná kořenem systému (např. /home/user/file.txt nebo C:\Users\file.txt).
* Relativní – relativní k pracovnímu adresáři aplikace.
==== Best practices ====
* Vždy používej try-with-resources – eliminuje riziko nezavření souboru.
* Při práci s textem dej pozor na kódování (UTF-8 doporučeno).
* Bufruj vstupy/výstupy pomocí Buffered* – zvyšuje výkon.
* Nezachytávej holou ''Exception''; loguj a přeposílej konkrétní typy.
* Při sériové práci se soubory preferuj nové NIO API (''java.nio.file.Path'') – ošetří limity ''File'' a umí asynchronní I/O, ale porozumění ''java.io'' je nutný základ.
===== Sokety ===== Soket je objekt, který propojuje aplikaci se síťovým protokolem. Umožňuje síťovou komunikaci mezi dvěma koncovými body (např. klientem a serverem) prostřednictvím definovaných API. V Javě se sokety používají pro komunikaci přes protokoly TCP a UDP. ==== Typy soketů a API ==== | Třída | Protokol | Povaha | Typická použití | |————————-|——————|————————–|——————————-| | Socket / ServerSocket | TCP | spojované, spolehlivé | chat, přenos souborů | | DatagramSocket | UDP | nespojované, best-effort | streaming, multiplayer hry | | MulticastSocket | UDP multicast | skupinový přenos | discovery, video broadcast | ==== TCP vs. UDP – typy spojení ==== TCP (Transmission Control Protocol) * Spojovaný protokol – před komunikací naváže spojení * Zaručuje doručení, pořadí zpráv a detekci chyb * Pomalejší, ale spolehlivý UDP (User Datagram Protocol) * Nespojovaný protokol – žádné spojení, zprávy mohou být ztraceny nebo doručeny jinak * Rychlý, nízká latence, žádné záruky * Nutno si případně ošetřit potvrzení a opakování ručně ==== Ukázkový TCP server/klient ====
// === Server === try (ServerSocket srv = new ServerSocket(9000)) { while (true) { try (Socket s = srv.accept(); BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream())); PrintWriter out = new PrintWriter(s.getOutputStream(), true)) { String line = in.readLine(); out.println("Echo: " + line); } } } // === Klient === try (Socket s = new Socket("localhost", 9000); BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream())); PrintWriter out = new PrintWriter(s.getOutputStream(), true)) { out.println("Ahoj světe"); System.out.println(in.readLine()); }
==== Primitiva soketu ====
* create – vytvoření nového soketu
* bind – přiřazení lokální adresy a portu
* connect – navázání spojení (klient)
* listen – čekání na spojení (server)
* accept – přijetí spojení
* send / receive – odeslání a příjem dat
* shutdown / close – uzavření spojení
==== Best practices ====
* Porty a backlog: při new ServerSocket(port, backlog) nastavíš frontu nepřijatých spojení.
* Nastav setSoTimeout() → ochrana proti zablokování čtení.
* Na mnoho paralelních spojení použij java.nio.channels + selektory (non-blocking I/O).
* Pro šifrovaný přenos sáhni po SSLSocket / SSLServerSocket nebo moderním klientovi java.net.http.
* Vždy uzavírej soket v try-with-resources nebo finally.
===== 6. Paralelismus =====
Paralelismus umožňuje efektivnější využití systémových prostředků (více jader CPU, paralelní zpracování vstupů, vyšší výkon, reakce na události). V Javě se používá vícevláknové programování pomocí třídy `Thread`, rozhraní `Runnable`, případně thread poolu (`ExecutorService`).
Nutné dodržet pravidla synchronizace, aby se zabránilo deadlocku.
==== Vytváření a spouštění vláken ====
Vlákno lze vytvořit buď děděním od třídy `Thread`, nebo implementací rozhraní `Runnable`:
Objekty jsou odvozené od třídy Thread, tělo nezávislého výpočtu definujeme v metodě run().
public class Worker extends Thread { private final int numberOfJobs; public Worker(int id, int jobs) { super("Worker " + id); myID = id; numberOfJobs = jobs; stop = false; System.out.println("Worker id: " + id + " has been created threadID:" + getId()); } public void run() { doWork(); } } Worker thread = new Worker(1, 10); thread.start(); //new thread is created System.out.println("Program continues here");
Pokud nelze použít dědění od Thread implementujeme rozhraní Runnable:
public class WorkerRunnable implements Runnable { private final int id; private final int numberOfJobs; public WorkerRunnable(int id, int jobs) { this.id = id; numberOfJobs = jobs; } public String getName() { return "WorkerRunnable " + id; } @Override public void run() { ... } } WorkerRunnable worker = new WorkerRunnable(1, 10); Thread thread = new Thread(worker, worker.getName()); thread.start(); public void run() { Thread thread = Thread.currentThread(); for (int i = 0; i < numberOfJobs; ++i) { System.out.println("Thread name: " + thread.getName()); } }
==== Synchronizace a problémy souběhu ==== Při práci s více vlákny vznikají typické problémy: * Race condition – více vláken přistupuje ke sdíleným datům bez koordinace. Výsledek je nedefinovaný.
* Řešení: synchronized bloky, volatile proměnné * Deadlock – dvě nebo více vláken čekají na zdroje, které drží jiná vlákna. * Řešení: správné pořadí zamykání, timeouty, algoritmy prevence deadlocku * Producer/Consumer – problém koordinace mezi vlákny produkujícími a spotřebovávajícími data (např. fronta). * Řešení: wait(), notify(), fronty z balíku java.util.concurrent * Reader/Writer – vícero vláken čte a/nebo zapisuje do stejného objektu. * Řešení: ReadWriteLock nebo rozdělení na operace pouze pro čtení a zápis
==== Synchronized, Volatile, Join ==== * synchronized – zajišťuje, že kód (kritická sekce) provádí v daném čase jen jedno vlákno * volatile – proměnná může být měněna z více vláken, JVM nezachová její hodnotu v cache (když se změní na false - informuje to ostatní vlákna, ale ne jako counter) * join() – čeká na dokončení jiného vlákna * wait() / notify() – vlákna čekají a signalizují si mezi sebou (pouze uvnitř synchronized bloku) ==== Thread Pool (Executor) ==== Používá se místo ručního vytváření velkého počtu vláken. Efektivnější správa a recyklace.
ExecutorService executor = Executors.newFixedThreadPool(4); executor.submit(new WorkerRunnable(1, 10)); executor.shutdown();
* ExecutorService – spravuje vláknový pool * submit() – spustí úkol * shutdown() – ukončí Executor * Future – objekt s výsledkem nebo stavem asynchronního výpočtu ==== Kdy použít paralelismus ==== * Více úloh může běžet souběžně – více jádrový procesor * Aplikace provádí dlouhé IO operace, ale musí zůstat responzivní (např. GUI) * Kontrola nebo úkoly na pozadí (např. notifikace, synchronizace)