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

Programování v jazyce Java
5. díl

David Štrupl

Statické proměnné a metody

Všechny proměnné a metody, o kterých jsme doposud mluvili, byly tzv. instanční proměnné a metody. Znamenalo to, že s každým vytvořením nového objektu dané třídy se naalokovalo místo a vytvořily se nové kopie deklarovaných proměnných. To má logický smysl, protože instanční proměnná je součástí definice objektu -- vytvoříme-li nový objekt, musí se mu vytvořit i nové vlastnosti.

V Javě můžeme použít ještě jeden druh proměnných -- tzv. statické (třídové) proměnné. Tyto proměnné jsou také součástí definice třídy, ale jsou označeny klíčovým slovem static. Rozdíl oproti "normálním" proměnným je ten, že statická proměnná existuje pouze jednou pro celou třídu (proto se jí také někdy říká třídová proměnná). Tyto proměnné jsou dobré pro udržování globálních informací o dané třídě, např. můžeme sledovat, kolik objektů dané třídy máme vytvořeno. S existencí statických proměnných je úzce svázána i existence statických metod. Tyto metody se podobně jako statické proměnné vztahují k celé třídě, a mohou tedy používat pouze statické proměnné dané třídy (a samozřejmě i lokální proměnné deklarované ve svém těle). Ukázkou statické metody je definice metody main. Při volání statických metod nemusí (a většinou ani není) vytvořen objekt dané třídy (při volání metody main nemusí být vytvořen objekt třídy, ve které se metoda main nachází).

Speciálním druhem statických proměnných jsou konstanty, které jsou uvedeny klíčovým slovem final. Pokud se před deklarací proměnné objeví slovo final, znamená to, že hodnota této proměnné nesmí být v průběhu výpočtu změněna. Typická kombinace klíčových slov je tedy public static final:

public class MojeTrida {
public static final int MAX_POČET = 100;
static final float PI = 3.14;
public static void main(String args[]) {
System.out.println("Pi je : " + PI);
}
}

Pokud jsou proměnné (konstanty) deklarovány s modifikátorem public, můžeme jejich hodnotu použít i vně definice dané třídy. V tomto případě je možné použít před jménem proměnné jméno třídy, ve které je tato proměnná (statická) deklarována:

...

int k = MojeTrida.PI;

Konstanty většinou označujeme identifikátorem, který je celý napsán velkými písmeny -- pro snazší orientaci v programu.

Pole a řetězce

Nyní se zaměříme na používání dvou pravděpodobně nejčastějších datových struktur. Pole i řetězce mají podobnou syntaxi jako v jazyce C, ale jsou zde rovněž poměrně podstatné rozdíly. Nejprve se zmíníme o konstruktorech a potom budeme pokračovat částí o polích a řetězcích.

Konstruktory

Při vytváření objektů příkazem new se vždy volá speciální metoda -- konstruktor. Konstruktor je metoda, která nevrací žádnou návratovou hodnotu (při použití new by to nemělo smysl). Konstruktor píšeme podobně jako metodu, jediný rozdíl je v tom, že má vždy stejné jméno jako třída, ke které patří. Lze napsat několik konstruktorů pro jednu třídu, které se mohou lišit počtem a druhem parametrů. Konstruktory většinou deklarujeme jako public -- veřejné:

class Trida {
int velikost;
public Trida() {
...
}
public Trida(int jaka) {
velikost = jaka;
}
..
}

První konstruktor nemá žádné parametry, bude tedy vyvolán, pokud při vytváření nezadáme žádný parametr. Pokud při volání new zadáme jeden parametr typu int, bude zavolán druhý konstruktor. O tom, který konstruktor se zavolá, rozhodneme při vytváření objektu následujícím způsobem:

...

NovaTrida x = new NovaTrida(10); // zavolá konstruktor s parametrem int

Kromě vyvolávání mají konstruktory ještě několik zvláštních vlastností. Důležitou vlastností odlišující konstruktory od normálních metod je to, že se nedědí. Místo toho však je na začátku konstruktoru zavolán zděděný konstruktor. Pokud chceme zavolat zděděný konstruktor s parametry, musíme jako první příkaz konstruktoru uvést speciální příkaz super s příslušnými parametry:

