Každý programátor je vždy jen člověk. Člověk je z podstaty věci nedokonalý. Cokoliv, co člověk dělá (stavařinu, vaření, učitelství, politiku), nedělá bez chyb. Stavitel může špatně navrhnout most, který spadne, kuchař může přesolit polévku. Stejné je to i s programátory – také svoji činnost nedělají perfektně, ale téměř vždy jejich programy budou obsahovat chyby.
Nejčastěji chyby vznikají tak, že programátor na něco prostě zapomene. Zapomene vytvořit objekt, zapomene inicializovat proměnnou, zapomene zavolat klíčovou metodu, atd. Některé chyby vznikají tak, že se programátor uklepne a udělá překlep, který překladač programu potom nezpracuje. Oba dva tyto typy jsou poměrně jednoduché a snadno odhalitelné. Pak jsou tady ještě ale další chyby, které vznikají tak, že programátor něco naprogramuje, ale myslí si, že naprogramoval něco jiného. Jednoduchý příklad: programátor pro třídu Tvar naprogramoval metodu posuňDoleva(), kde k x-ové souřadnici tvaru přičte 10. Programátor si myslí, že naprogramoval metodu, která posune tvar doleva (navíc se ta metoda tak jmenuje), ale neuvědomil si, že musí od souřadnice odečítat, ne přičítat, když chce tvar posunout doleva. Po zavolání metody posuňDoleva() se pak tvar posune doprava.
Této nerovnosti se programátor velice těžko zbavuje a odhalení těchto chyb může být poměrně složité. Je totiž obecně známo, že lidé po sobě chyby nevidí. Proto autoři nějakého důležitého textu, např. knihy, nechávají tento text projít jazykovou korekturou. Cizí osoba má totiž větší šanci odhalit přítomné chyby. A stejný princip platí jak o psaní, tak o programování.
Formálně lze tedy chyby v Javě rozdělit do třech skupin:
Naštěstí se všechny chyby dříve nebo později projeví tak, že program začne dělat něco jiného, než má dělat nebo od něj čekáme. Pak následuje prohlídka kódu, který je zodpovědný za danou činnost a jeho analýza. Je důležité si uvědomit, co by mohlo být příčinou problému – u hledání chyby je nutné myslet. Například pro příklad uvedený výše by chyba mohla mít dvě příčiny: chybný kód metody posuňDoleva(), nebo volání metody posuňDoprava() místo metody posuňDoleva(). Programátor poté obvykle zkontroluje všechny možné příčiny problému a chybu odhalí. Může se také stát, že ani jedna z možných příčin chybu nezpůsobuje – pak je skutečná příčina skrytá hlouběji v programu a ke slovu se dostanou pokročilé metody ladění, jako jsou například kontrolní výpisy nebo debugger.
Pokud programátor zjistí příčinu chyby, pak je obyčejně způsob opravy již zřejmý. Následuje poté pouze úprava kódu. Ovšem i během ní je nutné si dávat pozor, neboť úpravou taky můžete zavléci nebo způsobit jinou chybu.
Aby vaše oprava chyby byla co nejkvalitnější a nejúspěšnější, seznámím vás s několika zásadami při opravě chyb (podle knihy Dokonalý kód od Steve McConnella):
V následujících odstavcích vás seznámím s jednotlivými typy chyb a uvedu, jak tyto chyby vznikají, jak je odhalit a jak je opravit.
Zde vám popíšu několik chyb, které může překladač při zpracování kódu vypsat:
Tato chyba se vyskytne, když překladač očekává nějaký příkaz, avšak text
na daném řádku příkaz nepředstavuje. Nejčastěji tento typ chyby zapříčiněn
překlepy. Tuto chybu způsobí například následující kód:
String jmeno;
jmeno == "Pepa";
Překladač očekává operátor přiřazení, ale vy jste omylem napsali rovnítka
dvě, což je operátor porovnání. Chyba se také může vyskytnout, pokud máte
dlouhý příkaz rozdě-lený na několik řádků a některý řádek jste omylem
ukončili středníkem.
Typická chyba, jíž se dopouštějí i ti nejzkušenější programátoři.
Vzniká, když překladač detekuje příkaz, který není ukončen středníkem. Ale
pozor, chyba může vzniknout i z jiných příčin, například pokud špatně uzavřete
závorky. Další typickou příčinou je, když spojujete řetězec z více částí na
více řádcích a zapomenete napsat mezi řádky znaménko plus:
String zprava = "Pro vaše
jméno " + jméno + " nemáte vytvořen účet. "
"Chcete ho vytvořit nyní?";
Chyba, která vzniká téměř výlučně díky tomu, že máte v kódu špatně zapsány
závorky (chybějící nebo přebývající složené závorky). Nejčastěji se stává
to, že zapomenete metodu uzavřít závorkou a překladač si poté myslí, že
další metody jsou uvnitř první metody, což není přípustné:
public class
Cisla
{
public
int get1(){
return 1;
public
int get2(){
return 2;
}
}
(Zde není metoda get1() uzavřena.)
Chyba, která souvisí s chybou předchozí. Vzniká, pokud vám v kódu jedna složená závorka přebývá. Překladač si poté myslí, že tato přebytečná závorka uzavírá definici třídy a metody napsané po této závorce poté způsobí tuto chybu. Závorka způsobující tuto chybu je obyčejně umístěna bezprostředně nad řádkem, který chybu ohlásil.
Chyba, která také souvisí s chybou předchozí. Vzniká, pokud vám v kódu chybí jedna složená závorka na konci souboru, která uzavře definici třídy.
Chyba vzniká, když překladač narazí na nějaký symbol (vlastnost, proměnnou, metodu, konstruktor, třídu…), ale tento symbol nezná nebo není schopen určit jeho původ. Chyba vzniká, pokud používáte proměnou/vlastnost/metodu, kterou jste ale nedefinovali, nebo třídu, kterou jste zapomněli importovat pomocí deklarace import. Chyba také může vzniknout překlepem v názvu (nejčastěji záměnou velkých/malých písmen). Pro konstruktory může vzniknout také tak, že voláte konstruktor, který není definován (například s jinými parametry). Pro metody může chyba také vzniknout tím, že metoda, kterou používáte, není viditelná (je protected nebo private). Pro metody je chyba svázaná s další chybou:
Tato chyba vzniká, pokud se pokoušíte zavolat metodu s jinou sadou
parametrů, než s jakou byla definována. Například když máte metodu:
public String
udelejNeco(int i,
String s){
tak všechna následující volání budou chybná:
udelejNeco() žádné parametry
udelejNeco("bla") chybí první parametr
udelejNeco("bla", 3) parametry v nesprávném pořadí
udelejNeco("bla", 1, 2) příliš mnoho parametrů.
Správné volání vypadá takto:
udelejNeco(3, "bla")
Tato chyba vznikne, pokud se pokoušíte přiřadit data určitého typu do
proměnné, která má jiný, nekompatibilní typ (například se pokoušíte uložit
řetězec do číselné proměnné). Chyba také může vzniknout překlepem v
konstrukci if, když se použije pouze jedno rovnítko:
if(číslo = 3);
výraz „číslo = 3“ se vyhodnotí jako typ
int, nicméně if vyžaduje
boolean a vznikne tato chyba.
Pozor!, pokud je daný typ proměnné boolean,
chyba není vyhozena – viz dále kapitola Přiřazení v podmínkách.
Chyba se také objeví, pokud se v metodě snažíte vrátit hodnotu jiného typu,
než s jakým byla metoda deklarována (viz dále chyba missing return statement).
Tato chyba vzniká, pokud nedeklarujete hlavičku metody správě. Toto se
nejčastěji stává tak, že deklarujete metodu, ale nedeklarujete její
návratový typ. Pokud nechcete, aby metoda něco vracela, použijte návratový
typ void:
public void
nicNevracim(){
Chyba se také objeví, pokud uděláte překlep v názvu konstruktoru. Jelikož
poté není název konstruktoru shodný s názvem třídy, překladač ho
identifikuje jako normální metodu a vyžaduje u něho návratovou hodnotu.
S návratovými typy metod se váží ještě další chyby:
Tato chyba vzniká, pokud se pokoušíte vrátit hodnotu z metody, jejíž
návratový typ je void (tedy metoda je deklarována,
že nic nevrací):
public void
getJednicka(){
return 1;
}
Pokud chcete, aby metoda vracela nějakou hodnotu, změňte její deklaraci.
Pokud nechcete, aby metoda něco vracela, zrušte příkaz
return nebo z něj odstraňte hodnotu.
Tato je opakem předchozí chyby: deklarujete, že metoda vrací hodnotu, ale
pak v jejím těle žádnou hodnotu nevrátíte:
public int getJednicka(){
//žádný kód
}
Pamatujte, že metoda buď hodnotu vrací, nebo hodnotu nevrací, a pokud vrací,
tak vždy jenom jednoho typu. Nemůžete mít proto metodu, která vrací hodnotu,
pouze pokud jsou splněny určité podmínky nebo která může vrátit hodnoty více
datových typů.
Pro metody s návratovou hodnotou je nutné, aby všechny možné způsoby
zpracování v metodě na konci narazily na příkaz
return. Např. toto je chybně:
public int getPočetVěcí(){
if(početVěcí > 0){
return 1;
}
}
Protože když bude počet věcí ≤ 0, metoda neví, co má vrátit (a je jedno, že
víte, že během běhu nikdy počet věcí nebude ≤ 0, to překladač vědět nemůže).
Tato metoda je také chybná:
public intgetPočetVěcí(){
if(početVěcí > 0){
return početVěcí;
}
else{
return "Nejsou zde žádné věci";
}
}
protože jednou vracíte int a podruhé
String. Pokud chcete vypsat počet věcí textově, je
lepší rozdělit metodu do dvou, jednu s návratovou hodnotu
int a druhou s návratovou hodnotou String:
public int getPočetVěcí(){
return početVěcí;
}
public String vypišPočetVěcí(){
if(početVěcí == 0){
return "Nejsou zde žádné věci";
}
else{
return "" + početVěcí;
}
}
Tato chyba vzniká, když se ze statického kontextu (statická metoda nebo
statický inicializační příkaz/blok) pokusíte získat atribut instance nebo
se pokusíte zavolat metodu instance. Například:
class Barva{
private Color barva;
public
static Color getBarva(){
return barva;
}
atd…
Zde je nutné si uvědomit rozdíl mezi instančním a statickým kontextem. Každá
instance dané třídy má svůj instanční kontext, tj. ty věci, která má odlišná
od jiných instancí. Např. v příkladu výše je atribut barva v instančním
kontextu, tj. různá kola (různé instance třídy Kolo)
mohou mít různou barvu. Statický kontext je ale naopak sdílený pro všechny
instance dohromady. Kdyby byl atribut barva statický, pak by všechna kola měla
stejnou barvu. Když poté ve statické metodě getBarva()
vracíme instanční atribut barva, metoda neví, které
instance to má barva být.
Je ovšem možné tuto instanci poskytnout a barvu vrátit z poskytnuté instance,
např.:
public
static Color getBarva(){
return něco.getKolo().barva;
}
Zde už máme konkrétní instanci třídy Kolo (z objektu
něco) a její barvu již můžeme vrátit. (Předpokladem
ovšem je, že ve statickém kontextu třídy Kolo je nějaký
objekt něco s metodou getKolo()
, která vrátí instanci třídy Kolo.)
Tato chyba vzniká, pokud vaše třída neimplementuje abstraktní metodu ve třídě, jejíž je potomkem nebo neimplementuje všechny metody v rozhraní, které implementuje.
Např. pokud programujete třídu Kruh, která dědí (rozšiřuje, extends) od abstraktní třídy Tvar. Tvar má deklarovánu abstraktní metodu getObvod() s návratovou hodnotou int. (Metoda slouží tomu, aby bylo možno pro všechny tvary získat jejich obvod, a je abstraktní, protože se pro různé tvary počítá obvod jinak.) Tím, že jste rozšířili třídu Tvar, zdědili jste od ní také povinnost implementovat metodu getObvod(). Tj. ve vaší třídě musí být deklarována a implementována metoda getObvod() s návratovým typem int.
Pro kruh by mohla její implementace vypadat takto:
return 2 * Math.PI * průměr;
Pro čtverec takto:
return 4 * strana;
Pro úsečku takto:
return délka;
Pro bod takto:
return 0;
Alternativně můžete svoji třídu deklarovat abstraktní. Povinnost implementovat
metodu getObvod() se poté přenese na vaše potomky.
V tomto případě by to dávalo smysl, pokud byste programovali např. třídu
EnUhelnik, která by představovala jakýkoliv n-úhelník.
Běhové chyby se projeví tak, že program během svého zpracování tzv. „vyhodí chybu“, tj. v určitém okamžiku se program dostane do nestandardního stavu a vygeneruje výjimku. Některé tyto výjimky jsou zachytitelné a zpracovatelné.
K výjimkám se zde nebudu obšírněji vyjadřovat, pouze vás odkážu na literaturu. Ve skriptech od manželů Pavlíčkových je celá kapitola 12 věnována výjimkám a je dokonce přístupná na java.vse.cz. Nabízí popis nejčastějších výjimek a jejich příčiny (některé výjimky jsou popsány detailněji v jiných kapitolách).
V knize „Myslíme objektově v jazyku Java“ od R. Pecinovského jsou výjimky popsány do nejmenších detailů, a to shodou okolností také v kapitole 12. Nabízí obšírnější a detailnější výklad toho, jak s výjimkami pracovat, ale k jednotlivým výjimkám nenabízí příliš mnoho popisu.
Na oficiálních stránkách Javy je k dispozici také tutoriál k výjimkám zde a jednotlivé výjimky si lze vyhledat v dokumentaci zde.
Logické chyby jsou chyby, které způsobíte vy sami tím, že program naprogramujete sice bez syntaktických nebo běhových chyb, program se poté ale nechová tak, jak má (resp. tak, jak od něj očekáváte). Spousta těchto chyb je specifických a nelze na ně uplatnit jednu univerzální poučku.
Poměrně velice častá chyba, kdy programátor napíše program jinak, než jak to zamýšlel. Například zapomene zavolat nějakou klíčovou metodu nebo přiřadí do proměnné špatnou hodnotu.
Zde je vždy důležité si uvědomit, co by danou chybu mohlo způsobit. Je nutné analyzovat symptomy problémů a podle nich si uvědomit, co se při zpracování programu udělá špatně. Např. pokud Tvar při metodě začerni() z plátna zmizí, chyba bude v metodě začerni() nebo v některé metodě, kterou metoda začerni() volá. Můžeme si být téměř zcela jistí, že chyba nebude v metodě posuňDoprava() ani ve třídě Barva. Nebo například když metody začerni() i otoč90() samostatně fungují, ale pokud se zavolá otoč90() po začerni(), tvar se přebarví zpět na svoji původní barvu, je poměrně jasné, že to musí být metoda otoč90(), která se zde chová nestandardně.
Ještě horší chyby jsou chyby v návrhu. Chyba v návrhu je taková, když zjistíte, že se chyba nedá opravit, aniž byste museli změnit vnitřní strukturu programu – tj. to, jak je program navržen. Velice jednoduchá chyba v návrhu by například byla, pokud byste chtěli do metody přidat metodu barvaZpět(), která by změnila barvu Tvaru zpět na předchozí, a během programování byste zjistili, že si historii barev Tvaru vůbec neukládáte. V tomto případě byste si museli do třídy Tvar přidat atribut minuláBarva a tento správně nastavovat v metodě setBarva().
Cykly jsou užitečné, pokud chcete, aby program vykonal něco vícekrát. Program sám nepozná, kolikrát se má něco vykonat, musíte mu to říct. A když mu to řeknete špatně, tak cyklus nikdy nezačne nebo naopak nikdy neskončí.
Nekonečný cyklus je takový, kde jeho podmínka ukončení bude vždy taková,
aby cyklus nikdy neukončila. Například cyklus
for(int i = 0; i < 10; i++){
//udělej něco
i = 0;
}
bude přičítat donekonečna, neboť před každou iterací (= průchodem cyklu), se
řídicí proměnná i vždy vynuluje, tj. nikdy nebude větší
nebo rovno 10.
Nekonečné cykly se využívají tam, kde ani za běhu není jasné, kolikrát se bude cyklus opakovat (např. při komunikaci po síti – zde není jasné, kolik zpráv ze sítě obdržíme, neboť to nezáleží na nás). Cykly se pak běžně ukončují příkazem break. Záměrně nekonečný cyklus se běžně zapisuje jako for(;;) nebo jako while(true).
Nulový cyklus je takový cyklus, který neproběhne ani jednou. Např.:
for(int i = 0; i > 10; i++){
System.out.println(i);
}
Takovýto cyklus měl vypsat číslo od 0 do 9, ale nevypíše nic. V podmínce je
překlep – je použito opačné znamínko. Při spuštění cyklu se přiřadí i=0
a přejde se na podmínku. Ta vyhodnotí, že i není větší než 10 a cyklus ukončí.
Další typickou chybou bývá použití zkráceného zápisu bez složených závorek:
for(int i = 0; i < 10; i++)
něco();
Pokud omylem za cyklus for přidáte středník, cyklus proběhne, aniž by cokoliv
zpracoval a příkaz něco() proběhne pouze jednou:
for(int i = 0; i < 10; i++);
něco();
S tím souvisí i další úskalí zkráceného zápisu. Pokud totiž budete chtít
do cyklu přidat další příkaz, nesmíte to napsat takto:
for(int i = 0; i < 10; i++)
něco();
něcoJiného();
protože pak se na příkaz něcoJiného() již cyklus nevztahuje.
Cyklus je nutné zapsat standardním zápisem:
for(int i = 0; i < 10; i++){
něco();
něcoJiného();
}
Toto úskalí zkráceného zápisu se týká také podmínek if
a if-else, cyklu for-each a
cyklu while.
S touto chybou se také můžete občas setkat. Také souvisí s cykly, tentokrát s cyklem for-each. (Tj. s cyklem, který prochází Iterable objekt, nejčastěji nějakou kolekci - instanci rozhraní Collection – např. for(String jmeno : jmena).)
Jde o to, že během procházení dané kolekce cyklem nesmíte tuto kolekci změnit.
Pokud ji změníte, vyhodí se ConcurentModificationException.
Např. nesmíte udělat tohle:
for(String jmeno : jmena){
System.out.println(i);
jmena.remove(jmeno);
}
Oprava této chyby je jednoduchá, pokud je daná kolekce implementací rozhraní
List – daný cyklus převeďte na klasický
for cyklus:
for(int i = 0; i < jmena.size(); i++){
System.out.println(i);
jmena.remove(i);
}
Pro rozhraní Set není tato operace zcela jednoznačná,
neboť neumožňuje získání elementů pomocí jejich indexů.
Například byste mohli vypsání a vymazání rozdělit do dvou příkazů:
for(String jmeno : jmena){
System.out.println(jmeno);
}
jmena.clear();
Nejkorektnějším způsobem je pak použití iterátoru:
Iterator<String> iterátor = jmena.iterator();
while(iterátor.hasNext()){
System.out.println(iterátor.next());
iterátor.remove();
}
Ne všechny implementace iterátoru ale příkaz next()
podporují – některé při jejich použití vyhodí UnsupportedOperationException.
Jedna z nejzákeřnějších chyb je použít pouze jedno = pro boolean
proměnné v podmínkách, např.:
if(jePlny = true)
nebo
if(jePlny = batoh.jePlny())
Tato chyba vzniká, když omylem uvedete jedno rovnítko místo dvou rovnítek.
Překladač v tomto případě nenahlásí žádnou chybu, ale program se začne chovat
chybně (místo porovnání dojde k přiřazení hodnoty na pravé straně do proměnné
na levé straně a tato hodnota se poté použije pro vyhodnocení příkazu if).
Pokud nevíte, kde přesně máte hledat, je tato chyba velmi obtížně odhalitelná.
Tato konstrukce již byla několikrát omílána v učebnici a jistě i na přednášce. Je ale velice důležitá, tak si jí zde zopakujeme s tím, co její nedodržení způsobí za chybu.
Základní rozdíl mezi == a equals() je ten, že operátor == porovnává, zda jsou oba operandy shodné. Pro primitivní typy (int, boolean, double, atd…) porovnává, jestli mají oba stejnou hodnotu. Pro objektové typy porovnává, zda oba odkazují na stejný objekt, tj. na stejnou jednu konkrétní instanci.
Např. si představte, že máte třídu Kolo, kde se
kola neliší ničím jiným než svou barvou. Dále se podívejte na následující kód,
který vytváří kola:
Kolo kolo1 = new
Kolo("červená");
Kolo kolo2 = kolo1;
V tomto případě by kolo1 == kolo2 vrátilo true – oba odkazy kolo1 i
kolo2 ukazují na jeden a ten samý objekt. Pokud
byste ale druhý řádek zaměnili za
Kolo kolo2 = new
Kolo("červená");
pak již příkaz kolo1 == kolo2 vrátí false, neboť každá proměnná již ukazuje na jiný objekt,
a nezáleží na tom, že obě kola jsou úplně stejná.
Metoda equals() dělá vpodstatě totéž – v defaultní implementaci ve třídě Object porovnává instance. Rozdíl je v tom, že metoda equals() se dá přetížit – a implementovat zcela jiný systém pro porovnávání objektů. Např. třída String má tuto metodu přetíženou a porovnává obsah daného řetězce, ve třídě Integer se porovnávají uložená čísla, atd. Pro výše uvedenou třídu Kolo by se dala metoda equals() přetížit tak, aby porovnávala barvy kol.
Neříkám, že byste měli porovnávat vždy pomocí metody
equals(), protože někdy se porovnávání instancí hodí. Měli byste si
ale dávat pozor a rozlišovat mezi těmito dvěma možnostmi. Nejvíce chyb se
dělá při porovnávání Stringů. Lidé si myslí, že
když napíší
jméno == "pepa"
tak porovnávají obsah řetězců. Proměnná jméno sice může obsahovat řetězec
"pepa", ale objekt to může být zcela jiný a operátor
pak vrátí false.
Dalším kamenem úrazu je kolekce HashSet (ale
problém se obecně týká všech implementací rozhraní Set),
která nedovoluje vložení více shodných prvků, a tato shodnost je vyjádřena právě
metodou equals(). Situace je sice o něco složitější,
vás ale může zajímat jen to, že nesmíte udělat například tohle:
HashSet<String> =
new HashSet<
String>();
jména.add("Pepa")
jména.add("Jirka")
jména.add("Pepa")
V tomto případě se při druhém přiřazení hodnoty "Pepa"
usoudí, že už jeden Pepa v kolekci je a do kolekce přidán nebude. Při výpisu
kolekce:
for(String jméno : jména){
System.out.println(jméno);
}
se tedy vypíše pouze jeden Pepa a jeden Jirka, a ne dva Pepové a jeden Jirka,
jak by se mohlo na první pohled zdát.
Pozn.: pro řetězce může někdy operátor == fungovat. Je to zapříčiněno optimalizátorem, který totožné řetězce zapsané jako literály (tj. natvrdo v kódu) nevytváří vícekrát, ale místo toho vždy odkáže na jeden daný řetězec. Obecně by ale bylo chybou na tuto funkci v kódu spoléhat.
Tuto výjimku vám program vyhodí, když chcete udělat nějakou operaci s odkazovou
proměnnou, která však neukazuje na žádný objekt (tj. ukazuje na
null). Nejčastěji se tato chyba stává, když si ve třídě deklarujete
nějaký atribut, který ale následně zapomenete inicializovat (tj. přiřadit mu
hodnotu). Další častou příčinou je, že používáte nějakou metodu, která může
vrátit null, ale s tímto už dále v kódu nepočítáte:
/* může vrátit null pokud není nikdo přihlášen */
String jmeno = getPrihlasenyUzivatel();
/* může vyhodit NullPointerException, pokud není nikdo přihlášen */
jmeno.toLowerCase();
Problémem této chyby je, že pokud se vyskytne na nějakém řádku, kde se děje
několik věcí, nelze jednoznačně určit, která proměnná zrovna byla null:
uzivatele.add(getPrihlasenyUzivatel().getJmeno().toLowerCase());
Pokud se na tomto řádku vyskytne NullPointerException,
může to být proměnná uzivatele, výsledek metody
getPrihlasenyUzivatel() nebo výsledek metody
getJmeno(), co mohlo výjimku způsobit. Chybová hláška vám ale neřekne,
která to byla, na to musíte přijít sami.
Jedním z postupů, jak tohoto dosáhnout, je rozdělit si složitý příkaz na
jednotlivé elementární příkazy:
Uzivatel prihlasenyUzivatel = getPrihlasenyUzivatel();
String jmeno = prihlasenyUzivatel.getJmeno();
String jmenoMalymiPismeny = jmeno.toLowerCase();
uzivatele.add(jmenoMalymiPismeny);
Podle toho, na kterém řádku pak dojde k NullPointerException
pak poznáte, která proměnná byla null.
Pokud dojde na řádku 2, pak metoda getPrihlasenyUzivatel()
vrátila null. Pokud na řádku 3, pak metoda
getJmeno() vrátila null.
Pokud na řádku 4, pak proměnná uzivatele má hodnotu
null.
Tato chyba je spíše již teoretická pro vstupní kurzy, protože se s ní nesetkáte přímo, ale je dobré, abyste věděli, že existuje a že představuje nejenom chybu, ale v některých případech také bezpečnostní riziko.
Zde jde o to, že uživatelé interagují s programem pomocí ovládacích prvků (vstupní pole, vstupní formuláře, databáze, atd.) Tento vstup ale nemusí být vždy korektní. Např. když programujete textovou adventuru, tak uživatel jako příkaz může napsat nějaký nesmyslný řetězec, popřípadě příkazu může zadat nesmyslné parametry. Pokud byste tento vstup nevalidovali, tak byste zjistili, že se pokoušíte vykonat příkaz, který není definovaný, nebo obsloužit parametr, který nedává smysl.
Proto existuje uživatelská validace. Než program začne pracovat s uživatelským vstupem, měl by si zkontrolovat, že tento vstup dává smysl (např. že příkaz existuje). Při validaci pak záleží na vás, jak uživatelovi řeknete, že zadal neplatné příkazy. Běžnou praxí pro programy s grafickým rozhraním je zobrazit dialogové okno, pro textové rozhraní vypsat chybovou hlášku přímo pod příkaz.
Chyba dostává ještě jiný rozměr v případě, že uživatelský vstup používáte přímo v příkazech pro databázi. Pokud tento vstup nevalidujete, vystavujete se možnosti útoku pomocí SQL injection (více na http://cs.wikipedia.org/wiki/SQL_injection).
Jedna z nejzákeřnějších chyb, které můžete ve svých programech udělat. Překrytelná metoda je taková metoda, kterou mohou potomci zdědit a překrýt vlastní verzí metody. Jsou to všechny metody, které nejsou private a zároveň nejsou final.
Ačkoliv použití takovéto metody v konstruktoru není chybou samo o sobě, představuje rizikové místo, které může vyvolat chybu až dlouho poté, co byla třída naprogramována (zpravidla v tu nejméně vhodnou dobu). Jde o to, že když použijete v konstruktoru překrytelnou meto-du, nikdy nevíte, kdo vaší třídu zdědí a co budou s tímto potomkem dělat, jaké metody překryjí a co v těchto metodách budou dělat.
Zde je příklad: Máte třídu Čtverec, který se po
vytvoření ihned namaluje na plátno (čísel si zatím nevšímejte):
class Ctverec
{
int strana;
Platno platno;
Ctverec(int strana, Platno platno){
2) this.strana = strana;
3) this.platno = platno;
4) namaluj();
}
void namaluj(){
platno.namalujCtverec(strana);
}
}
a pak máte třídu barevný čtverec, který k tomuto základnímu čtverci přidává
barvu:
class BarevnyCtverec extends Ctverec
{
Barva barva;
Ctverec(int strana, Platno platno, Barva barva){
1) super(strana, platno);
6) this.barva = barva;
}
void namaluj(){
5) platno.namalujCtverec(strana, barva);
}
}
Zde je si důležité uvědomit, jak jdou postupně příkazy za sebou. Já jsem příkazy
očísloval, jak jdou za sebou při zavolání konstruktoru
BarevnyCtverec(Platno platno, int strana, Barva barva). Zde si všimněte,
že došlo k chybě tím, že k malování (příkaz 5) dojde ještě před tím, než se
přiřadí barva (příkaz 6), tj. čtverec se namaluje bez barvy. (Což např. může
vyvolat NullPointerException v metodě namalujCtverec(), protože se v parametru barva předá
hodnota null.)
Tato chyba vzniká, pokud chcete přistupovat k prvku pole nebo řetězce na indexu, který neexistuje. Toto je téměř vždy zapříčiněno tím, že si neuvědomíte, že první prvek má index 0 a poslední prvek má index délka-1.
Příklad pole:
Prvek | "Jana" | "Lída" | "Adéla" | "Klára" | "Kristýna" |
---|---|---|---|---|---|
Index | 0 | 1 | 2 | 3 | 4 |
Chyba se vždy projeví tím, že se program chová jinak, než zamýšlíte. Jak jsem již uvedl, měli byste začít vyšetřovat příčinu chyby. Pokud na příčinu nepřijdete (nebo jste líní analyzovat hromadu kódu příkaz po příkazu), musíte začít program ladit (debuggovat).
Dva nejběžnější ladící postupy jsou logování a programový debugging. Zatímco logování je jednodušší forma a používá se pro odhalení jednoduchých chyb, programový debugging je mocný a sofistikovaný nástroj, se kterým lze odhalit téměř všechny chyby (ale na malé chyby se nehodí – je to jako vzít příslovečný kanón na mouchu).Ladění má za cíl poskytnout programátorovi stav zpracování programu v určitých daných časových okamžicích. Programátor tento skutečný stav musí porovnat se stavem chtěným, a pokud se tyto dva stavy liší, měl by program opravit, aby se tyto stavy již nelišily. Znovu zde ale zdůrazňuji, že svému programu musíte opravdu rozumět. Pokud nevíte, jak se má program v určitém okamžiku chovat, nevíte ani, jestli je daný stav správný nebo ne.
Tato technika se dá použít pouze pro ty nejjednodušší chyby. Nejčastěji se používá, pokud znáte příčinu chyby již od začátku, pouze musíte v kódu nalézt dané inkriminované místo, které chybu vyvolává.
Pro složitější ladění je tato technika nevhodná. Zaprvé si nemůžete zapamatovat všechen relevantní kód, který s chybou může souviset. A za druhé je všeobecně známo, že člověk své chyby nevidí – je příliš ovlivněn svou představou co by mělo být a špatně od tohoto odlišuje co skutečně je.
Pokud ve svém programu narazíte na chybu, u které si nejste zcela jistí, co ji způsobuje, měli byste chybu zkusit odhalit logováním. Nejzákladnějším logovacím nástrojem jsou systémové hlášky System.out.pritnln(). Ty vám umožňují na standardní výstup zapsat jakoukoliv informaci a tyto zápisy dokonce po ukončení programu nezmizí (tj. pomocí nich můžete upravovat kód).
Je samozřejmě poněkud hloupé uvádět logovací hlášky za každý příkaz – zaprvé 99% příkazů nebude mít s chybou nic společného, a za druhé by těchto hlášek bylo takové množství, že byste se v nich stejně nevyznali. Dejte jednu, dvě či tři tyto hlášky na místa, která by mohla mít s chybou něco společného. Např. pokud chcete nakreslit na plátno Tvar, ale ten se při spuštění programu nenakreslí, umístěte do metody nakresli() třídy Tvar jednoduchou hlášku System.out.println("Teď se kreslím"). Po spuštění programu uvidíte, zda se tato hláška objeví (a problém bude ve třídě Plátno, které daný Tvar odmítlo nakreslit), nebo se neobjeví (a vy budete vědět, že je problém ve třídě Tvar, jehož metoda nakresli() není nikdy zavolána).
Pokud těchto hlášek dáváte více, měli byste vědět, v jakém pořadí budete
očekávat jejich výpis. Pokud např. kreslíte tvary, ale jeden tvar bude ve
výpisu chybět, měli byste začít zkoumat, proč se tento tvar nenakreslil,
ale ostatní ano. Pokud chcete zkoumat zpracování cyklu, a v těle tohoto
cyklu je více příkazů a více vypisujících hlášek, měli byste si na začátek
cyklu umístit jednoduchou hlášku
System.out.println("-------------");
abyste pak ve výpisu jasně poznali, kdy začala nová iterace cyklu.
Do hlášek můžete samozřejmě dynamicky přidávat hodnoty proměnných. Např.
místo jednoduchého
System.out.println("Teď se kreslím")
můžete vypsat něco více smysluplného, jako např.:
System.out.println("Kreslím obdélník na souřadnicích ["
+ x + ", " + y +
"] a velikosti " + šírka +
" na " + výška +
" s barvou " + barva);
Výpis takové hlášky pak může vypadat např.:
Kreslím obdélník na souřadnicích [25, 90] a velikosti
80 na 120 s barvou červená
což vám o zpracování kreslení obdélníku poví o
mnoho více, než jednoduché „Teď se kreslím“.
Jak jsem již uvedl výše, pokud dojde v programu k neošetřené výjimce, program
automaticky vypíše chybu na výstup. S touto chybou se vypíše i tzv. stack trace,
tj. popis toho, jak se program k chybě dostal. Informace z tohoto stack trace
často bývají dostačující k určení příčiny chyby. Výpis chyby se stack tracem
může vypadat např. takto:
Exception in thread "main" java.lang.IllegalArgumentException: Takto pojmenovanou barvu neznam.
at tvary.Barva.getBarva(Barva.java:93)
at tvary.Obdelnik.setBarva(Obdelnik.java:238)
at tvary.Main.main(Main.java:13)
Takový výpis vám říká, že v příkazu getBarva() ve
třídě Barva na řádku 93 došlo k chybě IllegalArgumentException. Do této metody se program
dostal z příkazu na řádku 238 v metodě setBarva()
ve třídě Obdelnik. A do této metody se program dostal
z metody main() ve třídě Main
na řádku 13.
Stack tracy bývají někdy desítky příkazů dlouhé, ale obvykle stačí zanalyzovat první dva nebo tři příkazy, abyste příčinu chyby objevili. Jakmile začnete programovat složitější programy, mohou se vám objevit výjimky, kde bezprostřední příčina není na prvním příkazu. Pak je nutné projet výpis chyby tak daleko, až narazíte na příkaz, který pochází z vašeho kódu. Tam je potřeba začít hledat.
Někdy vám hlášky System.out.println() nebo standardní chybový výstup nemusí stačit. Především někomu může vadit, že pro každou hlášku musí napsat System.out.println(), což je poměrně dlouhý příkaz. Hlavní nevýhodou ale je, že si nemůžete vybrat, které hlášky zobrazíte (prostě se zobrazí všechny) a kam tyto hlášky zobrazíte (všechny půjdou na standardní výstup). Pokud programujete nějaký větší projekt a všechny hlášky v programu necháte, za chvíli se vám jich bude generovat tolik, že se v nich nevyznáte. Nebo pokud chcete výpis hlášek poslat kamarádovi, nemusí se vám vždy hodit to kopírovat ze standardního výstupu (např. ICQ má omezenou délku zprávy), lepší by bylo, kdyby se hlášky zapsaly do souboru, který pak kamarádovi pošlete.
Proto vám doporučuji vytvořit jednoduchou třídu Log:
public class Log
{
public static void log(String log){
System.out.println(log);
}
}
Tato třída má několik výhod. Zaprvé, je jednodušší a kratší psát Log.log() než System.out.println() . Dále kdykoliv budete chtít např. začít logovat do souboru, stačí si tuto třídu upravit, aby zprávy posílala také do souboru. Když budete chtít logovat jenom některé zprávy, stačí si do metody log() přidat parametr, který určuje skupinu zprávy. Ve třídě Log pak nastavíte, která skupina zpráv se má vypisovat. Více skupin má také tu výhodu, že pokud pracujete v týmu, můžete si přiřadit skupiny a pak se vám budou vypisovat jenom vaše zprávy a ne zprávy ostatních.
V tomto případě jsou možnosti téměř neomezené – se třídou
Log si můžete dělat, co se vám zlíbí a co se vám zrovna hodí. Např.
při odevzdání programu již nebudete chtít tyto zprávy vypisovat. Není nic
jednoduššího, než je v kódu zakázat:
public class Log
{
private static final boolean logujeme = false;
public static void log(String log){
if(logujeme == true){
System.out.println(log);
}
}
}
Takový kód už program ani nezpomalí – již v době překladu je jasné, že metoda
log() nikdy nic dělat nebude, tak optimalizátor její
volání ze zpracování vynechá. Pokud byste jen přeci potřebovali opět hlášky
zapnout, stačí změnit atribut logujeme na true.
Nevýhodou této třídy je, že ji musíte pokaždé importovat (což
System.out.println() nevyžaduje).
Pokud nejste ochotní nebo neumíte naprogramovat vlastní logovací třídu, můžete použít již hotové logovací frameworky. Ačkoliv logovací frameworky se v konceptu neliší od vlastní logovací třídy, jsou již dotažené do konce profesionálními programátory a cokoli od nich potřebujete, je pravděpodobně v nich již naprogramováno. Existují dva nejpoužívanější: zabudované Java Logging API a Apache Log4J.
Pokud na vhodná místa programu dáte takovéto aserce, pak je odhalení nové chyby potencionálně mnohem jednodušší. I když si chybu zanesete již do o testovaného kódu, nebo by se chyba projevila pouze nepřímo, aserce vás v tomto případě ihned upozorní, že něco není v pořádku a kde to není v pořádku. Ušetří vám tak mnoho času testováním.
Spíše než ladící nástroj se aserce používají jako nástroj pro kontrolu kvality. Kdykoliv program spustíte, mohou aserce pracovat jako pojistka proti nebezpečným činnostem, které by např. mohly poškodit uživatelská data. Defaultně je zpracování asercí v Javě vypnuto.