COMPUTERWORLD
Specializovaný týdeník o výpočetní technice
o Internetu
(CW 50/97)

Programování v jazyce Java

David Štrupl

Řetězce

Řetězce se používají k ukládání textových konstant. Syntaxe použití řetězců je podobná jazyku C, ale stejně jako u polí (a možná ještě více) jsou i zde rozdíly. Vyplývají z toho, že řetězce jsou v Javě objekty. Tedy na rozdíl od jazyka C řetězce v Javě nejsou pole znaků.

Pro práci s řetězci jsou v balíku java.lang dvě třídy: String a StringBuffer. První z nich slouží k ukládání konstantních řetězců a jsou s ní spojeny nové syntaktické konstrukce. Třída StringBuffer je obyčejná pomocná třída sloužící k manipulaci s jednotlivými znaky řetězce.

Do proměnných typu String lze přiřazovat textové (String) konstanty:

String s = "Ja jsem String";

Zápis s uvozovkami vlastně vytváří nový objekt třídy String. Na tento objekt lze vyvolávat všechny metody definované ve třídě String stejně jako na příslušnou proměnnou:

String t = s.toUpperCase();


String u = "hele".toUpperCase(); // povolený zápis

Objekty třídy String mají konstantní hodnotu, tj. řetězec jednou uložený do proměnné třídy String už nelze měnit. Metody, které zdánlivě mění hodnoty jednotlivých znaků, vždy vytvářejí nový objekt třídy String s příslušnými změnami. Důvodem pro toto (na první pohled nepochopitelné) chování řetězců je to, že konstantní řetězce jsou používány ve virtuálním stroji k ukládání jmen, a jsou tedy v class souborech ukládány zvláštním způsobem. Běžnému programátorovi nemusí konstantnost řetězců vadit, protože jejich použití v programu je relativně jednoduché a pochopitelné.

Jediné, na co musíme dávat při použití objektů třídy String pozor, je porovnávání řetězců. Použití operátoru == totiž většinou nepřinese kýžený efekt, protože tento operátor u typu reference na objekt (jímž typ String bezesporu je) porovnává odkazy a nikoliv obsahy příslušných objektů. Chceme-li porovnat dva řetězce, použijeme nejpravděpodobněji metodu equals nebo equalsIgnoreCase:

String s = "ahoj";


if (s.equals("ahoj"))


//běžný test na stejnost řetězců


...


if ("ahoj".equals(s))


// i toto je správný zápis


...


Kromě použití textových konstant je s typem String spojena ještě jedna zvláštnost -- lze na něj použít operátor +. Při sčítání řetězců dojde k jejich zřetězení. Výsledkem součtu je nový řetězec s příslušným obsahem. Operátor + lze použít buď samostatně, nebo v kombinaci s přiřazením:

String t = "Ja jsem" + "retez";


t += "ec"; // připojení na konec řetězce

Ani při jednom ze způsobů zápisu však nedochází k modifikaci žádného ze sčítaných řetězců, protože, jak již bylo řečeno, typ String reprezentuje konstantní řetězce. Vždy je naalokován nový objekt a vrácen odkaz na něj. Pokud chceme sami měnit hodnoty jednotlivých znaků, použijeme třídu StringBuffer. Objekty třídy String můžeme převést na objekty třídy StringBuffer pomocí konstruktoru třídy StringBuffer, který má jako parametr String:

String s = "retez";


StringBuffer sb = new StringBuffer(s);

S proměnnou třídy StringBuffer můžeme potom pracovat pomocí jejích metod, které přistupují k jednotlivým znakům nebo nějakým jiným způsobem modifikují obsah řetězce. Převod zpět do podoby řetězce je možný např. použitím metody toString().

Chyby

Nyní se podíváme na to, jak si prostředí Javy poradí s chybami, které mohou nastat při běhu aplikací. Jednotným mechanismem uplatňovaným jak v prostředí virtuálního stroje Java (JVM), tak v uživatelských programech, je mechanismus zpracování výjimek. Základní principy byly opět převzaty z jazyka C++.

Zpracování výjimek

S výjimkami jste se mohli setkat, pokud se vám něco nepodařilo a váš program skončil svoji činnost předčasně ohlášením nějaké chyby. Pokud se vám něco takového stalo, jistě jste si všimli, že prostředí Javy vypíše např. toto:

Exception occurred during event dispatching:

java.lang.NullPointerException


at java.io.File.<init>(File.java:108)