class NovaTrida extends Trida{
public NovaTrida() {
super(10);
}

Příkaz super volá zděděný konstruktor s uvedeným parametrem. Druhou možností je další speciální klíčové slovo this:

public NovaTrida(float x) {
this();
// zavolá konstruktor bez parametrů
...// provedení vlastních inicializací
}

Příkaz this volá jiný konstruktor ze stejné třídy (s příslušnými parametry). Pokud jako první příkaz konstruktoru neuvedeme ani jedno z těchto klíčových slov, konstruktor se chová, jakoby jeho první příkaz byl super(). Když domyslíme tento systém volání konstruktorů, dojdeme k závěru, že při konstrukci každého objektu je nejprve zavolán konstruktor ve třídě Object, pak následují všechny konstruktory na příslušné větvi v hierarchii dědění a nakonec se zavolá konstruktor napsaný ve vytvářené třídě.

Pokud u své třídy neuvedeme žádný konstruktor, překladač vytvoří implicitní prázdný konstruktor bez parametrů. To, že je prázdný, se projeví zavoláním implicitního super() jako prvního a posledního příkazu, tj. provede se zděděný konstruktor.

This a super

Klíčová slova this a super mohou být použita kromě konstruktorů také pro přístup k proměnným a metodám daného objektu. Při tomto druhém použití se však s nimi váže jiná syntaxe.

Klíčové slovo this použijeme v případě, že chceme přistoupit k proměnné nebo metodě aktuálního objektu. Protože však k aktuálnímu objektu se přistupuje implicitně, tj. vždy, pokud nestanovíme jinak, má použití klíčového slova this smysl asi pouze v případě, překryjeme-li si nějakou proměnnou lokální proměnnou:

class T {
int x;
void nastavX(int x) {
this.x = x;
}
}

Proměnná x příslušející instanci objektu byla v uvedené metodě zastíněna parametrem x -- hodilo se nám tedy použít klíčové slovo this pro přístup k proměnné objektu.

Klíčové slovo super se, podobně jako u konstruktorů, odkazuje na zděděnou metodu nebo vlastnost. Jeho nejčastější použití je u metod. Pokud již nějaká metoda obsahuje kód a my chceme v potomkovi její chování rozšířit, můžeme zavolat zděděný kód pomocí super s následující syntaxí:

class U extends T {
void nastavX(int x) {
super.nastavX(x);
// proveď další akce
}
}

Při použití this a super je třeba si vždy uvědomit, zda jsme v konstruktoru nebo v obyčejné metodě -- v každém z uvedených případů se totiž používají trochu jiným způsobem. Pokud se zamyslíme nad účelem volání zděděných metod, zjistíme, že při volání super.metoda() musí být rozhodnuto o tom, která metoda se zavolá, již při překladu. Použijeme-li terminologii C++, klíčové slovo super v tomto případě vypíná mechanismus volání virtuálních metod, který je jinak přítomen všude při normálním volání metod v Javě.

Pole

Pole je datová struktura umožňující ukládat posloupnosti hodnot stejného typu. Proměnnou typu pole deklarujeme následujícím způsobem:

int pole1[];
float [] pole2;

Po jménu typu ukládaných elementů následuje deklarace jména proměnné. Že se jedná o pole, to signalizuje pár hranatých závorek umístěných před nebo za deklarovanou proměnnou. Tyto deklarace pouze uvádějí příslušnou proměnnou -- ještě je však třeba vytvořit vlastní pole. To se provede příkazem new -- pole je v Javě v podstatě objekt speciálního typu.

Vytvoření vlastního pole můžeme uvést např. již v deklaraci proměnné:

int pole3[] = new int[50];

Po vytvoření pole se můžeme na jednotlivé prvky odkazovat indexováním -- index uvádíme v hranatých závorkách za jménem proměnné -- rozsah je od 0 do velikost-1.

POZOR: Velikost pole nelze uvést přímo v deklaraci (jako v jazyce C)! Je nutné vytvořit pole příkazem new tak, jak je uvedeno v našem příkladu. To, že je pole vlastně obyčejný objekt, poznáme podle toho, že na proměnnou typu pole lze volat všechny metody deklarované ve třídě Object. Každá proměnná typu pole má navíc jednu speciální proměnnou -- length. Tato proměnná určuje délku pole -- její hodnota je stanovena při volání new pro vytvoření daného pole a během výpočtu ji nelze měnit. Chceme-li tedy vypsat obsah vytvořeného pole3, můžeme použít následující cyklus:

for (int i=0; i < pole3.length; i++) {
System.out.println(pole3[i]);
}

Podobně jako jednodimenzionální pole můžeme vytvořit i jeho vícedimenzionální variantu:

int pole4[][] = new int[10][10];

Vícedimenzionální pole je pole polí, tj. každá další dimenze je samostatné pole. Můžeme toho využít, pokud např. chceme vytvořit trojúhelníkovou matici (při volání new je možné uvést rozměr pouze u několika prvních dimenzí):

float tMatice[][] = new float[10][];
for (int i=0; i < tMatice.length; i++) {
tMatice[i] = new float[i];
}

Tuto matici však můžeme procházet stejným cyklem jako každou jinou dvojdimenzionální matici, pokud použijeme správným způsobem proměnnou length:

for (int i=0; i < tMatice.length; i++) {
for (int j=0; j < tMatice[i].length; j++){
System.out.print(tMatice[i][j]);
}
System.out.println();
}

Pokud vytváříme pole objektů, je třeba si uvědomit, že po vytvoření pole voláním new máme vytvořen příslušný počet proměnných daného typu, ale nebyly ještě vytvořeny příslušné objekty. V proměnných pole budou tedy hodnoty null, dokud do nich nedosadíme odkazy na vytvořené objekty:

MojeTrida x[] = new MojeTrida[100];
//v proměnných x[0] .. x[99] je hodnota null
x[0] = new MojeTrida();
// teprve nyní je hodnota x[0]
// odkaz na příslušný objekt

Místo vytvoření pole pomocí new lze použít tzv. konstantní inicializátor pole. Tento zápis je podobný jazyku C a umožňuje nám rovnou zadat hodnoty jednotlivým proměnným v poli:

int p[] = { 15, 7, 17, 2 };
MojeTrida y[] = { new MojeTrida(),
new MojeTrida() };

V prvním případě jsme vytvořili pole velikosti 4 typu int a ve druhém pole s velikostí 2 typu MojeTrida.

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


| <<< | COMPUTERWORLD | IDG CZ homepage |