JAK HLEDAT CHYBY V PROGRAMECH

ÚVOD

JAK CHYBY VZNIKAJÍ

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:

  1. Syntaktické chyby – tyto chyby se projevují tak, že nedovolí programu, aby se vůbec přeložil. Překladač obecně při překladu uživateli napíše, co brání přeložení kódu a v jakém souboru a na jakém řádku byla chyba nalezena.
     
  2. Běhové chyby – tyto chyby nebrání překladu kódu, nicméně při spuštění programu nebo při uživatelské akci se program dostane do nestandardní situace, kterou program ohlásí vygenerováním výjimky nebo systémové chyby (instance tříd Exception nebo Error). Takováto chyba může být programátorem zachycena a zpracována, ale často není zpracování chyby možné. Programové vlákno, ve kterém se takováto chyba stala, je poté ukončeno.
     
  3. Logické chyby – nejzávažnější chyby, kterých se programátor může dopustit. Tyto chyby jsou závažné tím, že často nemají žádné bezprostřední symptomy nebo se projevují pouze nepřímo. Z pohledu překladače ani JVM nedojde k žádné nestandardní situaci a programu je povoleno pokračovat. Podstatou chyby je rozpor mezi tím, co má program dělat, a tím, jak byl naprogramován.
     

JAK CHYBY NAJÍT

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.

JAK CHYBY OPRAVIT

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):

TYPICKÉ CHYBY

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.

SYNTAKTICKÉ CHYBY

Zde vám popíšu několik chyb, které může překladač při zpracování kódu vypsat:

not a statement

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.

; expected

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í?"
;

illegal start of expression

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.)

class, interface or enum expected

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.

reached end of file while parsing

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.

cannot find symbol

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:

[metoda(parametry)] in [třída] cannot be applied to ( parametry)

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")

incompatible types

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).

invalid method declaration

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:

cannot return a value from method whose result type is void

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.

missing return statement

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í;
   }
}

non-static [jméno] cannot be referenced from a static context

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.)

[jméno třídy] is not abstract and does not override abstract method in [třída]

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

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

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.

Program nedělá to, co si myslíme

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().

Nekonečný / nulový cyklus

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.

ConcurentModificationException

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.

Přiřazení v podmínkách

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á.

== a metody equals(), kolekce HashSet

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.

NullPointerException

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.

Uživatelský vstup

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).

Použití překrytelné metody v konstruktoru

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.)

IndexOutOfBoundsException

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"
Index01234

Ačkoliv dané pole má 5 prvků (tj. length = 5), prvek na pozici 5 neexistuje, poslední prvek má index 4. Pokud chcete přistupovat k poslednímu prvku pole bez ohledu na délku, musíte napsat length - 1. S tím také souvisí cyklus pro procházení polí:

for(int i = 0; i <= pole.length; i++)

je špatně, protože pole projde od 0 do length. Správně:

for(int i = 0; i < pole.length; i++)
for(int i = 0; i <= pole.length - 1; i++)

Chyba také vzniká, pokud používáte metodu substring() na řetězci. Např. pokud chcete získat celý String kromě prvního písmena, musíte napsat string.substring(1, string.length-1).

JAK LADIT (DEBUGGING)

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.

RUČNÍ PROCHÁZENÍ KÓDU

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.

LOGOVÁNÍ

Logování je jednoduchá ladící procedura spočívající v tom, že do kódu programu přidáte příkazy, které vypisují (nejčastěji na standardní výstup) stav programu. Tento výpis dokonce Java sama generuje v případě, že se v programu vyskytne neošetřená výjimka – místo toho, aby program spadl bez jakékoliv informace, napíše vám, co se stalo za chybu a kde.

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“.

STACK TRACE

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.

VLASTNÍ LOGOVÁNÍ

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.

ASSERT

Příkaz assert byl do Javy přidán s verzí 1.4. Je to přímo konstrukce v jazyce Java, která vám umožní porovnávat danou hodnotu s true nebo false. Příklad:

assert uživatel.getJmeno().equals("Pepa");

Pokud je příkaz za slovem assert vyhodnocen jako true, program pokračuje dál. Pokud je vyhodnocen jako false, program vygeneruje výjimku AssertionError, která, pokud není zachycena, zastaví chod programu.

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.

LADĚNÍ S POMOCÍ DEBUGGERU

Mnoho integrovaných vývojových prostředí poskytuje vývojářům debugger. Debugger je vestavěná utilita, která umožňuje programátorům zkoumat svůj program a řídit jeho zpracování v reálném čase za běhu.
I BlueJ obsahuje tento nástroj, i když primitivnější než jaký mají profesionální vývojová prostředí. Jeho použití je popsáno v povinné literatuře a i na Internetu (v BlueJ tutoriálu na straně 27 – viz. zde). Jeho praktické použití najdete demonstrované na některých videích o kousek dole.

ODHALENÍ NEJČASTĚJŠÍCH CHYB

Zde najdete několik videí, která vám přiblíží postup ladění na několika nejčastějších chybách. Doporučuji videa shlédnout po sobě, neboť na sebe jednotlivě navazují. Základní neupravený projekt je k dispozici zde.

PROGRAM NEDĚLÁ, CO SI MYSLÍME

Video je k dispozici zde. Projekt je ke stažení zde.

OPERÁTOR PŘIŘAZENÍ V PODMÍNCE

Video je k dispozici zde. Projekt je ke stažení zde.

NULOVÝ CYKLUS

Video je k dispozici zde. Projekt je ke stažení zde.

ZÁMĚNA == A EQUALS()

Video je k dispozici zde. Projekt je ke stažení zde.

METODA EQUALS() A TŘÍDA HASHSET

Video je k dispozici zde. Projekt je ke stažení zde.

PŘEKRYTELNÁ METODA V KONSTRUKTORU

Video je k dispozici zde. Projekt je ke stažení zde.

PŘEKRYTELNÁ METODA V KONSTRUKTORU – VYHOZENÍ

Video je k dispozici zde. Projekt je ke stažení zde.