at MujProgram.handleEvent(MujProgram.java:103)


at java.awt.Window.postEvent(Window.java:409)


at java.awt.MenuComponent.postEvent(MenuComponent.java:128)


at java.awt.MenuComponent.dispatchEventImpl(MenuComponent.java:156)


at java.awt.MenuComponent.dispatchEvent(MenuComponent.java:138)


at java.awt.EventDispatchThread.run(EventDispatchThread.java:65)


Každá chyba (výjimka, Exception) je reprezentována jedním objektem. Tento objekt je vytvořen v místě, kde k chybě došlo. Z objektu reprezentujícího chybu se můžeme dozvědět, o jakou chybu se jedná a kde nastala. Třída, která reprezentuje chyby, je potomkem třídy Throwable.

To však není všechno. Pokud víme, že některá část programu může způsobit výjimku, můžeme část kódu označit jako tzv. chráněný blok a na konci tohoto bloku určit, co se má provést, pokud při jeho provádění dojde k výjimce. Strážený blok zapíšeme pomocí příkazu:

try {


// strážený blok


} catch (ExceptionType1 name1) {


// kód zpracovávající chybu 1


} catch (ExceptionType2 name2) {


// kód zpracovávající chybu 2


}

Příkazy stráženého bloku jsou prováděny postupně -- stejně jako v každém jiném bloku příkazů. Pokud však nastane během jejich provádění nějaká výjimka, bude se její typ porovnávat s typem uvedeným v klauzuli catch. Provádění stráženého bloku je přerušeno a nastává zpracování chyby. Klauzule catch jsou probírány postupně, při nalezení první, která dokáže chybu zpracovat, je proveden blok jejího kódu. Po provedení kódu zpracovávajícího chybu výpočet pokračuje dalším příkazem za příkazem try.

Porovnávání typů výjimek probíhá tak, že typ uvedený za catch musí být buď stejný jako typ chyby, nebo musí být jejím předkem v hierarchii dědění. Pokud se typ chyby nevyskytuje v žádné z klauzulí catch, chyba nebyla příkazem try ošetřena a provádění celého bloku, ve kterém se příkaz try nachází, skončí neúspěchem. V bloku klauzulí catch může být jeden příkaz finally, jehož blok se provede vždy při ukončení stráženého bloku -- v případě, že nastala chyba, i v případě, že žádná nenastala:

try {


// strážený blok


} catch (ExceptionType1 name1) {


// kód zpracovávající chybu 1


} finally {


// kód provedený vždy před opuštěním bloku


}

Způsobení chyby

V jazyce Java máme možnost chyby (výjimky) nejen zpracovávat, ale také je můžeme způsobovat. K vyvolání chyby slouží příkaz throw. Jako parametr musí být objekt -- potomek třídy Throwable, který bude reprezentovat nastalou chybu.

Celé způsobení chyby může vypadat např. takto:

...

throw new FileNotFoundException();

Příkaz throw musí být buď uveden ve stráženém bloku, nebo musíme označit metodu, která jej obsahuje. Metodu způsobující výjimku označíme klíčovým slovem throws následovaným druhem výjimky:

void mojeMetoda() throws FileNotFoundException {


...


throw new FileNotFoundException();


}


Z deklarace (a tedy také z dokumentace) k jednotlivým balíkům (knihovnám) je potom vždy jasně patrné, které metody mohou způsobovat výjimky.

Druhy chyb

Jak již bylo uvedeno výše, všechny způsobitelné chyby jsou potomky třídy Throwable. Tyto třídy lze dále rozdělit na tři velké skupiny -- potomky tříd: Error, Exception a RuntimeException.

Třída Error a její potomci reprezentují chyby JVM. Příkladem takové chyby může být OutOfMemoryError. Tyto chyby by v uživatelských programech asi ani neměly být zpracovávány, protože se jedná o chyby, z nichž je velice obtížné se zotavit.

Třída Exception a její potomci jsou standardní chyby, které uživatel vyvolává a ošetřuje. Na tyto výjimky se v plné míře vztahuje ustanovení předchozího odstavce o tom, že musí být buď chyceny, nebo deklarovány příkazem throws.

Třída RuntimeException, přestože je potomkem třídy Exception, má jednu zvláštnost -- výjimky tohoto druhu nemusí být deklarovány. Do této kategorie spadají totiž běžné výjimky NullPointerException, ArrayIndexOutOfBoundsException a podobně, které by musely být deklarovány prakticky u všech metod.

