Programovací jazyk Java
4. přednáška
Obsah
Rozhraní
- Rozhraní je souhrn informací, kterými třída specifikuje, co by o ní okolí mělo vědět a jakým způsobem je možné s ní komunikovat.
- Pro rozhraní je v jazyce Java zavedena speciální konstrukce – interface.
- Rozhraní definuje soubor metod, které v něm ale nejsou implementovány, tj. v deklaraci je pouze hlavička metody, stejně jako je to u abstraktní metody.
- Třída, která toto rozhraní implementuje (tj. jakoby dědí), musí implementovat (tj. jakoby překrýt) všechny jeho metody.
- Rozhraní je vhodné používat jako datový typ formálních parametrů metod a při deklaraci proměnných. Potom všude tam, kde je uvedeno rozhraní jako datový typ, je očekávána instance jakékoliv třídy
implementující toto rozhraní.
- Poznámka: od Javy 8 mohou rozhraní obsahovat i výchozí (default) metody, které obsahující implementaci, ne jen hlavičku. Třída, která implementuje toto rozhraní, tak získá i tuto funkcionalitu – jedná se tak
v podstatě o vícenásobnou dědičnost. – K defaultním metodám rozhraní se ještě vrátíme v jedné z příštích přednášek.
Konstrukce rozhraní
Implementace rozhraní
- Třída, která implementuje rozhraní, musí uvést jeho jméno v hlavičce za klíčovým slovem implements. Při implementaci více rozhraní se uvede jejich seznam oddělený čárkou.
- Příklad:
public class Usecka implements Info {
int delka;
Usecka(int delka) { this.delka = delka; }
@Override
public void kdoJsem() {
System.out.println("Usecka");
}
}
public class Koule implements Info, Trojrozmerny {
int polomer;
Koule(int polomer) { this.polomer = polomer; }
@Override
public void kdoJsem() {
System.out.println("Koule");
}
@Override
public double objem() {
return 4 / 3 * Math.PI * Math.pow(polomer, 3);
}
@Override
public double getPovrch() {
return 4 * Math.PI * Math.pow(polomer,2);
}
}
public class TestKoule {
public static void main(String[] args) {
Usecka u = new Usecka(5);
Koule k = new Koule(3);
u.kdoJsem();
k.kdoJsem();
System.out.println("Objem koule: " + k.objem());
System.out.println("Povrch koule: " + k.getPovrch());
}
}
Použití rozhraní
- Rozhraní implementujeme, když chceme třídě „vnutit“ zcela konkrétní metody. Typicky se jedná o případ, kdy potřebujeme metodě předat referenci na objekt, který
bude „umět“ nějaké věci. Napíšeme rychle rozhraní, formální parametr metody bude typu právě tohoto rozhraní a v metodě můžeme volat metody deklarované v rozhraní.
Při volání takové metody pak můžeme jako skutečný parametr použít instanci jakékoliv třídy, která implementuje dané příslušné rozhraní. Příklad:
interface Zobrazitelny {
void zobraz(int x, int y);
void presun(int offsetX, int offsetY);
void zmiz();
}
class UkazkovaTrida
void ukazkovaMetoda(Zobrazitelny o) {
o.zobraz(5, 8);
o.presun(3, 14);
o.zmiz();
}
}
- Rozhraní použijeme, když vidíme zcela jednoznačné podobnosti v jednotlivých třídách, ale jejich začlenění do společného předka
- může být obtížné, protože by byl evidentně vykonstruovaný, nebo
- může být nemožné, pokud naše třídy vznikly děděním z knihovních (tedy námi neovlivnitelných) tříd.
Operátor instanceof
Překrývání
- Potomek nedědí členské proměnné a metody:
- které jsou deklarovány jako soukromé (private),
- pokud je nadtřída (rodič) z jiného balíku a tyto proměnné a metody nejsou veřejné (public) nebo chráněné (protected),
- které mají v potomkovi stejné jméno (u metod i typy parametrů ve stejném pořadí) jako v rodičovské třídě. Tyto členské proměnné a metody
jsou předefinovány – hovoří se o zastínění (hiding) proměnných a překrytí (overriding) metod.
- Potomek překrývá metodu předka tehdy, pokud definuje metodu se stejnou hlavičkou a stejným návratovým typem.
- Definice metody v potomkovi se stejnou hlavičkou a různým návratovým chybem je chyba!
- POZOR! Při překrývání metod nelze omezit jejich přístupnost. Např. nelze public metodu překrýt metodou protected.
- K překrytým metodám a zastíněným proměnným se lze v případě potřeby dostat pomocí klíčového slova super: super.metoda(...), resp. super.promenna.
- V Javě 1.5 a výše se překrytí doporučuje explicitně vyznačit anotací @override. Kompilátor poté kontroluje, zda skutečně dochází k překrytí a ne k přetížení (v případě
chybně zadaných typů parametrů) nebo dokonce k vytvoření zcela nové metody (při chybně zadaném názvu metody).
- Každý objekt v Javě je potomkem objektu Object → je běžnou technikou překrývat následující metody zděděné ze třídy Object:
Překrytí metody equals
- Metoda boolean equals(Object obj) slouží k porovnání objektů. (Operátor == porovnává pouze reference, tedy zda dvě objektové proměnné odkazují na stejné místo v paměti).
- Programátor si ve své třídě překrytím metody equals určí podmínky pro označení dvou objektů za stejné (rovné).
- Příklad – dvě osoby budeme považovat za stejné, pokud budou mít stejná rodná čísla:
class Osoba {
private String adresa;
private int rodneCislo; // Rodné číslo není příliš vhodné ukládat jako číslo, ale to nyní pro jednoduchost ignorujme.
public void setAdresa(String adresa) {
this.adresa = adresa;
}
public void setCislo(int rodneCislo) {
this.rodneCislo = rodneCislo;
}
@Override // tato anotace vyznačuje překrytí
public boolean equals(Object obj) {
if (obj instanceof Osoba) {
Osoba b = (Osoba) obj;
return (this.rodneCislo == b.rodneCislo);
}
return false;
}
public static void main(String[] args) {
Osoba o1 = new Osoba();
Osoba o2 = new Osoba();
Osoba o3 = new Osoba();
o1.setAdresa("Praha");
o2.setAdresa("Brno");
o3.setAdresa("Ostrava");
o1.setCislo(9154185425);
o2.setCislo(9154185425);
o3.setCislo(8609065146);
System.out.println(o1.equals(o2)); // vypíše TRUE
System.out.println(o1.equals(o3)); // vypíše FALSE
}
}
- Při překrývání metody equals() musíme dodržet následující pravidla:
- musí být reflexivní, tj. x.equals(x) == true,
- musí být symetrická, tj. x.equals(y) == true právě tehdy, když y.equals(x) == true,
- musí být tranzitivní, tj. když x.equals(y) == true a y.equals(z) == true, tak x.equals(z) musí vrátit také true.
- musí být konzistentní, tj. pro nezměněné instance vrací vždy stejnou hodnotu,
- pro parametr null musí vracet hodnotu false, tj. x.equals(null) vrátí false.
Překrytí metody hashCode
- public int hashCode()
- Pokud překryjeme metodu equals, vždy musíme překrýt i metodu hashCode. Obě metody obvykle umí vygenerovat vývojové prostředí (IDE).
- Musí platit:
- Pro tentýž objekt musí hashCode() vracet vždy stejný hešovací kód.
- Rozhodla-li equals(), že jsou si dva objekty rovny, musí hashCode() vrátit stejný hešovací kód. Je tedy nutné počítat hodnotu hashcode přesně z těch hodnot, které porovnáváme v metodě equals().
Naopak dva různé objekty by měly generovat dva různé hash kódy.
- Nejsou-li si objekty rovny podle equals(), mohou mít stejný hešovací kód.
- Samostudium:
Finální metody
- Pokud z jakéhokoliv důvodu chceme znemožnit překrytí nějaké metody v případných podtřídách,
označíme ji jako finální. Použijeme klíčové slovo final v deklaraci hlavičky metody.
- Příklad: public final int getI() {return i;}
- Poznámka: Označení metody jako finální zabrání jejímu překrytí, ne však přetížení!
Abstraktní metody a třídy
- Abstraktní metoda je v jistém smyslu opakem metody finální. Nejenže je možné ji v odvozené třídě překrýt, je
to dokonce nutné (pokud tato podtřída není také abstraktní). Jinak se program nepřeloží. Takto můžeme programátora
přinutit, aby při použití naší třídy jako rodičovské naprogramoval určené metody.
- Příklad: Třída GrafickyObjekt definuje abstraktní metody vykresli() a posun(),
aby odděděné třídy (již konkrétních) grafických objektů (např. Kruh) poskytovali metody k vykreslení a
posunu objektu. Konkrétní definice těchto metod budou totiž v třídách (Kruh, Ctverec, ...)
různé, takže je není možné provést v obecné třídě GrafickyObjekt.
- Pro označení abstraktní metody slouží klíčové slovo abstract.
- Abstraktní metody se nedefinují, ale pouze deklarují v podobě hlavičky bez těla. Např.
abstract void vykresli();
- Třída, která obsahuje alespoň jednu abstraktní metodu, se označuje celá jako abstraktní (uvedením klíčového slova abstract v hlavičce třídy) a tudíž není
možné vytvářet její instance. Taková třída je určena pouze k dědění. Podtřída takové třídy, pokud není také abstraktní, musí překrýt všechny metody, které jsou označeny jako abstraktní.
- Poznámka: Pokud třída implemetuje rozhraní, může obejít povinnost implementovat všechny metody rozhraní tím, že bude abstraktní:
public interface Rozhrani {
int max(int a, int b);
int min(int a, int b);
}
public abstract class CastecneImplementujiciRozhrani implements Rozhrani {
@Override
public int min(int a, int b) {
return a < b ? a : b;
}
}
Finální třídy
- Na rozdíl od abstraktních metod a tříd nemusí být třída, která obsahuje finální metody, třídou finální. Pokud ji však
jako finální označíme (public final class XY {...), není možné tuto třídu použít jako rodičovskou (a tím pádem
ani překrýt žádnou z jejích metod).
Polymorfismus (mnohotvarost)
- Významná vlastnost OOP.
- Polymorfismus umožňuje, aby instance různých tříd na stejný podnět (na volání stejné metody) mohly reagovat různě.
- Instance více tříd pak poskytují svému okolí stejnou službu, ale každá instance na vyžádání této služby provede něco jiného.
- Polymorfismus dovoluje jednotným způsobem manipulovat s příbuznými objekty (mají společného předka).
- Základní „trik“ polymorfismu vyplývá z toho, že potomek může nahradit předka, tzn. může být použit všude tam, kde je
očekáván předek (nadtřída nebo rozhraní).
- Příklad:
Třídy Kruh a Obdelnik budou mít společného předka – abstraktní třídu
GrafickyObjekt (viz výše).
- Obě podtřídy pak musí implementovat metodu vykresli(), která však bude kruh vykreslovat samozřejmě jinak,
než obdélník.
- Přestože nelze vytvořit instanci abstraktní třídy GrafickyObjekt, lze deklarovat proměnnou tohoto
objektového typu. Do této proměnné lze pak umístit odkaz jak na instanci třídy Kruh, tak i Obdelnik,
např.:
GrafickyObjekt o = new Obdelnik(10, 10, 3, 4);
GrafickyObjekt o = new Kruh(12, 15, 5);
- Abstraktní třídu GrafickyObjekt lze použít i jako návratový typ metody nebo typ parametru metody. Ve skutečnosti
se pak bude opět pracovat s instancemi tříd Kruh nebo Obdelnik.
Brzká vazba vs. pozdní vazba
- Jedná se o vazbu mezi objektem a metodou, přesněji mezi příkazem volajícím metodu a konkrétní implementací metody.
- V případě brzké vazby (early binding) je již při překladu programu příkaz volání metody nahrazen skokem na určitou předem danou
adresu v paměti, kde je kód metody uložen. Brzkou vazbu tedy vytváří překladač (kompilátor) programu.
- V případě pozdní vazby (late binding) nemusí být při překladu programu jasné, která implementace metody má být spuštěna. Vazba je tedy
vytvořena runtime systémem až v okamžiku provádění příkazu volání metody. Konkrétní implementace metody je vybrána z tabulky
virtuálních metod (Virtual Method Table) podle skutečného
typu objektu, nikoliv podle deklarovaného.

Zdroj: https://slideplayer.cz/slide/3100413/
- Příklad:
V okamžiku překladu nelze určit, jestli má být volána metoda vykresli() třídy Kruh nebo třídy Obdelnik:
GrafickyObjekt o;
...
if (a > b)
o = new Kruh(5, 8, 3);
else
o = new Obdelnik(6, 7, 3, 2);
o.vykresli();
V Javě jsou všechny metody virtuální (tzn. používá se výhradně pozdní vazba) až na metody označené private nebo final (což je logické). |
Vnitřní třídy
- Platí pravidlo, že v jednom souboru se zdrojovým kódem může být nanejvýš jedna třída s přístupem public.
Lze však do třídy přidávat další vnitřní třídy, které jsou jí podřízené:
public class A {
public class B {
public class C {
}
}
}
- Kompilátor přeloží každou třídu do samostatného .class souboru. Vnitřní třídu pojmenuje tak, že před její jméno přidá
jména nadřízených tříd oddělená znakem $, tj. vyprodukuje soubory A$B$C.class A$B.class A.class.
- Trochu překvapivá je syntaxe volání konstruktorů vnitřních tříd, protože potřebují odkaz na instance nadřízené třídy:
A a = new A();
A.B b = a.new B();
A.B.C c = b.new C();
- Vnitřní třídy mají přístup k privátním proměnným nadřízených tříd:
public class A {
private int a = 1;
public class B {
private int b = a;
public class C {
private int c1 = a;
private int c2 = b;
}
}
}
- Častým problémem je, jak zavolat ve vnitřní třídě metodu obklopující třídy, pokud má vnitřní třída metodu stejného jména. Pak se přistupuje pomocí
kvalifikátoru JménoObklopujícíTřídy.this, tj.
public class A {
public String metoda() {
return "A";
}
public class B {
public String metoda() {
return "B";
}
public class C {
public String metoda() {
return "C";
}
public void volani() {
System.out.println("C = " + metoda());
System.out.println("B = " + B.this.metoda());
System.out.println("A = " + A.this.metoda());
}
}
}
}
- K omezení přístupu do vnitřních tříd lze také používat modifikátory přístupu private, public a protected.
- Možné důvody k použití vnitřních tříd:
- Logické seskupení tříd, které jsou využity v jednom místě aplikace.
Když je třída B využita pouze ve třídě A, je logické ji do třídy A vnořit. Mj. se tím omezí počet souborů aplikace (přehlednost).
- Řešení problému zapouzdření.
Uvažujme dvě samostatné třídy A a B, kde B potřebuje přístup k prvkům třídy A, které by jinak byly soukromé (private).
Vnořením třídy B do A mohou být prvky třídy (statické metody + proměnné) deklarovány jako soukromé a třída B k nim má přesto přístup.
Navíc, třída B může být skryta před vnějším světem.
- Vnitřní třídy mohou vést k přehlednějšímu a lépe udržovatelnému kódu.
Vnořováním jednoduchých tříd do jiných tříd lze umístit kód blíže k místu jeho použití.
Lokální vnitřní třídy
- Vnitřní třída může být deklarována i v těle metody nebo dokonce jen bloku příkazů.
- Její použitelnost je pak omezena jen na tuto metodu nebo blok.
Anonymní třídy
- Mnohdy potřebujeme vytvořit instanci podtřídy nějaké třídy, případně nějakého interface, ale použít ji jen jednou, takže je zbytečné vymýšlet jméno této třídy.
- Proto existuje syntaxe vytvářející anonymní vnitřní třídu:
new TridaNeboInterface() {
... implementace anonymni tridy ...
}
- Kompilátor pak vytvoří vnitřní třídu, která bude pojmenovaná jen číslem, tedy např. A$1.class.
- Příklad – neanonymní třída:
public static void main(String[] args) {
class ReagujeNaTlacitko implements java.awt.event.ActionListener {
public void actionPerformed(java.awt.event.ActionEvent e) {
// zpracování stisku tlačítka;
}
}
javax.swing.JButton tlacitko = new javax.swing.JButton("OK");
tlacitko.addActionListener(new ReagujeNaTlacitko());
}
- Příklad – anonymní třída:
public static void main(String[] args) {
javax.swing.JButton tlacitko = new javax.swing.JButton("OK");
tlacitko.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent e) {
// zpracování stisku tlačítka;
}
});
}
- V Javě může anonymní třída využívat lokálních proměnných pouze pokud jsou final (tzn. konstanty), tj.
final int POCITADLO = 1;
ActionListener posluchac = new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Pocitadlo = " + POCITADLO);
}
};
Anotace kompilátoru
- Od verze 5 je v jazyce Java možné přidávat do programu metadata ve formě anotací.
- Anotace mohou být přidány k jakémukoliv programovému elementu, jako jsou třídy, balíky, metody, globální a lokální proměnné.
- Kromě možnosti vytváření vlastních anotací v Javě existuje pět standardních anotací používaných kompilátorem:
- @Deprecated – touto anotací máme možnost označit element jako deprecated (zavržený), tzn. že by neměl být už dále používán.
Elementem se rozumí třída, metoda, field (proměnná), konstruktor nebo interface. Při použití takového prvku v programu vypisuje
překladač varovné hlášení.
Takto označený eleemnt by měl být v JavaDoc dokumentaci opatřen vysvětlením, proč je zavržen a co má být použito místo něho.
Příklad:
/**
* @deprecated
* Metoda je zavržena, protože... Použijte místo ní metodu...
*/
@Deprecated
static void deprecatedMethod() { }
- @Override – tato anotace říká kompilátoru, že daný element je překrytím elementu z nadřazené třídy (rodiče).
Kompilátor provádí kontrolu, zda překrytí proběhlo v pořádku (zda nejde např. o přetížení u metod).
Příklad:
@Override
public String toString() {
return "Kruh o poloměru " + r + ".";
}
- @SupressWarning – pomocí této anotace můžeme kompilátoru říci, že pro daný element nemá generovat specifický warning.
Můžeme tak např. potlačit vygenerování varování o tom, že používáme metodu nebo třídu, která je označena jako deprecated.
Příklad:
@SuppressWarnings("deprecation")
void useDeprecatedMethod() {
objectOne.deprecatedMethod(); // Jinak by zde překladač vypisoval varování.
}
- @SafeVarargs – ujištění, že metoda používající proměnný počet parametrů neobsahuje potenciálně nebezpečné operace. Používá se od verze 7 jazyka Java.
- @FunctionalInterface – anotace označující funkční rozhraní, což je rozhraní obsahující pouze jednu hlavičku metody. Používá se od verze 8 jazyka Java.
The End