Třídy
3. Třídy Základní pojmy objektového programování Jak už víme, je Java objektovým programovacím jazykem. V úvodu této kapitoly si objasníme základní pojmy objektové teorie. Objekt představuje souhrn dat a činností, které od něj můžeme žádat. Každý objekt má určité rozhraní, prostřednictvím kterého komunikuje s jinými objekty. Pro začátek si můžeme představit, že když tvoříme program pomocí objektů, skládáme ho obdobně jako např. stavbu z Lega. Je pro nás důležité, aby bylo možno je spojit a celek byl funkční, ale jak jednotlivé objekty dojdou uvnitř k výsledkům nás nezajímá. Když vytváříme program v objektovém jazyce, nepopisujeme jednotlivé objekty, ale třídy objektů. Třída (class) je obecný popis vlastností a činností objektů. Například budeme chtít pracovat se studenty, vytvoříme třídu Student, která bude obecně popisovat všechny studenty, jejich vlastnosti (např. jméno, příjmení, datum narození, adresu, absolvované kurzy, známky atd.) a činnosti (vytvoření nového studenta, zapsání předmětu, změna adresy atd.). Pokud potom vytvoříme konkrétního studenta Jana Nováka vytvoříme instanci třídy. Těchto instancí může k jedné třídě existovat libovolné množství. Java nám poskytuje řadu tříd, uložených v standardních knihovnách, které nám ulehčí práci (např. třídy pro práci se soubory, textovými řetězci, třídy poskytující matematické funkce, třídy pro tvorbu grafického rozhraní atd.). Třída popisuje vlastnosti jednotlivých instancí prostřednictvím proměnných (ekvivalentní jsou označení datové členy nebo datové atributy). Naše třída Student bude mít např. proměnnou jméno, tato proměnná bude v každé instanci nabývat různé hodnoty (např. Jan, Pavel, Petra, Tereza, Pavel,...). Při definici proměnné musíme určit její datový typ, tj. jestli v proměnné bude uloženo číslo, řetězec textu, instance jiné třídy atd. Takováto proměnná se označuje jako proměnná instance. Můžeme si také nadefinovat proměnnou počet, která bude součástí třídy Student a bude sloužit pro evidenci počtu studentů. Takováto proměnná není součástí jednotlivých instancí, existuje pouze jednou a je přístupná všem instancím třídy, označujeme ji jako proměnnou třídy. Dále třída obsahuje metody popisující chování (činnosti) jednotlivých instancí (metody instance) nebo společné všem instancím (metody třídy). Zapouzdření je jedna ze základních vlastností objektů. Jde vlastně o skrytí detailů implementace při zachování funkčnosti dostupné jiným objektům. Část objektu, která je dostupná ostatním objektům, se nazývá rozhraní objektu. Dědičnost je dalším ze základních rysů objektového programování. Umožňuje nám lépe využít již vytvořené třídy. Pokud již existuje třída, která částečně splňuje naše požadavky, pouze potřebujeme přidat nějakou vlastnost nebo činnost, využijeme v rámci objektového programování právě dědičnost. Již existující třídy lze rozšiřovat a doplňovat tak, že novou třídu definujeme jako potomka (odvozenou
20
Třídy třídu, podtřídu, dceřinou třídu) stávající třídy (nadtřídy, rodičovské třídy, bázové třídy). Potomek zdědí všechny proměnné a metody nadřízené třídy. Např. máme třídu Tvar, která obecně popisuje společné vlastnosti a činnosti všech dvourozměrných tvarů. Můžeme vytvořit několik dalších tříd, které budou tyto vlastnosti přebírat a rozšiřovat např. Kruh, Čtverec a Trojúhelník. Grafické znázornění pomocí zjednodušeného zápisu z UML vidíme na obrázku č. 1.
Tvar
Kruh
Čtverec
Trojúhelník
obrázek 1 Jednoduché znázornění dědičnosti pomocí UML
Do nové třídy můžeme přidat další proměnné a metody. Třída Tvar bude obsahovat činnost nakresli. Všechny třídy, jejichž předkem je třída Tvar, budou tedy obsahovat metodu nakresli. V našem případě se ale obecná metoda nakresli pro jednotlivé podtřídy nehodí, potřebujeme již kreslit konkrétní tvary. Tato situace však neznamená, že dědičnost nelze použít. Využijeme další z objektových vlastností a to je polymorfismus tj. vytvoříme novou metodu stejného názvu ale jiného obsahu a tou původní metodu nakresli překryjeme (v angličtině se používá termín overriding). V jedné třídě může být definováno několik metod stejného jména, které se liší pořadím a počtem parametrů. Tomuto říkáme přetěžování (overloading) metod. Například pro kreslení kruhu můžeme chtít vytvořit metodu, která nakreslí kruh barvou, jaká je právě nastavena v systému, a jindy budeme chtít metodu, která nakreslí kruh námi zadanou barvou. Nemusíme hledat jiný název pro kreslení se zadanou barvou, i tato metoda se může jmenovat nakresli a bude mít navíc parametr pro určení barvy. Obecně lze vytvořit i třídu, která má více přímých předků, tento jev se nazývá vícenásobná dědičnost. Java však umožňuje pouze jednonásobnou dědičnost tj. třída v Javě může mít jen jednoho předka ( to ale neznamená, že tento předek nesmí mít svého předka atd.). V jazyce Java jsou všechny třídy potomkem třídy Object, jedině tato třída nemá v Javě předka.
21
Třídy
Deklarace třídy v Javě Deklaraci třídy v Javě lze schématicky popsat následovně: [ package JménoBalíku; ] [ import JménoTřídy, …; ] public [final | abstract] class jmeno [extends JménoRodičovskéTřídy] [implements JménoRozhraní …] { proměnné třídy; proměnné instancí; vnořené třídy; konstruktory; metody; } případné neveřejné třídy; Klauzule package slouží pro zařazení třídy do určitého balíku (package). Pokud se tato klauzule neuvede, bude třída zařazena do tzv. nepojmenovaného balíku. Popis práce s balíky je nad rámec rozsahu těchto skript. Nepovinná klauzule import slouží pouze pro zjednodušení psaní odkazů na jednotlivé třídy. Např. pokud chci ve své třídě vytvořit instanci třídy java.util.Random, mám dvě možnosti, jak se na tuto třídu odkázat: •
použít celé jméno java.util.Random (přehlednější, jednoznačné, náročnější na psaní)
•
zadat na začátku klauzuli import java.util.Random a poté používat jméno Random
V klauzuli import lze zadat více tříd a lze též použít i hvězdičky. Např. zápis import java.util.*; umožňuje zkráceně psát jména všech tříd z balíku java.util. Používání hvězdiček však může vytvářet velmi nepříjemné a těžko odhalitelné chyby v případě, že existují duplicity ve jménech objektů v různých balících. Překladač Javy automaticky při překladu doplní import třídy java.lang, tj. nemusíme pro používání prvků tohoto balíku uvádět příslušný import.
Modifikátory tříd Před klíčovým slovem class lze použít klíčová slova public, final a abstract, jejichž význam a způsob použití si nyní vysvětlíme. Je-li třída definována jako public, může se na ni odkazovat jakákoli jiná třída. Pokud klíčové slovo public nepoužijeme, může být třída zpřístupňována pouze ostatními třídami z balíku. V jednom zdrojovém souboru může být nejvýše jedna třída, která je definovaná jako public.
22
Třídy Klíčové slovo final označuje třídu, od níž již nelze definovat potomky. Klíčovým slovem abstract označujeme třídu, od které nelze vytvářet žádné instance a která se používá jako předek pro další třídy (lze zde nadefinovat, co vše mají tyto třídy společné). Jedině abstraktní třída může obsahovat abstraktní metody. Je možná pouze kombinace public abstract. Definice a používání abstraktních tříd není součástí těchto skript. Jméno třídy by mělo začínat velkým písmenem. Pokud chceme novou třídu vytvořit jako potomka některé jiné třídy, uvedeme za jménem třídy klíčové slovo extends následované jménem třídy předka (třída musí mít pouze jednoho předka, pokud není uveden, automaticky se doplní jako předek třída Object11). Pokud chceme aby naše třída implementovala rozhraní, použijeme klíčové slovo implements následované jménem jednoho či více rozhraní. Je možno uvést obojí tj. dědění i implementaci rozhraní. V definici třídy není ani jedna část povinná, viz náš úvodní příklad, který obsahoval pouze metodu main. Jestli bude námi vytvářená třída obsahovat proměnné, vnořené třídy nebo konstruktory a kolik v ní bude metod, závisí na logice toho, co tvoříme.
Proměnné tříd a instancí Deklarace proměnných Proměnné mohou být buď primitivního typu nebo to jsou instance tříd. Vše co jsme si řekli o deklaraci primitivních typů, které používáme jako pomocné proměnné v metodách, platí i pro proměnné objektů, pouze s tím rozdílem, že před identifikátorem typu mohou být uvedeny některé modifikátory (public, protected, private, static, final). Modifikátory public, protected a private určují rozsah dostupnosti proměnné a může být uveden maximálně jeden z nich. Jestliže označíme proměnnou modifikátorem private, bude přístupná pouze ve třídě obsahující její definici. Proměnná deklarovaná jako private není dostupná ani potomkům této třídy. Proměnná označená jako protected je dostupná pro všechny třídy se stejného balíku a ve všech třídách vytvořených jako potomci této třídy, a to i v případě, že třída potomka je z jiného balíku. Proměnná označená jako public je dostupná z jakékoli třídy. Pokud není žádný z těchto modifikátorů uveden, je u proměnné definován implicitní (přátelský) přístup - proměnná je přístupná v jakékoli třídě, která je součástí stejného balíku a nepřístupná mimo něj. Je tedy rozdíl mezi deklarací s modifikátorem protected a bez modifikátoru. Modifikátor static určuje, že se jedná o proměnnou třídy. To znamená, že v dané třídě existuje právě jeden výskyt této proměnné a všechny instance této třídy jej sdílejí. Změna hodnoty této proměnné tedy znamená změnu pro všechny instance. Pokud tento modifikátor není uveden, jedná se o proměnnou instance. Každá vytvořená instance této třídy má svou kopii proměnné instance a její změna v jedné instanci se nedotkne ostatních instancí.
23
Třídy Modifikátor final označuje konstantu. Takto označené proměnné může být přiřazena hodnota pouze jednou. Jakékoli další přiřazení nové hodnoty je překladačem označeno jako chyba. Příklady: protected int pocet = 10; private float objem; public static final int MAXIMALNI_POCET = 100; protected String jmeno;
Práce s proměnnými Pokud potřebujeme s proměnnou pracovat v rámci metod třídy ve které byla definována, odkazujeme se na ni pouze jejím jménem. Jestliže potřebujeme s proměnnou pracovat v jiné třídě, odkazujeme se na ni v závislosti na typu proměnné. Když je to proměnná třídy, tak jménem třídy, tečkou a jménem proměnné. Když je to proměnná instance, tak jménem instance, tečkou a jménem proměnné. Je však třeba si uvědomit, že pokud byla proměnná definována jako private, není přímý přístup k této proměnné mimo třídu vůbec možný.
Metody Další část deklarace třídy je tvořena deklaracemi metod. Konstruktor je speciální metoda, o které si povíme později. Metoda je část spustitelného kódu. Metodě mohou být při volání předány parametry a metoda může vrátit hodnotu. Deklarace metody vypadá následovně: [ modifikátory ] typ_návratové_hodnoty jméno_metody ( [parametry] ) [ throws jméno_třídy ] Klauzule throws bude vysvětlena později v kapitole o výjimkách.
Modifikátory metod Mohou být tyto: public, protected, private, static, abstract, final, native, synchronized. Modifikátory public, protected a private mají stejný význam jako u proměnných a také přístupnost metody bez uvedení jednoho z těchto modifikátorů je stejná jako u proměnných. Modifikátor static určuje, že metoda je metodou třídy tj. voláme ji jménem třídy a před jejím volání nemusí být vytvořena žádná instance této třídy. Pokud tento modifikátor není uveden, je metoda metodou instance. Metodu označenou jako static nelze u potomka třídy předefinovat. Metoda, která je definována s modifikátorem final, je metoda, kterou již nelze předefinovat. To tedy znamená, že žádný z potomků této třídy nesmí obsahovat metodu stejného jména se stejným počtem a pořadím parametrů. Modifikátor final však neznamená, že metoda nemůže být přetížena. Abstraktní metoda (s modifikátorem abstract) musí za deklarací končit středníkem, nesmí tudíž obsahovat žádnou implementaci, pouze deklaraci návratového typu a případně parametrů. Abstraktní
11
Úplné jméno je java.lang.Object
24
Třídy metody mohou být deklarovány pouze v abstraktních třídách. Implementace této metody musí být uvedena u potomka abstraktní třídy, ve které je metoda uvedena. Není přípustná kombinace modifikátoru abstract s modifikátorem private ani static. Modifikátor native označuje metodu zapsanou v jiném programovacím jazyce. Modifikátor synchronized se používá k synchronizaci metod v rámci více vláken jednoho procesu.
Typ návratové hodnoty V deklaraci metody musí být vždy uveden typ návratové hodnoty. Pokud metoda nevrací hodnotu, je typ označen jako void. Metoda, která má definován jiný typ návratové hodnoty než void, musí být ukončena příkazem return vracejícím hodnotu odpovídajícího typu.
Vstupní parametry metody Za jménem metody uvádíme v kulatých závorkách seznam formálních parametrů. Počet parametrů není omezen, ale budete-li někdy psát metodu s více než pěti parametry, zamyslete se jestli do ní nesdružujete zbytečně mnoho činností. Při definování parametrů uvádíme jejich typ a uvnitř metody s nimi pracujeme jako s proměnnými. Jako oddělovač jednotlivých parametrů používáme čárku. U každého parametru musí být jeho typ uveden samostatně, zápis static int max (int a, b) není možný, je třeba uvést static int max (int a, int b). Skutečné hodnoty parametrů dosadíme při volání metody. Pokud metoda nemá žádné vstupní parametry, je třeba při deklaraci i při volání takovéto metody uvést za jejím jménem prázdné závorky. Příklad 1: metoda, která vrací hodnotu většího ze vstupních parametrů: static int max (int a, int b) { if (a > b) return a else return (b); System.out.println.(”Konec metody”); //toto se neprovede } V tomto příkladě jsme deklarovali metodu max (jméno metody by mělo začínat malým písmenem), která je metodou třídy, vrací hodnotu typu int a má dva parametry typu int. Vrácení většího z čísel je realizováno pomocí příkazu return. Příkazem return provádění metody končí. Nemá tedy smysl uvádět poslední řádek v našem příkladě. Také si všimněte, že hodnota výstupu z metody u příkazu return byla přiřazena pokaždé jinak, obě varianty (se závorkami i bez) jsou správné. Pokud je metoda max deklarována ve třídě Pokus a spouštíme ji v rámci jiné metody stejné třídy, může vypadat její volání následovně: System.out.println("Vetsi je :"+max(cislo1,cislo2)); nebo int vetsi = max(cislo1,cislo2);
25
Třídy Pokud je tato metoda volána z jiné třídy, vypadá její volání takto: System.out.println("Vetsi je :"+Pokus.max(cislo1,cislo2)); nebo int vetsi = Pokus.max(cislo1,cislo2); Metoda max je metodou třídy (modifikátor static), proto ji lze volat buď jen jménem nebo jménem třídy a jménem metody bez nutnosti vytvoření instance. Příklad 2: metoda pro jednoduchý tisk. static void tisk(String s){ System.out.println(s); } Zde jsme definovali metodu tisk, jako metodu s návratovým typem void a jedním parametrem typu String. V této metodě nemusí být uveden příkaz return a metoda končí provedením všech příkazů v ní uvedených. Pokud uvedeme v takovéto metodě příkaz return, musí být prázdný tj. nesmí za ním být uveden žádný výraz. Volání této metody je v podstatě stejné jako volání v minulém příkladě, pouze s tím rozdílem, že se uvádí jako samostatný příkaz. Např.: tisk("AHOJ"); Pokus.tisk ("Nejaky text");
Předávání hodnot parametrů Při předávání parametrů metodě je třeba si uvědomit, že primitivní typy jsou předávány jinak než referenční. Primitivní typy jsou předávány hodnotou, tj. po dobu činnosti procedury se vytvoří kopie původní hodnoty a s tou metoda pracuje. Po skončení činnosti metody je tato kopie zrušena, ale její hodnota není převedena do původní proměnné. Důsledky si ukážeme na následujícím příkladě. static int zvetseniOCtyri (int i){ i += 4; return i; } část metody main: int a, b =1; b = zvetseniOCtyri(a); System.out.println("a = "+ a+"
b = "+b);
Výpis bude vypadat takto: a = 1
b = 5
Hodnota proměnné a je použita jako vstupní hodnota pro spuštění metody zvetseniOCtyri, ale dál už se s ní nepracuje. Při spuštění metody se vytvoří lokální proměnná (zde pojmenovaná písmenem i) a její změna se do proměnné a nepromítne. Parametry referenčních typů jsou předávány odkazem, tzn. že se pracuje i v metodě se stejnými daty. Důsledek si nejlépe uvědomíme na příkladě:
26
Třídy public void pridej(StringBuffer s){ s.append ("konec");//pripoji do retezce text "konec" } část metody main: retezec = new StringBuffer ("Zacatek "); pridej(retezec); System.out.println(retezec); Výpis bude vypadat takto12: Zacatek konec
Přetěžování metod O přetížení metod se mluví v případě, že metody mají stejné jméno a liší se pouze typem, počtem nebo pořadím parametrů. Pozor metodu nelze přetížit pouze změnou typu návratové hodnoty. Obvykle se přetěžují metody v rámci jedné třídy, lze však přetížit i metody zděděné od předka. V předchozím příkladě jsme definovali metodu pridej s jedním parametrem typu String, která na konec řetězce přidá text "konec". Jestliže chceme vytvořit metodu, která na konec řetězce přidá text, který jí předáme jako parametr, nemusíme vymýšlet jiné jméno metody, stačí přetížit metodu pridej. K původní metodě pridej přibude tato: public void pridej(StringBuffer s1, String s2){ s1.append (s2);//pripoji do retezce text uloženy v s2 } Která metoda bude použita, závisí na použití parametrů při volání. Část metody main: retezec = new StringBuffer ("Zacatek "); pridej(retezec); //jeden parametr, vyvola se puvodni pridej System.out.println(retezec); pridej (retezec, " dalsi text"); //dva parametry, vola se nova varianta pridej System.out.println(retezec); Výpis bude vypadat takto: Zacatek konec Zacatek konec dalsi text
Konstruktory Konstruktory jsou metody volané při vytváření instancí. Platí pro ně několik specifických pravidel. Konstruktor se vždy jmenuje stejně jako třída a je to jediná metoda, která nemá, a ani nesmí mít návratovou hodnotu. To znamená, že neuvádíme návratový typ (ani void) a v kódu konstruktoru se nesmí objevit return s výrazem. Z významu konstruktoru jako metody pro vytvoření instance vyplývá, že je nesmysl použít modifikátor static. Modifikátory pro určení přístupnosti metody se používají. 12
V ukázce je použita třída StringBuffer a ne String, neboť třída String je read-only a nelze změnit hodnotu instance této třídy. 27
Třídy Konstruktor lze také přetížit, tj. vytvořit jich několik s různými parametry. Pokud není ve třídě žádný konstruktor vytvořen, překladač vytvoří implicitní konstruktor bez parametrů. Konstruktor nelze použít jinak, než při vytvoření instance (tj. pouze za příkazem new).
Vytvoření instance Při vytváření instance je nutné definovat typ (tj. od které třídy vytvářím instanci) a jméno instance (pro jména instancí platí stejná pravidla jako pro jména proměnných). Například jsme si nadefinovali třídu Kruh a chceme vytvořit instanci. Nejdříve nadefinujeme proměnnou, která bude na instanci odkazovat: Kruh kr; Pak je nutné pomocí new vytvořit v paměti vlastní instanci a odkaz na ni (referenci) přiřadit do proměnné: kr = new Kruh(); Při vytváření instance se automaticky spouští konstruktor – za slovo new se píše konstruktor a ne jméno třídy (konstruktor se ale vždy jmenuje stejně jako třída). Při vytváření instance samozřejmě nemusíme použít konstruktor bez parametrů, ale jakýkoliv z konstruktorů definovaných ve třídě. Deklaraci a vytvoření instance lze zapsat i jedním příkazem. Kruh kr = new Kruh(); Někdy Vám může připadat, že se vytváří instance objektu bez příkazu new. Např. výsledkem řádku String retezec1 = String.valueOf(3.7629) je nová instance retezec1 třídy String. V tomto případě proběhla operace new v rámci metody String.valueOf a odkaz na novou instanci se vrací jako návratová hodnota.
Zrušení instance Ukázali jsme si, jak vytvořit instanci, logicky by mělo být možné také instanci zrušit. V Javě se o rušení nepotřebných instancí se nemusíme starat – o uvolňování nepotřebných instancí se stará tzv. garbage collector (čistič paměti). Nepotřebná instance je instance, na kterou není odkaz z žádné referenční proměnné. Toho lze dosáhnout několika způsoby, nejčastější jsou následující:
28
Třídy •
skončí rozsah platnosti proměnné,
•
do proměnné vložíme odkaz na jiné místo v paměti, původní instance je tedy bez reference a tato část paměti bude uvolněna: Kruh kr = new Kruh( ); . . kr = new Kruh( );
•
do proměnné vložíme hodnotu null, která znamená, že proměnná neukazuje na žádnou instanci. Původní instance je tedy bez reference a paměť bude uvolněna. Kruh kr = new Kruh( ); . . kr = null;
V okamžiku, kdy zaniknou všechny odkazy na instanci, tato instance ještě nezaniká – zaniká až při nejbližším průchodu garbage collectoru, který se spouští na pozadí v pravidelných intervalech. Pokud je potřeba při zániku instance provést nějakou speciální akci (tj. nejenom uvolnit paměť), je možné nadefinovat speciální metodu finalize().
Rozhraní Java povoluje pouze jednoduchou dědičnost, tj. každá třída má právě jednoho přímého předka a všechny třídy mají společného předka – třídu Object. Jako kompenzaci nevýhod tohoto řešení Java zavádí použití rozhraní. Rozhraní definuje metody, které v něm nejsou implementovány, tj. deklarací rozhraní je jen hlavička rozhraní a hlavičky metod. Třída, která toto rozhraní implementuje, musí všechny metody implementovat. Může se tedy zdát, že rozhraní je totéž jako abstraktní třída, která má všechny metody abstraktní. Existuje však několik podstatných rozdílů: •
rozhraní nemůže deklarovat proměnné kromě konstant
•
třída může implementovat více rozhraní
•
třída implementující rozhraní zároveň dědí - buď od třídy Object nebo od třídy uvedené v deklaraci třídy jako předek
Rozhraní implementujeme u tříd, kterým chceme vnutit zcela konkrétní činnost nezávisle na dědičné hierarchii. Stejné metody (metody se stejnou hlavičkou a činností) mohou mít tedy třídy, které jinak nemají společné předky (kromě třídy Object). V balíku java.lang je nadefinováno několik rozhraní. Například rozhraní Comparable obsahující jedinou metodu int compareTo (Object o). Pokud nějaká třída implementuje toto rozhraní, říká o sobě, že dvě její instance lze porovnat a určit, která je větší nebo zda se rovnají. Toho se využívá např. v implementaci seznamů (viz dále) v metodě pro třídění (standardní metodou lze třídit pouze objekty,
29
Třídy které mají implementované toto rozhraní). Rozhraní Comparable mají implementovány například třídy String, Integer, Double, Character, File nebo Date. Jméno rozhraní se může objevit jako parametr v metodě (např. v metodě Collections.sort pro třídění seznamů je jako parametr uvedeno rozhraní Comparator). Při použití těchto metod se musí jako parametr uvést instance třídy, která příslušné rozhraní implementuje. Některé metody vracejí jako návratovou hodnotu instanci rozhraní. V tomto případě, může proměnná typu rozhraní využívat pouze metody definované v tomto rozhraní.
30