Definování vlastních výjimek

Pokud naše metodu způsobuje nějaký nový druh chyby, můžeme si vytvořit vlastní druh výjimky. Tento postup je naprosto běžný. Stačí vytvořit potomka třídy Exception:

class MojeException extends Exception {


public MojeException() { }


public MojeException(String what) {


super(what); // parametr popisující chybu


}


}

Máme-li nový druh chyby deklarován, můžeme jej použít podobně jako již existující výjimky:

throw new MojeException();

Vytváření vlastních výjimek je doporučený postup při psaní metod, které mohou skončit neúspěchem. Příkladem může být neexistence souboru, nemožnost navázat síťové spojení apod. Ve všech těchto případech se hodí použití mechanismu výjimek.

Abstraktní třídy

Výhodnou vlastností objektové knihovny je možnost použití tzv. abstraktních tříd. Jsou to třídy, které obsahují rozsáhlý kód, ale k jejichž zdárnému fungování něco chybí. To něco je třeba doplnit v potomcích takové třídy. Abstraktní třída obsahuje jednu nebo více abstraktních metod, tj. metod, které nemají uveden kód. Abstraktní metody se mohou vyskytovat pouze v abstraktní třídě:

abstract class MojeAbstraktni {


abstract void fce();


void pracuj() {


...


fce(); // volání abstraktní metody


}


}

Pokud chceme vytvořit potomka uvedené třídy, musíme v něm dodat (implementovat) kód všech abstraktních metod:

class Konkretni extends MojeAbstraktni {


void fce() { // deklarace musí být stejná jako v abstraktní metodě


// implementace fce


}


}


... // na jiném místě v kódu


Konkretni x = new Konkretni();


x.pracuj(); // volá pracuj zděděnou od MojeAbstraktni


// z pracuj se volá fce definovaná v Konkretni


Nyní je třeba odpovědět na otázku, k čemu nám může být použití abstraktních tříd a metod dobré. Při psaní knihovny autor často potřebuje pracovat s proměnnými nějaké třídy, kterou uživatel knihovny bude upravovat podle svého. Autor knihovny tedy nadeklaruje abstraktní třídu a napíše v ní všechny metody, které může napsat on. Metody, u nichž nezná jejich implementaci, protože tu bude vytvářet uživatel knihovny, pouze deklaruje a označí jako abstraktní. To mu však nebrání tyto abstraktní metody ve svém kódu volat. Při jejich skutečném volání bude příslušná proměnná ukazovat na konkrétní objekt, který bude mít všechny metody definovány.

Interface

Jedním z posledních rysů jazyka Java, se kterým se seznámíme, je použití tzv. interfaců. Je trochu podobné použití abstraktních tříd -- opět se používají jako rozhraní knihovny. Pokud chceme mít toto rozhraní ještě flexibilnější než při použití abstraktních tříd, můžeme právě deklarovat interface. Interface je vlastně množina deklarací metod a konstant:

public interface Rozhrani {


public void f1();


public int f2(int param1);


}


Pokud máme interface definován, můžeme deklarovat proměnné tohoto typu a používat je ve svém kódu:

...


void mojeMetoda(Rozhrani x) {


x.f1();


int k = x.f2(10);


...


}


Proměnné s typem rozhraní jsou z hlediska jazyka obyčejné proměnné typu odkazu na nějaký objekt. Při volání metody používající proměnnou typu rozhraní musíme za hodnotu této proměnné dosadit odkaz na objekt, který tzv. implementuje uvedené rozhraní. To, že nějaká třída implementuje rozhraní, je uvedeno v její deklaraci:

class A implements Rozhrani {


...


public void f1() { ... } // deklarace odpovídající Rozhrani


public int f2(int param1) { ... }


}


...


A y = new A(); // vytvoření objektu, který implementuje rozhraní


mojeMetoda(y); // dosazení příslušného objektu do proměnné x


Pokud naše třída implementuje rozhraní, musí obsahovat všechny metody uvedené v daném rozhraní. Tato vlastnost umožní fungování kódu, ve kterém se vyskytovaly proměnné typu rozhraní. Výhodou rozhraní oproti dědění z abstraktní metody je to, že za klíčovým slovem implements může být libovolný počet interfaců -- dědění od abstraktní třídy je omezeno pouze na jednoho rodiče.


| <<< | COMPUTERWORLD | IDG CZ homepage |