Programování v jazyku Java (1) - Úvod V tomto seriálu článků se budeme věnovat programovacímu jazyku Java, který našel své pevné místo nejen na Internetu, ale na většině operačních systémů a (nejen) počítačových platforem. 9.7.2004 10:00 | Petr Hatina | přečteno 140972× Historie Základy Javy lze nalézt v projektu Oak, který vznikl ve firmě Sun na počátku devadesátých let pro řízení elektronických výrobků. V roce 1994 byl přenesen jako programovací jazyk do prostředí počítačů pod názvem Java (horká káva). Velice významným faktorem pro rozvoj používání Javy se stalo v roce 1995 zařazení její podpory do tehdy velice populárního prohlížeče Netscape Navigátor 2.0. Tato podpora umožňovala rozšíření funkčnosti webových stránek, pomocí java appletů., programů v Javě, stahovaných současně s WWW stránkou a spouštěných přímo v prohlížeči na straně klienta. Později tato podpora byla zavedena již zavedena i do dalších prohlížečů a applety se staly nedílnou součástí internetových stránek. Tento mechanismus dokonce vedl ke vzniku myšlenky NC (net computers), které měly existovat bez pevných disků, operačního systému a lokálně instalovaných programů , měly obsahovat pouze integrovaný internetový prohlížeč a veškeré programy se měly spouštět skrz něj, ze síťových serverů ve formě java appletů. Jako výhody se kromě úspor na HW komponentách uváděla zjednodušená administrace počítačových sítí, zvýšená bezpečnost, rychlé instalace a aktualizace programů a další důvody. Tato vize se neujala, ceny těchto NC, bez diskových mechanik nebyly o mnoho nižší než standardní PC, a co bylo možná podstatnější, ze strany počítačových firem nedošlo k masovému přepsání aplikačního SW do formy appletů, i když pokusy zde byly. I přes neúspěch tohoto pokusu (který snad měl šanci nahradit stávající platformu PC) si Java své místo na slunci udržela, na WWW stránkách našla svojí "parketu" v plně internetových aplikacích, které zprostředkovávají komunikaci mezi klientem na internetovém prohlížeči a službách přístupných přes internetový server. Zde se využily výhody Javy, její robustnost, stabilita, rozsah funkcí a hlavně bezpečnost. I proto podporu Java appletů najdeme v největší míře na WWW stránkách internetových bankovnictví a dalších aplikací, vyžadujících vysokou míru stability a zabezpečení. Rozšíření Javy Java se však neomezuje pouze na java applety,právě naopak. Její výhodu, multiplatformitu, deklarovalo populární heslo : Write once run everywhere. Zdrojový kód je při vývoji přeložen do spustitelného mezikódu (bytecode) , který lze pak spouštět pomocí nainstalovaného runtime prostředí (Java Virtual Machine), přímo na různých typech počítačů či technických zařízeních bez nutnosti nového překladu. Protože však tato přenositelnost není a nemůže být, hlavně z technickým příčin rozhraní) 100procentní , vyvinulo se několik edicí Javy, lišících se drobnými rozdíly a rozšířeními a spojených společným jazykem a velkou skupinou knihoven. Mezi hlavní platformy Javy v současné době lze uvést : spustitelné aplikace na počítačích - J2SE (java 2 standard edition). Java je rozšířená na rozsáhlé skupině počítačů, od PC mainframy až k hadheldům, na Linuxu, Unixu, Windows , a další.Většina distribucí Linuxu již v sobě obsahují vývojové prostředky i runtime prostředí Javy.
Na rozdíl od jazyka C, který je rozšířen stejně nebo i více, by přenositelnost programu měla být dána ne pouze jen na úrovni zdrojového kódu, ale spustitelného programu, jak bylo řečeno výše. applety O těch již jsme již mluvili, jedná se o programy v jazyce Java, které se stahují a spouštějí společně s WWW stránkou, a rozšiřují její funkčnost. přímo v prohlížeči. servlety - J2EE (java 2 enterprise edition) webové služby na WWW serverech, generující dynamické HTML stránky Java Server Pages (JSP) umožnují kombinovat v jednom WWW dokumentu HTML kód i příkazy Javy,přičemž Java příkazy se zpracovávají a jejich výstupy generují do obsahu WWW dokumentu ještě na serveru. Tím se odlišují od Javascriptu, který se zpracovává až na klientovi v prohlížeči. Javascript je skriptovací jazyk zapisovaný přímo do HTML stránek a má s Javou společnou jen velmi úzkou skupinu příkazů. Java aplikace pro malá zařízení - J2ME (java 2 micro edition) mladá ale bouřlivě se rozšiřující platforma, vídaná v poslední době zvláště na mobilních telefonech. Výklad tohoto seriálu začne od prvních uvedených, tedy vývojem spustitelných aplikací v Javě (J2SE), plus samostatný oddíl o appletech. Bude-li zájem , bude seriál pokračovat i výkladem ostatních edicí. . Vlastnosti jazyka Java je vyspělý programovací jazyk,obsahující všechny vlastnosti, které jsou vyžadovány v moderním programování, od modularity programu, řídících konstrukcí, přes silnou typovou kontrolu, multithreading, ošetření vyjímek, správu paměti, i silnou podporu pro databáze, XML a síťové operace. K jejím výhodám patří kromě již zmíněné multiplatformity, patří robustnost, škálovatelnost a vysoká bezpečnost, která jí profituje pro používání na kritické aplikace na mainfraimových počítačích. Nižší rychlost, způsobená zpracováním v runtime prostředí může být urychlena s pomocí specializovaných překladačů na cílovém prostředí (Java just-in-time, JIT). I když základní vývojové prostředí obsahuje pouze řádkový překladač, existuje mnoho vývojových nástrojů a rozšíření dalších firem autorů včetně IDE, i s podporou RAD vývoje GUI aplikací. Obsah Javy však nelze omezit jen na výčet jejích příkazů. Java je především silně objektová, což umožňuje v ní modelovat, vytvářet, používat a rozšiřovat rozsáhlé knihovny a systémy. Právě objektově je třeba myslet ne jen při psaní programu, ale již při návrhu a analýze. Pro tyto účely byl vytvořen UML, Unified Modeling Language, modelovací jazyk slouží k objektovému modelování a popisu konstrukcí reálného světa, převáděných do světa počítačů a informačních systémů. To však již překračuje rozsah tohoto seriálu, který bude zaměřen přímo na programování v Javě. Příště si popíšeme instalaci, překlad a spouštění java programů a napíšeme a spustíme první vzorový program. Programování v jazyku Java (2) - instalace, překlad a spouštění V tomto díle si nainstalujeme vývojové prostředí Javy a přeložíme a spustíme první program v Javě.
16.7.2004 10:00 | Petr Hatina | přečteno 82575× Instalace vývojového prostředí Primární vývojové prostředí pro Java aplikace, nazývané Java Development Kit (JDK), tvoří skupina nástrojů pro práci v příkazové řádce (překladač, spouštěč programů, debugger, a další), knihovna základních tříd, a další komponenty. Vývoj a distribuci JDK má stále v rukou Sun, pouze několik jiných výrobců vydává vlastní variantu Java SDK. Distribuce Linuxu obsahují obvykle Javu, často ale pouze běhové prostředí pro spouštění Java programů (JRE), SDK pro vlastní vývoj programů už tak samozřejmý není (bývá obvykle v komerčních distribucích). Nicméně, si Javu SDK si lze stáhnout zdarma ze stránek firmy Sun, http://java.sun.com. Zde narazíme na tabulku výběru různých edic Javy, jak jsme o tom mluvili v předešlé kapitole. Pro účely našeho kurzu zvolíme zatím J2SE (Core/Desktop) což je edice pro vývoj a provozování java programů na počítačových stanicích (pc) a serverech ve formě spustitelných aplikací. Lze v ní vytvářet i java applety. Poslední stabilní verze (červenec 2004) ke stažení je J2SE 1.4.2, a vedle ní ještě J2SE 5.0 Beta 2, což je vlastně J2SE 1.5, nicméně Sun jí označuje pod přelomovým číslováním J2SE 5.0 - Tiger. Tato verze je zatím ve stádiu betaverze,a proto bude výklad prováděn pro verzi 1.4.2 a zvýrazněny budou změny které se týkají nové verze 1.5. Po výběru verze jsou ke stažení přístupné tyto soubory: Java(TM) 2 SDK, Standard Edition xxxxx (vývojové prostředí pro překlad) J2SE v.. xxxxx JRE (runtime, běhové prostředí pro spouštění java aplikací) J2SE xxxxx Documentation (programátorská dokumentace.) Součástí SDK vývojového prostředí, je současně JRE runtime, takže není třeba si ho na stejný počítač instalovat duplicitně, pouze na jiné počítače na kterých plánujeme své přeložené programy spouštět. Dokumentaci stáhnout doporučuji, obsahuje podrobný popis java knihoven. Pro stažení musíme vždy napřed potvrdit souhlas s licenčním ujednáním, a následně si vybrat pro který operační systém chceme soubory stáhnout. Instalace se standardně provádí spuštěným staženého souboru bližší pokyny pro jednotlivé operační systémy jsou uvedeny na stránce s instalací jako Installation Notes. Pro Linux jsou instalační soubory posledních uvedených verzí nabízeny ke stažení ve 2 formátech, jednak jako .rpm balíček, který se nainstaluje do společného /usr/Java/j2sdkxxxxx, (musí být instalován uživatelem root), a jednak jako jednak jako spustitelný jdk-xxxx.bin . Ten si může nainstalovat i běžný uživatel, třeba pro odzkoušení, po nakopírování do svého určeného adresáře a spuštění se zde vytvoří podadresář j2sdkxxxxx se všemi potřebnými soubory. Vytvořený adresář j2sdkxxxxx obsahuje podadresář /bin s řádkovými programy javy a je k němu potřeba nastavit cestu rozšířením systémové proměnné PATH. Další důležité soubory, knihovny základních tříd, jsou uloženy v podadresáři jre/lib. Pro nastavení přístupnost knihoven se doporučuje nastavení proměnné prostředí CLASSPATH . Píši doporučuje, protože od verze Javy 1.2 již není třeba tuto proměnnou nastavovat při použití standardní knihoven a tříd Javy. Pokud používáme knihovny třetích stran, nebo vlastní vyvinuté (k tomu se v našem
kurzu dostaneme), je třeba cestu k těmto knihovnám nastavit v proměnné CLASSPATH, nebo což může být jednodušší zapsat tuto cestu při spouštění přeloženého programu : java -classpath cestakeKnihovnam program. Pokud používáme nějaký nástroj s IDE prostředím, ze kterého lze volat překlad a spouštění Javy, lze obvykle nastavit cestu k adresářům Javy v jeho nastavení. Kromě SDK od Sun lze pro vývoj Javy použít i jiná SDK prostředí, mezi něž patří IBM Java 2 SDK Blackdown JDK případně Kaffe, které je obsažené v mnoha distribucích. Tady ale pozor na možnou nekompatibilitu, Kaffe není certifikovaná od Sun jako "pure java". Pro psaní vlastních zdrojových textů vystačíme v jednoduchých případech s libovolným textovým editorem, výhodný je takový který podporuje zvýrazňování syntaxe Javy, jako třeba BlueFish. Na velké projekty se hodí komplexnější nástroje : Sun NetBeans IDE IBM WebSphere Studio Borland JBuilder Pokud si zvolíte jiné vývojové prostředí, doporučuji aby obsahovaly verzi Javy minimálně 1.2, kterou Sun někdy označuje jako Java2. Překlad a spuštění Takto vypadá první příklad programu v Javě: public class Hello { public static void main(String args[]) { System.out.println("Hello Universe"); } } Zdrojový soubor lze napsat v libovolném textovém editoru a uložit pod jménem třídy, uvedeným v řádku class, s příponou .java , v tomto případě Hello.java. Po napsání a uložení programu se zdrojový program přeloží příkazem javac jménosouboru.java , tedy zde javac Hello.java. Překladem se vytvořil soubor stejného jména s příponou .class tedy zde Hello.class , který již obsahuje přeložený bytecode programu a lze ho spouštět z příkazového řádku: java jmeno souboru (bez přípony), tedy java Hello Výstupem programu je obligátní pozdrav Hello Universe. V případě že během překladu, nebo při spuštění byly zjištěny chyby je nutné je opravit ve zdrojovém souboru a spustit překlad znovu. Nejčastější zdrojem chyb je, kromě běžných překlepů i skutečnost že Java je kontext senzitivní, takže je nutné dodržet velká i malá písmena tak jak jsou uvedena v tomto příkladu a v dokumentaci Javy, včetně názvu souborů, které musí odpovídat přesně jménu třídy na řádku class ve zdrojovém textu.
Máme-li k dispozici i jinou platformu (např. vedle Linuxu i Windows) a na ní nainstalované alespoň runtime prostředí Javy, lze si přenést vytvořený soubor Hello.class a ověřit si že opravdu na jiném prostředí rovněž poběží s příkazem java Hello Příště si na detailnějším příkladu probereme základní syntaxi jazyka. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=263 Programování v jazyku Java (3) - Základy syntaxe V této části seriálu si probereme základy syntaxe v Javě. 23.7.2004 10:00 | Petr Hatina | přečteno 69890× Pro názornost si to ukážeme na rozšířeném příkladu z minulé části : import java.util.*; public class Hello2 { static void Tisk(String s) { System.out.println(s);} public static void main(String args[]) { String pozdrav; pozdrav="Ahoj dneska je "+ (new Date()); Tisk(pozdrav); } } Jak je z příkladu vidět, pro syntaxi Javy platí tato základní pravidla: Veškerá prováděná činnost i identifikátory programu je uložena v definici tříd, neexistují tedy žádné samostatné, globální proměnné či funkce jako v jiných, plně neobjektových programovacích jazycích. Zdrojový soubor který má být spustitelný musí obsahovat veřejnou třídu, uvozenou klíčovými slovy public class. Soubor musí být uložen pod jménem této třídy, s příponou .java. (zde Hello2.java) Tato třída musí obsahovat metodu se záhlavím public static void main(String args[]), která se spouští při startu programu. Kód programu je hierarchicky strukturován v blocích, uzavřených v složených závorkách {}. Nejvyšší úroveň je celá třída která obsahuje, kromě deklarace svých atributů, samostatně uzavřené metody, bloky na ještě nižší úrovně mohou uzavírat skupiny příkazů, které se mají zpracovat společně (například při splnění určité podmínky). Příkazy jsou ve zdrojovém textu odděleny znakem ; středník. Na řádku může být i více příkazů, ale zejména z důvodu ladění se doporučuje jeden. Kromě příkazů zdrojový kód obsahuje volání metod tříd (ze standardních knihoven Javy, třetích stran, nebo svých vlastních) ve formátech :
metoda(parametry) Tak volá třída metody své třídy (viz Tisk) objektTridy.metoda(parametry); Standardní způsob volání metod. Objekt musí být předem vytvořen. trida.metoda(parametry); Tento způsob je povolen u tříd, které jsou označeny jako static. (viz System.out.println(s)) Při použití třídy (nebo i jiného identifikátoru) z knihovny musíme překladači sdělit že tuto knihovnu používáme. Plně by tedy použití objektu Date mělo být (new java.util.Date()), ale seznam použitých knihoven se obvykle uvádí společně do prvního řádku zdrojového kódu v příkazu import. (vyjímku tvoří knihovna java.lang která se připojuje automaticky, proto jsme ji pro použití standardní metody pro výstup System.out.println nemuseli uvádět.) Používání a posléze i vytváření objektů a jejich tříd budeme věnovat několik budoucích dílů, předešlý odstavec tedy berte jako první pomoc pro základní využití knihoven, zvlášť pokud jste si nainstalovali doporučenou dokumentaci knihovny javy, a v ní najdete podrobný popis všech standardních tříd v podadresáři api adresáře doc. Identifikátory (názvy proměnných, metod, atributů, tříd, atd) mohou obsahovat malá i velká písmena, číslice, a znaky podtržítko a $.Velká a malá písmena se považují za rozdílné znaky. První znak musí být písmeno nebo podtržítko. Neoficiální konvence doporučuje používat první písmeno v názvech tříd velké. Komentáře jsou ve zdrojovém textu značen třemi způsoby: //toto je komentář do konce řádku /* takto se okomentuje i více řádků až sem */ /** speciální komentář, který se pak generuje do vývojové dokumentace příkazem javadoc */ Čísla se do zdrojového kódu zapisují buď klasicky, dekadicky, (nesmí začínat nulou),hexadecimálně- v šestnáctkové soustavě (musí začínat 0x), nebo oktanově - v osmičkové soustavě (musí začínat 0). Desetinná čísla se zapisují s tečkou. Pro zápis čísel lze použít exponenciální tvar s E - např. 3E5. Samostatný jeden znak (typ char) se zapisuje mezi apostrofy. Textové řetězce jsou uzavřeny v uvozovkách "" .Java používá pro vnitřní ukládání řetězců UNICODE. V řetězcích mohou být použity tzv.escape znaky s řídícím významem, např.: \n přechod na nový řádek \b backspace \t tabulátor \f nová stránka \r návrat vozíku (CR) \" uvozovky \' apostrof \\ zpětné lomítko \xxx znak zapsaný v osmičkovém kódu (000-0377) \uxxxx Unicode znak zapsaný hexadecimálně
Kromě příkazů a volání metod obsahuje zdrojový soubor výrazy s proměnnými, čemuž budeme věnovat příští díl. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=289 Programování v jazyku Java (4) - Proměnné a operace s nimi V této části si probereme základní datové typy proměnných a operace s nimi. 30.7.2004 10:00 | Petr Hatina | přečteno 49890× Datové typy proměnných V Javě rozlišujeme 2 základní skupiny typů proměnných: primitivní typy a (referenční) odkazy na objekty. Přehled primitivních typů Typ Obsahuje Rozsah boolean pravdivostní hodnota true/false char znak Unicode byte celé číslo -128 až 127 short celé číslo -215 až 215-1 int celé číslo -231 až 2 31 -1 long celé číslo -263 až 263-1 float racionální číslo jednoduchá přesnost double racionální číslo dvojnásobná přesnost Deklarace použití proměnné se provádí ve tvaru typ proměnná; Lze ihned přiřadit i prvotní, inicializační hodnotu. int i=0; Operátory Manipulaci s proměnnými provádíme pomocí operátorů. Dělíme je do několika skupin. Přiřazení hodnoty do proměnné dosáhneme pomocí operátoru = int a; int b; a=6; b=a+5; Matematické operátory pro práci s čísly jsou standardní, tedy + - * / a % ( modulo, tj.zbytek po dělení)) např. a=a*b; Lze použít i zkrácený formát po přiřazení ve tvaru proměnná+=hodnota; namísto proměnná=proměnná+hodnota; Častěji než zkrácený formát se používají operátory inkrementace (++) a dekrementace(--). Ve spojení s proměnnými číselných typů mají význam :přičti, resp. odečti 1. a++; //odpovídá a=a+1; Používají se postfixově (a++), ale někdy i prefixově (++a). Rozdíl se projeví pokud v jednom příkazu současně provádíme s proměnnou jinou operaci, prefixový (++a) se provede ještě před touto operací, kdežto postfixový až po ní. a=1; b=a++; //b je 1,a je 2
a=1; b=++a; //b je 2,a je 2 Relační operátory porovnávají hodnoty primitivních proměnných. Patří mezi ně < (menší než), > (větší než), <=, >= (menší, či větší nebo rovno), == (rovná se), != (nerovná se)). Připomínám že v Javě stejně jako v jazyce C se pro porovnání používá == (dvě rovnítka). if(a == 4) System.out.println("OK"); Logické operátory spojují pravdivostní výsledky více relačních operací porovnání. && (a, současně), || (nebo), ! (negace). if (a==0 && b>5 ) a=1;//pokud a je 0 a b>5 Speciální skupinu tvoří bitové operátory, které dokáží porovnávat (&, |, ^, ~) , nebo posouvat (<<, >>) jednotlivé bity v bytech proměnných. Přetypování , neboli změnu datového typu (např. při přiřazení do proměnné jiného primitivního typu) provádí Java většinou automaticky. Pouze v určitých případech je třeba použít operátor přetypování (výstupní typ v závorce) před proměnnou. int i=100; long l= (long) i; Odkazy Velice krátce se dotknu skupiny referenčních typů, protože se jim budeme plně věnovat až budeme probírat třídy u kterých se hlavně používají. Referenční proměnné (odkazy) obsahují pouze odkaz na objekt proměnné, který se dynamicky vytváří v paměti příkazem new. Date dat; dat=new Date(); System.out.println (dat.getDate()); Jen předem upozorním na riziko používání operátorů při práci s odkazy. Pokud totiž použijeme na odkazy standardní operátory, operace provádějí se nad hodnotou odkazu , a nikoliv nad hodnotou objektu na který odkazuje. Například operátor == pak neporovná hodnotu odkazovaných proměnných, ale to, zda dva různé odkazy odkazují na stejný objekt v paměti. Pro operaci s objekty musíme buď použít jejich metody (např. equals()), nebo provádět operace nad jejich atributy, pokud jsou přístupné. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=312 Java (5) - Řízení programu Tento díl je věnovám řídících příkazům, které větví či cyklí běh programu dle podmínek. 6.8.2004 10:00 | Petr Hatina | přečteno 47987× Řízení programu Řízení programu obvykle neprobíhá jen v pořadí příkazů ve zdrojovém kódu, ale často se větví či cyklí v řídících příkazech vyhodnocování podmínek. Stanovení podmínek Obvyklým typem testované podmínky je hodnota proměnné. Můžeme testovat na : int a=0;boolean ok; if (a==0 ) ok=true; //shodu s hodnotou operátorem == if (a!=0 ) ok=true; // neshodu s hodnotou operátorem != if (a>0 ) ok=true; // zda je větší if (a<=0 ) ok=true; // zda je menší
if (a>=0 ) ok=true; // zda je větší nebo rovno if (a<=0 ) ok=true; // zda je menši nebo rovno Lze testovat více podmínek dohromady pomocí logických operátorů && (a, současně), || (nebo), ! (negace). if (a==0 && b>5 ) a=1; //pokud a je 0 a b>5 Podobně jako hodnoty proměnných můžeme testovat hodnoty vracené z metod objektů. Například u minule uvedeného Date lze testovat . Date dat=new Date(); if ((dat.getDate()==13) ) System.out.println ("Je 13.teho"); Další oblast která by spadala do oblasti řízení programu je ošetřování chybových stavů, což se ale v Javě řeší jinak, pomocí vyjímek, kterým budeme věnovat samostatnou kapitolu, a proto teď rovnou přejdeme k řídícím příkazům. If Nejčastějším příkazem pro řízení běhu programu dle podmínky je příkaz if . Používá se ve formátech if (podmínka) Příkaz;, nebo if (podmínka) Příkaz; else JinyPříkaz; V tomto případě se vykoná Příkaz při splnění podmínky, zatímco při nesplnění podmínky se vykoná JinýPříkaz, uvedený v klauzuli else. Pokud je v případě splnění podmínky třeba vykonat více příkazů, uzavřou se jako blok do složených závorek. int a=0; boolean splneno; if (a==0) {splneno=true;a=1;} Výraz ? : Jako zkrácenou náhradu příkazu if pro přiřazení hodnoty lze použít výraz ? : ve formátu vracenáhodnota = podmínka ? hodnota1 : hodnota2 což značí, že při splnění podmínky je vrácena hodnota1 , při nesplnění je vrácena hodnota2 Příklad : int a =0 ;boolean b; b= a==0 ? true :false; //b je true a=1; b= a==0 ? true :false; //b je false Cykly while a do - while Cykly do a do while provádějí opakování části kódu dokud je plněna testovaná podmínka. Formát prvního je while (podmínka ) příkaz; , i když obvykle pracuje s celým blokem příkazů jako v tomto příkladu. int i=10; System.out.println("CountDown"); while (i >= 0) { System.out.println(""+i); i--; }
System.out.println("Boom"); Druhý příkaz , do - while se liší tím že se podmínka vyhodnocuje až na konci bloku, čili blok se provede alespoň jednou. int i=10; do System.out.println(i) while(>=0); for Cyklus for se používá k opakování bloku kódu v zadaném, pevném počtu opakování. Jeho obecný formát je for (inicializace; podmínka ukončení;příkaz kroku) příkaz. Inicializace se provádí před zahájením cyklu, v každém průchodu se provádí test podmínky, a po ukončení každého průchodu cyklem se provede příkaz kroku. Ten by měl souviset s testovanou podmínkou, obvykle jde o navýšení proměnné která počítá průchody cyklem. Konkrétnější , jasnější a nejčastěji používaný formát vypadá takto : for(int i =0; i < n; i++) {blok příkazů} Což značí, opakuj blok příkazů n krát. V Javě je zvykem počítat v cyklu ne od 1 do n, ale od 0 do n-1. Důvodem je to, že toto číslování používají striktně některé struktury Javy, jako jsou například pole. //tento kod vytiskne ASCII Znaky od 30 do 127 for (char c=32;c < 128;c++) System.out.println("ASCII znak "+ (int)c + " je " + c); break V některých případech je vhodné přerušit a ukončit předčasně zpracování cyklu (do, while i for) ještě před provedením všech průchodů. K tomu se používá příkaz break, kterým lze z cyklu vystoupit, (obvykle na základě další, vnitřní podmínky ). int hledcislo=13;boolean nalez=false; for (int i =1;i<20;i++) { if (i==hledcislo) {System.out.println(i);nalez=true;break;}} if (nalez) System.out.println("hledane cislo nalezeno"); else System.out.println("hledane cislo nenalezeno"); V praktickém případě bychom samozřejmě hledané číslo nezadávali jako konstantu. continue Podobný příkazu break je continue, liší se tím, že z cyklu nevystupuje, pouze přeskočí ještě nevykonané příkazy do konce cyklu, a přejde zpět na začátek cyklu, a pokračuje další iterací. Příklad vytiskne čísla dělitelná 17 int i=0; while(++i<52) { if (i % 17 !=0 ) continue; System.out.println(i); } Návěští V případě složitějšího kódu ve kterém dochází ke vnořování více cyklů do sebe, lze využít možnost přesunutí řízení do vyšších cyklů na určené místo, označené návěštím. ve tvaru jmenonavesti: (za názvem musí být dvojtečka). Tento název se pak využije v příkazech break a continue. Obecný případ vypadá takto navesti1: for (int i=1;i<1000;i++) {
do { //vnitřní cyklus if (k==0) continue navesti1; //dalsi prikazy } while(j<50) } Nicméně tento příkaz až nebezpečně připomíná nestrukturovaný příkaz goto, známý z jiných jazyků. (kdo se nebojí goto, ten se nebojí ničeho, říkalo se v osmdesátých letech). Je vhodné psát cykly jednodušší. switch Příkaz switch je formou podmínkového příkazu, který větví zpracování na základě konkrétní hodnoty: switch(proměnná) case hodnota1: příkaz1;break; case hodnota2: příkaz1;break; case hodnota3: příkaz1;break; default: příkaz; Pokud testovaná proměnná obsahuje hodnotu1 , provede se příkaz1 , při hodnotě 2 příkaz 2, atd . Samozřejmě, místo jednoho příkazu může být uvedený celý blok, a každá větev musí být ukončena příkazem break . Tedy obecně vzato nemusí, pokud se to hodí (a není to jen omylem), bez ukončení příkazem break dojde ke zpracování příkazů další větve. Pokud je uvedena klauzule default, příkaz za ní se provede pokud proměnná obsahuje jinou hodnotu než uvedenou v předešlých hodnotách. Jen upřesněním že proměnná musí být celočíselná. Doplnění k předešlému dílu V diskusi k předešlému k dílu se objevil dotaz čtenáře ohledně zápisu čísel pro proměnné typu float. Pokud se ve zdrojovém kódu napevno zapisují hodnoty do proměnných, lze stanovit jejich typ přidáním znaku za číslem: (lze použít velká i malá písmena) long l=10000L; float f=1.02F; double d= 3.14111111D; } Online verze článku: http://www.linuxsoft.cz/article.php?id_article=333 Programování v jazyku Java (6) - Řetězce I V tomto díle si ukážeme jak jsou v Javě ukládány a zpracovávány textové informace. 20.8.2004 14:00 | Petr Hatina | přečteno 63047× Řetězce I Řětězce slouží k ukládání textových informací. V Javě jsou vnitřně kódovány v UNICODE. Používají dvě základní objektové třídy - String a StringBuffer. Třída String slouží k ukládání konstantních textových informací, které nehodláme (nebo jen málo) měnit. Hodnota v objektu typu String totiž je konstantní, při změně nebo přiřazení jiné hodnoty je vytvořen v paměti nový objekt a přiřazen původní proměnné. Původní hodnota objektu je posléze v paměti smazána. Což spotřebuje určitou časovou i systémovou režii.
Proto je pro ukládání textových informací, které jsou v programu častěji modifikovány, rozšiřovány či spojovány lépe použít třídu StringBuffer, které se budeme věnovat příště. String Proměnnou typu String lze obvykle inicializovat přiřazením textové konstanty: String s= "Helo World"; Kromě přímého přiřazení pevné textové hodnoty můžeme řetězec inicializovat již při jeho deklaraci i jinak: String s= new String() ; // prázdný řetězec String s= new String(String t); //rovnou překopíruje jiný řetězec String s= new String(char ch[] ); // řetězec z pole znaků String s= new StringBuffer(); // řetězec z objektu StringBuffer String s = new String(byte[] pole,"ISO8859_2") /*vytvoří a převede řetězec z pole znaků v jiné znakové sadě" */ Řetězce můžeme spojovat (s výhradou vůči optimalizaci, viz výše ) operátorem + String Jmeno="Jan";String Prijmeni="Novak"; String Vizitka= Jmeno+ " "+ Prijmeni; System.out.println("Jeho inicialy jsou " + Jmeno+ " "+ Prijmeni); Aktuální délku řetězce vrací metoda length : int delka=Jmeno.length(); Co provádět nemůžeme, to je porovnání řetězců operátorem ==,protože typ String je odkaz. Musíme použít jeho metodu equals (resp. equalsIgnoreCase) , která ignoruje rozdíly mezi velkými a malými písmeny). String s1="Jan";boolean ok; ok=s1.equals("Jan"); //vraci true ok=s1.equals("jan"); //vraci false ok=s1.equalsIgnoreCase("jan"); //takhle je to OK ok=s1.equals("franta"); //vraci false Potřebujeme-li při porovnání určit který z řetězců je větší, použijeme metodu compareTo String s1="Jan"; int i=s1.compareTo("franta"); // vrací záporné číslo Metoda compareTo vrací 0 pokud jsou oba řetězce stejné,záporné číslo pokud je porovnávaný řetězec menší než předaný poarametr a kladné číslo pokud je větší. Pro porovnání zda text obsahuje pouze určité skupiny znaků slouží metoda matches, která porovnává řetězec podle regulárních výrazů, tak jak je známe z linuxových příkazů. S regulárním výrazy pracují i metody replaceAll, replaceFirst(), které nahrazují část řetězce jiným textem. Regulárním výrazům v řetězcích budeme věnovat samostatnou kapitolu. Jednotlivé znaky v řetězci lze měnit pomocí metody replace: String s="01234567890"; s=s.replace('0','X'); //změní všechny znaky 0 na X
K metodám, měnícím obsah řetězce patří také toUpperCase a toLowerCase, (převod na velká, resp. malá písmena), a trim, která odstraňuje úvodní a koncové bílé znaky (tj.mezery, tabelátor, konec řádku). Vyhledávat text v řetězci lze pomocí metody indexOf, která vrací polohu podřetězce v řětězci, (tedy přesně řečeno, vrací index, takže se počítá od 0) či startsWith, která testuje zda řetězec začíná uvedeným textem. Opačnou funkci, tedy vrácení řetězce na určité pozici obstarávají metody substring a charAt(ta vrací znak na pozici). String s="01234567890" int poz= s.indexOf("456") //vyhledá pozici podřetězce v celém řetězci boolean ok=s.startsWith("0123") // String sub= s.substring(0,2); //vrací podřetězec od indexu 0 do 2-1 Online verze článku: http://www.linuxsoft.cz/article.php?id_article=341 Programování v jazyku Java (7) - Řetězce II V této části si povíme o třídě StringBuffer, sloužící k ukládání často měněných řetězců, a následně ještě o konverzích mezi řetězci a dalšími typy. 27.8.2004 12:00 | Petr Hatina | přečteno 44511× Řetězce II StringBuffer Jak již bylo uvedeno, třída StringBuffer se používá pro ukládání textových řetězců, které budou častěji modifikovány. Pro jejich deklaraci lze použít jeden z těch to způsobů: StringBuffer strbuf1= new StringBuffer(); //prázdný StringBuffer strbuf2= new StringBuffer("Text");//z prom.typu String StringBuffer strbuf3= new StringBuffer(80);//s kapacitou 80 znaků Rozšíření řetězce pak provést metodami: StringBuffer strbuf= new StringBuffer("text"); strbuf.append("text2"); // rozšíří řetězec strbuf.insert(1,"ret"); //vloží text za 2.znak (počítá se od 0) Metody append a insert nemusí vkládat pouze řetězec, ale i další primitivní typy (např. čísla) nebo pole znaků. Modifikace obsahu řetězce lze provádět pomocí metod replace(od, do-1 , novýtext) či setCharAt (jen jeden znak). StringBuffer strbuf= new StringBuffer("stary text"); strbuf.replace(0,5,"novy"); // změní "stary" na "novy" // nutno přičíst k pozici do +1 strbuf.setCharAt(0,'X'); //přepíše znak na pozici 0 Xkem Obsah řetězce lze i rušit pomocí metod delete(od,do-1), a deleteCharAt(index)), ta zruší jen znak
na určené pozici indexu. StringBuffer strbuf= new StringBuffer("textbezKonec"); strbuf.delete(4,7);//smaž text bez(od poz. 4 do 6+1) strbuf.deleteCharAt(0); Podobně jako u třídy String lze hledat v řetězci pozici určitého podřetězce metodou indexOf a opačně načíst obsah řetězce od určité pozice metodami substring a charAt. Při popisu inicializace byla zmíněna kapacita. Její princip spočívá v tom, že pro uložení znaků v paměti má proměnná typu StringBuffer přiděleny určitý prostor (buffer), když ho překoná, požádá o přidělení dalšího. Aby toto přidělování bylo efektivní,lze předem určit, jaký prostor bude vyžadován, popřípadě ho v programu regulovat těmito metodami: StringBuffer strbuf1 = new StringBuffer(); int i= strbuf1.capacity(); //aktuální kapacitu strbuf1.ensureCapacity(99);//zajisti kapacitu 99 znaků strbuf1.trimToSize(); //zmenši buffer //aby pokryl jen prostor již obsazený znaky
Samotnou délku řetězce pak můžeme zjistit metodou length() , a změnit jí pomocí setLength(délka). V případě zvětšení je doplněna znaky null ('\u0000'). Verze 1.5 Javy doplňuje novu třídu StringBuilder , která která je obdobná třídě StringBuffer. Rozdíl je v tom, že původní třída StringBuffer zajišťuje bezpečnou a správnou funkci i při současném běhu více podprocesů v úloze. To nová třída StringBuilder neumožňuje, odměnou je pak uváděna vyšší rychlost zpracování ve většině implementací. Vstupy a výstupy Již v první lekci jsme si ukázali způsob jak provést výstup obsahu proměnné na obrazovku pomocí metody objektu System.out.println. Opačný směr, tedy načtení obsahu proměnné z klávesnice lze provést pomocí tříd BufferedReader a InputStreamReader: String vstup; BufferedReader in; in = new BufferedReader(new InputStreamReader(System.in)); try{ vstup = in.readLine(); } catch(IOException e) {vstup="";} Jak je z příkladu vidět, byl vytvořen datový tok s vyrovnávací pamětí, přečte řádek ze standardního vstupu. Příkazy catch a try zajištují, v tomto případě povinné ošetření možného vzniku chybového stavu - vyjímky. Převody mezi řetězci a čísly I proto, že v minulém odstavci uvedená metoda readLine načítá a vrací pouze textové řetězce, potřebujeme prostředky pro mezi řetězcem a čísly. Celé číselné typy (byte,int,short,long) mají vestavěnou metodu parseXXX. příslušného typu: String s="777"; int i=Integer.parseInt(s);
Všechny číselné typy pak obsahují metodu valueOf, která provádí to samé. String s="1.5"; float f=Float.valueOf(s); Zpětný převod z čísla na text lze nejlépe provést pomocí metody toString (která se volá při použití operátoru + imlicitně), nebo metodu valueOf(). for (int i=0;i<5;i++) System.out.println("tohle je "+ (i+1) + "."); Metoda toString Když již byla zmíněna metoda toString pro převod čísel na řetězec, je třeba dodat, že metodu toString obsahuje většina tříd v knihovnách Javy a vrací textovou informaci o obsahu objektu. Můžeme jí volat přímo : System.out.println((new Date().toString())); Nicméně pokud program zjistí že v prováděné operaci je požadován místo předávaného objektu typ String, použije tuto metodu automaticky. Což se použije například při přiřazení do řetězce, nebo přímo výstupu v objektu out : Date dat=new Date(); String Pozdrav= "Ahoj, právě je " + dat;//připojení k řetězci System.out.println(dat); // je očekáván řetězec Online verze článku: http://www.linuxsoft.cz/article.php?id_article=353 Programování v jazyku Java (8) - Pole I Pole jsou jednou ze základních datových struktur, umožňující práci s hromadnými daty. 3.9.2004 10:00 | Petr Hatina | přečteno 69940× Pole jsou jednou ze základních datových struktur, umožňující práci s hromadnými daty. Bylo by velmi nepohodlné, pokud bychom při zpracování většího počtu proměnných stejného druhu a použití museli každou označovat samostatným jménem a pak je v řadě samostatně zpracovávat, např. při sečítání položek: suma= pol1+ pol2+ pol3 + .....+ pol50; Pokud tyto hodnoty uložíme do pole namísto do samostatných proměnných , jsou rozlišeny indexem: pol[1],pol[2], přičemž indexu obvykle neuvádíme přímo číslem, ale indexovou proměnnou, kterou řídíme pomocí cyklu: for(int i=0;i<50;i++) suma=suma + pol[i];} Deklarace a inicializace pole Pole hodnot deklarujeme pomocí indexu [] u datového typu: int [] aPole; //vytvoří pole proměnných typu integer int bPole[]; //je povoleno i toto pořadí Tím jsme deklarovali proměnnou typu pole, ovšem protože pole je typu odkaz, musíme před
použitím ještě pole vytvořit - určit počet prvků a inicializovat místo v paměti. Inicializace se provádí jedním ze 2 způsobů : aPole= new int [20]; //příkazem new vytvoříme pole 20 prvků typu int aPole= {0,5,19}; //výčtem hodnot vytvoříme pole 3 prvků Deklaraci i vytvoření lze sloučit do jednoho příkazu, tedy: int []aPole= new int[20]; Je možné vytvářet i vícerozměrná pole, například matici 3x3 prvky vytvoříme: int [][] mat= new int [3][3]; Pokud vytváříme pole objektů, musíme si uvědomit že pole i objekty jsou typu odkaz, takže obojí musíme po deklaraci i vytvořit: Date []Datumy=new Date[5]; for (int i=0;i<5;i++) Datum[i]= new Date(); Přístup k položkám pole K položkám pole přistupujeme prostřednictvím indexu v hranaté závorce. Obvykle je zpracováváme v cyklu. Následující příklad vygeneruje a vytiskne pole náhodných čísel. Random rd=new Random(); //nutno v úvodu import java.util.* int [] nahCisla=new int[10]; for(int i=0;i<10;i++) nahCisla[i]=rd.nextInt(100); for(int i=0;i<10;i++) System.out.println (nahCisla[i]); Jak je z ukázky patrné, indexy v poli se číslují od 0 do n-1 a nikoliv od 1 do n. Na to je potřeba dávat pozor, pokud se to opomine, program způsobí chybu a zobrazí výjimku Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 10 Základní algoritmy pro práci s polem Pro práci s polem existuje široká řada obecně používaných algoritmů, několik z nich si ukážeme. Výpis pole Tento příklad jsme si už ukazovali, tak ještě jednou, plný kód pro deklaraci a naplnění pole pak předpokládáme i v dalších příkladech kde není uveden: import java.util.*; public class Priklad8 { public static void main(String[]args) { Random rd=new Random(); //nutno v úvodu import java.util.* int [] nahCisla=new int[10]; for(int i=0;i<10;i++) nahCisla[i]=rd.nextInt(100); for(int i=0;i<10;i++) System.out.println (nahCisla[i]); } }
Vyhledávání prvku v poli int hledCislo=77;int nalezindex=-1; for(int i=0;i<10;i++) if (nahCisla[i]==hledCislo) {nalezindex=i;break;} if (nalezindex==-1) System.out.println("Hledany prvek v poli nenalezen") ; else System.out.println("Hledany prvek nalezen na pozici" + nalezindex); } Kopírování pole Vzhledem k tomu, že pole je typu odkaz, nemůžeme přímo přiřadit jedno pole druhému operátorem = , obě pole by pak ukazovaly na stejné pole, musíme pole překopírovat samostatně po prvcích. int [] druhePole=new int[10]; for(int i=0;i<10;i++) druhePole[i]=nahCisla[i]; Podobně nelze celá pole přímo porovnávat operátorem ==., ale položku po položce. Nicméně, tyto algoritmy použijeme pouze v jednodušších případech. Java má totiž přímo ve svých knihovnách zabudované metody, které tuto namáhavou práci udělají za nás. O nich si povíme příště. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=384 Programování v jazyku Java (9) - Pole II Toto je první článek seriálu o programování v Javě, který navazuje na původní seriál Petra Hatiny, přerušený loni v září. Budeme se zabývat funkcemi, které nám usnadňují manipulace s poli a vykonávají za nás nepříjemnou "černou" práci. 25.2.2005 15:00 | Lukáš Jelínek | přečteno 46409× Využití třídy System Kopírování prvků polí Třída java.lang.System obsahuje metodu arraycopy(), která umí zkopírovat prvky z jednoho pole do jiného. Lze kopírovat libovolný počet prvků pole, nesmíme však samozřejmě "vyběhnout" mimo meze některého z polí. Kopírovat lze i v rámci téhož pole, zdrojový a cílový úsek se mohou překrývat (vnitřně to funguje tak, že se kopíruje do dočasného pole a pak zpět do toho původního). Prvky pole mohou být jak primitivní, tak referenční (objektové) typy (kopírují se samozřejmě jen odkazy, instance zůstavají stejné). Následující příklad ukazuje zkopírování části pole čísel typu double do jiného takového pole (kopíruje se 5 prvků od indexu 3 ve zdrojovém poli na cílové prvky od indexu 0): double src[] = new double[10]; double dest[] = new double[5]; ... // zde se přiřazují prvky System.arraycopy(src, 3, dest, 0, 5); Prvky primitivních typů se nesmí typově lišit. U referenčních typů je povolena jen taková odlišnost, kdy lze cílovému prvku ten zdrojový přiřadit (konkrétní typ se přitom může lišit). Porušení tohoto pravidla má za následek vyhození výjimky ArrayStoreException. V níže uvedeném příkladu je ukázka kopírování prvků referenčního typu v rámci téhož pole (ve čtyřprvkovém poli se
první dva prvky zkopírují na zbývající místa): Thread ta[] = new Thread[4]; ta[0] = new Thread(); ta[1] = new Thread(); System.arraycopy(ta, 0, ta, 2, 2); Používání arraycopy() namísto klasického cyklu s postupným přiřazováním je (kromě usnadnění práce) výhodné také z hlediska rychlosti. Metoda má totiž obvykle nativní implementaci, takže je rychlejší než kód přímo v Javě. Mechanismy poskytované třídou Arrays S pouhým kopírováním polí se určitě nespokojíme, v praxi bývá potřeba mnohem více operací. Právě pro tyto účely je určena třída java.util.Arrays, poskytující množství statických metod pro práci s poli. Pro pole primitivních typů je chování těchto metod předem dáno, u polí referenčních typů velice záleží na vlastnostech a chování prvků. Tak se do toho pusťme... Převod na řetězec Toto není metoda příliš typická pro pole (také byla přidána až ve verzi 1.5), uvádím ji však jako první, protože se hodí pro ladění a testování programů. Její smysl je podobný jako u stejnojmenné metody třídy Object. Funguje tak, že vytvoří řetězec, kde jsou v hranatých závorkách textové hodnoty jednotlivých prvků. Zde jsou dva příklady: int ia[] = new int[3]; ... // zde se přiřazují prvky System.out.println(Arrays.toString(ia)); Object oa[] = new Object[10]; ... // zde se přiřazují prvky System.out.println(Arrays.toString(oa)); Pokud je některý z prvků sám polem, nepřevedou se na text jeho prvky, nýbrž se mu zavolá metoda toString() z třídy Object. Plnění polí Často potřebujeme naplnit pole nebo jeho část nějakou konkrétní hodnotou. K tomu slouží metoda fill(), které na jediné zavolání pole takto naplní. Máme dvě varianty této metody - jedna naplní celé pole, u druhé určujeme počáteční a koncový index v poli (plní se počínaje počátečním indexem až ke koncovému, ten už není naplněn). Nejlépe vše ukáže příklad (první plní najednou celé pole, druhý postupně dvě části pole). int ia[] = new int[100]; Arrays.fill(la, 0); String sa[] = new String[5]; Arrays.fill(sa, 0, 2, "text1"); Arrays.fill(sa, 2, 5, "text2"); Netřeba snad připomínat, že při plnění pole prvků referenčního typu se odkazuje ve všech prvcích na tentýž objekt. Porovnávání polí Máme dvě pole a potřebujeme zjistit, zda jsou shodná. To dokáže metoda equals(), porovnávající dvě pole z hlediska počtu prvků a jejich hodnot. Dvě pole jsou shodná právě tehdy, když mají stejný počet prvků a odpovídající prvky mají stejnou hodnotu; shodné jsou i dvě prázdné (null) reference
na pole. Porovnávat lze pole prvků stejného primitivního typu (pak se porovnávají hodnoty prvků), nebo pole prvků referenčního typu (v tom případě jsou prvky shodné tehdy, prohlásí-li je za shodné metoda equals() dané třídy nebo jsou obě reference prázdné). byte ba1[] = new byte[10]; byte ba2[] = new byte[8]; boolean b = Arrays.equals(ba1, ba2); // vrátí false - různá délka int ia1[] = new int[5]; int ia2[] = new int[5]; Arrays.fill(ia1, 0); Arrays.fill(ia2, 0); boolean b = Arrays.equals(ia1, ia2); // vrátí true - shodná pole ia2[3] = 5; b = Arrays.equals(ia1, ia2); // vrátí false - různé hodnoty 1 prvku Seřazení pole Řazení prvků pole patří mezi složitější úlohy, proto je dobře, že je k dispozici mechanismus, který to za nás vykoná. Příslušná statická metoda se jmenuje sort() a opět může pracovat jak na celém poli, tak na jeho části. Řadit můžeme pole primitivních prvků i (s určitým omezením - viz dále) prvků referencí na objekty. Prvky primitivních typů jsou řazeny optimalizovaným algoritmem quicksort, řadícím v čase n*log(n). long la[] = new long[1000]; ... // zde se přiřazují prvky Arrays.sort(la); Prvky referenčních typů můžeme řadit pouze tehdy, implementuje-li jejich třída rozhraní java.lang.Comparable (ještě o něm bude řeč někdy později) a jsou navíc vzájemně porovnatelné (jejich metoda compareTo() musí akceptovat typ, s nímž se prvek srovnává). Na to je třeba dát pozor a skutečně srovávat jen to, co srovnávat lze. Pro řazení objektů se používá algoritmus mergesort, řazení je stabilní (předem seřazené posloupnosti již nejsou měněny). Object oa[] = new Object[3]; oa[0] = "abc"; oa[1] = "123"; oa[2] = "%%%%%"; Arrays.sort(oa); // toto lze oa[1] = new Double(1); Arrays.sort(oa); // nelze, způsobí výjimku ClassCastException Vyhledávání v poli Poslední z věcí, na kterou se dnes podíváme, je vyhledávání prvku v poli. To je zde implementováno jako hledání binárním dělením - proto je nutné, aby pole bylo předem seřazeno metodou sort(). Pokud se neseřadí, není chování algoritmu definováno. Metoda binarySearch() vrací hledaného index prvku, nebo zápornou hodnotu v případě nenalezení. Pro referenční typy platí stejná pravidla jako u metody sort() - protože tuto metodu musíme stejně předtím zavolat, není již co řešit. float fa[] = new float[200]; ... // zde se přiřazují prvky Arrays.sort(oa); // seřazení pole int i = Arrays.binarySearch(0.4); // vrátí index hledaného prvku Pohodlnější práce s hromadnými daty Pole jsou jednoduchou formou uchovávání většího množství dat. Na jejich omezení ale narazíme již v okamžiku, kdy potřebujeme měnit počet prvků nebo dělat nějaké složitější operace. Proto
existují tzv. kontejnery (o nichž bude řeč příště), což jsou objekty, ve kterých máme uložena svá data a s jejichž pomocí můžeme provádět různé operace. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=693 Java (10) - Kontejnery I. Kontejnery (v Javě obvykle nazývané "kolekce") jsou objekty, které nám umožňují snadno a efektivně spravovat variabilní hromadná data. Dají se s nimi dělat úžasné věci. 23.3.2005 15:00 | Lukáš Jelínek | přečteno 64658× Úvod do problematiky Proč kontejnery Pole, s nimiž jsme se seznámili v předchozích kapitolách, mají několik zásadních nevýhod. Nemohou měnit svoji velikost (mají předem daný počet prvků), lze k nim přistupovat jen podle číselného indexu, řazení prvků se musí provádět externě a explicitně, k manipulacím s poli nelze přidávat vedlejší efekty (které se někdy hodí). Proto už hodně dávno vzniklo něco, čemu se obecně říká kontejnery. Kontejner je datový objekt, obsahující "v sobě" nějaké další objekty, ke kterým lze různým způsobem přistupovat a pracovat s nimi. Podle způsobů uložení dat a práce s nimi rozlišujeme různé druhy kontejnerů a při jejich použití si vždy vybereme ten, který se pro daný případ nejlépe hodí. V Javě se kontejnerům většinou říká kolekce (proto budu v dalším textu používat i tento termín), jejich chování se částečně podobá kontejnerům, které programátoři v C++ znají z knihovny STL. S javovskými kolekcemi se však pracuje trochu odlišně - dá se říci názorněji, avšak za cenu většího množství napsaného kódu. The Collections Framework Pod tímto názvem (dále budu používat "CF") se skrývá celý systém rozhraní a tříd, které bezprostředně souvisí s kolekcemi. Naprostou většinu z nich najdeme v balíku java.util (na jiné umístění včas upozorním). Důležitým faktem je, že v Javě 5.0 (tj. od JDK 1.5.0) došlo v CF k významným změnám (které CF částečně posunuly směrem k STL). Všechna další vysvětlení a příklady se budou týkat JDK verze 1.4.2, změny provedené ve verzi 1.5.0 uvedu na závěr. Práce s kolekcemi se rozhodně neomezuje jen na knihovnu CF, právě naopak. S výhodou si můžeme vytvářet vlastní kontejnery a implementovat např. různé vedlejší efekty operací, vlastní způsob uložení dat (když to hodně přeženu, kontejner lze třeba i přímo napojit na databázi), přidávat své vlastní operace atd. Kdo si dobře zažije práci s hotovými objekty, sám přijde na to, co mu případně chybí. Důležité pojmy I když není dobré přehánět to se suchou teorií, zde se jí (v zájmu dobrého pochopení funkce jednotlivých kontejnerů) težko vyhneme. Ale nebude jí mnoho... Iterátor je prostředek zajišťující sekvenční přístup k datům. Pracuje krokově, v každém dalším poskytne přístup k dalšímu prvku. Používá se tehdy, když potřebujeme postupně pracovat s jednotlivými prvky a buď nezáleží na pořadí, nebo pořadí vyplývá z vlastností kolekce, nad níž iterátor pracuje. Potřebujeme-li pracovat s prvky právě tímto způsobem (např. z každého něco přečíst), měli bychom vždy používat iterátory, protože bývají implementovány tak, aby to pro uvedené použití bylo nejrychlejší.
Porovnatelnost objektů je důležitá vlastnost, kterou potřebujeme pro uložení do některých kontejnerů. Je to tehdy, když se objekty uspořádávají v přirozeném pořadí (v základním způsobu uspořádání). Zjišťuje se, zda jsou objekty sobě rovny, nebo je jeden z nich "větší" či "menší" než druhý (u číselných hodnot je význam jasný; ostatní datové objekty mohou mít porovnatelnost implementovánu prakticky libovolným způsobem). V Javě je porovnatelnost zaručena tím, že třída implementuje rozhraní Comparable (obsahuje jedinou metodu compareTo()). Většina standardních tříd rozhraní Comparable neimplementuje - ani tam není vše ztraceno. Můžeme totiž vytvořit tzv. komparátor (rozhraní Comparator), který lze použít neomezeně, s libovolnými objekty (záleží pouze na jeho implementaci). Komparátor zajišťuje úplné uspořádání objektů (pomocí operací porovnání a testu rovnosti), můžeme ho použít jak při explicitním řazení prvků, tak ho předat některým kolekcím pro použití k vnitřnímu uspořádání. Volba vhodného kontejneru I když u každého typu kontejneru vždy vysvětlím, k čemu se daný typ hodí, obecný úvod udělám ihned. Máme totiž k dispozici širokou škálu kolekcí v různých kategoriích a správná volba je důležitá z hlediska dobré funkce programu a jeho rychlosti. Obvykle posuzujeme tato hlediska: Způsob práce (sekvenční nebo náhodný přístup) - různé typy kontejnerů se hodí pro určité způsoby přístupu, je nutné s tím předem počítat. Lze říci, že kolekce vhodné pro náhodný přístup se dají docela dobře použít i pro přístup sekvenční, ale spotřebuje se vždy více paměti, než by bylo nutné při volbě kolekce přímo pro sekvenční přístup. Četnost čtení, přidávání, odebírání, přesunů - platí totéž co pro náhodný a sekveční přístup. Je dobré si toto předem ujasnit, při nejasnosti provést během ladění testy s různými typy kolekcí a měřit rychlost a spotřebu paměti (i když se to někdy dělá dost špatně). Je třeba si rovněž uvědomit, že ne všechny kontejnery musí nutně implementovat všechny manipulační operace. Vláknová bezpečnost - k vláknům se dostaneme sice až někdy později, ale už teď je důležité, že by během manipulačních operací kontejneru (kdy je kontejner v nekonzistentním stavu) k němu nemělo přistupovat více vláken. Musí se "nějak" zajistit synchronizace přístupu - některé kolekce synchronizaci obsahují (spíše ty starší), jiné nikoliv (je to rychlejší). Pokud použijeme kontejner bez synchronizace a mohlo by dojít k současnému přístupu více vláken, je třeba synchronizovat přístup přímo v programu. Volba vhodného rozhraní - při vlastní práci s kolekcemi (kromě jejich vytváření) je vhodné používat rozhraní, která jsou společná většímu počtu kolekcí. Kód je pak obecnější a usnadní se tím případná změna implementační třídy kolekce. Poznámky pro programátory v C++ Tvůrci Javy říkají, že zatímco v STL jsou středobodem iterátory, v javovských kolekcích jsou podružnou záležitostí - v řadě případů se obejdeme bez nich. Skutečně, u spousty použití kontejnerů v Javě o iterátory ani nezavadíme. Dostaneme se k tomu později v příkladech. Příjemnou věcí je naopak fakt, že narozdíl od STL se javovské kolekce chovají na všech platformách stejně (jako většina věcí v Javě). Další podstatný rozdíl je ten, že v Javě se obecně nevyskytují (až na malé výjimky), narozdíl od C++, přetížené operátory - to platí i pro kontejnery a s nimi spojené iterátory. Pracujeme s nimi prostřednictvím volání různých metod. Základy práce s kolekcemi Jednoduchý příklad Než se pustíme do postupného poznávání světa The Collections Framework, bude dobré si nejprve ukázat, co to vlastně umí. Jako příklad jsem zvolil práci se třídou Vector, která byla jednou z prvních implementací kontejnerů (ještě před vznikem CF) a představuje vlastně pole s proměnnou velikostí:
Vector v = new Vector(); // vytvoří instanci s výchozími parametry v.add("abcd"); // vloží textový řetězec v.add(new Integer(6)); // vloží celé číslo v.add(1, "efgh"); // vloží textový řetězec na pozici 1 System.out.println("Na pozici 1 je: " + v.elementAt(1)); System.out.println("Obsah vektoru:"); Iterator it = v.iterator(); while (it.hasNext()) { // opakuj, dokud jsou položky System.out.println(it.next()); // tisk další položky } v.clear();
// smazání obsahu
V uvedeném příkladě se vytvoří instance třídy Vector s výchozími parametry. Pak do něj přidáme tři objekty: řetězec (přidá se na konec), celé číslo (opět na konec) a znovu řetězec (vloží se na pozici 1, data od této pozice se tím posunou). Indexy se (stejně jako u polí) číslují od nuly. Pokud chceme vložit primitivní datový objekt, musíme použít "zapouzdřující" referenční typ - v tomto případě pro int použijeme typ Integer. Na dalším řádku je vidět přímý přístup k prvku přes jeho index. Metoda elementAt pracuje obdobně jako operátor hranatých závorek u polí - a to včetně toho, že se při přístupu mimo platný rozsah vyvolá výjimka ArrayIndexOutOfBoundsException. Co se naopak liší, je vrácený typ. U polí jsme zvyklí na to, že máme vždy hodnotu toho typu, jakého je pole. U kolekcí tomu tak není, prvky se vrací jako typ Object a o správné přetypování se musíme postarat sami. Ve výše uvedeném případě na tom nezáleží, neboť se stejně vše převede na řetězec. Pak tu máme ukázku práce s iterátorem. Ten získáme obecně metodou iterator() a pracujeme s ním pomocí dvojice metod hasNext() (zjistí, zda jsou k dispozici další položky) a next() (vrátí následující položku). Prvky jsou opět v podobě referencí na Object. Závěrečné volání vymaže obsah celého vektoru (odstraní všechny položky). Výše uvedený příklad a další podobný (složitější) najdete i s komentáři v ukázkovém zdrojovém souboru ContainerExample.java, který si můžete stáhnout a zkompilovat. Pozn.: Pokud budete zkoušet kompilovat na Javě 5.0 (JDK 1.5.x), kompilátor vypíše varování. Nedělejte si s ním starosti, brzy si řekneme, proč to dělá. Podrobnější seznámení s kolekcemi Doufám, že jsem vás tímto nezáživným, obecným a téměř ryze teoretickým úvodem do světa javovských kontejnerů neznechutil natolik, že byste o ně ztratili zájem. Byla by to škoda, příště totiž přijde řada na jednotlivé kategorie a konkrétní implementace kolekcí - jak pracují, co umí a neumí, k čemu se hodí a jak je správně použít. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=735 Java (11) - Kontejnery II. Po minulém obecném úvodu do světa kolekcí se podíváme na jednotlivá rozhraní a implementace. Správný výběr je důležitý pro tvorbu rychlých a úsporných programů.
6.4.2005 15:00 | Lukáš Jelínek | přečteno 68340× Kategorie kontejnerových rozhraní Jak jsem říkal už minule, namísto práce s implementačními objekty je vhodné při práci používat rozhraní. Systém kolekcí je na rozhraní založen, existuje jich zde celá hierarchie a my můžeme pracovat na té úrovni, která nám nejlépe vyhovuje (resp. na té nejvyšší použitelné). Máme dvě základní množiny kontejnerů, každá z nich vychází z jednoho rozhraní. První z nich jsou běžné (normální) kolekce odvozené od rozhraní Collection, do druhé kategorie patří kolekce asociativní ("mapované", obsahující dvojice klíč-hodnota) implementující rozhraní Map. Normální kolekce Typickým znakem "normálních" kolekcí je to, že pracujeme s jednoduchými prvky (prvek je jeden objekt). Vnitřní implementace kontejneru může být jakákoli a liší se podle toho, k čemu je tento kontejner určen. V rámci CF rozeznáváme dvě skupiny těchto kolekcí - množiny a seznamy (sekvence). Podívejme se na ně blíže: Množiny (rozhraní Set) - každý prvek může být v kontejneru pouze jednou. Není zde obdoba kontejneru multiset (s možností vícenásobné přítomnosti téhož prvku) známého z STL v C++. Prvek obecně nemá určenu žádnou polohu v množině, na základě které by k němu bylo možné přistupovat. Zvláštním případem je podrozhraní SortedSet, což je seřazená množina. Seznamy (rozhraní List) - prvek může být v kontejneru vícekrát a má určenu jednoznačnou polohu (index). Asociativní kolekce Často jsme v situaci, kdy potřebujeme k datům přistupovat ne podle číselného indexu, ale podle nějakého obecného klíče. Asociativní kolekce obsahují prvky jakožto dvojice klíč-hodnota, kdy každý člen této dvojice může být libovolného referenčního typu. Obdobně jako u množin, ani zde nemáme něco jako multimap z STL. Ke každému klíči může příslušet nejvýše jedna hodnota. Další shodná vlastnost s rozhraním Set je existence seřazené varianty - je to rozhraní SortedMap. Obecné implementační třídy Nyní přichází čas na bližší seznámení s různými implementací kolekcí. Každá se hodí pro určitý způsob použití. HashSet - množina s hešovací tabulkou Třída HashSet používá pro ukládání dat hešovací tabulku. Práce je velice rychlá (většina operací probíhá v konstatním čase), nezaručuje ale pořadí prvků. Prázdné reference jsou povoleny. HashSet můžeme použít prakticky vždy, kdy nepotřebujeme pracovat s prvky v konkrétním pořadí (tj. ve většině případů). Pro optimální výkon je důležité správně zvolit výchozí kapacitu kontejneru - příliš velká způsobuje zbytečnou alokaci paměti a navíc i pomalejší sekvenční přístup (přes iterátor). Proto je dobré před použitím promyslet, kolik prvků se zde bude obvykle vyskytovat a podle toho kontejner nadimenzovat (výchozí kapacita je 101 prvků). Set s = new HashSet(); // vytvoření nové množiny s.add(new Integer(15)); // vložíme postupně 3 prvky s.add(new Integer(-2));
s.add(new Integer(123)); s.add(null); // vložíme null - je to povoleno s.add(new Integer(-2)); // -2 už v množině je, podruhé se nevloží System.out.println(s.size()); // vypíše počet prvků, tedy 4 (včetně hodnoty null) TreeSet - množina se stromovou strukturou Pro práci se seřazenými prvky použijeme třídu TreeSet. Protože je to seřazená množina, implementuje rozhraní SortedSet. Většina operací se provádí v logaritmickém čase. Okruh použití TreeSet je vymezen potřebou seřazené množiny. Pokud vkládané prvky neimplementují rozhraní Comparable, musíme pro ně definovat komparátor (tj. implementovat rozhraní Comparator) a předat ho konstruktoru objektu TreeSet. SortedSet s = new TreeSet(); // vytvoření množiny s.add(new Double(12.3)); // vložíme 3 prvky s.add(new Double(100.456)); s.add(new Double(-0.001)); System.out.println("První prvek je: " + s.first()); // vypíše první prvek Iterator it = s.iterator(); // iterujeme přes všechny prvky while (it.hasNext()) { // budou se vypisovat ve vzestupném pořadí System.out.println(it.next()); } ArrayList - pole proměnné velikosti Podobně jako minule zmíněná třída Vector (o ní ještě bude řeč) poskytuje i tato třída implementaci pole o proměnné délce. Přístup přes index je v konstatním čase, přidávání na začátek nebo modifikace uvnitř posloupnosti v čase lineárním. ArrayList použijeme ve všech případech, kde bychom jinak použili běžné pole a potřebujeme měnit délku. Výjimkou jsou případy, kdy velmi často dochází k přidávání prvků na začátek nebo provádět změny uvnitř seznamu - tam se ArrayList nehodí. Následující příklad ukazuje podobnost práce s polem a s kontejnerem typu ArrayList, a samozřejmě výhodu použití kontejneru: String sa[] = new String[3]; // vytvoření pole pro 3 položky sa[0] = ""; // vložíme do něj prvky sa[1] = "abcd"; sa[2] = "123w"; // tady končíme - pole nejde zvětsit List l = new ArrayList(3); // vytvoření seznamu pro 3 položky l.add(0, ""); // vložení prvků l.add(1, "abcd"); l.add(2, "123w"); // přidání dalších prvků - 1. varianta (vhodná pro přidání více prvků) l.ensureCapacity(5); // zvětšíme kapacitu seznamu (zde na 5) l.add(3, "tohle JDE"); // přidáme prvek l.add(4, "pokračujeme"); // přidáme prvek...
// přidání dalších prvků - 2. varianta (vhodná pro jednotlivé prvky) l.add("tohle také JDE"); // přidáme prvek l.add("pokračujeme"); // přidáme prvek... LinkedList - spojový seznam Třída LinkedList představuje dobře známý spojový seznam prvků. Co to znamená, lze snadno domyslet. Přístup podle indexové pozice je v lineárním čase, zatímco přidávání a mazání probíhá konstantní rychlostí. Doména použití třídy LinkedList je omezena na případy s častými modifikacemi na jiných místech, než je konec seznamu. Opět připomínám, i přes snadný přístup přes index prvků je při sekvenčním přístupu obecně mnohem efektivnější používat iterátory (pro tuto třídu to platí dvojnásob). HashMap - asocitativní kontejner s hešovací tabulkou Obdoba HashSet pro asociativní přístup k hodnotám. Pro rychlost i paměťovou náročnost platí to, co bylo řečeno u třídy HashSet. Prázdné (null) klíče a hodnoty jsou povoleny. Použijeme prakticky ve všech případech, kdy nepotřebujeme seřazené klíče. Viz následující příklad s příponami a MIME typy dat: Map m = new HashMap(); // vytvoří se asociativní kontejner m.put("txt", "text/plain"); // vložení dvojic klíč-hodnota m.put("html", "text/html"); m.put("jpg", "image/jpeg"); m.put("mpg", "video/mpeg"); // nyní pomocí klíče přistupujeme k hodnotám System.out.println("Přípona .jpg má typ: " + m.get("jpg")); // vypíše "image/jpeg" System.out.println("Přípona .gif má typ: " + m.get("gif")); // vypíše "null" (nebylo nalezeno) TreeMap - asocitativní kontejner se stromovou strukturou Opět obdoba - tentokrát TreeSet. Oproti HashMap přináší seřazení klíčů. Používá se při nutnosti mít seřazené klíče. Samozřejmostí je splnění podmínek pro porovnatelnost klíčů (viz HashSet). Speciální implementační třídy Jsou určité případy, kdy nám žádná z výše uvedených implementací nevyhoví. Proto máme k dispozici ještě další implementace, s jejichž pomocí můžeme dosáhnout požadovaných cílů. Podívejme se na některé z nich: LinkedHashSet - zřetězená množina s hešovací tabulkou Kompromis mezi HashSet a TreeSet, má zaručené pořadí prvků při rychlosti řádově stejné jako HashSet. Prvky jsou uloženy v pořadí, v jakém se vkládají, pokud ovšem nedojde k vícenásobnému vložení téhož prvku (prvek se znovu nevkládá, zůstane na původní pozici). Je třeba si uvědomit, že i když je zaručeno pořadí, nejedná se o seřazenou množinu a třída tedy neimplementuje rozhraní SortedSet. LinkedHashMap - zřetězený asociativní kontejner s hešovací tabulkou Obdoba LinkedHashSet pro asociativní kontejner. Platí to, co bylo řečeno u LinkedHashSet. Tato třída se hodí pro tvrobu různých pamětí s omezenou asociativitou (LRU apod.).
K další příkladům speciálních implementací se dostaneme při seznámení se změnami CF v Javě 5.0. Zastaralé implementace Již před vznikem CF existovaly v Javě určité implementace kontejnerů. Protože zde přetrvávají i nadále, není od věci je také trochu prozkoumat. Vector - pole proměnné velikosti Chování třídy Vector je prakticky shodné s třídou ArrayList s drobnými rozdíly. Vector má některé metody navíc (např. hledání od určitého indexu), lze mu stanovit přírůstek kapacity a je synchronizovaný (viz dále). V běžných případech dnes nemáme důvod používat Vector namísto třídy ArrayList. Hashtable - hešovací tabulka Chování obdobné jako u třídy HashMap, nepovoluje však prázdné klíče ani hodnoty a má synchronizaci. Wrappery pro práci s kolekcemi Existuje více způsobů, jak měnit vlastnosti instancí nějakých objektových tříd - lze to udělat např. parametry v konstruktoru nebo použitím tzv. wrapperů, což jsou objekty, které se "předřadí" před původní objekty, převezmou navenek jejich chování a přidají, odeberou či změní právě to, co je třeba. Pro změnu chování kontejnerů tu takové wrappery máme. Synchronizační wrappery Všechny standardní implementace kolekcí jsou bez synchronizace vícenásobného přístupu k objektu, takže se ve vícevláknovém prostředí může stát, že k témuž objektu přistupuje více vláken současně a dojde k přečtení nekonzistentních dat nebo k poškození objektu. Máme-li záruku, že ke kolizi nemůže dojít (program je pouze jednovláknový), není třeba zajišťovat synchronizaci. Na tento předpoklad ale nemůžeme prakticky nikdy spoléhat (zejména pokud píšeme opakovaně použitelný kód) a musíme synchronizaci zajistit buď explicitně (což je pracnější, přestože výkonnostně efektivnější) nebo použít synchronizační wrapper. Netýká se to "starých" kontejnerů Vector a Hashtable, ty mají synchronizaci již v sobě. Wrapper funguje prostě tak, že příslušné statické metodě z třídy Collections (pozor, neplést s rozhraním Collection!) předáme původní objekt a metoda nám vrátí wrapper k tomuto objektu, který už pak používáme stejně jako původní objekt (zde je jasně vidět, jak se hodí pracovat s rozhraním namísto konkrétní implementace). Více ukáže příklad: // typický příklad - nepotřebujeme nesynchronizovanou instanci List l = Collections.synchronizedList(new LinkedList()); // pro speciální použití - zachovává přístup k původní instanci SortedSet s1 = new TreeSet(); SortedSet s2 = Collections.synchronizedSortedSet(s1); Wrappery pro zákaz modifikací Někdy je třeba zakázat jakékoliv změny v kontejneru. Nejčastěji tehdy, když z nějakého uceleného systému (který se stará o přípravu dat) předáváme někam ven referenci na kontejner a nechceme, aby ho někdo zvenku měnil. Způsob vytvoření wrapperu je obdobný jako v případě synchonizačního wrapperu, opět je to
zřejmé z příkladu. Pokus o modifikaci wrapperového kontejneru způsobí výjimku UnsupportedOperationException. Původní kontejner samozřejmě lze (přes referenci na něj) i nadále měnit. // vytvoření seznamu List lst = new ArrayList(); // příprava dat apod. // nyní se vytvoří neměnná kolekce Collection c = Collections.unmodifiableCollection(lst); // pokus o změnu - vyvolá výjimku UnsupportedOperationException c.add(new Object()); Praktická ukázka Kdo by měl zájem, může si prohlédnout a vyzkoušet praktickou ukázku použití kontejnerů. V souboru PhoneBook.java najdete velice jednoduchý program, který ukazuje práci s kolekcemi na primitivním telefonním seznamu. V Javě 5.0 došlo u kontejnerů k poměrně významným změnám - přibyla typová bezpečnost, nová rozhraní a implementace atd. K tomu se dostaneme příště, podobně jako k algoritmům, které mohou nad kolekcemi pracovat. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=750 Java (12) - Kontejnery III. Pojednání o kontejnerech by nebylo úplné, kdybychom vynechali algoritmy, které nad nimi pracují. Také se podíváme na nové (a vesměs příjemné) věci, které se do The Collections Framework dostaly v Javě 5.0. 26.4.2005 06:00 | Lukáš Jelínek | přečteno 65794× Algoritmy Implementace algoritmů pro práci s kolekcemi jsou shromážděny ve třídě Collections převážně jako statické metody. Jsou obecně navrženy tak, aby bez ohledu na implementaci kontejneru zajišťovaly minimální operační složitost (i za cenu vyšší spotřeby paměti). Seřazení seznamu Máme nějaký obecný seznam (tj. nějakou implementaci rozhraní List) a potřebujeme ho seřadit. K tomuto účelu máme k dispozici dvě statické metody sort(), jedna řadí pouze porovnatelné prvky, druhá jakékoli - s tím, že poskytneme nějaký komparátor. Obě používají upravený algoritmus mergesort, řazení probíhá v čase n.log(n) a je stabilní. Podívejme se, jak to vypadá: List list = new ArrayList(); // vytvoření seznamu ... // naplnění atd. Collections.sort(list); // seřazení Promíchání seznamu Opakem seřazení je náhodné zamíchání seznamu. I to se může občas hodit (a to nejen v případě, že chceme přehrávat písničky v náhodném pořadí). I zde jsou metody dvě (shuffle()), jedna používá standardní, druhá uživatelský generátor náhodných čísel. Pracují v lineárním čase. List list = new LinkedList(); // vytvoření seznamu
... // naplnění atd. Collections.shuffle(list); // promíchání Obrácení pořadí Opět velmi jednoduchá, avšak užitečná činnost. Poskytuje ji metoda reverse(), pracující opět v lineárním čase. Hledání binárním dělením Podobně jako u polí, i u seřazených seznamů může s úspěchem použít hledání binárním dělením. Pro seznamy s možností náhodného přístupu (tj. implementující rozhraní RandomAccess) pracuje v čase log(n), pro ostatní bude čas řádově lineární. List list = new ArrayList(); // vytvoření seznamu list.add("abc"); // vložíme prvky list.add("efg"); list.add("cde"); Collections.sort(list); // seřazení System.out.print("Hledaný řetězec má pozici "); System.out.println(Collections.binarySearch(list, "efg")); // vypíše "2" Plnění seznamu Opět zjevná analogie s poli, co k tomu říci více... List list = new ArrayList(); // vytvoření seznamu list.addAll(Collections.nCopies(100, new Double(3.3))); // první naplnění Collections.fill(list, new Double(5.0)); // další naplnění Kopírování seznamu Zkopírovat seznam lze v zásadě třemi cestami. Jednou je vytvoření úplně nového seznamu pomocí "kopírovacího" konstruktoru (v uvozovkách proto, že zde nejde o skutečný kopírovací konstruktor). Tím se vytvoří nový seznam obsahující prvky toho původního (resp. obecněji, prvky libovolné kolekce implementující rozhraní Collection). List list1 = new ArrayList(); // vytvoření prvního seznamu ... // nějaké operace List list2 = new LinkedList(list1); // nový seznam obsahuje všechny prvky původního Druhou možností je volání statické metody copy() s obdobným efektem jako u polí, tedy se zkopírováním jen určitých prvků (aniž by ostatní byly dotčeny). Nový seznam musí být vytvořen předem. Pozor, jako první argument se uvádí cílový seznam, zdrojový až jako druhý. List list1 = new ArrayList(); // vytvoření prvního seznamu ... // nějaké operace List list2 = new LinkedList(); // vytvoření druhého seznamu Collections.copy(list2, list1); // kopírujeme Třetí způsob není v podstatě skutečné kopírování, vytváří totiž pouze pohled na tentýž seznam (při modifikaci se mění data v nové i v původním seznamu). Používáme metodu subList(), kterou získáme seznam stejného typu, jako byl ten původní. List list1 = new ArrayList(); // vytvoření prvního seznamu ... // nějaké operace List list2 = list1.subList(0, 10); // získání podseznamu list2.set(0, list2.get(1)); // zkopíruje prvek z pozice 1 na pozici 0 (v obou seznamech!) Konverze kolekcí na pole a naopak Běžné kolekce lze převádět na normální pole dvojicí metod toArray(). Metody se liší tím, že jedna
vytvoří pole s prvky typu Object, zatímco druhá pole prvků určeného typu. Více napoví příklad. Pozor - kromě určení typu je nutné vrácené pole vždy ještě přetypovat na správný typ (na to se často zapomíná)! Navíc je chování ovlivněno tím, jaké pole se metodě předá - pokud je alespoň stejně velké jaké daná kolekce, naplní se prvky (případné přebytečné pozice se nastaví na null), v opačném případě se vytvoří úplně nové pole. List list = new ArrayList(); // vytvoření seznamu Object oa[] = list.toArray(); // převedení na pole objektů String sa[] = (String[]) list.toArray(new String[0]); // převedení na pole řetězců Opačným případem je vytvoření seznamu (nebo jiné kolekce) z pole. K tomu slouží statická metoda asList() ze známé třídy Arrays. Ta vytvoří nový seznam, který je ovšem jen vnějším rozhraním k původnímu poli - je tedy neměnný. Pokud chceme vytvořit modifikovatelný seznam nebo nějakou jinou kolekci, musíme vytvořený seznam předat konstruktoru nového kontejneru. String sa[] = new String[10]; // vytvoření pole ... // naplnění apod. List list = Arrays.asList(sa); // vytvoření neměnného seznamu nad polem list.add("bbbb");
// nelze - způsobí výjimku UnsupportedOperationException
list = new List(list); // zkopírujeme seznam list.add("bbbb"); // tohle už lze Zjišťování informací o prvcích Ve třídě Collections existuje skupina statických metod, zabývajících se zjišťováním různých informací o prvcích obsažených v kontejnerech. O nich si povíme jen stručně. Máme zde metody min() a max(), každou ve dvou variantách (bez uvedení komparátoru a s ním). Již z jejich názvu vyplývá, že budou zjišťovat největší a nejmenší prvek. Ovšem pozor na to, že pro prázdné kolekce vyhodí výjimku NoSuchElementException! Set set = new HashSet(); ... System.out.println("Minimum: " + Collections.min(set)); System.out.println("Maximum: " + Collections.max(set)); Dvojice metod indexOfSubList() a lastIndexOfSubList() zjišťuje první, resp. poslední místo výskytu podseznamu v seznamu. Pokud žádný podseznam nenajde, vrátí -1. Ostatní algoritmy Seznam můžeme "zrotovat" o určitý počet pozic. Použijeme k tomu metodu rotate(). Dále lze prohodit dva prvky v seznamu metodou swap() nebo pomocí replaceAll() nahradit všechny výskytu určitého prvku. K dalším algoritmům se dostaneme za chvíli, jsou totiž k dispozici až od JDK 1.5. Novinky v kontejnerech od Javy 5.0 Java 5.0 (tedy JDK 1.5) přináší dost podstatné změny v rozhraní i implementaci kolekcí. Byly tak vyslyšeny časté stížnosti některých programátorů na napříliš bezpečný způsob práce s kolekcemi, na složité používání primitivních typů a další problémy. Současně přibyly některé funkce, které usnadňují práci s kontejnery. Podívejme se tedy blíže... Typová bezpečnost Programátoři v C++ jsou zvyklí, že pokud potřebují nějaký kontejner, vytvoří si instanci příslušné šablony s takovým typem, kterého jsou vkládané hodnoty. Pro takovou práci dříve javovské kolekce neposkytovaly žádnou podporu, do kontejneru bylo možné vkládat prakticky cokoliv a pokud někdo
vyžadoval typovou bezpečnost, musel si vše ošetřit sám. Nová verze Javy ale přináší podstatnou změnu. Nyní lze vytvořit typově určený kontejner, čímž máme zaručeno, že prvky v něm obsažené budou konkrétního typu. Pokus o porušení typové kontroly bude ohlášen již během kompilace. Podmínkou ale je, aby byl kontejner nejen vytvořen jako typový (tj. při volání konstruktoru), ale musí tak být deklarována příslušná proměnná. Kolekce bez typové kontroly lze nadále používat, kompilátor však bude vypisovat varování. // starý způsob - chceme pracovat jen s celými čísly List list = new ArrayList(); // seznam bez určení typu list.add(new Integer(5)); // vložíme číslo... list.add(""); // ...ale klidně i něco jiného // nový způsob List list = new ArrayList(); // seznam celých čísel list.add(new Integer(5)); // vložíme číslo... list.add(""); // ...a tohle by kompilátor nedovolil Uvedený způsob typové kontroly má jednu nevýhodu - je statický, takže lze použít jen tam, kde typ známe předem. V řadě případů je tomu však jinak, proto musíme použít dynamickou typovou kontrolu. Máme k dispozici wrappery na generování typově bezpečných kolekcí, které se používají podobně jako jiné wrappery (viz minulý díl). Při pokusu o porušení ochrany je vyvolána výjimka ClassCastException. // vytváření seznamu - použijeme wrapper List list = Collections.checkedList(new ArrayList(), Integer.class); ForeignObj obj = new ForeignObj(); obj.setList(list) // nyní se seznam někam předá... // ...a tam to může vypadat třeba takto: public class ForeignObj { ... public void setList(List lst) { lst.add(new Integer(5)); // tohle je v pořádku lst.add("abc"); // tohle v pořádku není a způsobí to ClassCastException } } Přímá práce s primitivními typy Komplikací při práci s primitivními typy (int, byte apod.) byla nutnost vytvářet zapouzdřující objekty při vkládání do kolekce. To už nyní není nutné. Objekty se sice stále vytváření, ale programátor může jako argumenty používat přímo příslušné primitivní typy (kontroverzní, nečisté řešení - ale ulehčuje práci). Typové kontejnery je ovšem nutné deklarovat s uvedením zapouzdřující třídy. List list = new ArrayList(); // seznam celých čísel list.add(new Double(2.75)); // starý způsob list.add(2.75); // nový způsob Speciální cykly pro snadnou iteraci Při sekvenčním přístupu k prvkům přes iterátor jsme museli napsat poměrně hodně kódu, který se při každém takovém použití opakoval. Proto vznikla (opět podle mého názoru nepříliš čistá) berlička, spočívající v "rozšířeném" (resp. speciálním) cyklu for. Tento speciální cyklus řeší
syntakticky to, co se dosud provádělo ručně. Posuďte sami: List<String> list = new ArrayList<String>(); // původní způsob (klasický cyklus) for (Iterator<String> i = list.iterator(); i.hasNext(); ) { System.out.println(i.next()); } // nový způsob (rozšířený cyklus for) for (String s : list) { System.out.println(s); } Fronty Často používanými strukturami jsou fronty, proto se dostaly i do CF. Máme zde nová rozhraní Queue (obecná fronta, rozšíření rozhraní Collection o operace typické pro frontu) a BlockingQueue (potomek Queue, přidává blokující operace). BlockingQueue (a její implementace, viz dále) je součástí balíku java.util.concurrent, o kterém bude řeč někdy později - na tuto dobu bych také přenechal další detaily ohledně front, bude to (z hlediska souvislostí) vhodnější. Nyní tedy jen řeknu, že jednou z implementací front je i spojový seznam - LinkedList. Kolekce "téměř jen ke čtení" V řadě případů kolekci někdy na počátku vytvoříme a pak už se nemění buď vůbec, nebo jen zřídka. Pro takové situace se hodí implementace, která zajišťuje maximální rychlost při operacích čtení, bez ohledu na rychlost manipulačních operací. V Javě 5.0 tuto skupinu reprezentují třídy CopyOnWriteArrayList a CopyOnWriteArraySet (obě z balíku java.util.concurrent). Při přístupu k prvkům pracují velmi rychle, modifikace způsobí zkopírování celého kontejneru (je to podobné jako u tzv. konstantních databází), což je sice pomalé, ale tady to nevadí. Výhodou je, že se vůbec nemusíme starat o synchronizaci, problémy se současným přístupem nejsou. Nové algoritmy Ve třídě Collections přibylo několik statických metod, poskytujících poměrně příjemné funkce: frequency() - zjistí četnost výskytu určitého prvku v kolekci disjoint() - zjistí, zda jsou dané kolekce disjunktní (nemají společné prvky) addAll() - přidá do kolekce všechny prvky pole reverseOrder() - vytvoří komparátor, který funguje přesně obráceně (zajišťuje obrácené uspořádání) než ten původní Možná toho bylo o kolekcích až příliš, ale doufám, že to nevadí. Příště se vrátíme až na úplný začátek a povíme si zase něco o psaní programů, kompilaci, spouštění apod. Od doby, kdy seriál začal (tj. od loňského léta) se totiž leccos změnilo, současně tím ale budu reagovat i na reakce čtenářů, že by rádi do těchto věcí pronikli hlouběji Online verze článku: http://www.linuxsoft.cz/article.php?id_article=770 Java (13) - JDK, vývojová prostředí Dnes se vrátíme zpět k úvodu - od začátku seriálu se totiž v Javě leccos změnilo (k lepšímu). Chtěl bych také připomenou některé časté problémy při a po instalaci Javy, a jejich řešení. 5.5.2005 07:00 | Lukáš Jelínek | přečteno 30475× Pokrok nelze zastavit
Když tento seriál začínal (tehdy ještě pod taktovkou Petra Hatiny), existovala pod kódovým názvem Tiger beta verze něčeho, co sice mělo úplně nové označení, ale ve skutečnosti to bylo pokračování toho předchozího. Ano, řeč je o Javě 5.0, která přináší do jazyka i do knihoven řadu nových věcí. Ale nepředbíhejme, vezmeme to postupně... Co instalovat Pravděpodobně již každý ví, že k běhu programů napsaných v Javě je (s výjimkou případů, kdy se javovské programy zkompilují do nativního strojového kódu) bezpodmínečně nutné provozní (též "běhové", "spouštěcí" apod. - v angličtině "runtime") prostředí nejméně v té verzi, pro kterou je daný program určen. Zpětná kompatibilita se plně zachovává (až na pár výjimek), takže program napsaný a zkompilovaný třeba pro Javu 1.1 na Javě 1.5 spustíme bez problémů. Pro kompilaci zdrojových kódů potřebujeme vývojářský balík, nazvaný Java Development Kit (příp. u některých verzí mírně odlišně). Jak už bylo řečeno ve druhém dílu seriálu, kromě "originálního" JDK od Sun Microsystems existují ještě další verze od jiných producentů A pozor, i tyto verze mají, jak si hned řekneme, svůj smysl. Přesto bude pro většinu vývojářů první volbou právě sunovská Java. Kdy má smysl instalovat jiné verze JDK? Např. tehdy, když zjistíme, že originální verze nefunguje. Typickým případem jsou např. víceprocesorové servery, tam někdy funguje jen Blackdown Java, zatímco JDK od Sunu ani od IBM nikoliv. Obecně ale doporučuji začít vždy Javou od Sunu, a to buď hned verzí 1.5.0 (poskytuje nejvíce možností a přitom umožňuje kompilovat i programy, které potom poběží na starších verzích - současně bude ale pravděpodobně obsahovat dost chyb), nebo 1.4.2 (stabilní, dobře odladěná verze). JDK 1.5.0 má v tuto chvíli pouze Sun, u ostatních producentů najdeme verze končící 1.4.2. Dokumentace Dokumentace k nástrojům JDK a ke knihovnám Javy není součástí instalačních balíků JDK, instaluje se zvlášť. Balík je poměrně objemný (pro JDK 1.5.0 má 45 MB, po rozbalení na disku zabere několikanásobek, většinou přes 250 MB), proto kdo ho nechce stahovat a má rychlé síťové připojení, může pracovat přímo s on-line verzí. Znamená to ale, že nebude mít přímý přístup do dokumentace z vývojových prostředí. Platformy Jak provozní, tak vývojové prostředí máme k dispozici pro celou řadu platforem - nás bude samozřejmě nejvíce zajímat GNU/Linux. Potěšující je, že Javu 5.0 můžeme používat i na 64bitových systémem (v 64-bitovém režimu) a využít tak jejich plnou sílu. Postup instalace Instalace JDK Nyní popíši typický postup instalace JDK 1.5.0 na běžnou linuxovou distribuci (uplatnitelný např. na Fedora Core, Mandrake Linuxu nebo SuSe Linuxu). Pokud není uvedeno jinak, provádí se příslušné kroky bez rootovských práv. Předpokládejme systém, na kterém dosud žádná (standardní) Java nebyla. Stáhneme příslušný instalační balík. Lze stahovat balík obsahující jen JDK, anebo větší balík, jehož součástí je také vývojové prostředí NetBeans (viz níže). Můžeme si vybrat ze dvou formátů: univerzálního (obsahuje grafický instalátor, lze instalovat prakticky na všechny distribuce, na libovolné místo v souborovém systému), a formátu RPM (instaluje se předem - již v balíku definované místo, vždy jako root, musí být podpora RPM). Máme-li univerzální balík, jednoduše ho spustíme (je samorozbalovací, extrahuje a spustí virtuální stroj Javy, potom aktivuje průvodce instalací). Lze ho spouštět jak jako root (a instalovat kamkoliv,
tedy i nahradit stávající verzi), nebo i jako běžný uživatel (kamkoliv, kam daný uživatel smí zapisovat), což umožňuje uživateli nainstalovat si tuto Javu jen pro sebe - ostatní budou užívat tu, která byla nainstalována rootem do systému. Instalace se implicitně spustí v grafickém režimu. Lze si vynutit konzolový režim přepínačem -console, ovšem ne všechny terminály jsou podporovány! Existuje také "tichý" režim, ale ten běžně nelze doporučit. Po instalaci pokračujeme bodem 4. RPM balík je nejprve nutné rozbalit, protože je uložen opět v samorozbalovacím souboru s příponou .bin, teprve pak získáne vlastní RPM balík. Ten se instaluje standardním způsobem - tedy rpm -i nazev_baliku.rpm apod. Rootovská práva jsou nutná, JDK se nainstaluje na standardní místo ve stromě /usr (adresář pak bude něco jako jdk1.5.0_01). Máme nainstalováno, proto zkusíme spustit program java, nejlépe z domovského adresáře (program spuštěn bez parametrů by měl vypsat seznam přepínačů). Podle toho, kam se Java nainstalovala a jakým způsobem, se toto může a nemusí podařit. Pokud to nepůjde (nejsou nastaveny cesty), máme dvě možnosti - buď přidat do proměnné PATH podadresář bin v adresáři JDK, nebo druhou možnost (podle mého názoru lepší), vytvořit symbolické odkazy na programy někde, kam se lze přes PATH dostat (např. v /usr/local/bin). Pokud vše funguje, jak má, stáhneme a nainstalujeme dokumentaci. Je v samostatném balíku ZIP, který stačí rozbalit, nejlépe do adresáře, kde je nainstalován JDK (podle umístění mohou být potřebná rootovská práva - viz výše). Rozbalením se vytvoří adresář docs, obsahující veškeré soubory dokumentace. Tím je instalace základního vývojového balíku u konce. Pokud bylo součástí balíku vývojové prostředí NetBeans, mohlo být nainstalováno již v této fázi. Pokud jsme ho neinstalovali, můžeme ho nainstalovat později nebo pracovat s nějakým jiným (případně i bez něj). Časté problémy Instalaci Javy bohužel často provázejí problémy. S verzí JDK 1.5.0 je jich většina odstraněna, ale zejména pro ty, kdo zkusí instalovat některou ze starších verzí, uvedu několik častých problémů a jejich řešení. Zmíním se o některých speciálních situacích. Pod rootem všechno funguje, pod běžným uživatelem ne. Častá chyba u starších verzí, problém bývá v tom, že se (zcela nepochopitelně) pro celý strom souborů nastaví práva číst a spouštět soubory pouze pro vlastníka. Řešení je jednoduché - nastavit souborům a adresářům práva (oktalově) 0755. Chci vytvořit v adresáři (/usr/bin apod.) symbolické odkazy, ale jsou tu nějaké soubory se stejnými názvy. Soubory pravděpodobně patří ke GJC (tedy k javovské komponentě kompilátoru GCC) nebo k nějaké implementaci Javy (třeba Kaffe). Nejlepší je začít zjištěním, zda není něco takového nainstalováno (typicky přes RPM) - pokud je, odinstalovat to (zde mohou bohužel vadit závislosti). Pokud se nic nenašlo nebo to nejde bez problémů odinstalovat, doporučuji vytvořit speciální adresář, tam soubory přesunout a pak vytvořit odkazy na soubory z nové Javy. Toto řešení má výhodu v tom, že lze vše rychle vrátit zpět. Instalátor zatuhne, podle top Java stále běží a bere si 100 % procesoru. To se někdy stává - už jsem zmínil ony víceprocesorové servery, ale může to nastat i jinde. Pravděpodobně je nějaká chyba v implementaci JVM, která s daným hardwarem způsobuje zacyklení. Některé chyby už byly reportovány. Pokud se to stane, zkuste jinou (starší) verzi, anebo JDK od IBM nebo Blackdown (zejména tato implementace je z tohoto pohledu velmi spolehlivá). Chci mít nainstalováno víc verzí Javy současně, jak na to? Běžně není důvod mít více JDK, provozní prostředí i kompilátor umí emulovat starší verze. Přesto to ale poměrně snadno lze, stačí si balík nainstalovat do zvláštního adresáře (pokud to instalátor umožňuje) a spouštět buď přes absolutní cestu, nebo přes symbolické odkazy. Mám nějaké hotové (zkompilované) třídy a chtěl bych je použít. Jak to udělat? Cest je hned několik. Pro jednotlivé případy stačí "podstrčit" jejich umístění (tedy adresář nebo JAR archiv) programům javac a java - použije se argument -classpath nebo -cp . Pro častější použití se hodí nastavit proměnnou prostředí CLASSPATH na toto umístění (nebo více umístění oddělených dvojtečkou).
Vývojová prostředí Přestože lze programy psát v libovolném editoru, kompilovat je buďto z něj (pokud to umožňuje) nebo z příkazové řádky, zejména začínajícím javistům vřele doporučuji používat některé integrované vývojové prostředí - výhody se projeví hlavně při ladění. Rád bych tedy nyní představil některá z těchto prostředí. NetBeans - open-source projekt původně vyvíjený českými autory, později prodaný firmě Sun Microsystems, která z něj učinila základ pro svá (za nemalé peníze poskytovaná) vývojová prostředí. Pro většinu použití však "základní" NetBeans bohatě stačí. Podívejme se na funkce a vlastnosti programu: běží v Javě, uživatelské rozhraní používá knihovnu Swing ze standardního balíku Javy; prostředí využívá jako aplikační základ NetBeans Framework umožňuje snadnou editaci, kompilaci a spouštění programů má komfortní funkce pro debugging obsahuje dobře použitelnou správu verzí má grafického návrháře UI obsahuje řadu různých pomocných nástrojů (internacionalizace, práce s dokumentací, automatické aktualizace apod.), další lze přidat jako pluginy spotřebuje hodně paměti a mnoho času procesoru Eclipse - opět open-source projekt, ovšem podporovaný prozměnu firmou IBM, která na něm staví své WebSphere Studio. Eclipse je v podstatě velmi obecný framework, do kterého se funkcionalita přidává pomocí pluginů - existuje jich velké množství a další stále vznikají. Co tedy Eclipse nabízí: běží opět v Javě, ale jako GUI místo Swingu používá SWT (částečně nativní implementace) vývojové prostředí jako takové tvoří základní sada pluginů, další lze přidávat v základní sadě poněkud méně funkcí než NetBeans chybí grafický návrh GUI (lze přidat pluginem, ale zatím jsem neviděl dostatečně fungující nekomerční plugin pro tento účel) velká paměťová náročnost, o něco rychlejší než NetBeans BlueJ - společný projekt několika univerzit určený jako prostředí pro výuku Javy. Je to sice closedsource program, ale je zdarma. je navržen k výuce a je tedy velmi vhodný pro začátečníky obsahuje grafický, abstraktní, čistě objektový návrh dat běží v Javě bez závislosti na platformě rozšiřitelný pomocí pluginů JBuilder - vývojový balík od firmy Borland, dostupný v několika variantách, jedna z nich je bezplatná. běží opět v Javě množina funkcí velice podobná jako u NetBeans, spíše ještě širší Všechna tato prostředí fungují (mimo jiné) pod Linuxem, proto můžeme zvolit kterékoli z nich. Doporučit některé z nich je těžké (s BlueJ navíc ani nemám zkušenosti, proto ho nemohu hodnotit), záleží na osobním vkusu. Nejlepší cestou asi bude si každé z prostředí vyzkoušet a pak se rozhodnout. Eclipse má handicap v chybějící podpoře grafického návrhu GUI (i když grafický návrh GUI není vždy ta nejlepší cesta). Osobně mám nejlepší zkušenosti s Eclipsem, z prostředí s grafickým návrhářem pak s NetBeans. V JBuilderu jsem se vždycky poněkud ztrácel, ale poslední verzi (JBuilder 2005) jsem zatím nezkoušel, třeba se ovládání změnilo k lepšímu. Ve všech případech je potřeba zajistit dostatek paměti, vývojová prostředí jsou velmi náročná (a s méně než 256 MB je nelze rozumně provozovat). Odkazy Java Development Kit Sun JDK 1.5.0 IBM JDK 1.4.2 (bohužel vyžaduje registraci)
Blackdown 1.4.2 Instalace Javy pod Linuxem Postup instalace Jak nastavit cestu ke třídám Struktura souborů Vývojová prostředí NetBeans Eclipse BlueJ JBuilder Nyní máme nainstalováno vše, co je třeba (tedy JDK včetně dokumentace a vývojové prostředí), posuneme se tedy dále. Při psaní programů (a to i těch nejjednodušších) je dobré dodržovat určité pravidla, jak má kód vypadat - usnadňuje to jak porozumnění kódu jinými lidmi, tak i orientaci v programu. Označme to termínem "štábní kultura". Protože je zbytečné vymýšlet vymyšlené, podíváme se na pravidla psaní kódu tak, jak je doporučují přímo tvůrci Javy. Druhá část příštího dílu bude věnována věcem, které jsou typické pro jazyk Java a měl by je každý znát. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=797 Java (14) - štábní kultura, specifika Při psaní programů je vhodné dodržovat určité konvence - nejlépe ty, které specifikovali tvůrci jazyka. Na tato pravidla se dnes podíváme. Poté přijde řada i na některá specifika Javy, která se v jiných jazycích buď nevyskytují, nebo mají poněkud odlišný význam. 19.5.2005 09:00 | Lukáš Jelínek | přečteno 26639× Konvence pro psaní kódu V následujících odstavcích společně projdeme jednotlivými pravidly pro psaní kódu. Nebude to vyčerpávající popis, zájemci o detaily nechť si přečtou přímo příslušné dokumenty. Nejedná se samozřejmě o žádné dogma, ale vřele každému doporučuji, aby se těchto pravidel držel. Je velmi nepříjemné, když někdo čte sám po sobě kód napsaný před několika měsíci, a již není schopen rozluštit, co bylo jak míněno. O problémech při předávání kódu mezi programátory ani nemluvím. Jmenné konvence Při pojmenovávání čehokoliv ve svých programech se v maximální možné míře snažíme používat angličtinu. U mezinárodních projektů je to naprosto nezbytné, ale i v ostatních případech se to hodí. Je to přehlednější, názornější a logičtější, už vzhledem k obrovskému množství tříd ve standardních knihovnách (čím větší míra konzistence jmen s těmito knihovnami, tím lépe). Každý název by měl co nejlépe vystihovat smysl toho, co se jím pojmenovává. Je dobré se vyvarovat nejednoznačností, podobností znaků a dalších vizuálně problematických věcí, které by činily kód méně přehledný. Vyhýbáme se také zbytečně dlouhým názvům. Pro jednotlivé kategorie identifikátorů používáme různá pravidla: Balíky (package) pojmenováváme podle svého určení a dodržujeme hierarchii vycházející z URL. Používáme opačný zápis oproti klasickým URL, za doménou pokračujeme dalšími upřesňujícími názvy (např. podle projektů a jejich částí). Názvy píšeme malými písmeny, např. net.somejavaproject.mail, org.somejavaproject.db.cache Rozhraní (interface) mají název podle schopnosti dělat nějakou činnost (Runnable, Throwable, Serializable) nebo podle svého určení (ResultSet, Collection, Iterator). Používají se malá písmena, velké jen na začátku celého názvu a případně dalších slov v názvu.
Třídy (class) se pojmenovávají podle účelu a vlastností třídy. Škála může být velmi široká. Je dobré se vyhýbat již existujícím názvů, pokud lze třídu pojmenovat jinak a není to na úkor srozumitelnosti. Název by měl být tvořen jen podstatnými a přídavnými jmény. Příklady: Object, String, ClassCastException Konstanty pojmenováváme podle svého logického významu, nikoli podle hodnoty, kterou nesou; tedy např. FOREGROUND_COLOR a ne LIGHT_GREEN_COLOR. Mohou existovat výjimky, ale to se bude týkat spíš knihoven než normálního programu. Používají se velká písmena, případně číslice, slova se oddělují podtržítky. Proměnné se opět pojmenovávají podle významu. Název se tvoří z malých písmen, velká se používají na začátku slov uvnitř názvu. Pozor na podtržítko (je to platný znak, ale silně nedoporučený), velmi snadno se přehlédne. Pro pomocné proměnné (indexy, řídicí proměnné apod.) si lze vystačit s jednopísmennými názvy. Příklady: fileName, result, i, x Názvy metod by měly vycházet ze sloves (provádějí činnosti), používají se malá písmena, na začátku slov uvnitř názvu pak velká (run(), createObject(), eraseAllItems()). Pro přístupové metody (tedy ty, jejichž prostřednictvím pouze přistupujeme k datům uloženým v objektu) se používá speciální konvence. Metody, které získávají data z objektu, začínají buď get... (např. getColor(), getLastIndex()), nebo, pokud je návratová hodnota typu boolean, podle situace buď is... nebo has... (isReady(), hasFocus()). Metody zapisující hodnotu do objektu mají potom prefix set... (tedy setColor(), setFocus(), setEnabled()). Soubory a struktura adresářů Java počítá s tím, že každá třída (kromě vnořených) nebo rozhraní má svůj vlastní soubor. Soubor nese název třídy (rozhraní) a u zdrojového souboru se přidává přípona .java. Zkompilovaný soubor má potom příponu .class, pokud jsou ve zdrojovém souboru deklarovány nějaké vnořené třídy, pro každou kompilátor vytvoří zvláštní soubor (k názvu zapouzdřující třídy přidá název vnořené třídy oddělený znakem dolaru; podobně i u vícenásobného vnoření). Anonymní třídy mají také své soubory .class, ale místo názvu vnořené třídy kompilátor použije pořadové číslo. Příklad: Máme třídu MyClass a v ní vnořenou třídu MyNestedClass. Budeme mít tedy zdrojový soubor MyClass.java a z něj kompilátor vytvoří soubory MyClass.class a MyClass$MyNestedClass.class. Adresářová struktura musí odpovídat členění balíku. Máme-li tedy balík net.mycompany.jproj.db, potom musíme mít adresářovou cestu net/mycompany/jproj/db. Obecná pravidla pro psaní kódu Vlastní kód píšeme vždy tak, aby byl co nejpřehlednější a nejsrozumitelnější. Vyvarujeme se zejména příliš dlouhých řádků (přes 80 znaků), ale i zbytečného "rozcourávání" kódu. Odsazujeme obvykle po čtyřech mezerách (i když někdy mohou být vhodnější dvě). Řádky zalamujeme vždy tak, aby věci, které k sobě logicky patří, byly na stejném řádku. Vhodné místo pro zalomení je za čárkou a před operátorem. Výrazy se snažíme zalamovat na co nejvyšší úrovni vnoření. Text na novém řádku by měl začínat tam, kde na předchozím řádku začíná daná úroveň, případně, pokud by to nevypadalo dobře, odsadíme text osm mezer od začátku řádku. Příklady nebudu uvádět, odkazuji na velmi srozumitelné příklady v popisu konvencí. Pro zvýšení čitelnosti vkládáme do kódu prázdné řádky. Jsou vhodné zejména mezi metodami, před blokovým nebo řádkovým komentářem, mezi deklaracemi lokálních proměnných a výkonnými příkazy, a dále všude tam, kde to přispěje k čitelnosti kódu. Někde (např. mezi jednotlivými sekcemi v kódu) vkládáme dva prázdné řádky. Podobné to je s mezerami v řádcích. Také ty dáváme do míst, kde zvýší přehlednost (přesná místa uvádí specifikace). Unární, inkrementační a dekrementační operátory však nikdy od svých operandů neoddělujeme.
Další podobná pravidla (např. co psát na jeden řádek a co rozdělit, v jakém pořadí deklarovat členské proměnné apod.) uvádí specifikace. Jejich porušení není velkou chybou, ale přece jen může poněkud zhoršit čitelnost kódu. Pravidla pro programování Zatímco předchozí pravidla byla ryze formální záležitostí bez vlivu na funkci programu, nyní přejdeme k závažnějším pravidlům - a ta už se budou týkat samotného programování. Jejich nedodržováním si můžeme snadno přivodit zbytečné nepříjemnosti. Nejmenší možná viditelnost. Každá členská proměnná třídy a každá metoda by měla mít jen takový stupeň viditelnosti, který nezbytně potřebuje. Je hrubou chybou (která je bohužel často k vidění) deklarovat všechny metody (nebo dokonce proměnné) jako public, tedy s viditelností odkudkoliv. Dobrou cestou je deklarovat proměnné jako private (příp. default nebo protected) a poskytovat přístupové metody. Také metody určené jen k vnitřnímu použití ochráníme proti přístupu zvnějšku. Veřejné proměnné mají svůj smysl pouze u velmi jednoduchých tříd (nahrazujících struktury známé z C/C++), které používáme pouze k předávání dat (viz např. java.awt.Point). Nepoužívat vícenásobná současná přiřazení. Někteří programátoři "odkojení" jazykem C rádi používají složité výrazy, kde se současně přiřazuje, vyhodnocuje atd. celá řada věcí. Sice to zabere málo místa v kódu, ale je to nečitelné a zcela proti duchu Javy. Každé přiřazení si zaslouží samostatný řádek. Raději více závorek než méně. Protože úrovní priority operátorů je celá řada, je lepší se jistit závorkami navíc, než spoléhat na to, že se to vyhodnotí ve správném pořadí. Platí to zejména u ternárního operátoru, kde to navíc významně přispěje k přehlednosti. Pozor na zbytečné testy boolovské hodnoty. Má-li metoda vracet pravdivostní hodnotu, a tato je již k dispozici, není samozřejmě nutné ji znovu testovat. Tento občas viděný zlozvyk pochází z C++ a je způsoben schizmatem v reprezentaci pravdivostní hodnoty (bool vs. "BOOL definovaný jako int") - i když to nebylo nutné, často se dělala explicitní porovnání. Proto se to občas objeví i v Javě. Ovšem pozor - v Javě není hodnota boolean přetypovatelná na žádný jiný primitivní typ (ani obráceně), takže pokud máme číselnou (třeba int) hodnotu, kompilátor by její přímé vracení jako boolean ani nepovolil. Při deklaraci proměnnou vždy hned inicializovat. To je dobrý zvyk, protože máme zajištěno, že proměnná bude mít vždy nějakou předem určenou hodnotu, i když se na ni zapomene. Každý programátor určitě potvrdí, že neinicializované proměnné jsou v každém jazyce hodně nepříjemnou komplikací. Specifika Javy Každý jazyk má své specifické věci, které buď jinde nenajdeme, anebo mají (více či méně) jiný význam. Proto je dobré tyto věci znát (aspoň trochu) dřív, než začneme tvořit nějaké větší programy. Modifikátory V deklaracích rozhraní, tříd, metod a proměnných můžeme uvádět tzv. modifikátory, které určitým způsobem mění vlastnosti toho, co deklarujeme. Některé modifikátory jsou přítomny i v jiných jazycích (např. v C++), jiné nikoli. final - uvádí se u třídy, metody či proměnné a znamená, že daná entita je "konečná" a nelze ji měnit. U třídy to znamená, že od ní nelze odvodit potomka, metodu takto označenou nemůžeme v potomkovi předefinovat, proměnná nesmí po inicializaci změnit hodnotu (jinými slovy - je to konstanta). Pokud je proměnná referenčního typu, odkazovaný objekt lze měnit, ale "hodnotu odkazu" nikoliv. abstract - uvádí se u třídy nebo u metody, a znamená abstraktní třídu či metodu. Abstraktní třída je třída neúplně definovaná (má některé metody deklarovány, ale ne definovány), nelze vytvářet její instance - je určena k odvozování potomků. Pokud metodu deklarujeme jako abstraktní, nepíšeme už její tělo a musíme jako abstraktní deklarovat i třídu.
static - uvádí se u metod a proměnných. Takto deklarovaná metoda je metodou třídy (tedy nikoli instance), z dané třídy může přistupovat jen ke staticky deklarovaným proměnným a volat jen staticky definované metody. transient - uvádí se u proměnných. Znamená, že při serializaci (tedy transformaci objektu do datové podoby určené k uložení nebo přenosu) se bude tato proměnná ignorovat. Zabrání zbytečnému uložení nepotřebných dat, někdy je dokonce podmínkou úspěšné serializace (implicitně se serializují veškeré objekty, na něž daný objekt odkazuje - a ty nemusí vždy serializaci podporovat). volatile - uvádí se u proměnných. Označuje proměnnou, která může změnit svoji hodnotu nějakým "nestandardním" způsobem a přístup k ní tedy nelze optimalizovat (běžně si každé vlákno vytváří pracovní kopii sdílených dat, modifikace se musí synchronizovat) - každý přístup bude znamenat přístup přímo k této proměnné. synchronized - používá se u metod, případně bloků (synchronizované bloky). Cílem je zajistit synchronizaci přístupu vláken k danému objektu. Při zavolání metody (nebo vstupu do synchronizovaného bloku) se objekt zamkne a vlákno, které metodu zavolá následně, bude pozastaveno do doby, než první vlákno metodu opustí. strictfp - používá se u rozhraní a tříd. Vynutí použití striktní matematiky plovoucí řádové čárky (pro zajištění plné kompatibility, stejných výsledků na všech platformách). Finalizace Protože se v Javě nepoužívají destruktory, nemáme možnost při rušení objektu provést činnosti, které uvolňují prostředky použité objektem. Existuje ale jedna cesta, jak to částečně nahradit finalizace. Třída může definovat speciální metodu, tzv. finalizátor (metoda finalize()). Tato metoda se volá po tom, co se na objekt přestalo odkazovat, ale ještě předtím, než se uvolní (garbage collectorem) paměť obsazená danou instancí objektu. Používání finalizace se ovšem nedoporučuje. Není totiž záruka, kdy, a zda vůbec, se finalizátor zavolá (záleží to na implementaci JVM). Proto na finalizaci nelze spoléhat a není tedy prakticky žádný důvod ji používat. Lepším postupem je vytvořit si pro daný účel nějakou normální metodu a tu ve správný okamžik explicitně zavolat. Vnořené třídy Třídy lze prakticky libovolně vnořovat. Běžně není důvod vytvářet vnořené třídy, výjimkou jsou případy, kdy potřebujeme vytvořit třídu, kterou budeme používat pouze (nebo téměř pouze) v kontextu třídy, do níž vnořujeme: public class OuterClass { protected class InnerClass { ... } ... } Podobně jako třídy lze vnořovat i rozhraní. Vnořený objekt pak identifikujeme pomocí tečkové notace, pro výše uvedený příklad to bude OuterClass.InnerClass; jako příklad vnořeného rozhraní mohu uvést třeba java.util.Map.Entry. Pro každou vnořenou třídu kompilátor samostatný soubor - viz výše (odstavec o adresářové struktuře). Anonymní třídy
Speciálním případem vnořených tříd jsou třídy anonymní. Nemají vlastní název a definují se až v místě, kde je použijeme. Jako základ můžeme použít kteroukoli třídu nebo rozhraní (nesmí být final; všechny abstraktní metody musíme implementovat). Anonymní třídy jsou nesmírně silný nástroj, ale současně také nebezpečný. Snadnost použití svádí k nadužívání, což vede ke zbytečnému opakování stejného kódu, zhoršení přehlednosti a v neposlední řadě také zbytečné paměťové i časové složitosti. Správným postupem je používat je jen tam, kde se daný kód použije opravdu jen jednou - při opakování je lepší vytvořit standardní třídu. Thread t = new Thread() { public void run() { System.out.println("startuji..."); ... } }; t.start(); Uvedený příklad ukazuje vytvoření anonymní třídy jako potomka třídy Thread. Předefinováváme zde jedinou metodu run(). Pro každou anonymní třídu vytvoří kompilátor samostatný soubor. Vstup a výstup Tímto bych tedy tuto kapitolu, věnovanou javově specifickým věcem, uzavřel. Příště opět přejdeme k praktičtějším věcem, konkrétně k základům vstupně/výstupních operací. V Javě k tomu máme k dispozici silný aparát, proto je práce samotná úplnou hračkou. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=815 Java (15) - I/O operace I. Jednou z klíčových činností v programech je výměna dat s okolním světem. V Javě to jde velice snadno a elegantně. 1.6.2005 15:00 | Lukáš Jelínek | přečteno 40809× Základní informace o streamech Vstupně/výstupní operace lze v Javě realizovat několika způsoby. My se nyní zaměříme na ten základní způsob, tedy práci se streamy. Téměř každý, kdo programuje v nějakém jazyce, bude pojem stream znát. A právě v Javě se tomuto slovu dostává plného významu. Stream si lze představit jako trubku, jejíž konec máme k dispozici a můžeme "čerpat" data z něho (tedy číst) nebo naopak do něho (tj. zapisovat). Streamů existuje (z hlediska implementace) celá řada, z pohledu uživatele se však všechny streamy daného směru (tedy "dovnitř" nebo "ven") chovají skoro stejně - většinu funkcionality mají společnou. Pro streamy je charakteristická především jejich sekvenčnost. I když to neplatí úplně stoprocentně (existují streamy se "zpětným chodem"), s daty se pracuje v konkrétním neměnném pořadí. Streamy v Javě V Javě je každý stream reprezentován jako objekt, tedy instance nějaké třídy. Balík java.io obsahuje hierarchii základních tříd, se kterými si pro běžné operace vystačíme (další streamy najdeme i v jiných standardních balících). Pokud budeme potřebovat něco speciálního, není žádný problém si vhodnou třídu vytvořit (resp. odvodit od nějaké existující). Známe dvě hlavní kategorie streamů: binární a textové. Liší se způsobem práce se znaky, binární
streamy pracují se "surovými" bajty (tak, jak jsou), zatímco streamy textové pojímají bajty, resp. skupiny bajtů způsobem, který odpovídá nastavení prostředí. Binární vstupní streamy jsou odvozeny od abstraktní třídy InputStream, výstupní od třídy OutputStream. Textové pak od třídy Reader, resp. Writer. Základy práce se streamy Každý stream má svůj životní cyklus - je velmi jednoduchý a sestává z těchto etap: Vytvoření (zavolání konstruktoru) - může se vytvářet přímo tam, kde se používá, anebo ho může vytvořit nějaký jiný objekt a předat. Otevření - stream se často otvírá už při vytváření, pokud však otevřen nebyl, musí se otevřít později, jinak s ním nelze pracovat. Otevření znamená, že se alokují potřebné systémové prostředky a stream se připraví pro práci. Vlastní práce - se streamem se provádějí požadované operace, tzn. volají se jeho metody. Uzavření - toto je velmi důležité, bohužel se na to často zapomíná. I když korektně finalizovaný objekt streamu zaručuje správné zavření (u standardních streamů), podobně i normální ukončení programu, nikdy se na to nesmí spoléhat. Jednak se vyčerpávají systémové prostředky (file deskriptory apod.), a za druhé může mnoho dat zůstat nezapsaných (u výstupních streamů), a to i hodně dlouho. Streamy se prostě musí uzavírat, a to ihned, když s nimi přestáváme pracovat (s jednou výjimkou, kterou za chvíli zmíním). Ošetření chyb Důležitým aspektem práce se streamy je ošetření chyb, které se mohou vyskytnout. Téměř všechny chybové stavy jsou řešeny výjimkami. Tou základní je IOException, kterou mohou vyhodit naprostá většina streamových metod. Tato výjimka je synchronní (musí se tedy povinně zachytávat nebo deklarovat k předání výše), zahrnuje do sebe celou škálu podtříd (výjimek signalizujících konkrétní chybové stavy) a je společná všem streamům bez ohledu na implementaci. Kromě IOException mohou streamy produkovat i jiné výjimky, to ale závisí na jejich určení a implementaci. Jak jsem již uvedl, výjimku IOException může vyhodit prakticky kterákoli metoda streamu. Musíme to mít na zřeteli, ani takové uzavírání streamu není "bezpečná" operace. Když se na to zapomene, připomene to rázně kompilátor. Základní druhy streamů Se streamy pracujeme prakticky stejně, ať už se jedná o data v souborech na disku, o síťovou komunikaci, komunikaci mezi vlákny apod. I když je tato práce téměř shodná, existují podstatné rozdíly zejména v přípravě streamů před komunikací. Podíváme se tedy blíže na jednotlivé druhy streamů. Souborové streamy Asi nejčastějším způsobem komunikace s vnějším prostředím je čtení a zápis souborů. Z hlediska Javy se nerozlišují vlastnosti souborového systému, se soubory se pracuje vždy stejně, jen nás zajímá, zda soubor existuje, lze z něj číst nebo do něj zapisovat. try { InputStream is = new FileInputStream("soubor.dat"); // stream se hned otevře int i = 0; while ((i = is.read()) >= 0) { // čte se, dokud není konec souboru ... } is.close(); // zavření souboru } catch (IOException e) { ... // zpracování výjimky }
Příklad ukazuje základní způsob čtení ze streamu, v tomto případě souborového. Stream se otevře, v cyklu se z něho čte po bajtech (pozor - i když se čtou bajty, hodnota je typu int; je to z více důvodů, ale důležité je, že pokud je přečtena hodnota -1, bylo dosaženo konce souboru). Všimněte si, že všechny operace jsou uzavřeny do bloku try k zachycení výjimek. Řetězcové streamy Podobně jako třeba v C++, i v Javě lze snadno číst z řetězce a zapisovat do něj streamovým způsobem. Stream pracuje nad objektem typu StringBuffer, ke kterému můžeme získat přímý přístup - lze ale také ze streamu "vytáhnout" také instanci třídy String, ta se ale samozřejmě musí vždy vytvořit, neboť je neměnná. Následující příklad ukazuje, jak se streamově zapisuje do řetězce. Stream je samozřejmě textově orientovaný, což je naprosto v souladu s daným účelem. Pro výstupní řetězcové streamy je charakteristické, že operace nevyhazují výjimku IOException - a to ani při pokusu dělat nějaké operace po uzavření streamu (operace uzavření totiž nic nedělá). StringWriter sw = new StringWriter(); sw.write("abcd"); sw.write(sw.toString()); // obsahu streamu se zapíše zpět do streamu System.out.println(sw); Copak asi uvedený příklad dělá? Zapíše uvedený řetězec dvakrát za sebou (nejprve přímo, potom prostřednictvím metody toString() zavolané na streamu) a celý obsah vypíše na standardní výstup. Zbývá si ještě zodpovědět jednoduchou otázku, k čemu je to vlastně dobré - samozřejmě hlavně k tomu, že vytvořený stream můžeme předat k nějakému vnějšímu použití, kde nezáleží na tom, kam se data zapisují (resp. odkud se čtou). Mezi základní streamy patří ještě některé další druhy, ale o těch si řekneme až později. Nejdřív by se totiž hodilo znát něco jiného... Filtrové streamy Největší množství streamů patří do obrovské množiny, které se říká filtrové streamy. Takový stream si lze představit skutečně jako nějaký kus trubky s filtrem. Nejobecněji to vypadá tak, že tento stream napasujeme na nějaký jiný stream. Data, která přes filtrový stream procházejí, mohou být různě pozměněna, stream je může všelijak zkoumat a něco počítat atd. Filtrové streamy lze prakticky libovolně řetězit za sebe (pokud na sebe navazují stejné kategorie, ve smyslu binární a textové). Filtrových streamů je celá řada, řekneme si tedy nejprve o těch nejdůležitějších. Bufferované streamy Protože často pracujeme s malými objemy dat, nebývají vstupně/výstupní operace příliš operačně výkonné. Záleží na prostředcích operačního systému, druhu streamu atd., a většinou nemůžeme na nic spoléhat (píšeme platformově nezávislé programy!). Proto existují streamy, které obsahují vlastní buffer a optimalizují přístupy k datům. Představme si, že zapisujeme data třeba po jednom bajtu - to by za normálních okolností mohlo znamenat třeba mnoho zbytečných přístupů na disk, posílání "prázdných" paketů po síti apod. Přitom obvykle není žádný důvod, aby se data okamžitě sunula někam dál. Pro optimalizaci tedy použijeme bufferovaný stream. try { BufferedReader br = new BufferedReader(new FileReader("soubor.txt"));
String s = ""; while ((s = br.readLine()) != null) { ... } br.close(); } catch (IOException e) { ... } Příklad ukazuje hned několik aspektů práce s bufferovaným (textovým) streamem. Filtrové streamy obvykle vytváříme tak, že předáme jejich konstruktoru jako parametr podřízený stream, v daném případě textový souborový. Dále je vidět, že zde můžeme číst celé řádky - to je věc specifická právě pro tento stream, ale i zde můžeme stále číst jednotlivé znaky, toto je jen usnadnění. Po skončení práce uzavřeme "nejvrchnější" stream, ten už zajistí kaskádovitě uzavření všech ostatních. Ještě důležitá poznámka - při použití bufferovaných výstupních streamů není zaručeno, kdy se data z bufferu přesunou do navazujícího streamu. K zajištění zápisu dat z bufferu proto v případě potřeby voláme metodu flush(). Konverzní streamy Název není úplně přesný, řeč bude pouze o konverzi mezi textovými a binárními streamy. V řadě případů totiž odněkud získáme binární stream, a přitom potřebujeme textový. Vřadíme tedy mezičlánek, který nám konverzi zajistí. Viz příklad: try { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int i = 0; while (i >= 0) { i = br.read(); if (i >= 0) { char c = (char) i; ... } } br.close(); } catch (IOException e) { ... } V příkladu je použit standardní vstup (čili obvykle klávesnice), který však je binární. Protože potřebujeme znaky, musíme provést převod, a to nejlépe hned "na cestě", pomocí konverzního streamu. Streamy pro kompresi/dekompresi dat Jako takovou třešničku na dortu, která ukáže, co se také se streamy dá dělat, si nyní vyzkoušíme dekompresi dat a zároveň výpočet kontrolního součtu. Na samém konci budeme číst po řádcích textová data. Stačí jen streamy zřetězit za sebe... try { CheckedInputStream cis = new CheckedInputStream(new GZipInputStream(new FileInputStream("data.gz"))); BufferedReader br = new BufferedReader(new InputStreamReader(cis));
String s = ""; while ((s = br.readLine()) != null) { ... } System.out.println("Kontrolni soucet je: " + cis.getValue()); br.close(); } catch (IOException e) { ... } Jak snadné... A tohle zdaleka není všechno, co streamy dovedou. Příště se podíváme na některé další druhy streamů (těch zajímavých je ještě řada), řekneme si něco o tom, jak efektivně a bezpečně přenášet různá data, a také jak si vyrobit vlastní stream pro specifické účely. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=836
Java (16) - I/O operace II. Zajímavých streamů je mnohem víc, než jsme si ukázali minule. Proto nyní dojde i na některé další. Přijde řada i na tvorbu vlastních streamů pro specifické účely. 23.6.2005 07:00 | Lukáš Jelínek | přečteno 30940× Další zajímavé streamy Ve standardních knihovnách se nachází řada zajímavých streamů, na které stojí za to prozkoumat. Tedy vzhůru do toho, podívejme se na některé z nich! PrintStream PrintStream je výstupní stream, který nemá svůj vstupní protějšek a slouží k tisku uživatelsky srozumitelných dat různým způsobem. Může být napojen přímo na výstupní soubor nebo (protože je to filtrový stream) na libovolný jiný výstupní stream. Pozn.: Napojení PrintStream na soubor lze používat od JDK verze 1.5, u dřívějších verzí je nutno použít přístup přes FileOutputStream. Charakteristickou vlastností třídy PrintStream je, že nevyhazuje výjimky IOException - chyby jsou v podstatě ignorovány (lze je ale zjistit voláním metody checkError()). Proto se hodí hlavně tam, kde fungování streamu není z hlediska funkce celého programu důležité (logování, informativní výpisy apod.). Ještě důležitější je ovšem, že PrintStream poskytuje přímou podporu převodu různých primitivních typů na textovou reprezentaci, a následně na proud bajtů. Při tomto převodu se uplatní kódová tabulka platformy nebo (pokud byl zavolán příslušný konstruktor) kódování poskytnuté vytvářené instanci streamu. Třída samozřejmě disponuje metodami write(), které se chovají tak, jak je pro výstupní streamy obvyklé. Hlavní síla je však v metodách print() a println() - rozdíl je pouze ten, že druhá z metod navíc vloží konec řádku. Právě tyto metody provádějí výše uvedené konverze. Od verze 1.5 lze použít i volání printf() s podobným chováním, jako má stejnojmenná funkce v jazyce C (stejnou službu ale poskytne i metoda format()).
Konstruktoru lze poskytnout argument říkající, že se má provádět tzv. autoflush (automatický zápis výstupního bufferu). Pokud je tato funkce zapnuta, buffer se zapíše ihned po zavolání některé metody println(), po zápisu znaku pro odřádkování anebo po zápisu pole bajtů. Stream vytvořený přímým napojením na soubor má funkci autoflush vypnutou. Více ukáže následující příklad: PrintStream ps = null; try { ps = new PrintStream("vystup.txt"); } catch (FileNotFoundException e) { System.err.println("Vystupni soubor nelze otevrit"); ps = System.out; // použije se standardní výstup } ps.println("nejaky text"); ps.printf("%X", new Integer(120)); // vypíše hexadecimální číslo ... ps.println(); ps.close(); Konstruktor je v příkladu uzavřen do bloku try - to je nezbytné kvůli výjimce FileNotFoundException, kterou konstruktor může vyhodit. Pokud k vyhození dojde (nastane problém s otevřením souboru), použije se v příkladu standardní výstupní stream. Zbylá část programu už nemusí mít (a nemá) kontrolu výjimek. Zvláštním případem jsou dva standardní (systémové) streamy: standardní výstup (System.out) a standardní chybový výstup (System.err). Tyto streamy (v příkladu jsou použity) jsou v každém programu k dispozici a navenek (z pohledu operačního systému) se chovají úplně stejně jako jejich céčkovské obdoby. Pro úplnost uvádím, že je k dispozici i standardní vstup (System.in), na který se díváme přes rozhraní InputStream. PipedInputStream/PipedOutputStream a PipedReader/PipedWriter Tyto dva páry streamů představují tzv. roury (pipe), což jsou vlastně vzájemně propojené streamy. Vezmeme jeden vstupní a jeden výstupní stream, propojíme je, a s každým koncem pracujeme úplně stejně, jako by to byl normální vstupní, resp. výstupní stream. K čemu je to dobré? Nejčastějším použitím je komunikace mezi vlákny (o vláknech samotných bude řeč někdy později), kdy se v jednom vlákně vytvářejí nějaká data a současně se ve druhém tato data zpracovávají. Je to výhodné hlavně proto, že se nemusíme starat o synchronizaci přístupu k datům, práce se streamy je velmi jednoduchá, můžeme využít veškeré možnosti nabízené streamy a při změně způsobu práce není třeba příliš do programu zasahovat. Tyto streamy se vytvářejí tak, že vytvoříme jeden z nich a druhému ho předáme jako argument v konstruktoru. Jinou cestou je vytvořit je nezávisle a pak na některém z nich zavolat metodu connect(). Viz příklad: // první možnost PipedInputStream is = new PipedInputStream(); PipedOutputStream os = new PipedOutputStream(is); // druhá možnost PipedOutputStream os = new PipedOutputStream(); PipedInputStream is = new PipedInputStream(os);
// třetí možnost PipedReader pr = new PipedReader(); PipedWriter pw = new PipedWriter(); pr.connect(pw); ByteArrayInputStream/ByteArrayOutputStream Jedná se o dvojici streamů, které pracují nad polem bajtů. Je to podobné jako u známých tříd StringReader/StringWriter (a dalších podobných, kam patří třeba StringBufferInputStream nebo CharArrayWriter). Máme nějakou oblast v paměti, kam se zapisují (resp. odkud se čtou) data streamu. S těmito daty pak můžeme naložit dle libosti. Výstupní stream funguje tak, že si spravuje vlastní buffer, a dokud se tento nevymaže, stále se zapisováním plní. Pokud potřebujeme jeho obsah, získáme kopii dat (ne tedy přístup k původnímu bufferu) zavoláním toByteArray. To je třeba si dobře uvědomit kvůli výkonnostním úvahám! Data lze získat i ve formě textového řetězce (voláním toString()). Vstupní stream naopak pracuje vždy s bufferem pevné velikosti. Přečíst lze jen tolik bajtů, kolik jich v bufferu je. Serializace/deserializace dat Velice často máme nějak uložená data (v primitivních nebo složitějších datových typech) a potřebujeme je uložit nebo přenést na jiné místo. Musíme to udělat tak, aby se v jiném čase nebo na jiném místě data správně zrekonstruovala do původní podoby. Těmto činnostem říkáme serializace a deserializace. Serializace je konverze obecných dat (nějakým způsobem uložených) na proud bajtů tak, aby je šlo následně snadno zrekonstruovat. Naopak deserializace je právě rekonstrukce proudu bajtů na data použitelná v programu. Java k těmto činnostem poskytuje výraznou podporu. Serializace/deserializace primitivních typů Celý mechanismus okolo serializace/deserializace je docela složitý, proto bych se nyní chtěl zaměřit jen na to, co je důležité pro základní práci. Protože v Javě nikdy nevíme, jak jsou jednotlivé datové typy uloženy (i když třeba známe jejich číselné rozsahy), nelze jednoduše rozsekat třeba long na 8 bajtů (většinou by to sice šlo, ale ztrácíme tím plnou přenositelnost - obecně se totiž může stát, že "předpoklady" nejsou zcela naplněny), natož něco kopírovat rovnou (pořadí bajtů!). Naštěstí se zrovna o toto nemusíme starat. Máme totiž dvě třídy, DataInputStream a DataOutputStream, které potřebné konverze bezpečně udělají za nás. Streamy mají metody pro uložení/načtení všech primitivních datových typů. Pozor samozřejmě na to, v jakém pořadí se data ukládají. Tento způsob serializace neumožňuje jednotlivé typy zpětně identifikovat! Příklad naznačí, jak se s uvedenými třídami pracuje: int i = 165; float f = 0.35; try { DataOutputStream os = new DataOutputStream(new FileOutputStream("soubor.dat")); os.writeInt(i); // bezpečné uložení hodnoty typu int os.writeFloat(f); // bezpečné uložení hodnoty typu float os.close(); } catch (IOException e) { ... }
Serializace/deserializace objektů Trochu složitější je to s instancemi objektů. Ale i tady máme podobné prostředky - v podobě tříd ObjectInputStream a ObjectOutputStream. Ty nejenže ukládají a načítají instance objektů, ale poradí si i s primitivními typy (takže pokud je používáme, nemusíme už používat třídy DataInputStream/DataOutputStream). Nelze ukládat všechny objekty. Nutnou podmínkou je, aby implementovaly rozhraní Serializable (pokud se pokusíme serializovat nevyhovující objekt, dočkáme se výjimky NotSerializableException). Protože se instance serializuje i se všemi odkazovanými objekty, musí být i tyto serializovatelné, anebo označené modifikátorem transient (tedy že nebudou uloženy). Narozdíl od primitivních typů, u objektů lze při deserializaci zjistit jejich typ (a nejen to, k úspěšné deserializaci musí být k dispozici příslušná třída - jinak to skončí výjimkou ClassNotFoundException; případné poškození dat vyvolá zase jiné výjimky). Metoda readObject() sice vrací referenci na typ Object, ale třídu si můžeme zjistit voláním getClass() na vrácené instanci nebo jiným způsobem, a následně přetypovat podle potřeby. Více opět napoví příklad: ArrayList list = new ArrayList(); // vytvoříme seznam list.add("nejaky text"); // vložíme hodnoty list.add(new Double(1.655)); list.add(new Integer(123)); try { ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("seznam.dat")); os.writeObject(list); // celý seznam se bezpečně uloží os.close(); } catch (IOException e) { ... } Zde je dobře vidět, že můžeme snadno uložit nebo přenést celý kontejner i s obsahem. Jen pozor na to, že všechny obsažené objekty musí být serializovatelné. K procesu serializace se ještě někdy později vrátíme a podíváme se na něj podrobněji - toto by jako úvod stačilo. Vytváření vlastních streamů Někdy potřebujeme stream, který má nějaké speciální vlastnosti. Proto si můžeme (pokud nám žádný z dostupných streamů nevyhovuje) vytvořit vlastní stream, do kterého přidáme potřebné funkce. Nejlepší je rozšířit nějaký už existující stream. Ukážeme si to na streamu filtrového typu. Požadujeme, aby stream sledoval četnost jednotlivých bajtů (tedy hodnoty 0-255). Nový stream odvodíme od třídy FilterInputStream předefinováním potřebných metod. Mohlo by to vypadat třeba takto: public class CounterInputStream extends FilterInputStream { private long cnt [] = new long[256]; // pole pro uložení četností public FilterInputStream(InputStream in) { super(in); // konstruktor pouze zavolá předka } // Metoda resetuje počitadla četností public void resetCounters() { for (int i=0; i<256; i++) {
cnt[i] = 0; } } // Vrací četnost daného bajtu. // Meze indexu se netestují. public long getCount(int index) { return cnt[index]; } // Základní metoda - přečtení bajtu public int read() throws IOException { int b = super.read(); // přečte se bajt if (b >= 0) cnt[b]++; // pokud je platný, inkrementuje se počitadlo return b; } // Metoda pro čtení bloku bajtů public int read(byte[] b, int off, int len) throws IOException { int r = super.read(b, off, len); if (r > 0) { for (int i=0; i=0; i++) { b = cis.read(); if (b >= 0) { ... // nějaká činnost } } cis.close(); System.out.println("Cetnost hodnoty 54 je " + cis.getCount(54)); } catch (IOException e) { ... } Příklad ukazuje analýzu dat načítaných ze standardního vstupu. Po skončení čtení (přečte se 100 bajtů, při chybě už se dál nečte) se vypíše četnost hodnoty 54.
Práce se soubory Dostali jsme se na konec úvodní části o výměně dat mezi programem a vnějším prostředím. Příště se vrhneme na důležitou věc, které se při psaní aplikací nikdo nevyhne, a to je práce se soubory. Prozkoumáme, jak jsou řešeny takové operace, jako je mazání nebo přejmenování souborů, jak se vytvářejí dočasné soubory, a v neposlední řadě, jak je řešena rozdílnost různých platforem, na kterých Java může běžet. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=855 Java (17) - práce se soubory Velmi častým prostředkem komunikace programu s okolním světem je soubor. Protože je Java koncipována jako platformově nezávislá, musí se vypořádat s různými nástrahami, které rozličné souborové systémy skýtají. Že to není jednoduchý úkol, ale že není neřešitelný, se přesvědčíme při zkoumání javovských prostředků, které jsou pro tento účel k dispozici. 1.9.2005 06:00 | Lukáš Jelínek | přečteno 58552× Úkol nelehký Pokud potřebujeme uložit data z programu (nebo je načíst), ve většině případů k tomu použijeme soubor. A přestože soubor samotný se skoro na všech platformách tváří úplně stejně (je to různě dlouhá posloupnost bajtů), způsob práce se liší, a to dost zásadně. Zajímají nás především dvě nejrozšířenější skupiny souborových systémů: systémy unixového typu - je jich celá řada (UFS, Ext2, ReiserFS atd.), pro všechny z nich je typické, že celý systém tvoří jediný adresářový strom, jako oddělovač jednotlivých úrovní se používá běžné (dopředné) lomítko, rozlišují se velká a malá písmena, téměř všechny znaky ASCII jsou platné a pro ukládání názvů lze obecně používat různé kódové stránky. systémy firmy Microsoft (FAT a NTFS) - rozlišují se jednotlivá logická zařízení ("písmena disků"), oddělovačem je zpětné lomítko, velikost písmen se nerozlišuje, okruh platných znaků je poměrně malý (i když to některé systémové funkce řádně nekontrolují a lze tak na disk propašovat i soubory s neplatnými názvy, které už pak nelze odstranit), kódování se liší podle jednotlivých systémů (NTFS používá Unicode, FAT potom staré kódové stránky, např. pro češtinu CP 852). Situaci ještě komplikuje používání názvů souborů UNC. Speciální situace se pak řeší, pokud se nějaký souborový systém používá z operačního systému, ve kterém je cizí (typicky třeba FAT v Linuxu). Pak platí pravidla, která jsou směsicí výše uvedeného a je třeba si dávat zvlášť velký pozor. Třída File V balíku java.io najdeme třídu, která nám poskytne vše potřebné k práci se soubory - je to třída File. Nepředstavuje přímo konkrétní soubor, nýbrž tzv. abstraktní cestu (tedy obecně jakoukoli cestu identifikující nějaký soubor). Může odkazovat na platný soubor, ale také nemusí. Důležité ale je, že je to (podobně jako třeba String) invariant, jak se jednou vytvoří, už nelze změnit. V řadě tříd ze standardní knihovny Javy najdeme metody, které vyžadují jako svůj argument název souboru. Prakticky ve všech případech lze použít jak textový řetězec (String; to jsme doposud běžně dělali), tak právě instanci třídy File, což je přenositelnější a robustnější řešení, protože můžeme již předem zjistit o daném názvu souboru nějaké informace nebo provést se souborem potřebné operace. Jako příklad mohu uvést třeba konstruktor třídy FileInputStream. Specifikace abstraktní cesty
Zdůrazňuji, že objekt File může představovat jak soubory (běžné, ale i speciální, třeba soubory zařízení), tak adresáře. Cesta může být absolutní i relativní, může být dokonce i prázdná. Abstraktní cesta se vždy skládá z prefixu (např. označení kořenového adresáře, úvodní označení UNC cesty; u relativních cest prefix samozřejmě chybí) a z posloupnosti názvů jednotlivých adresářových úrovní (samozřejmě včetně případného názvu souboru na konci) oddělených separátorem. Jak se s cestami pracuje, záleží na nastavení vlastností systému (system properties; někdy později se na ně podíváme důkladněji), a toto nastavení se samozřejmě liší podle platformy. Nejdůležitější je oddělovač názvů v cestě; ve třídě File je určen hodnotou konstanty separatorChar (znakové vyjádření), resp. separator (řetězcové vyjádření). Na unixových systémech je oddělovačem samozřejmě dopředné lomítko, na microsoftích systémech lomítko obrácené. Když už jsme u těch konstant, třída File obsahuje ještě konstanty pathSeparatorChar a pathSeparator. Tyto konstanty představují oddělovač cest (v případech kdy máme zapsaných několik cest za sebou) a mají hodnotu dvojtečky (unixové systémy), resp. středníku (microsoftí systémy). Vytvoření instance File Jak jsem již řekl, jednou vytvořený objekt File už nemůžeme měnit. Má to svoji logiku, protože jestliže řetězec je v Javě neměnný, musí být jeho speciální případ (což abstraktní cesta bezpochyby je) také neměnný. Objekt File můžeme vytvořit třemi způsoby: názvem souboru, názvem souboru vzhledem k rodiči, a pomocí URI (Uniform Resource Identifier; viz RFC 2396). Pokud ho vytáříme přímo z celé (absolutní nebo relativní) cesty, je situace jednoduchá, řetězec se pouze převede na abstraktní cestu. Pokud je dán rodič (ať už názvem nebo instancí File, a není null), je abstraktní cesta vytvořena jako relativní vůči této rodičovské cestě (adresáři), resp. proti výchozímu adresáři (pokud je rodič prázdná abstraktní cesta). Pokud se instance File vytváří z URI, musí být splněny určité požadavky (schéma musí být "file", cesta nesmí být prázdná atd.). Separátor v řetězci nemusí odpovídat dané platformě. Pokud je konstruktor schopen ho normalizovat (tzn. převést do správné podoby), zpracuje se cesta bez problémů. Operace s instancí třídy File Práce s cestou k souboru Protože, jak bylo řečeno, je objekt File abstraktní cestou k souboru, můžeme s touto cestou pracovat a získávat různé její varianty. Lze získat celou cestu v různých podobách, části cesty a některé další verze. getPath() - vrátí (normalizovanou) abstraktní cestu v podobě, jak byla zadána při vytváření objektu (tedy absolutní zůstane absolutní atd.). Stejný efekt má i metoda toString(). getAbsolutePath() - vrátí cestu převedenou do absolutního tvaru. Mechanismus případného převodu z relativní cesty na absolutní je platformově závislý, většinou se ale jako báze použije domovský adresář uživatele. getCanonicalPath() - vrací kanonický tvar cesty. V praxi to znamená, že se pokusí cestu maximálně "vyhodnotit", zpracovat. Odstraní všechny označení stejného nebo nadřazeného adresáře (tečku a dvě tečky), zpracuje symbolické odkazy atd.. Chování je silně platformově závislé a liší se podle toho, zda cesta (nebo její části) existuje či nikoli. getName() - získá z cesty pouhý název souboru (bez adresářové cesty). getParent() - vrací rodičovský adresář souboru. Vychází se pouze z cesty, soubor ani rodičovský adresář nemusí existovat. isAbsolute() - zjistí, zda je cesta absolutní.
compareTo() - lexikograficky porovná tuto abstraktní cestu s jinou (ani jedna nemusí existovat). Porovnávání je platformově závislé, podle systému se použije rozlišení malých/velkých písmen. Podobně pracuje metoda equals(), která pouze zjišťuje, zda jsou abstraktní cesty totožné. Všechny uvedené metody, které vrací cestu nebo její část, mají jako návratovou hodnotu textový řetězec. Kromě nich existují jejich obdoby, které vracejí novou instanci typu File. Jsou to metody getAbsoluteFile(), getCanonicalFile() a getParentFile(). Abstraktní cestu lze také převést (metodou getURL()) na odpovídající URL, anebo na URI (metodou getURI()). Protože však metoda getURL() neumí správně naložit se zakázanými znaky, doporučuje se vždy použít getURI() a získaný objekt převést jeho metodou getURL() na URL (protože URL jsou podmnožinou URI, často se ve skutečnosti ani vnitřně nic převádět nebude). Zjišťování informací o souboru Nyní už přejdeme k operacím, které se týkají souboru, na který abstraktní cesta odkazuje. Množina operací není velká, musí být totiž dostatečně přenositelná mezi platformami. exists() - základní věc: zjištění, zda vůbec soubor existuje. Pokud se chystáme dělat s ním nějaké další věci, je dobré zavolat nejprve tuto metodu a ověřit si jeho přítomnost. isFile(), isDirectory() - pomocí těchto metod poznáme, zda se jedná o "normální" soubor nebo o adresář. Opět je to platformově závislé, např. symbolický odkaz na soubor se tváří jako běžný soubor. Platí ale, že jakékoli soubory/adresáře vytvořené z Javy zcela jistě projdou správně těmito testy. canRead(), canWrite() - dozvíme se, zda můžeme číst či zapisovat do daného souboru. Pokud byl ale přístup odepřen, už se nedozvíme proč. To je daň za přenositelnost, nemáme možnost zjišťovat třeba přístupová práva. isHidden() - zjišťuje, zda je soubor označen jako "skrytý". V unixových systémech za skryté soubory považuje ty, jejich název začíná tečkou, ve Windows pak soubory s nastaveným atributem "hidden". length() - zjistí velikost souboru. Protože vrací hodnotu typu long, není problém ani s opravdu velkými soubory. lastModified() - jediný časový údaj, který můžeme o souboru zjistit, je čas poslední modifikace. Ne všechny souborové systémy poskytují další časové informace, proto je to takto omezeno. Navíc v praxi je to právě ten nejpotřebnější údaj, podle něhož můžeme např. zjišťovat, že někdo změnil konfigurační soubor. Adresářové informace Předchozí metody se týkaly všech souborů bez rozdílu, tedy včetně adresářů. Pro adresáře samotné máme k dispozici speciální sadu metod, které využijeme pro přístup k souborům v těchto adresářích: list() - nejjednodušší varianta. Prostě vrátí pole obsahující seznam všech souborů v daném adresáři položky tohoto pole budou textové řetězce s názvy souborů. Pořadí souborů není definováno, může být libovolné (závisí na implementaci; v Linuxu budou soubory pravděpodobně uspořádány tak, jak jsou zaznamenány v adresáři). To samozřejmě není problém, protože si soubory můžeme (ať už podle názvu nebo jinak) seřadit podle potřeby. listFiles() - dělá přesně totéž co list(), ale místo pole textových řetězců vrací pole objektů File, tedy abstraktních cest. Zda použijeme tuto nebo přechozí metodu, záleží na konkrétní situaci. list(FilenameFilter f) - modifikace metody list() s tím, že předem vybíráme jen některé soubory. Které to budou, to určí implementace rozhraní FilenameFilter. O filtraci ještě bude řeč. listFiles(FilenameFilter f) - opět metoda vracející pole objektů File, tentokrát s filtrací (viz výše). listFiles(FileFilter f) - další modifikace, ale s jiným typem filtru. listRoots() - v souborových systémech unixovského typu je hierarchie přísně stromová, vždy máme jediný kořen. V jiných systémech to ale platit nemusí (a také neplatí), proto je třeba mít možnost
dostat se ke všem dostupným kořenům - a to zajišťuje právě tato statická metoda. Vrací pole všech kořenů adresářových stromů, které jsou v danou chvíli k dispozici. O filtraci souborů Výše uvedené metody provádějí filtraci souborů podle poskytnutého rozhraní. To je typická ukázka toho, jak se v Javě podobné věci řeší - existuje mnoho a mnoho objektových metod, které jako mají parametr nějaké jednoduché rozhraní, obsahující třeba jen jedinou metodu. Chování je pak plně v režii implementace tohoto rozhraní. Pokud budeme implementaci potřebovat jen v jednom jediném případě, s výhodou využijeme možnosti vytvořit anonymní třídu přímo na daném místě. Viz příklad: File f = new File("/home/username/docs"); // vybereme adresář String list[] = f.list(new FilenameFilter() { boolean accept(File dir, String name) { return name.endsWith(".pdf"); // jen názvy *.pdf } }); Arrays.sort(list);
// abecední seřazení
for (int i=0; i<list.length; i++) { System.out.println(list[i]); } Uvedený příklad vypíše v abecedním pořadí všechny soubory z daného adresáře, jejichž název končí na .pdf (jsou to tedy dokumenty formátu PDF). Manipulační operace Zatím jsme o souborech pouze zjišťovali různé informace. S tím si rozhodně nelze vystačit, občas musíme také někde něco změnit. Třída File nabízí několik manipulačních operací, tak se na ně podívejme: renameTo(File f) - metoda přejmenuje soubor podle zadání. Všimněte si, že se jako parametr zadává jiná instance objektu File. Jak jsme si již řekli, instance File je neměnná, proto i po úspěšném přejmenování souboru zůstane tak, jak je (bude obsahovat původní cestu k souboru). Naopak nové jméno souboru bude odpovídat zadané instanci, o čemž se můžeme přesvědčit tak, že zavoláme metodu exists(). Chování je silně platformově závislé, nemůžeme spoléhat, že metoda bude dělat vždy to, co dělala na některé platformě. Návratovou hodnotu je třeba vždy testovat. delete() - pokusí se smazat soubor. Pokud to jde, smaže ho. Neprázdné adresáře mazat nelze, musí se nejdřív explicitně vyprázdnit. deleteOnExit() - zajímavá metoda, naplánuje smazání souboru při ukončování programu. Zafunguje pouze při čistém ukončení programu, tedy ne při "sestřelení" (na Linuxu signálem SIGKILL) nebo při zavolání metody System.halt(). Metoda se používá pro automatické mazání dočasných souborů (viz níže). Pozor - naplánované smazání už nejde zrušit! createNewFile() - vytvoří nový prázdný soubor. Metoda nemá příliš velké využití, ale někdy se hodí. mkdir(), mkdirs() - dvojice metod pro vytváření adresářů. Liší se pouze tím, že ta první vytvoří pouze ten jediný adresář, na který odkazuje instance File, kdežto ta druhá vytvoří, pokud je třeba, i všechny nadřazené adresáře. setLastModified(long time) - změní časový údaj o poslední změně souboru. Někdy se to může hodit. setReadOnly() - nastaví, že soubor bude pouze ke čtení. Nepříjemné je, že to je pouze jednosměrná
operace a v Javě nemáme prostředky, jak to vrátit zpět. Následující příklad ukáže několik operací provedených na souboru identifikovaném abstraktní cestou. Nejdříve se adresářová cesta převede do kanonického tvaru, potom se daný adresář vytvoří, v něm se založí nový soubor a ten se nakonec přejmenuje. Všimněte si, že některé operace vyžadují ošetření výjimky IOException. File dir = new File("../user2/texty"); try { dir = dir.getCanonicalFile(); } catch (IOException e) { System.err.println("Nelze ziskat kanonicky tvar: " + dir); System.exit(1); } if (!dir.makedir()) { System.err.println("Nelze vytvorit adresar " + dir); System.exit(2); } File f = new File(dir, "test.txt"); try { if (!f.createNewFile()) { System.err.println("Soubor " + f + " jiz existuje"); } } catch (IOException e) { System.err.println("Nelze vytvorit soubor " + f); System.exit(3); } File f2 = new File(f.getParent(), "test2.txt"); if (!f.renameTo(f2)) { System.err.println("Nelze prejmenovat soubor na " + f2); System.exit(4); } Dočasné soubory Občas potřebujeme uložit nějaká data do dočasného souboru, abychom je použili později. Lze to samozřejmě udělat ručně, tedy zvolením nějakého umístění souboru, to ale nebude přenositelné. Ve třídě File máme k dispozici dvě statické metody, kterými tento problém snadno vyřešíme: createTempFile(String prefix, String suffix) - vytvoří dočasný soubor s daným prefixem (min. 3 znaky dlouhým) a danou koncovkou (může být null, pak se použije .tmp). Soubor vytvoří v adresáři pro dočasné soubory (např. /tmp), vrací instanci třídy File. createTempFile(String prefix, String suffix, File directory) - od předchozí metody se liší tím, že umožňuje specifikovat adresář pro uložení souboru (pokud je null, chování této metody je shodné s chováním té předchozí). Vytvořený soubor se nemaže automaticky při skončení programu. Pokud to potřebujeme (což je skoro vždy), použijeme metodu deleteOnExit(). Od souborů k síti Kapitola (pravda, poněkud "výčtově" orientovaná) o práci se soubory tímto dospěla ke svému konci. Ke konci ale nedospěly I/O operace, protože Java v této oblasti nabízí velmi mnoho. Příště
přijde řada na komunikaci po síti, která je v dnešní době stejně důležitá jako práce se soubory. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=866 Java (18) - síťová komunikace I. Jestliže jsem minule zdůrazňoval důležitost práce se soubory, nyní přijde na řadu možná ještě důležitější téma - výměna dat po síti. Podpora TCP/IP (včetně IPv6) je v Javě na výborné úrovni. 21.9.2005 06:00 | Lukáš Jelínek | přečteno 37102× Doba síťová Dnešní dobu lze bez nadsázky označit za období síťové komunikace. Význam dálkového přenosu dat stále vzrůstá, proto je i v Javě věnována této oblasti mimořádná pozornost. Je to důležité hlavně proto, že právě sítě jsou častým místem pro nasazení Javy, ať pro stranu serverovou nebou klientskou. Z tohoto důvodu jsem se rozhodl věnovat se této oblasti už nyní, v souvislosti se vstupně/výstupními operacemi. Začneme samozřejmě úplnými základy, tedy tím, jaká je vůbec filosofie síťové komunikace v Javě, jak se navazuje spojení, jak jsou reprezentovány adresy apod. "Vyšší stupeň" síťové komunikace (tedy podporu vzdáleného zpracování dat, přístup do databází, distribuované zpracování dat atd.) si necháme na později. Základy síťové komunikace Pro další výklad budu předpokládat, že čtenáři znají základy technologie TCP/IP verze 4, tedy zejména způsob reprezentace adres, základní vlastnosti jednotlivých komunikačních protokolů, mechanismus navazování a ukončování spojení atd., bude se to totiž zatraceně hodit. Protože IP verze 6 je poměrně nová záležitost, a zatím se moc nepoužívá, budu při popisu používat (tam, kde na tom záleží) vždy verzi 4 a o odlišnostech verze 6 se zmíním (většina API však verzi vůbec nerozlišuje). Nyní ještě raději velice stručně obecný úvod do TCP/IP: Datagramová komunikace Začneme tím jednodušším, datagramovou komunikací (reprezentovanou zde protokolem UDP). Kdo tuto komunikaci zná, tak ví, že vytvoříme balík dat (paket, datagram) a ten odešleme na nějaký (jiný, ale klidně i tentýž) počítač. Zde ho může (ale také nemusí) někdo očekávat - pokud ho nějaký program očekává, naslouchá na tzv. portu, a když mu data přijdou, zpracuje je. Komunikace probíhá prostřednictví naprosto nezávislých datagramů, které odesílatel posílá v dobré víře, že je někdo přijme a zpracuje. Není nijak zajištěno, že datagram dojde v pořádku (resp. že vůbec dojde), ani že datagramy přijdou v pořadí, v jakém se odesílaly. Pokud je to nutné, musí si tyto mechanismy zajistit příslušné aplikace. V praxi se datagramová komunikace používá např. pro službu doménových jmen (DNS), některé způsoby multimediálních přenosů nebo třeba pro vzdálené volání procedur (RPC). Spojová komunikace Vyšším stupněm je spojová komunikace, zde protokolem TCP. Mezi počítači (přesněji řečeno porty na počítačích) se vytvoří komunikační kanál, který je zabezpečený z hlediska poškození nebo ztráty dat, a zaručuje správné pořadí přenášených dat. Data se sice posílají v paketech, ale to je aplikačním programům prakticky skryto. Před použitím je nutno komunikační kanál vytvořit, co předpokládá, aby jeden z komunikujících programů naslouchal na příslušném portu a druhý se na tento port připojil. Spojovou komunikaci používá většina aplikací, jmenovitě třeba HTTP, FTP, poštovní protokoly
(SMTP, POP3, IMAP4), telnet a SSH apod. Adresy a porty v Javě Než začneme komunikovat, musíme nejdřív vědět s kým. Proto je důležité vědět, jak se v Javě pracuje s adresami a porty. Není to nic těžkého, dokonce se dá říct, že oproti jiným jazykům (třeba C/C++) máme hodně práce usnadněno. Reprezentace IP adres Většina základních síťových záležitostí se nachází v balíku java.net. Tam také najdeme třídu InetAddress, která představuje obecnou IP adresu. V dřívějších verzích znamenala automaticky adresu IP verze 4, to už ale neplatí - existují potomci pro jednotlivé protokoly (Inet4Address a Inet6Address). Pro většinu použití na verzi nezáleží, proto metody přijímají instance této obecnější třídy. Všechny uvedené třídy se vyznačují tím, že nemají žádný veřejný konstruktor. Instance se vytvářejí statickými metodami a lze přitom použít některou z následujících cest: číselná reprezentace adresy - použije se metoda getByAddress(byte[] addr), která vytvoří instanci adresy IPv4 (předá-li se pole 4 hodnot) nebo IPv6 (pro pole 16 hodnot). Adresa se použije tak, jak je, žádný zpětný převod na jméno se nedělá. Pořadí bajtů je takové, jak jsme zvyklí (tedy nejvýznamnější bajt jako první). číselná + jmenná reprezentace - metoda getByAddress(String host, byte[] addr). Umožňuje vycházet z číselné reprezentace s tím, že se uvede i jmenná adresa, aniž by se přitom prováděl DNS dotaz. Používání tohoto postupu je lépe se vyhnout. jmenná reprezentace - adresa se zadá obvyklým způsobem metodě getByName(String host), ta provede DNS dotaz (přesněji řečeno, dotáže se systému, který může převody jmenných adres na číselné provádět různě) a vrátí instanci objektu Inet4Address nebo Inet6Address. Místo jmenné adresy lze uvést i číselnou (v textovém tvaru), ta se pak přímo použije (jako při volání metody getByAddress()). získání lokální adresy - pokud potřebujeme lokální adresu, použijeme metodu getLocalHost(). Ovšem pozor, nemůžeme ovlivnit, kterou z lokálních adres metoda vrátí (pokud má počítač víc síťových rozhraní, může to být dost nepříjemné). Vytvořená instance je neměnná. Adresa obecně nemusí znamenat pouze jediný cíl (unicast), ale může být i typu multicast. Zvláštním případem je tzv. nespecifikovaná (též anylocal) adresa, která se nesmí použít pro určení cíle, ale má význam při přidělování lokálních adres (říká, že lze použít libovolnou místní adresu, tedy i libovolné rozhraní). Instance třídy InetAddress není pouze "kontejner na pár bajtů", ale má také vlastní funkcionalitu. Lze provádět reverzní DNS dotazy (getHostName() a getCanonicalHostName()) a dokonce zjistit dosažitelnost daného stroje (voláním některou z metod isReachable(); ovšem pozor na to, že správce zkoumaného cíle mohl používané služby, tedy ICMP ECHO a TCP ECHO, na firewallu zablokovat (tyto metody jsou k dispozici od JDK 1.5). Jak se s objekty InetAddress pracuje, ukazuje příklad: byte ip4[] = { 82, 208, 29, 37 }; // adresa IPv4 try { InetAddress ia = InetAddress.getByAddress(ip4); InetAddress ia2 = InetAddress.getByName("82.208.29.37"); InetAddress ia3 = InetAddress.getByName("www.linuxsoft.cz"); InetAddress ia4 = InetAddress.getByAddress("www.linuxsoft.cz", ip4);
InetAddress local = InetAddress.getLocalHost(); // místní adresa System.out.println(ia.getCanonicalHostName()); if (!ia.isReachable(5000)) System.err.println("Stroj je nedostupny"); } catch (UnknownHostException e) { e.printStackTrace(); } Je to snad dostatečně zřejmé. První čtyři volání statických metod budou mít (pokud vše půjde, jak má) za následek vytvoření stejné instance (předpokládám, že www.linuxsoft.cz má jedinou IP adresu). Jinak by tomu bylo v situaci, když by např. nefungovala správně služba DNS - pak by třetí volání selhalo. Získání lokální adresy je jasné, stejně jako vypsání kanonického názvu pro první vytvořenou instanci (opět musí fungovat DNS). Poslední volání pak otestuje dostupnost daného stroje (s timeoutem 5 sekund). Čísla portů Jak známo, čísla portů jsou šestnáctibitová, tedy v rozsahu 0-65535. V Javě však používáme k jejich reprezentaci typ int, tedy číslo s mnohem větším rozsahem. Proto musíme dát pozor, abychom používali pouze platné hodnoty (pokud si pozor nedáme, dočkáme se obvykle výjimky IllegalArgumentException). Úplná specifikace Máme k dispozici i třídu, která sdružuje adresu a číslo portu do jediného objektu. Je to třída InetSocketAddress (je potomkem abstraktní třídy SocketAddress; to má svůj smysl, protože jako argument metod obvykle figuruje tato nadtřída, což umožňuje univerzálnější použití, třeba i pro jiné komunikační protokoly), vytvářená obvykle normálním způsobem, tj. voláním konstruktoru (kterému předáme potřebné informace). Zvláštním způsobem vytvoření je zavolání statické metody createUnresolved(), která je k dispozici od JDK 1.5. Ta vytvoří instanci podle jmenné adresy, kterou se nesnaží přeložit na číselnou. To se hodí pro případy, kdy není k dispozici DNS, ale můžeme využít proxy server. Následující příklad ukazuje různé cesty vytvoření objektů IP adresy s číslem portu: try { // vytvoření z instance přeložené IP adresy InetAddress ia = InetAddress.getByName("www.linuxsoft.cz"); InetSocketAddress isa = new InetSocketAddress(ia, 80); // dtto, ale nevyhazuje se výjimka při chybě DNS InetSocketAddress isa2 = new InetSocketAddress("www.linuxsoft.cz", 80); // nepřeložená adresa InetSocketAddress isa4 = InetSocketAddress.createUnresolved("www.linuxsoft.cz", 80); // nespecifikovaná adresa, automatický port InetSocketAddress isa3 = new InetSocketAddress(0); } catch (UnknownHostException e) { e.printStackTrace(); }
První cestou je vytvořit instanci InetAddress a na jejím základě pak instanci adresy s portem. Druhá cesta (tedy předání jmenné adresy přímo konstruktoru InetSocketAddress) funguje podobně, avšak s tím rozdílem, že pokud se nepodaří jmennou adresu přeložit, nevyhodí se žádná výjimka a adresa zůstane nepřeložená. V této chybové situaci to tedy dopadne přesně tak, jak to udělá třetí z cest, tedy vytvoření nepřeložené adresy. A konečně poslední cestou je vytvoření nespecifikované ("wildcard") adresy, která se používá ve smyslu libovolné místní zdrojové adresy - zadává se pouze číslo portu. Ale v příkladu je jako toto číslo uvedena nula, která má zvláštní význam: systém přiřadí číslo zdrojového portu automaticky. Síťová rozhraní Jak jsem se zmínil v odstavci o získání lokální adresy, uvedená metoda neumožňuje určit, ke kterému síťovému rozhraní má daná adresa patřit - v počítači může být těchto rozhraní (síťových karet, modemů apod.) větší počet. Přesto ale nejsme zbaveni možnosti určit, se kterým rozhraním se bude pracovat. Máme totiž třídu NetworkInterface. Rozhraní můžeme určit buď podle jeho IP adresy, anebo podle názvu zařízení, např. eth0 (to je sice platformově závislá věc, ale v mnoha případech jiné cesty není). Instance objektu se vytvářejí statickými metodami getByInetAddress() a getByName() (veřejné konstruktory nejsou k dispozici). Můžeme také zavolat metodu getNetworkInterfaces(), čímž získáme všechna aktivní rozhraní. Hlavním smyslem třídy NetworkInterface je získání místní IP adresy. Je třeba si uvědomit, že každé rozhraní může mít přiřazeno více IP adres - k jejich zjištění slouží metoda getInetAddresses(), která vrátí jejich seznam. Pak už s nimi můžeme pracovat dle libosti. Více ukáže příklad: try { // rozhraní identifikované názvem NetworkInterface ni = NetworkInterface.getByName("eth0"); // zpracování seznamu IP adres k rozhraní Enumeration adr = ni.getInetAddresses(); while (adr.hasMoreElements()) { InetAddress ia = adr.nextElement(); ... } } catch (SocketException e) { e.printStackTrace(); } Z příkladu je jasně vidět, jak se získá přístup k rozhraní identifikovanému svým názvem, a jak se pak pracuje se seznamem IP adres, které má rozhraní přiřazeno. Většinou je tato adresa jediná, ale musíme počítat s tím, že jich může být víc, ale nemusí být také žádná (třeba při chybě v konfiguraci sítě). Komunikujeme Toto bylo nutné minimum, bez kterého se nelze obejít při jakékoli síťové komunikaci. Zatím jsme ještě nekomunikovali, ale k tomu se dostaneme příště. Budeme posílat UDP datagramy a navazovat spojení protokolem TCP. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=895 Java (19) - síťová komunikace II.
Různé druhy komunikací na internetovém protokolu - to bude téma tohoto článku. Budeme komunikovat protokoly UDP a TCP, podíváme se také na vyšší vrstvy komunikačního modelu. 12.10.2005 07:00 | Lukáš Jelínek | přečteno 38510× Odeslání datagramu Začneme tím nejjednodušším. Potřebujeme poslat samostatný paket (datagram) nějakému příjemci, jehož adresu (jmennou nebo číselnou) a port známe. Bude to, jak je pro protokol UDP typické, nezabezpečený přenos dat - datagram se může poškodit, zcela ztratit, může přijít několikrát, pořadí příchodu datagramů k příjemci se může lišit od pořadí odeslání. Veškerá režie proto leží na uživatelské aplikaci (popis však přesahuje prostorové možnosti tohoto článku). Zkusme si tedy vytvořit datagram, který příjemci pošleme. V nám již důvěrně známém balíku java.net je třída DatagramPacket, která představuje v podstatě pole bajtů připravené k odeslání. Instance tohoto objektu obsahuje také cílovou adresu a číslo portu (nemusí být nastaveny). Jak vytvořit takový datagram, napoví příklad: String s = "testovací datagram"; byte ba[]; try { ba = s.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { ... } DatagramPacket dgp1 = new DatagramPacket(ba, ba.length); DatagramPacket dgp2 = new DatagramPacket(ba, 2, 3); DatagramPacket dgp3 = null; DatagramPacket dgp4 = null; try { dgp3 = new DatagramPacket(ba, ba.length, InetAddress.getByName("pocitac.domena.cz"), 12345); dgp4 = new DatagramPacket(ba, ba.length, new InetSocketAddress("pocitac.domena.cz", 12345)); } catch (Exception e) { System.err.println(e.getMessage()); } První dva uvedené pakety se vytvoří bez přiřazené adresy a portu. Jeden pojme celý textový řetězec (protože se, jak je zřejmé, datagram vytvoří z celého pole), zatímco druhý se vytvoří od druhého bajtu s délkou 3 bajty. Pozor ovšem, takový postup je u textových dat (které jsem zde použil pro názornost) nepříliš vhodný, protože zejména pro kódování UTF-8 obvykle předem nevíme, jak se znaky zakódují - proto je lepší znaky vybrat předem. Pro obecná (binární) data ovšem můžeme klidně s úsekem pole pracovat. Druhé dva pakety obsahují adresu a port - v obou případech stejné, liší se pouze způsob vytvoření. Všimněte si, že je potřeba zachytávat výjimky, případně je předávat výš. Abych byl přesný, výjimky vyhazuje pouze konstruktor přijímající objekt typu SocketAddress, u InetAddress se zde jedná o
ošetření výjimky z metody getByName(). Máme datagram, ale co s ním? Poslat, samozřejmě. Na to ale potřebujeme prostředek, který to zajistí. Tím prostředkem je UDP socket, v Javě reprezentovaný třídou DatagramSocket. Socket obsadí určitý port na některé z adres počítače (viz minulý díl - rozhraní a adresy). Adresu a port si můžeme zvolit, ale také si je můžeme nechat přidělit automaticky. Můžeme také socket "připojit" na adresu příjemce - odesílat pakety pak bude možné jen tomuto příjemci (totéž se týká příjmu). Datagram odešleme zavoláním metody send(). Viz příklad: try { DatagramSocket sock1 = new DatagramSocket(); sock1.send(dgp1); // chyba - datagram ani socket nemá cíl. adresu a port sock1.send(dgp3); // funguje DatagramSocket sock2 = new DatagramSocket(55555); sock2.connect(InetAddress.getByName("pocitac.domena.org"), 44444); sock2.send(dgp2); // funguje sock2.send(dgp4); // chyba - výjimka kvůli neshodě adres } catch (Exception e) { e.printStackTrace(); } Příklad využívá datagramy vytvořené výše (v reálném programu se ale samozřejmě musí zajistit, aby pakety skutečně existovaly). Nyní tedy vytvoříme dva sockety - první z nich bude mít adresu a port přidělené automaticky (což v našem případě nehraje roli) a nebude omezen na konkrétní cílovou adresu. Z toho vyplývá, že první datagram by odmítl odeslat (chybí mu cílová adresa a port), druhý by odeslal bez problémů. Druhý socket má ručně zvolený místní port 55555 a je pevně nastaven na komunikaci s určenou adresou a portem. Proto první paket odešle, zatímco druhý nikoli, protože se cílová adresa a port v datagramu liší od cílové adresy a portu socketu. Všechny chybové stavy se hlásí výjimkami - ovšem pozor, některé stavy závisí na bezchybné funkci ICMP komunikace (kterou mohou někteří příliš horliví správci sítí blokovat), a ani pak nevyhození výjimky automaticky neznamená úspěch. Na druhém konci Datagram tedy úspěšně odešel z našeho programu (a pokud nemá lokální adresu příjemce, pak i z počítače). Na druhém konci by na něj měl někdo čekat - nemusí, ale pak samozřejmě posílání dat nemá smysl. Vytvoříme tedy úplně stejný socket jako při posílání datagramu. Paket se přijímá metodou receive(). Toto volání vždy blokuje do přijetí paketu nebo vypršení časového limitu (v tom případě ovšem končí vyhozením výjimky SocketTimeoutException). Následující příklad ukazuje implementaci "UDP echo" - program čeká na data, a když přijdou, pošle je zpět odesílateli. try { DatagramSocket sock = new DatagramSocket(20000); DatagramPacket dgp = new DatagramPacket(new byte[1000], 1000); sock.setSoTimeout(600000); // timeout 10 minut while (true) { sock.receive(dgp); sock.send(dgp); }
} catch (Exception e) { ... } Je to opravdu velmi jednoduché. Vytvoříme socket na portu 20000, připravíme datagram s bufferem pro 1000 bajtů, a nastavíme socketu časový limit 10 minut. To bude také jediný způsob ukončení (skrze výjimku) tohoto primitivního prográmku, pokud nepočítám stisknutí Ctrl-C a podobné způsoby. Metoda receive() pracuje poněkud "nejavovským" způsobem - předáváme jí již vytvořený objekt, který metoda pouze naplní. Zde je nutno zdůraznit, že buffer datagramu musí být dostatečně velký, aby se do něj data vešla (jinak se oříznou). Platnou délku metoda zapíše do datagramu, odkud ji zjistíme zavoláním getLength(). Velikost bufferu volíme podle velikosti očekávaných dat, nemá smysl vytvářet zbytečně velké buffery pro data o několika bajtech. Pozor - použití čísel portů podléhá omezením práv na konkrétním systému. Obvykle lze bez omezení používat porty od 1024, nižší vyžadují administrátorská práva. TCP klient Spojově orientovaná komunikace je v Javě ještě jednodušší než ta paketová. Pracuje se totiž se streamy - úplně stejně, jako při práci se soubory. Dá se říci, že díky této abstrakci se nemusí program vůbec zabývat, jak je komunikace řešena. Prostě získá příslušný stream a hotovo. Pro sestavení spoje se používá opět socket. Vše je vidět na následujícím příkladu: try { Socket sock1 = new Socket(); // vytvoření socketu sock1.connect("www.linuxsoft.cz", 80); // připojení sock1.close(); // zavření socketu Socket sock2 = new Socket("www.linuxsoft.cz", 80); // hned se připojíme BufferedReader br = new BufferedReader( new InputStreamReader(sock2.getInputStream())); BufferedWriter bw = new BufferedWriter( new OutputStreamWriter(sock2.getOutputStream())); bw.write(request); // zapíšeme předem připravený požadavek bw.flush(); // odeslání z bufferu String line = ""; // dokud jsou data, opakuj while (line != null) { line = br.readLine(); if (line != null) System.out.println(line); // platná data vypisuj } sock2.close(); // zavření socketu } catch (Exception e) { ... }
Tento kus kódu představuje velmi primitivního HTTP klienta. Nejprve k vytvoření socketu - v prvním případě vytvoříme nepřipojený a připojíme ho teprve ve druhém kroku. Po použití (resp. zde bez použití) se socket zavře, čímž se zruší (korektním způsobem) TCP spoj mezi počítači. Ve druhém případě již socket využijeme. Zde se vytváří s okamžitým připojením. Ze socketu získáme streamy, ty jsou napojeny na další tak, že pak máme k dispozici textové bufferované streamy. Do toho výstupního zapíšeme předem připravený (není součástí příkladu) HTTP požadavek, a ze vstupního streamu můžeme číst data. Ta se v tomto případě vypisují na standardní výstup. Po použití socket opět zavřeme. Parametry socketů (timeouty, přenosové třídy, buffery apod.) můžeme různě nastavovat - je samozřejmě potřeba vědět, k čemu nám změna výchozího nastavení bude. Jednoduchý server Typické chování serveru je, že čeká na připojení klienta, a až ten se připojí, obslouží ho - a současně čeká na připojení případných dalších klientů. V Javě to vypadá tak, že si vytvoříme instanci třídy ServerSocket a zde metodou accept() nasloucháme na příslušném portu. V okamžiku, kdy se připojí klient, metoda vrátí instanci třídy Socket. Z ní, úplně stejně jako na klientské straně, získáme vstupní a výstupní stream, a přes tyto streamy již normálně komunikujeme. Zbývá ještě vyřešit, jak během obsluhy klienta zajistit čekání na další klienty. Java je výrazně multithreadově orientovaná - a z toho vyplývá i řešení tohoto problému. Prostě vytvoříme nové vlákno, které obsluhuje klienta, zatímco původní vlákno opět zavolá metodu accept() a čeká na další připojení. Ještě se nelze nezmínit, že počet klientů čekajících na obsloužení je omezený. Výchozí hodnota je 50, v konstruktoru lze určit jinou - ovšem s rozumem, protože je lepší klienta odmítnout, než obsluhovat neúnosně pomalu. boolean quit = false; try { ServerSocket ss = new ServerSocket(22222); while (!quit) { final Socket sock = ss.accept(); Thread t = new Thread() { public void run() { try { InputStream is = sock.getInputStream(); OutputStream os = sock.getOutputStream(); ... sock.close(); } catch (IOException e) { ... } } }; t.setDaemon(true); t.start(); } } catch (Exception e) { ...
} Příklad ukazuje řešení pomocí anonymní třídy. Na místě použití se předefinuje metoda run() třídy Thread, vlákno se nastaví jako démon (tzn. jeho běh není překážkou ukončení programu) a spustí se. Všimněte si, že je proměnná sock deklarována s modifikátorem final - to je nutné, protože se s ní pracuje v metodě vytvořené anonymní třídy. Program běží, dokud je (v okamžiku připojení klienta) proměnná quit nastavena na false (odkud se hodnota změní, teď není důležité); poslední klient již samozřejmě není obsloužen (a obsluhování jiných klientů může být také přerušeno). Není to úplně nejlepší, ale vhodné řešení ukončování serverů je poněkud složitější - budeme se jím zabývat někdy později. HTTP klient Sliboval jsem vyšší vrstvy komunikace, zde je jedna z nich. Protokol HTTP je jeden z nejpoužívanějších, proto mám v Javě k dispozici snadno použitelnou implementaci HTTP klienta. Je to třída HttpURLConnection (jejím potomkem je velice podobná třída HttpsURLConnection pro připojení přes SSL). HttpURLConnection je abstraktní třída, její instance tedy nemůžeme vytvářet. Vzhledem k charakteru HTTP spojení na tom není nic překvapivého. Instanci získáme např. metodou openConnection() na instanci třídy URL - tato metoda však vrátí instanci URLConnection, proto si ji (po potřebné kontrole) musíme přetypovat. Důležitou vlastností HttpURLConnection je sdílení prostředků a jejich efektivní správa. Proto je výhodné tento mechanismus používat. Další výhodou je opět abstraktní přístup, kdy stačí mít URL a o nízkoúrovňové operace se nemusíme starat. try { URL url = new URL("http://www.linuxsoft.cz/"); HttpURLConnection con = (HttpURLConnection) url.openConnection(); System.out.println("Response code: " + con.getResponseCode()); System.out.println("Content type: " + con.getContentType()); BufferedReader br = new BufferedReader( new InputStreamReader(con.getInputStream())); String s = ""; while (s != null) { s = br.readLine(); if (s != null) System.out.println(s); } con.disconnect(); } catch (Exception e) { ... } Příklad ukazuje jednoduché získání dokumentu z HTTP serveru. Na základě URL vytvoříme spojení na server, příslušná instance třídy HttpURLConnection sama odešle požadavek (při přetypování v příkladu se mlčky předpokládá, že je objekt této třídy skutečně vrácen - pokud by to tak nebylo, skončí to výjimkou ClassCastException). Příklad vypíše kód odpovědi serveru a MIME typ dat. Pak se získá vstupní stream a data z něj se vypisují na standardní výstup. To je jedna z cest, jak s daty naložit. Existují i další cesty, ale ty si necháme na jindy. Využití proxy serveru
V některých případech není k dispozici přímé připojení do Internetu nebo jiné sítě. Jediné spojení zajišťuje proxy server. Je to sice nepříjemné, nicméně řešitelné, a to poměrně snadno, zejména od Javy 5.0. Tam je totiž přímo třída Proxy, jejíž instanci předáme metodě openConnection(). Celé je to poněkud rozsáhlejší, ale v tomto okamžiku stačí vědět, že proxy serverů je několik typů, z nichž zde vybereme HTTP (ještě se může hodit DIRECT, což značí přímé spojení bez proxy). Opět bude nejlepší ukázat si to na příkladu: try { URL url = new URL("http://www.linuxsoft.cz/"); Proxy p = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("10.0.0.200", 3128)); HttpURLConnection con = (HttpURLConnection) url.openConnection(p); ... } catch (Exception e) { ... } Je to úplně stejné jako v předchozím příkladu - s tím rozdílem, že použijeme proxy server. Ten běží na počítači s adresou 10.0.0.200 na (obvyklém) portu 3128. Pokud proxy server pracuje, jak má, nebude se chování od předchozího příkladu lišit (pouze bychom měli přítomny HTTP hlavičky specifické pro tento druh připojení). Bez vláken ani ránu Komunikační činnosti nyní na chvíli opustíme (vrátíme se k nim ještě později), a přejdeme k tomu, bez čeho se například při síťové komunikaci prakticky nelze obejít - k vláknům a práci s nimi. Vlákna se již několikrát objevila v příkladech, ale byla to vždy jen malá ukázka toho, co se s nimi dá dělat. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=977 Java (20) - vlákna Vlákna (threads) jsou v Javě velmi důležitá. Je to prakticky jediný způsob, jak používat mnohé blokující operace. Využití mají vlákna také pro činnosti prováděné na pozadí. 2.11.2005 07:00 | Lukáš Jelínek | přečteno 42039× Úvod do vláken v Javě Vlákna představují způsob, jak v rámci jednoho procesu provádět více činností paralelně (nebo - u jednoprocesorového stroje - pseudoparalelně). V rámci každého z vláken je vykonáván kód nezávisle na ostatních vláknech. Od procesů se vlákna liší tím, že spolu sdílejí data procesu, v němž běží. Standardní knihovny Javy obsahují poměrně rozsáhlou podporu pro práci s vlákny. Než je ale člověk začne používat (což je samo o sobě velice jednoduché), je dobré znát pár věcí, které jsou s používáním vláken v Javě nerozlučně spjaty. Kvůli platformové nezávislosti Javy není přesně definováno, jak se vlákna budou konkrétně chovat. Existují ale v zásadě dva druhy vláken: native threads - vlákna poskytovaná operačním systémem. V Linuxu dříve výhradně LinuxThreads
(klasické procesy se sdílenými daty), od JDK 1.4.2 lze využít i Native Posix Threads, což poskytuje podstatně vyšší výkon hlavně v těch případech, kdy se často vytvářejí nová vlákna. green threads - vlákna zajištěná na úrovni virtuálního stroje Javy. Běžně se s nimi nesetkáváme, ale pro dobrou přenositelnost je dobré s nimi počítat. Native threads Nativní vlákna mají takové vlastnosti, jaké jim poskytuje jejich implementace v OS. Typicky lze využít ryze paralelní běh (na více procesorech současně) a preemptivní přidělování a odnímání procesoru jednotlivým vláknům. O tato vlákna se programátor prakticky nemusí starat, protože starosti leží na operačním systému. Green threads "Zelená" vlákna běží jen na jediném procesoru (jde tedy o pseudoparalelní běh) a tento procesor nelze vláknu zvenku odebrat. Proto se programátor musí postarat, aby žádné vlákno neběželo příliš dlouho (aby se dostalo na ta ostatní). Lze to realizovat různými cestami, brzy se k tomu dostaneme. Je dobrým zvykem navrhovat programy tak, aby vyhověly oběma modelům - a to přesto, že se s green threads už příliš nepočítá. Rozhraní Runnable a třída Thread Naprostým základem práce s vlákny v Javě je rozhraní Runnable. Je v balíku java.lang a obsahuje jedinou metodu run(). Právě tato metoda obsahuje kód, který se bude v rámci vlákna vykonávat. Dále tu máme třídu Thread, která toto rozhraní implementuje, a hlavně, obsahuje infrastrukturu pro řízení běhu vlákna. Potřebujeme-li použít vlákno, lze postupovat dvěma cestami: rozšíření třídy Thread (s nezbytným předefinováním metody run()) implementace rozhraní Runnable Z hlediska výkonného kódu je v podstatě jedno, kterou cestu použijeme - na implementaci metody run() se to většinou neprojeví. Liší se jen přípravné práce. V zásadě se dá říct, že v jednodušších případech je lepší rozšířit třídu Thread, a v těch složitějších použít druhý způsob. Vytvoření a spuštění vlákna Vlákna s bohatou činností vytváříme jako klasické pojmenované třídy, pro jednoduché operace si vystačíme s anonymními třídami. Nyní se podívejme na dva příklady. První z nich ukazuje, jak vytvořit vlákno na základě třídy Thread: Thread t = new Thread() { public void run() { // tady bude nějaký kód } }; t.start(); Takto můžeme v okamžiku potřeby snadno vytvořit vlákno v místě, kde ho použijeme. Vlákno se spustí zavoláním metody start(). Druhý příklad ukáže, jak vytvořit vlákno na základě rozhraní Runnable. Přestože by šlo použít anonymní třídu, zde bude použita třída pojmenovaná: class ThreadUser { static class MyRunnable implements Runnable {
public void run() { // tady bude nějaký kód } } public static void main(String args[]) { Thread t = new Thread(new MyRunnable()); t.start(); ... } } Výsledek bude tentýž jako v předchozím případě. Rozdíl je v tom, že zde vytvoříme instanci (nijak neupravené) třídy Thread samostatně a předáme jí instanci třídy implementující rozhraní Runnable. Když se pak instanci Thread zavolá metoda start(), způsobí to, že se v rámci vlákna začne vykonávat metoda run() v "asociovaném" objektu MyThread (implementující Runnable). Životní fáze vlákna Vlákno od svého vytvoření (myšleno konstruktorem třídy Thread) do finalizace prochází řadou fází - do některých z nich se může, ale také nemusí dostat. Jedná se o tyto fáze: Vytvořené, nespuštěné - vlákno bylo vytvořeno jako instance objektu, a dosud nebylo spuštěno (nevykonává se kód). Některá nastavení vlákna lze provést jen v tomto stavu. Spuštěné, běžící - vlákno běží, procesor vykonává jeho kód. Spuštěné, čekající - vlákno může běžet, ale čeká na přidělení procesoru. Spuštěné, uspané nebo zablokované - vlákno bylo uspáno metodou sleep() nebo zablokováno voláním wait(), join() či jiným způsobem (např. v blokující operaci). Ukončené - vlákno doběhlo (vyskočilo z metody run()) a pouze přečkává, než bude (po ztrátě referencí) odstraněno jako instance kteréhokoli objektu. Uvedené stavy jsou chápány z hlediska logického, nikoli implementačního (vnitřně se např. rozlišuje stav blokovaného vlákna a vlákna čekajícího na nějakou událost). Existují ještě další fáze, ale do nich se vlákno může dostat pouze zavoláním některé ze zavržených (deprecated) metod. Proto je lepší se o těchto stavech vůbec nezmiňovat, případné zájemce odkazuji na dokumentaci. Operace s vláknem Normální vlákno, jak jsme ho vytvořili v příkladech, běží vedle hlavního vlákna (toho, které běží od začátku programu), a program skončí až v momentě, kdy dokončí běh všechna taková vlákna. Někdy je ale třeba, aby běh programu závisel pouze na jediném vlákně nebo omezené skupině, a zbylá vlákna na to vliv neměla. K tomu slouží tzv. démoni. Démona vytvoříme z normálního vlákna (resp. to jde i obráceně) metodou setDaemon(). Musí se to ale udělat před spuštěním vlákna, jinak se dočkáme výjimky IllegalThreadStateException. Vláknům můžeme nastavovat priority. Nově vytvořené má výchozí prioritu (střední), můžeme nastavit větší nebo menší hodnotu. Interpretace záleží na konkrétní implementaci vláken. U native threads je převedena na prioritu vlákna v operačním systému, kdežto u green threads má vlákno s vyšší prioritou vždy absolutní přednost před vláknem s prioritou nižší (pozor na to!). Priorita se nastavuje voláním setPriority() v rozsahu od MIN_PRIORITY do MAX_PRIORITY; lze to provést před spuštěním vlákna i za běhu. Pro snazší práci (hlavně při ladění) si lze vlákna pojmenovávat. Jméno se určí buď v konstruktoru, nebo později metodou setName(). Jména vláken nemusí být unikátní.
Nesobecká vlákna Jak jsem se již zmínil, správně napsaný multithreadový program by neměl spoléhat na konkrétní implementaci vláken. S tím souvisí důležitá podmínka, aby vlákna tzv. "nebyla sobecká" - jinými slovy, aby si neusurpovala procesor tak, že tím ostatním vláknům brání v běhu. Je proto nutné zajistit, aby se každé vlákno dostatečně často vzdávalo procesoru. K tomu dochází v těchto případech: blokující I/O operace (např. read(), write(), accept()), synchronizační operace (wait(), join()) uspání vlákna voláním sleep() explicitním vzdáním se procesoru - metoda yield() volání některých zapovězených metod (nebudu uvádět) Uspání vlákna je spolehlivé, ale ne vždy ho potřebujeme. Spoléhání na blokující operace je ošemetné, protože často k zablokování dojít nemusí a vlákno poběží dál. Naproti tomu zavolání yield() vynutí nové naplánování vlákna, a proto funguje zcela spolehlivě (pozor ale na priority!). Uvedený způsob má ale jednu nevýhodu - vhodně umístit volání yield() totiž v řadě případů vůbec není triviální, a špatné rozmístění může mít podobný efekt, jako kdyby se to neudělalo vůbec. Proto existuje ještě jedna cesta - vytvoření speciální "plánovacího" vlákna. Toto vlákno bude mít maximální prioritu, většinu času bude uspáno, jen občas se probudí a zase hned usne. Tím dojde ale k naplánování jiného ze zbývajících vláken, takže to má ve výsledku podobný efekt, jako kdyby se vlákna plánovala nativně. Důležité je ale zvolit vhodnou granularitu (délku maximálního časového kvanta) - pro většinu případů lze použít hodnoty 5-50 ms. Synchronizace přístupu Multithreading přináší mnoho výhod, ale také určité nevýhody. Jednou z nich je nutnost synchronizovat přístup k datům tak, aby byla zaručena jejich integrita a konzistence. Obecně je lepší synchronizovat spíš více než méně, protože nadbytečná sychronizace pouze zpomaluje, kdežto nedostatečná vážně narušuje funkci programu. Vždy je ovšem potřeba dát si pozor, aby se vlákna nemohla vzájemně zablokovat (deadlock). Proto je nutné snažit se (již ve fázi návrhu), aby synchronizovaných míst bylo co nejméně. Pro synchronizaci máme opět více možností: synchronized metoda Metoda může být deklarována s modifikátorem synchonized. To znamená, že v okamžiku vstupu do metody se objekt zamkne a při opuštění odemkne. Zavolá-li metodu jiné vlákno, musí čekat, než ji opustí vlákno, které ji zavolalo dřív. class MyClass { private int x = 0; private int y = 0; public synchronized void setData(int x, int y) { this.x = x; this.y = y; } } Metoda setData() v příkladu pracuje tak, že je během modifikace dat vyloučen přístup z jiného vlákna. Je zde totiž žádoucí, aby se proměnné x a y měnily vždy současně, proto nelze připustit, aby
si někdo přečetl jejich hodnoty v okamžiku, kdy je jedna změněna a druhá nikoli. synchronized blok Předchozí řešení je velice jednoduché a elegantní, ale jednak může zamykat na zbytečně dlouhou dobu (což by se muselo řešit rozdělením na více metod), a za druhé vyžaduje, aby byla příslušná třída již takto implementována. Máme-li třídu, která (typicky z výkonnostních důvodů) nezamyká objekt při jeho modifikaci, nejjednodušší řešení je použít blok s deklarací synchronized a uvedením objektu, který se má zamknout. java.awt.Point p = new java.awt.Point(10, 20); ... synchronized (p) { p.setLocation(5, 50); } Metoda setLocation() není synchronizovaná, proto je nutno (při přístupu z více vláken) synchronizovat zvenku. Funkce uvedeného kódu je zřejmá. Poznámka: S třídou Point se běžně nepracuje tak, aby k ní mohlo přistupovat více vláken (proto je z výkonnostních důvodů bez synchronizace). Proč tomu tak je, si řekneme později, v úvodu do javovské grafiky. Synchronizační wrappery V kapitole o kolekcích jsme se setkali s tzv. synchronizačními wrappery. Použijí se v případě, kdy máme kolekce bez synchronizace přístupu. Wrapper navenek zapouzdří příslušný objekt, aniž by se změnilo jeho rozhraní, a o synchronizaci se postará. Čekání na konec jiného vlákna Zavoláme-li nějakému jinému vláknu metodu join(), aktuální vlákno se zastaví a bude čekat na skončení běhu onoho vlákna. Lze využít i verze s časovým limitem - pak se bude čekat maximálně po zvolenou dobu. Čekání na objektu Každý objekt (jakýkoli potomek třídy Object) má sadu metod wait(). Zavolání této metody způsobí, že se aktuální vlákno zastaví do doby, než bude uvolněno zavoláním metody notify() nebo notifyAll() tomuto objektu. První metoda uvolní právě jedno vlákno, druhá všechna vlákna. Vlákno, které bude některou z těchto metod volat, si objekt musí nejprve zamknout - buď v rámci synchronized metody, nebo v synchronized bloku. Čekání lze opět omezit časovým limitem. Modifikátor volatile Pracuje-li s jednou proměnnou více vláken, obecně není zaručeno, že každé z vláken uvidí správnou hodnotu, přestože je modifikační operace atomická. Je to proto, že se přístup k datům optimalizuje a vlákna používají lokální kopie, jejichž obsah se nemusí včas promítnout do původní proměnné. Pokud se proměnná deklaruje s modifikátorem volatile, každá změna je okamžitě viditelná pro všechna vlákna a další synchronizace již není nutná. Přerušení vlákna Přerušení je událost, kterou je nutno zvlášť ošetřit. Lze ji přirovnat k příchodu signálu do procesu. Pokud vlákno běží, je pouze nastaven příznak, že došlo k přerušení (lze zjistit zavoláním isInterrupted() nebo interrupted(); pozor - metoda interrupted() příznak resetuje!). Vlákno se
přerušuje voláním interrupt(). Pokud vlákno čekalo (uspáno nebo v blokující operaci), je navíc vyvolána výjimka InterruptedException a vlákno se rozběhne. Tato výjimka je synchronní, její ošetření je tedy u daných operací povinné. Přerušit čekající vlákno lze např. v okamžiku, kdy se má ukončit program a vlákno by po sobě mělo uklidit. V minulé kapitole bychom takto třeba uzavřeli otevřený socket: try { ServerSocket ss = new ServerSocket(22222); try { while (!quit) { final Socket sock = ss.accept(); Thread t = new Thread() { public void run() { try { InputStream is = sock.getInputStream(); OutputStream os = sock.getOutputStream(); ... sock.close(); } catch (IOException e) { ... } } }; t.setDaemon(true); t.start(); } } catch (InterruptedException e) { // zde se zachytí přerušení ss.close(); // uzavření socketu } } catch (Exception e) { ... } Skupiny vláken V rozsáhlejších programech často pracujeme s mnoha vlákny, která se dají rozdělit do různých logických skupin. V jedné mohou být třeba vlákna obsluhující síťové požadavky, ve druhé vlákna pro zpracování dat atd. Jejich správu si můžeme usnadnit využitím třídy ThreadGroup. Skupiny, tvořené instancemi ThreadGroup, jsou hierarchicky (stromově) organizovány. Lze tak vlákna ovládat na různých úrovních podle toho, jak právě potřebujeme. Každá skupina může být, podobně jako samotné vlákno, libovolně pojmenována. V rámci skupin lze např. určovat vláknům maximální prioritu nebo vlákna hromadně přerušovat. Do skupiny lze vlákno přidat jen v okamžiku jeho vytváření (skupina se předá jako parametr konstruktoru), později již změna není možná. ThreadGroup tg = new ThreadGroup("network server threads"); tg.setDaemon(true);
Runnable r = new Runnable() { public void run() { ... } }; Thread t1 = new Thread(tg, r); Thread t2 = new Thread(tg, r); V příkladě se vytvoří skupina vláken - tako skupina bude démon, tzn. bude automaticky zrušena v okamžiku, kdy doběhne poslední vlákno. Při vytváření vláken jim (kromě instance implementující Runnable) předáme i tuto skupinu, čímž se vlákna stanou jejími členy. Lokální data vláken Někdy je vhodné, aby každé vlákno pracoval se specifickými daty, a přesto naprosto stejným způsobem jako jiná vlákna. K tomu slouží třída ThreadLocal, která slouží jako "mikrokontejner" (pojímá jednu hodnotu) pro tato data. Instanci tohoto objektu může každé vlákno nastavit nějakou hodnotu - a je zaručeno, že při požadavku na hodnotu vlákno obdrží vždy právě tu svoji. Pokud si vlákno nic nenastaví, dostane hodnotu null, ledaže by byla (v potomkovi ThreadLocal) předefinována metoda initialValue(). Před JDK 1.5 bylo nutné hlídat si typ dat, a podle potřeby přetypovávat. Od JDK 1.5 (Java 5.0) má ThreadLocal generický charakter, a lze proto provádět automaticky typovou kontrolu. Další pohled pod kapotu Jsme na konci poměrně dlouhé kapitoly o vláknech. Přichází vhodná chvíle k dalšímu pohledu pod kapotu - tentokrát na datové typy. Podíváme se na přetypovávání, kontrole typů, zjišťování informací o typech atd. Právě tato část Javy patří k těm nejvíce propracovaným, podle mého názoru je to oblast velice zajímavá. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1006 Java (21) - datové typy Java je jazyk se silnou typovou kontrolou. To sice pomáhá eliminovat mnohé problémy, přesto nás to nezbavuje nutnosti dbát určitých pravidel. Jejich zvládnutí však poskytuje až netušené možnosti, co lze v Javě dělat. 29.11.2005 06:00 | Lukáš Jelínek | přečteno 25279× Jak nenarazit Jsou dvě skupiny programátorů. Jedna preferuje co největší volnost v práci s datovými typy, druhá naopak co nejpřísnější pravidla. Jazyků bez typové kontroly je málo, stejně tak jazyků se zcela striktní kontrolou. Java se nachází v té přísnější oblasti, pravidla však nejsou až tak tuhá. Jak známo, v Javě existují dva druhy datových typů: primitivní, a referenční. Primitivních typů je jen několik, nemůžeme od nich odvozovat typy nové (něco jako typedef neexistuje), naopak v typech referenčních spočívá těžiště práce. Teprve u nich se totiž uplatňuje objektové programování, které je v Javě jako doma. Primitivní typy
Tyto typy nesou pouze svoji hodnotu, podporují různé operátory pro operace nad těmito typy, a jsou do jisté míry vzájemně přetypovatelné. Platí pravidlo, že od typu s menším rozsahem nebo méně přesného k typu s větším rozsahem, resp. přesnějšímu, můžeme provést implicitní přetypování, opačně nikoliv. Explicitně lze přetypovávat i opačně. Pozor - typ boolean není vzájemně přetypovatelný s žádným datovým typem. Oproti např. jazyku C v Javě nikdy nevíme, jaké množství paměti ten který typ zabere - to je otázka implementace a na použití typu se to neprojeví. Co naopak známe, jsou meze rozsahu dat. Proto se můžeme vždy spolehnout, že určitý typ bude mít daný rozsah, bez ohledu na implementaci. Následující příklad ukazuje, jak s primitivními typy lze a nelze pracovat: int i = 10; // přiřazení hodnoty long l = i; // implicitní přetypování - lze byte b1 = l; // implicitní přetypování - nelze byte b2 = (byte) i; // explicitní přetypování - lze boolean f1 = i; // impl. přetypování na boolean - nelze boolean f2 = (boolean) i; // expl. přetypování na boolean - nelze double d = l; // implicitní přetypování - lze Referenční typy Mezi referenční typy patří rozhraní, objektové třídy a pole. Přistupuje se k nim zásadně pomocí referencí (které ale mají spíše charakter ukazatelů). Při přiřazení hodnoty proměnné referenčního typu proto dojde vždy k přiřazení reference (tzn. nová proměnná bude ukazovat na tentýž objekt jako ta původní), nikoli hodnoty - pro předání hodnoty, tj. zkopírování objektu, se musí použít buď kopírovací konstruktor (pokud ho třída poskytuje), anebo klonování (pokud třída implementuje rozhraní Cloneable a má přístupnou metodu clone(); tento postup se však nedoporučuje). U polí je trochu jiná situace, už jsme se tomu věnovali v kapitole o polích. Reference na hodnoty referenčních typů lze přetypovávat jen v případech, kdy jsou typy kompatibilní. Striktní typová kontrola vylučuje přetypování na nekompatibilní typy, bez ohledu na vnitřní datovou reprezentaci (oproti situaci v C++, kde si můžeme v tomto ohledu dělat prakticky cokoliv - samozřejmě na vlastní nebezpečí). Znamená to, že přetypování projde jen v těchto dvou situacích (přetypování na totožný typ vynechávám): Přetypováváme na nadtyp (nadřazená třída nebo rozhraní). Takto lze přetypovávat i implicitně, je to vždy bezpečné. Přetypováváme na podtyp. To lze jen v situaci, kdy se jedná o instanci typu, na který přetypováváme, nebo o instanci jeho podtypu. Tento druh typové konverze je nutno dobře ohlídat, a to i v případech, kdy očekáváme hodnotu určitého typu (implementace se může změnit). Podívejme se tedy, co je a co není v tomto ohledu legální: String s1 = "testovací objekt"; // lze - přetypování na nadtyp CharSequence cs = (CharSequence) s1; // lze - instance příslušného podtypu String s2 = (String) cs; // nelze - nekompatibilní typy // ohlásí kompilátor
StringBuffer sb1 = (StringBuffer) s1; // nelze - instance nekompatibilního podtypu // vyhodí výjimku (viz dále) StringBuffer sb2 = (StringBuffer) cs; Výjimka ClassCastException Již mnohokrát v tomto seriálu jsem upozorňoval na nutnost použití správného typu dat - s tím, že nedodržení bude "potrestáno" výjimkou ClassCastException. Je to asynchronní výjimka (běžně k ní nedochází), proto by se v normálních případech ani neměla ošetřovat. Naopak, mělo by se jí předcházet, takže její vyhození pak vždy signalizuje, že je někde něco (naprogramováno) špatně. Výjimky ClassCastException se dočkáme vždy, když se pokusíme o nelegální přetypování (takové, které nelze odhalit již při kompilaci). Musím varovat hlavně před záludnými případy s přetypováním polí. Více ukáže následující příklad: ArrayList<String> al = new ArrayList<String>(); al.add("text"); String s1 = (String) al.toArray()[0]; // funguje String sa1[] = (String[]) al.toArray(); // nelze! String s2 = (String) al.toArray(new String[0])[0]; // funguje String sa2[] = al.toArray(new String[0]); // správně První možnost (pro někoho možná překvapivě) vyvolá výjimku ClassCastException. V čem je problém? Obě funkce vrátí pole, jehož prvky jsou řetězce. Prvky těchto polí můžeme tedy zcela bezpečně přetypovat na řetězce. Toto už ale v žádném případě nelze říci o příslušných polích. Bez ohledu na to, jaké prvky v poli jsou, se prostě nejedná kompatibilní typy (typ Object[] není přetypovatelný na String[]) a proto nemůžeme tuto konverzi provést. Mám správnou instanci? Kdo provádí operaci na datech, která získal odněkud "zvenku", a potřebuje tato data přetypovat, musí si bezpodmínečně zjistit, že má správný typ. Spoléhání na to, že "to vyjde", není dobré. A možná ještě horší je řešení pomocí zachycování výjimky ClassCastException. To je přímo cesta do pekel, protože kromě velké režie na zpracování výjimky se může také stát, že někdo později změní hierarchii tříd, a příslušné typy najednou budou kompatibilní - výjimka se tedy nevyvolá, ale výsledkem bude nějaké nedefinované (potenciálně chybné) chování. Základním prostředkem pro zjištění správného typu je operátor instanceof. Ten použijeme v případech, kdy požadujeme instanci konkrétní třídy. Například takto (úprava kusu příkladu z kapitoly o síťové komunikaci): URL url = new URL(urlString); // urlString máme odjinud URLConnection con = url.openConnection(); // otevře se spojení if (con instanceof HttpURLConnection) { // test typu instance // bezpečné přetypování HttpURLConnection hcon = (HttpURLConnection) con; ... }
else { con.disconnect(); System.err.println("Toto není adresa protokolu HTTP"); ... } Tehdy jsem uvedl, že mlčky předpokládáme správný typ. Při adrese udané "natvrdo" v kódu to mohlo být použitelné (pro zkušební účely), ale obecně takové předpoklady dělat nesmíme. Zvlášť tehdy, má-li vliv na typ někdo jiný (např. uživatel, který někde zadá vstupní data). Kontrolu zkrátka nelze vynechávat. Třída Class Každý referenční typ má v Javě má svoji instanci třídy Class. Tato instance se vytvoří automaticky při načtení příslušného typu do JVM, a lze ji kdykoli získat zavoláním getClass() na instanci objektu. Stejná instance třídy Class je k dispozici také jako proměnná třídy/rozhraní. Přiblížíme si to na příkladě (zjednodušená verze výše uvedeného příkladu - s úpravou pro použití této cesty): URL url = new URL(urlString); // urlString máme odjinud URLConnection con = url.openConnection(); // otevře se spojení if (HttpURLConnection.class.isInstance(con)) { // test typu instance ... } if (con.getClass() == HttpURLConnection.class) { // test typu instance ... } Zbývá ještě vyřešit, co kdy použít. Operátor instanceof se hodí v případech, kdy požadovanou třídu známe již při psaní programu. Naopak metodu isInstance() použijeme spíš tam, kde tuto třídu získáme až za běhu. Třetí způsob uvádím proto, že to "také jde", ale moc se ve skutečnosti nepoužívá (je také poměrně omezený, protože porovnává naprosto striktně, přímo instance třídy Class). Třída Class toho ale umí mnohem víc. Pusťme se tedy do toho - nestihneme sice všechno, ale i tak to stojí za to. Zjišťování informací Třída Class poskytuje základní aparát pro zjišťování informací o daném typu. Podívejme se na některé z nabízených metod: isInstance(Object o) - Zjišťuje, zda je objekt předaný v argumentu instancí dané třídy (viz výše). Je to vlastně dynamický ekvivalent operátoru instanceof isAssignableFrom(Class c) - Zjišťuje, zda je objekt předávaný v argumentu přiřaditelný tomuto typu. isArray() - Zjišťuje, zda se jedná o pole. isInterface() - Zjišťuje, zda se jedná o rozhraní. isPrimitive() - Zjišťuje, zda se jedná o primitivní typ. Ono totiž úplně neplatí, že Class se váže jen k referenčním objektům. Zapouzdřující třídy (např. Integer pro int) obsahují konstantu TYPE, která nese právě objekt třídy Class pro příslušný primitivní typ. getName() - Vrací název typu. Tento název je zvláštní tím, že v sobě obsahuje "zakódovanou" dimenzi pole. getSimpleName() - Vrací název typu tak, jak je definován ve zdrojovém kódu. Metoda je k dispozici od Javy 5.0.
Vytváření tříd a instancí Nejen zjišťovat informace můžeme pomocí třídy Class. Máme tu totiž k dispozici nástroje mnohem silnějšího kalibru. Za normálních okolností vytváříme objekty zavoláním jejich konstruktoru nebo nějaké metody, která instanci vytvoří (zavoláním neveřejného konstruktoru). Existuje ale ještě další cesta, a tou je metoda newInstance() třídy Class. Ta se pokusí zavolat bezparametrický konstruktor - což samozřejmě nemusí dopadnout dobře (konstruktor nemusí existovat apod.), a v takovém případě se vyvolá příslušná výjimka (např. InstantiationException). Metodu běžně nevyužijeme, ale hodí se v kombinaci s druhou podobnou věcí - a tou je vytvoření třídy. Za normálních okolností lze již při startu rozhodnout, které třídy se mají načíst a inicializovat. Jsou to jednoduše ty, které se někde v programu používají. Někdy bychom ale potřebovali za běhu načíst nějakou třídu, která třeba při spuštění ani neexistovala. Typicky to může být třeba nějaký plugin (zásuvný modul). Ale i to je možné. Třída Class má dvojici statických metod forName(). My se nyní zaměříme jen na tu jednodušší z nich, ale princip je stejný. Metodě předhodíme řetězec s názvem třídy, a příslušná třída je načtena a inicializována. Pochopitelně jen tehdy, je-li k dispozici. O načtení se postará systémový zavaděč tříd (classloader), který soubor s třídou hledá v místech určených proměnnou CLASSPATH. Nenajde-li ho, způsobí to výjimku ClassNotFoundException. Podívejme se na příklad: try { Class c = Class.forName("MyDynamicClass"); // načtení třídy // lze přetypovat na Runnable? if (Runnable.class.isAssignableFrom(c)) { Runnable r = (Runnable) c.newInstance(); // vytvoření instance Thread t = new Thread(r); t.start();
// použití
} } catch (Exception e) { System.err.println("Třída nebyla nalezena"); } Příklad ukazuje, jak lze takto získaný objekt použít. Načteme třídu, zjistíme, zda implementuje rozhraní Runnable, a pokud ano, vytvoříme nové vlákno, které začne vykonávat metodu run() instance třídy MyDynamicClass. Podobnými mechanismy lze dělat ještě větší "psí kusy" (třeba načíst třídu odněkud ze sítě nebo z databáze a provést s ní totéž). Podobně lze také používat metody takových objektů, aniž bychom je předem znali - k tomu se používá postup zvaný reflexe, a o něm bude řeč někdy později (pro běžné použití se z řady důvodů nehodí). Přístup odepřen Načítání tříd odněkud zvenku (ať už při startu nebo za běhu) skýtá poměrně velké nebezpečí. Do třídy totiž někdo může podstrčit nějaký nebezpečný kód, který snadno udělá v běžícím programu paseku - samozřejmě ale jen tehdy, pokud mu to dovolíme. Jak mu to nedovolit, tedy jak omezit oprávnění pro provádění různých potenciálně nebezpečných operací, si vyzkoušíme příště. Jen naťuknu, že takové mechanismy se používají např. ve webových prohlížečích, aby applety ze sítě
nemohly sahat na lokální disk nebo dělat jiné zapovězené věci. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1025 Java (22) - omezování práv I. Minule jsme načítali objektové třídy za běhu programu. To je činnost potenciálně velmi nebezpečná, proto je žádoucí (nejen) v takovém případě omezit práva kódu získanému zvnějšku. Podívejme se, jak se to dělá. 30.1.2006 06:00 | Lukáš Jelínek | přečteno 14422× Bezpečnost především Spustíme-li normální javovskou aplikaci, běží ve výchozím nastavení s maximálními právy. Veškerý obsažený kód má tedy právo dělat si v systému cokoliv (samozřejmě s ohledem na práva v operačním systému). V minulém dílu seriálu jsme načítali třídy za běhu programu, vytvářeli instance těchto tříd a spouštěli je. Co by se stalo, kdyby nám někdo zlomyslně podstrčil třídu, která by obsahovala nějaký škodlivý kód? Odpověď je jasná - záleželo by pouze na tvůrci programu, jaké "potěšení" by nám připravil. Systém mu je zcela k dispozici a může tedy zcela nerušeně třeba smazat obsah domovského adresáře. Základním pravidlem je absolutní nedůvěra ke kódu získanému odněkud zvenku (z webového serveru apod.). Kód zkrátka nesmí mít možnost něco zlého provést. A jak mu tuto možnost odejmout, na to se nyní podíváme. Hlídač v systému K vynucení bezpečnostních omezení si přizveme na pomoc "hlídače" (budeme pro něj používat termín security manager). Hlídač bude schvalovat všechny potenciálně nebezpečné operace, a povolí jen takové, které budou odpovídat určeným bezpečnostním pravidlům. Hlídač je v Javě představován třídou java.lang.SecurityManager. V rámci jedné JVM může být aktivní pouze jediný security manager - můžeme ho nastavit už při spouštění programu v příkazové řádce, anebo i za běhu programu. Pozor ovšem, že i nastavení nového security managera je schvalovanou operací, ten stávající "hlídač" to tedy musí povolit. Lze používat přímo třídu SecurityManager, nebo si vytvořit jejího potomka a dodat mu potřebné vlastnosti. Hlavně ale můžeme používat ještě silnější a univerzálnější řešení (kde ani není potřeba mít explicitně vytvořeného security managera) - brzy se k němu dostaneme. Povolení ... máš? Termín "povolení" (permission) je dalším z pojmů, které nás velice zajímají. Zde ho představuje třída java.security.Permission, ale v tomto případě to není tak, že by tento objekt znamenal přímo oprávnění provést nějakou akci. Naopak, pomocí instance třídy Permission se dotazujeme security managera, zda tímto povolením disponujeme. Mezi potomky třídy Permission existují důležité vztahy. Některá povolení totiž mohou implikovat jiná. Např. třída AllPermission implikuje všechna ostatní povolení, proto je-li schváleno toto povolení, schválí se i jakákoli jiná. Jiným příkladem je povolení ke čtení všech souborů v kořenovém adresáři, implikující všechna povolení číst soubory kdekoli ve stromě. Potomci třídy Permission nejsou (narozdíl od většiny věcí souvisejících s bezpečností) všichni umístěni v balíku java.security, ale nacházejí se v různých balících, podle oblasti, které se týkají (jsou třeba v java.lang nebo java.net).
Důležitou otázkou je, jak schválení probíhá. Třída SecurityManager má jednak metody checkPermission(), a dále množství dalších metod začínajících slovem check. Ty první jsou primární, druhé ve výchozí implementaci volají ty první s patřičným objektem typu Permission. Chování lze samozřejmě v potomkovi změnit. Všechny tyto metody (až na jedinou výjimku, metodu checkTopLevelWindow()) se ale navenek chovají shodně - je-li operace povolena, metoda se tiše vykoná a běh normálně pokračuje dál. Když je ale operace zamítnuta, vyvolá se výjimka java.lang.SecurityException. Ještě dodám, že existují také třídy PermissionCollection a Permissions. První z nich je abstraktní kolekce povolení, druhá pak její standardní implementace. Umožňují práci s celou množinou povolení najednou (zejména předávání jiným objektům), včetně zjišťování implikací. Bezpečnostní model Javovský bezpečnostní model je založen na doménách. Každá doména vlastní povolení pro určitý rozsah operací (který je dán aktuálním nastavením bezpečnostní politiky). Programový kód (třída, instance) nemůže získat vyšší práva tím, že zavolá kód z jiné domény (ani tím, že je z této domény volán). Domény se dělí do dvou kategorií - aplikační a systémové. Přístup k prostředkům systému je povolen pouze v systémových doménách (běžně je pouze jediná). Důležitou skutečností je, že při průchodu různými doménami má kód oprávnění odpovídající jejich průniku - tedy jen ta oprávnění, která má ve všech doménách. Protože by samozřejmě jakékoli omezení v celém řetězci volání bránilo používat systémové prostředky a jiné potenciálně nebezpečné věci, existuje řešení: označit nějaký kód jako privilegovaný. Jak se to dělá, k tomu se vrátíme za chvíli. Implicitní mechanismus kontroly Původně (ve verzích JDK 1.0 a 1.1) byl bezpečnostní model jednoduchý a přímočarý, což se s příchodem další verze změnilo. Nyní máme k dispozici implicitní kontrolní mechanismus, který provádí kontrolu oprávnění v případech, kdy se nepoužije přímo security manager. Poskytuje ho třída java.security.AccessController, a kromě kontroly přístupu se stará také o běh privilegovaného kódu a některé další činnosti. Od této třídy se netvoří instance, všechny metody jsou statické. Podívejme se na to, jak se to dělá v praxi. Máme kus kódu, který potřebuje číst ze souboru /etc/passwd. Následující příklad ukazuje, jak by to vypadalo v různých případech: SecurityManager sm = System.getSecurityManager(); Permission p = new FilePermission("/etc/passwd", "read"); if (sm != null) { sm.checkPermission(p); sm.checkRead("/etc/passwd"); } AccessController.checkPermission(p); V příkladu jsou uvedeny tři cesty, jak se ověřují oprávnění. První je získání aktuální instance security managera, vytvoření instance třídy FilePermission (pro povolení číst příslušný soubor), a následná kontrola povolení security managera. Ve druhém případě se volá přímo metoda checkRead() se stejnými parametry, což provede naprosto totéž. A konečně poslední cestou je kontrola povolení u třídy AccessController. Tato třetí cesta je základní metodou volby, a měla by se používat vždy, pokud není zvláštní důvod postupovat jinak. Navíc metody třídy SecurityManager
volají také stejnou metodu třídy AccessController, pokud nejsou v potomkovi předefinovány. Hlavní výhodou tohoto postupu je, že se nemusí nic zvláštního programovat. Všechno máme k dispozici, a záleží pak na nastavení bezpečnostní politiky, co všechno kódu umožníme provádět. Privilegovaný kód Kus kódu, který by se měl provádět se zvláštní úrovní oprávnění (s vyšší, než by odpovídalo příslušným bezpečnostním doménám), je nutné označit jako privilegovaný. Dělá se to typicky javovským způsobem - tedy vytvořením instance implementace rozhraní PrivilegedAction a předáním metodě AccessController.doPrivileged(), která zajistí provedení s příslušnými právy. Pokud je nutné vyhazovat synchronní výjimku, použije se implementace rozhraní PrivilegedExceptionAction. Privilegovaný kód poběží s oprávněním domény, ze které byl přímo zavolán - předchozí domény v řetězci volání se neberou v úvahu. Použití přiblíží příklad: PrivilegedAction pa = new PrivilegedAction() { public Boolean run() { try { System.loadLibrary("mylibrary"); return new Boolean(true); } catch (Throwable e) { // musíme chytit i UnsatisfiedLinkError return new Boolean(false); } } }; if (AccessController.doPrivileged(pa).booleanValue()) { ... // nějaký kód závislý na načtení knihovny } Příklad ukazuje načtení knihovny do systému použitím privilegovaného kódu. Vytvoří se instance anonymní třídy vzniklé implementací PrivilegedAction. Metoda run() vykoná vše potřebné (všechny výjimky i chyby zde budeme zachycovat uvnitř) a vrátí výsledek operace. K úspěšnému (z hlediska bezpečnosti) provedení stačí, aby kód volající doPrivileged() patřil do domény disponující povolením RuntimePermission pro akci "loadLibrary.mylibrary". Bezpečnostní kontext Za normálních okolností se bezpečnostní kontroly vztahují ke vláknu, které určitý kód provádí. To není vždy úplně nejvhodnější - někdy může být potřeba poskytnout pro konkrétní účely vláknu jiná (typicky vyšší) oprávnění. Proto existuje třída AccessControlContext, která představuje kontext pro oprávnění vlákna. Její instanci vlákno získá zavoláním metody getContext() třídy AccessController, a pak ji může předat jinému vláknu k dalšímu použití (ve voláních checkPermission() a doPrivileged()). Asi bude nejlepší si to ukázat na příkladu. Máme dvě vlákna, jedno druhému poskytne svůj kontext, a to ho použije ke kontrole oprávnění: public class MyThread extends Thread { private AccessControlContext ctx = null; public void setContext(AccessControlContext ctx) { this.ctx = ctx;
} public void run() { ... Permission p = new SocketPermission("nejaka.adresa.com:12345", "connect"); try { if (ctx != null) ctx.checkPermission(p); // ověření povolení podle kontextu else AccessController.checkPermission(p); // není nenastaven kontext ... // právo pro přístup do sítě } catch (SecurityException e) { ... // zákaz přístupu do sítě } ... } } ... MyThread t = getWorkerThread(); // získání přístupu k pracovnímu vláknu t.setContext(AccessController.getContext()); ... Máme vlákno, které vykonává nějaké služební činnosti pro ostatní vlákna. Během této činnosti se potřebuje připojit někam do sítě, na což ovšem nemusí mít oprávnění. Proto mu vlákno, které na využití služeb má zájem, poskytne svůj kontext. Pokud v rámci tohoto kontextu existuje příslušné oprávnění, je přístup do sítě umožněn, jinak nikoliv. Nezíská-li vlákno cizí kontext, provede se ověření standardním způsobem. Příklad je samozřejmě velmi jednoduchý a naivní, skutečná implementace by byla složitější. Ještě důležitá poznámka - nově vytvořené vlákno dědí automaticky bezpečnostní kontext svého rodiče. To znamená, že se ho týkají všechna omezení daná doménami, kterými rodič prošel. Pokud by tomu tak nebylo, mohlo by to znamenat nepříjemné riziko. Proto implementace vytváření vláken zajišťuje, aby vlákno získalo bezpečnostní kontext od svého rodiče. Ztělesnění domény Byla řeč o bezpečnostních doménách - každý si tedy může klást otázku, jak jsou tyto domény vyjádřeny. K tomu máme třídu java.security.ProtectionDomain, která obsahuje tzv. zdroj kódu (URL + případné certifikáty k ověření podpisu kódu), zavaděč tříd, vlastnosti uživatele a staticky definovaná oprávnění (nezávislá na aktuální bezpečnostní politice). Efektivní oprávnění domény pak závisí samozřejmě na nastavení politiky. K čemu je to dobré? Všimněte si zejména, že zde figuruje zavaděč tříd. Právě ten totiž určuje, do které domény bude načtený kód patřit (a zkonstruuje i instanci objektu domény). V závislosti na použitém zavaděči se tak může - i pro stejný kód - velmi lišit množina oprávnění, kterou bude kód disponovat. Jak omezit práva?
Dostali jsme se do bodu, kdy máme k dispozici celý aparát pro kontrolu oprávnění, víme, jak pracuje, takže ještě zbývá určit, co komu povolíme. Protože je to poměrně rozsáhlá záležitost, podíváme se na to příště. Budeme se zabývat hlavně zavaděči tříd a nastavováním bezpečnostní politiky, ale dostaneme se i k problematice podepisování kódu, což je další z metod posilování bezpečnosti při současném zjednodušení práce. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1051 Java (23) - omezování práv II. Po minulém teoretickém úvodu do problematiky zabezpečení přejdeme k praxi - tedy k nastavování bezpečnostní politiky a práci se zavaděči tříd. Stranou nezůstane ani podepisování kódu. 14.3.2006 06:00 | Lukáš Jelínek | přečteno 12129× Zavaděče tříd Při vytváření instancí objektů (a samozřejmě při používání statických metod třídy) musí být příslušná třída přítomna v JVM - musí být zavedena. Jak jsme si již dříve řekli, nezavede se sama od sebe, udělá to tzv. zavaděč tříd (classloader), ať už implicitně nebo explicitně. Zavaděčů tříd můžeme mít v systému libovolný počet (a používat je podle potřeby), jeden z nich má však výsadní postavení. Je to tzv. prvotní zavaděč, sloužící k zavedení základních systémových tříd. Nemá svoji javovskou reprezentaci, jedná se o nativní kód a jeho chování je platformově závislé. Zavádění tříd pracuje na bázi delegování - jeden zavaděč tedy může nechat zavést nějakou třídu jiný zavaděč (nacházející se výše v hierarchii). Takto se může zavedení třídy postupně přenést až k prvotnímu zavaděči. Každá situace má svůj výchozí zavaděč. Pro aplikace je to URLClassLoader, pro applety AppletClassLoader (je potomkem URLClassLoader, ale jedná se o soukromou třídu v balíku sun.applet, takže nelze běžně používat) a při zavedení kvůli odkazu z již zavedené třídy se použije ten zavaděč, kterým se zavedla příslušná třída. Jak zavádět Přestože si můžeme vytvořit svůj vlastní zavaděč a používat ho, v drtivé většině si vystačíme s tím, co už je ve standardní knihovně - tedy se třídou URLClassLoader. Tento zavaděč má totiž vše potřebné k tomu, abychom mohli zavést neznámou třídu tak, aby nemohla udělat v systému paseku. Potřebujeme-li při zavádění použít jiný zavaděč, jednoduše ho specifikujeme v metodě Class.forName(). Při tomto volání lze rovněž určit, zda se má třída hned inicializovat (což mj. znamená, že se provede kód v jejím statickém inicializátoru) - pokud by inicializována nebyla, došlo by k tomu až při požadavku na vytvoření první instance. Příklad uvedu později. Existuje ještě jiná, nízkoúrovňovější cesta - zavolání metody loadClass() příslušného zavaděče. Zde už se ale nepracuje s běžnými názvy tříd, nýbrž s tzv. binárními názvy, které už známe odjinud. Stačí se podívat, jaké soubory kompilátor vytvoří pro vnořené třídy (ať už pojmenované nebo anonymní). Právě tyto názvy souborů odpovídají binárním názvům tříd, samozřejmě bez specifikace balíku (která je tvořena adresářovou hierarchií). Bezpečné zavádění Použijeme-li k zavedení třídy zavaděč URLClassLoader, můžeme (pomocí nastavení bezpečnostní
politiky) určit, jaká opravnění budou mít třídy zavedené z určitého zdroje. Lze tak definovat důvěryhodné zdroje a ty, které naši důvěru nemají - nezáleží na tom, zda se jedná o lokální zdroje (adresář, JAR soubor) nebo vzdálené. Jak již všichni vědí z minulé kapitoly, zdroj je představován třídou CodeSource a kromě URL může obsahovat i certifikáty. Nastavení bezpečnostní politiky je klíčová záležitost. Jsou-li aktivní bezpečnostní omezení, kód smí provádět jen ty operace, které mu bezpečnostní politika povolí - výjimkou je pouze čtení souborů ze zdroje, odkud byl kód získán. Než se k nastavování politiky dostaneme, podívejme se nejprve na příklad, jak se takové bezpečné zavádění tříd provádí: try { URL urls[] = { new URL("http://www.linuxsoft.cz/") }; URLClassLoader ucl = URLClassLoader.newInstance(urls); Class c = Class.forName("MyClass", true, ucl); ... } catch (Exception e) { ... } Pokud byste chtěli uvedený příklad vyzkoušet, nahraďte prosím URL nějakou vhodnější hodnotou (nejlépe místem, kam uložíte své třídy), aby nebyl server Linuxsoftu zbytečně "olizován" vaším classloaderem. Nastavování bezpečnostní politiky Bezpečnostní politika je v Javě představována abstraktní třídou java.security.Policy. Instancí této třídy může být libovolný počet, aktivní je však vždy pouze jediná. Nastavení lze za běhu aplikace změnit, ale samozřejmě jen tehdy, má-li k tomu kód potřebné oprávnění. Objekt má svoje úložiště nastavení (konfigurace), např. v souboru na disku - změnami v tomto nastavení měníme bezpečnostní politiku. Pokud ke změně dojde za běhu aplikace, změny se projeví až v okamžiku, kdy je nastavení aktualizováno metodou refresh(). Výchozí nastavení politiky je uloženo v souborech, jejichž umístění se určuje v konfiguraci bezpečnosti Javy (soubor java.security v adresáři /lib/security pod instalačním adresářem Javy). Typicky se používají dva soubory - jeden pro úroveň systému (java.policy v témže adresáři), a druhý pro uživatelská nastavení (.java.policy v domovském adresáři). V konfiguračním souboru java.security lze nastavit ještě mnoho dalších věcí - doporučuji do něj nahlédnout, jsou tam velice srozumitelné komentáře k jednotlivým položkám. Object Policy funguje tak, že pomocí dvou metod getPermissions() vrací kolekce povolení, které tato politika obsahuje. Jedna z těchto metod poskytuje globální povolení pro určitou doménu (ProtectionDomain), druhá pro zdroj kódu (CodeSource). Dále je tu ještě metoda implies(), která zjišťuje, zda tato politika poskytuje určité povolení pro danou bezpečnostní doménu. Soubor bezpečnostní politiky I když politiku lze uložit obecně jakkoliv, nejčastěji se používá soubor, proto se na něj nyní podíváme. Platí zde zásadní pravidlo, že implicitně není povoleno nic a my tedy musíme zvolit, co všechno povolíme. Syntaxe souboru je poněkud složitější, podíváme se tedy jen na základní věci zájemci o hlubší proniknutí do této problematiky nechť nahlédnou do bezpečnostní příručky Javy. První příklad ukáže, jak povolit úplně všechno všem. Důrazně varuji před aplikací v praxi, jen chci ukázat, jak by se to udělalo: grant {
permission java.security.AllPermission; }; Třída AllPermission, jak známo, implikuje všechna povolení, proto vložení tohoto pravidla povolí všechny operace. Protože není uvedeno, koho se to týká, uplatní se pravidlo globálně. Tohle je ale pravým opakem stavu, který chceme dosáhnout - tedy aby měl kód méně práv. Podívejme se tedy na další příklad: grant codeBase "file:/home/user/trusted/*" { permission java.security.AllPermission; }; grant { permission java.io.FilePermission "read","/-"; permission java.io.FilePermission "read,write","/tmp/*"; }; Těmito dvěma pravidly říkáme, že kód pocházející z adresáře /home/user/trusted má povoleno všechno, kdežto všechen ostatní kód smí číst v celém filesystému a zapisovat pouze do adresáře /tmp. Nemusí se jednat o lokální kód - můžeme specifikovat i pravidla pro kód odjinud. Podobně i parametry nemusíme psát "natvrdo", ale lze používat vlastnosti systému: grant codeBase "http://www.linuxsoft.cz/*" { permission java.io.FilePermission "read,write,execute,delete",\ "${user.home}/-"; permission java.lang.RuntimePermission "queuePrintJob"; }; Uvedené pravidlo se vztahuje na kód pocházející ze serveru Linuxsoft. Zajišťuje povolení provádět veškeré souborové operace v domovském adresáři a jeho podadresářích, a povolení poslat úlohu do tiskové fronty (může to vypadat jako malichernost, ale komu nějaký vtipálek nechá vyplýtvat drahou inkoustovou náplň, nebude považovat takové omezení za zbytečnost). Uvedená pravidla mají jedno společné - rozlišují kód jen podle zdroje, a již nehodnotí jeho vlastní důvěryhodnost. To není příliš bezpečné, ale vrátíme se k tomu až poté, co zběžně projdeme problematikou podepisování kódu. Aktivace bezpečnostních omezení Všechna bezpečnostní pravidla jsou sice hezká, ale zatím se nemohla nijak projevit. Nemáme je totiž aktivována. Pokud je chceme uplatnit, je potřeba spouštět Javu trochu jinak, než jak jsme byli zvyklí. Vypadá to zhruba takto: java -Djava.security.manager Aplikace Toto spuštění zavede výchozího security managera, a aktivuje tím i definovanou bezpečnostní politiku. Můžete si to vyzkoušet na některé běžné aplikaci - uvidíte, co všechno nebude fungovat (kvůli tomu, že množina hlídaných operací je opravdu velká). Kromě výchozích souborů pro politiku můžete nějaký specifikovat i při spuštění. Pravidla v něm se připojí k těm definovaným ve výchozích souborech (můžeme tak udělit nějaké aplikaci větší práva), anebo je úplně nahradí. Třeba takto: java -Djava.security.manager -Djava.security.policy=./java.security Aplikace
java -Djava.security.manager -Djava.security.policy==./java.security Aplikace Oba příkazy se liší pouze tím, že v prvním případě se politika přidává, kdežto ve druhém nahrazuje. Podepisování kódu Zejména u kódu stahovaného ze vzdáleného serveru je vždy problém s tím, že nám může někdo podvrhnout nebezpečný kód, a to i v případech, kdy server sám patří důvěryhodné osobě. Může být však napaden útočníkem, může být též obětí pharmingu (podvržení DNS odpovědi), čímž se snadno do systému zavleče lecjaká havěť. Proto je žádoucí, aby byla bezpečnost zajištěna lepšími mechanismy - kam patří i podepisování kódu. Když je kód podepsaný a máme veřejný certifikát pro kontrolu podpisu, lze snadno zjistit, zda je podpis platný a zda do kódu někdo nezasahoval. Základní vlastnosti jsou tu stejné jako u jiného použití elektronického podpisu, zaměřme se tedy na to, jak to použít při zajištění bezpečného prostředí. V Javě je tato oblast dost široce pojata, ale nám teď stačí jen malá podmnožina. Již dříve jsem se zmínil o tom, že object CodeSource obsahuje nejen samotný zdroj, ale i certifikáty. Má to ten význam, že si v souboru konfigurace politiky nastavíme, že určitá povolení se budou poskytovat pouze kódu podepsanému určitou osobou. Viz příklad: grant signedBy "TrustedPerson" { permission java.security.AllPermission; }; grant codeBase "file:/-" signedby "osoba1,osoba2,osoba3" { permission java.io.FilePermission "read,write","/-"; }; První pravidlo říká, že kód podepsaný osobou (přesněji řečeno aliasem) nazvanou TrustedPerson bude mít povoleno úplně všechno. Druhé potom, že kód podepsaný uvedenými osobami, který pochází z místního filesystému, bude mít plný přístup k souborům. Manipulace s certifikáty Nyní samozřejmě vyvstává otázka, kde vezmeme certifikáty pro ověření podpisu. Java používá tzv. úložiště klíčů a certifikátů (keystore), kam se tato data ukládají - umístění je určeno v bezpečnostním konfiguračním souboru. Certifikáty samozřejmě musíme získat bezpečnou cestou to se netýká těch, které jsou podepsány důvěryhodnou certifikační autoritou, jejíž certifikát máme k dispozici. S certifikáty a klíči lze manipulovat dvěma způsoby. Prvním je konzolový program keytool. Tím lze provádět nejrůznější operace nad úložištěm - importovat a exportovat certifikáty a klíče, vytvářet páry klíč-certifikát, odstraňovat položky z úložiště apod. Zde je několik příkladů: keytool -import -alias TrustedPerson -file TrustedPerson.cer keytool -export -alias osoba -file osoba.cer keytool -list keytool -selfcert -alias lukas -keypass nejakeheslo\ -dname "cn=Lukas Jelinek, ou=, o=Linuxsoft, c=CZ" První příkaz importuje uvedený certifikát do úložiště (pokud úložiště neexistuje, vytvoří ho) a přiřadí mu alias. Druhý řádek naopak certifikát exportuje. Třetí potom vypíše informace o všech
uložených certifikátech. A konečně poslední vytváří certifikát podepsaný sám sebou - použije k tomu klíč uložený pod uvedeným aliasem. Druhou možností je použít grafický program policytool. Je už poměrně obstarožní, ale pro jednoduchou správu úložiště, a také pro práci s pravidly bezpečnostní politiky ho lze s výhodou použít. Program jarsigner Poslední věcí, na kterou se ještě podíváme, je podepisování kódu. Pro distribuci takového kódu je samozřejmě výhodné, máme-li klíč/certifikát podepsané důvěryhodnou certifikační autoritou, ale k podepisování jako takovému to není třeba. Použijeme k tomu program jarsigner, který je opět standardní součástí balíku JDK. Lze podepisovat dokonce i přímo v aplikaci, ale to je poněkud složitější činnost. jarsigner lze použít jak k podepsání JAR archivu, tak ke kontrole podpisu, máme-li příslušný certifikát. Zde jsou dva příklady: jarsigner -storepass heslo_uloziste -keypass heslo_klice balik.jar lukas jarsigner -verify balik.jar První příkaz podepíše archiv balik.jar pomocí klíče pro alias lukas. Je k tomu potřeba jak heslo úložiště, tak heslo soukromého klíče. Lze samozřejmě balík podepsat i vícekrát. Druhý příkaz pak podpis balíku ověřuje. Uvedené příklady pochopitelně pokrývají jen nepatrnou část možností jak uvedených programů, tak oblasti podepisování jako takové. Dokumentace Javy se tím zabývá velmi široce, ale k dobrému pochopení jsou nezbytné obecné znalosti problematiky elektronického podepisování. Změňme téma... Sice jsem měl původně poněkud jiné plány, ale na základě četných žádostí čtenářů bude všechno jinak. Příští díl a několik dalších věnuji grafice a grafickým uživatelským rozhraním. Doufám, že se kromě jiného podaří i vyvrátit různé mýty, které se okolo této javovské oblasti vynořují. Bylo by totiž škoda, aby by byla Java zbytečně odmítána, přestože toho má tolik co nabídnout. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1123 Java (24) - úvod do grafiky a GUI Aplikace s grafickým uživatelským rozhraním, případně jinak užívající grafiku, se v Javě tvoří s lehkostí a elegancí. Určitou daní za to jsou poněkud vyšší nároky na procesor a hlavně na paměť ty však lze vhodným návrhem a implementací podstatně omezit. Právě proto máme dobrý důvod tvořit a používat grafické aplikace v Javě, mají nám totiž skutečně co nabídnout. 24.4.2006 06:00 | Lukáš Jelínek | přečteno 67225× Krátký výlet do historie Když Java vznikala, její tvůrci ušili grafickou část velice horkou jehlou. Původní návrh (jak API, tak "střev" grafických balíků) byl hodně nedokonalý, neumožňoval snadné a systematické využití. Proto se již s JDK 1.0 vytvořila u vývojářů jistá averze vůči javovské grafice. V dalších verzích však docházelo k neustálému zlepšování, takže se Java brzy stala vhodnou platformou pro tvorbu grafických aplikací. Grafiku jako takovou a GUI nelze v Javě dost dobře oddělovat. Existují prostě pohromadě,
využívají stejné třídy. Proto je v tomto i dalších dílech seriálu budu chápat jako jediný celek. Na odlišnosti včas upozorním. AWT První implementací grafiky byla knihovna zvaná Abstract Windowing Toolkit (AWT). Objevila se hned na začátku (JDK 1.0) a její nepříjemné a nesystematické API (byť s označením deprecated) přetrvává v Javě dodnes. Něco se stále používá, ale jsou to spíš věci související s obecnou grafikou, a nikoli s GUI. V dalších verzích bylo sice API přepracováno a podstatně rozšířeno, ale jiné nevýhodné vlastnosti přetrvaly. Filosofie AWT byla taková, že každá komponenta v Javě měla svůj nativní protějšek v systému. Byl to tedy podobný model, jako dnes používají některé GUI platformy v C/C++. I když se jednalo o abstraktní (platformově nezávislé) rozhraní, přenositelnost byla problematická, vzhledem k rozdílným vlastnostem nativní úrovně GUI. Swing Od verze 1.2 máme v Javě novou grafickou knihovnu zvanou Java Foundation Classes (JFC), známou spíše pod názvem Swing (dále již budu používat jen toto označení). Původní koncept byl zavržen, Swing byl vytvořen prakticky kompletně na zelené louce (i když v sobě obsahuje AWT API). Základní vlastnosti lze shrnout do těchto bodů: Kompletní implementace v Javě. GUI není napasováno na nativní komponenty, vše je zcela platformově nezávislé. Intenzivní využití dědičnosti a kompozice. Komponenty GUI využívají objektové vlastnosti Javy, složitější součásti jsou složeny či zděděny z jednodušších. Výrazné využití rozhraní. Různé operace v komponentách GUI (kreslení, editace apod.) se provádějí přes rozhraní. Standardní implementace lze nahrazovat vlastními a měnit tak chování komponent. Důsledné oddělení funkcionality od vzhledu GUI (look & feel). Témat vzhledu je několik k dispozici přímo ve standardní knihovně, další si lze vytvořit a použít. Oddělení dat od objektů GUI u složitějších komponent (strom, tabulka) pomocí tzv. modelů. To opět zlepšuje možnosti přizpůsobení a také opakovanou použitelnost komponent. Uvedené vlastnosti, pro programátory velice příjemné, mají druhou stránku věci - relativně vysoké nároky na rychlost procesoru a hlavně na paměť. V reakci na to vznikly grafické knihovny, které se snaží tento problém odstraňovat. Nejvýznamější z nich je SWT. SWT SWT (the Standard Widget Toolkit) je GUI knihovna s podobnými vlastnostmi, jako má Swing, ovšem s nativní implementací grafických komponent. Kdo používá IDE Eclipse nebo nějakou aplikaci na této platformě postavenou, s SWT se už setkal. Proto též zajisté zjistil, že původní cíl vzniku, tedy snížení procesorové a paměťové náročnosti, příliš naplněn nebyl a srovnatelné aplikace postavené na SWT jsou zhruba stejně náročné jako ty používající Swing. V čem tvořit? Navrhovat a tvořit GUI pro javovské programy lze samozřejmě i bez nějakých speciálních programů - návrh se připraví na papíře a příslušný kód se vytváří přímo, bez pomocných nástrojů. Není to sice nijak zvlášť těžké a mnoho vývojářů tak běžně pracuje (já občas také), ale pohodlná cesta to není. Pomocnou ruku nám podají různá vývojová prostředí (IDE), která obsahují kromě jiného i prostředky pro návrh GUI. NetBeans Po dřívějších neslavných výsledcích (z NB 3.x jsme leckdy získali nepříliš fungující GUI) se návrh
GUI v tomto IDE velice zlepšil. Zejména s poslední verzí se pracuje výborně. V grafickém prostředí si lze vše snadno sestavit, kostra kódu se vygeneruje automaticky a implementaci dopíšeme. Generované části kódu jsou chráněny proti úpravám - to je současně výhoda i nevýhoda, ostatně na to každý brzy přijde. JBuilder Nejnovější verze (2006) umí vynechat fázi generování kódu z návrhu GUI a vytvořit místo toho přímo hotové třídy. To je výhodné pro rutinní práci, pro cvičné účely je ovšem mnohem lepší nechat si kód vygenerovat. Práce s JBuilderem je o něco snazší než s NetBeans (více podpůrných funkcí pro tvorbu GUI), ale podle mého názoru je tento program (právě z tohoto důvodu) pro začátečníky méně vhodný. Ať už si zvolíte ten či onen program (existuje ještě řada dalších), vždy mějte v počítači dostatek fyzické paměti. Rychlost procesoru není tak podstatná, ale na paměti opravdu záleží. Principy javovské grafiky a GUI V celém následujícím výkladu (v této i dalších kapitolách) budu vše vztahovat zásadně k technologii Swing. Jsou k tomu dobré důvody: Swing je součástí standardní instalace Javy. Velká platformová nezávislost a přenositelnost. Kvalitně navržené a systematické API. Větší počet existujících aplikací - více možností k učení se ze zdrojových kódů. Samozřejmě, kdo se dobře naučí Swing, nebude mít problém přejít na SWT, případně na některou jinou implementaci GUI - hlavní vlastnosti se příliš neliší. Taktéž se zde hodí znalosti s programováním GUI v jiném jazyce (např. C++ a knihovny Qt). Principy fungování GUI Jak už je v moderních GUI zvykem, fungování aplikací je řízené událostmi. Na programátorovi je, aby vyřešil reakce na tyto události. Ve Swingu to funguje tak, že je zde smyčka obsluhující frontu asynchronních událostí (ty přicházejí z okenního systému, např. X11), a odtud se volá obsluha těchto událostí. Obslužné rutiny většinou volají další reakční metody vyšší úrovně, až se nakonec obsluha události dostane do aplikace ke konečnému zpracování. Pokud někdo např. najede myší na tlačítko, do fronty událostí se přidá událost o pohybu myši. V obslužné smyčce se událost odebere a zavolá se metoda pro její obsluhu. Tam se zjistí, že se kurzor myši dostal nad tlačítko, proto se vytvoří nové události (pro pohyb myši a pro najetí do prostoru) a ty se "pošlou" příslušnému tlačítku. Tlačítko každou z událostí zpracuje a nějak na ně zareaguje - a potom vrátí řízení zpět, a tak se vše postupně dostane zase až do obslužné smyčky. Detaily teď neuvádím, dostaneme se k nim později. Co nás na tom hlavně zajímá? Obsluha každé události funguje tak, že máme rozhraní pro reakci na událost, a toto rozhraní se nastavuje ve zdroji příslušné události. Většinou může být více implementací rozhraní (a tedy více reakcí) současně na stejnou událost, metody se zavolají postupně. Jak se kreslí Nebyla by to grafika, kdybychom nemohli nic nakreslit. Základní principy opět nejsou nic neobvyklého. Ke kreslení používáme tzv. grafický kontext, což je nějaká implementace abstraktního kreslicího rozhraní. O jakou konkrétní implementaci se jedná, nás často vůbec nezajímá - proto lze naprosto stejně kreslit na obrazovku, do paměti nebo na tiskárnu.
Při kreslení můžeme využívat širokou škálu nástrojů, které jsou ve standardní knihovně k dispozici. Je dobré je používat do té míry, jak je to jen možné, protože ve vztahu ke grafickému kontextu většinou poskytují maximální možnou optimalizaci. Zásady pro tvorbu GUI Než přejdeme k praxi, dovolte mi ještě připomenout několik důležitých zásad, bez kterých nelze rozumně programovat GUI v Javě. Opomenutí pak často vede k právě takovým programům, kvůli kterým mnozí lidé Javu zavrhují. Nevytvářet zbytečně nové instance, vytvořené používat opakovaně. Zejména se to týká polí. Vytváření a rušení objektů je jednak dost náročné, a hlavně se rychle zaplňuje paměť, kterou garbage collector uvolňuje jen zvolna. Nic by se nemělo zbytečně překreslovat. Využívejte optimalizační možnosti a nekreslete to, co určitě není vidět. Mnoho věcí si lze také připravit předem a pak najednou zobrazit, namísto ustavičného překreslování. Do objektů grafiky a GUI se smí sahat jen z jednoho jediného vlákna, zvaného event-dispatching thread. Je to to vlákno, které obsluhuje frontu událostí. Pokud potřebujete něco provést z jiného vlákna, jsou na to metody invokeLater() a invokeAndWait(), ještě o nich bude řeč. Je to proto, že mnohé grafické třídy neobsahují (z výkonnostních důvodů) žádnou synchronizaci. Neblokujte event-dispatching thread výpočetními operacemi nebo třeba čekáním na data ze sítě, výkonný kód umístěte do jiných vláken (i když to znamená dost práce navíc). Nemělo by se stát, že aplikace kvůli nějaké zdlouhavé operaci zatuhne na dobu delší než stovky milisekund - uživatelé bývají velice netrpěliví. Pokud to jde, používejte pro obsluhu událostí normální (opakovaně používané) pojmenované třídy namísto "chrlení" tříd anonymních, k čemuž někdy svádí vývojová prostředí. Důsledně se vyhýbejte zavrženým (deprecated) metodám, i když se chovají úplně stejně jako metody modernější. Používání zastaralých metod znepřehledňuje kód (důvodem k zavržení je často právě nevhodné pojmenování, vzniklé v dobách JDK 1.0) a hlavně může v budoucnu způsobit nefunkčnost programu, když budou příslušné metody odstraněny. Ošetřujte výjimky co nejblíže místu vzniku, ale ne ve větším rozsahu, než je nutné. Nezachycené asynchronní výjimky totiž nezpůsobí ukončení programu, budou odchyceny v hlavní smyčce fronty událostí. Proto také při podezřelém chování programu s GUI vždy nahlédněte do standardního chybového výstupu, zda tam není informace o výjimce (včetně výpisu zásobníku volání). Snažte se maximálně oddělovat data od GUI, ale současně zachovávejte logickou konzistenci. Tam, kde se používají modely, je to snadné. V ostatních případech (seznamy apod.) se s výhodou používá metoda toString(), která poskytne textovou reprezentaci nějakého objektu a umožní pracovat s referencemi na data. Potom je např. položka v seznamu spjata s konkrétní instancí datového objektu a nemusí se nikam nic kopírovat. U složitějších grafických struktur (stromy) zobrazujících velké množství dat zvažte, zda je v daném případě lepší vybudovat celou strukturu na začátku, anebo ji budovat podle činnosti uživatele. Mějte na paměti, že zavřené okno se nerovná zrušené okno. Pokud okno nezrušíte, bude existovat dál, se všemi výhodami a nevýhodami. Bude-li nějaký program tvrdošíjně odolávat pokusům o ukončení, možná někde zůstává zavřené nezrušené okno. Zruší-li se všechna okna a program už nemá nic jiného na práci, skončí. První grafická aplikace Tak to byla šedá a nudná teorie, proto se pusťme do první aplikace s GUI. Bude obsahovat pouze jediné okno pevné velikosti, a na něm bude tlačítko Konec. Po stisknutí tlačítka se - pochopitelně aplikace ukončí. Vytvoříme si třídu MyDlg, která bude odvozena od třídy javax.swing.JDialog (třída pro dialogová okna). public class MyDlg extends JDialog implements ActionListener { private JButton but = new JButton("Konec");
Deklarujeme tlačítko pro umístění na panel, hned ho též inicializujeme. Šlo by použít i jinou cestu, nedeklarovat členskou proměnnou a tlačítko vytvořit až později. To by byl ovšem tento objekt později obtížně přístupný, proto je zvykem ve většině případů grafické objekty deklarovat v rámci třídy. Všimněte si implementovaného rozhraní - umožňuje zpracovávat události z tlačítka. public MyDlg() { super(); setSize(200, 200); setLocation(100, 100); setResizable(false); setModal(true); setTitle("První GUI"); but.setSize(80, 30); but.setLocation(60, 85); getContentPane().setLayout(null); getContentPane().add(but); but.addActionListener(this); setDefaultCloseOperation(DISPOSE_ON_CLOSE); } V konstruktoru zavoláme konstruktor rodičovské třídy - u tříd tohoto druhu to děláme vždy, pokud nemáme dobrý důvod tak neučinit. Pak se nastaví rozměry dialogu a jeho pozice na obrazovce, rozměry a relativní pozice tlačítka. Zakážeme změnu velikosti a určíme, že dialog má být modální (tedy že dokud je otevřen, komunikuje se s uživatelem pouze přes něj; zde je to nutnost, protože jinak by program hned skončil, nemáme žádné hlavní aplikační okno). Další dva řádky vypnou správce rozložení (layout manager) a vloží tlačítko na plochu dialogu. Tím je hotovo sestavení dialogu, nyní je potřeba nastavit zpracování událostí od tlačítka. Než se k tomu ale dostaneme, upozorním ještě na poslední řádek. Dialog má totiž zavírací tlačítko okna - a to ve výchozím stavu nedělá vůbec nic. Protože uživatel logicky očekává, že se mu po stisku okno zavře, musíme nastavit výchozí zavírací operaci, a tou bude samozřejmě zrušení okna (konstanta DISPOSE_ON_CLOSE). public void actionPerformed(ActionEvent e) { dispose(); } Protože události od tlačíka budeme zpracovávat přímo ve třídě MyDlg, je nezbytné implementovat rozhraní java.awt.event.ActionListener a tedy vytvořit příslušnou metodu actionPerformed. Obsahuje jediné volání - zrušení dialogu. Máme tedy hotovo vše, co je potřeba k funkci aplikace. Tedy až na hlavní metodu. V ní se pouze vytvoří instance naší třídy a dialog se zobrazí. V metodě vidíte, že manipulace s GUI je přenechána event-dispatching threadu. public static void main(String args[]) { SwingUtilities.invokeLater(new Runnable() { public void run() {
MyDlg dlg = new MyDlg(); dlg.setVisible(true); } }); } Potřebné importy (a také uzavírací závorku třídy) jsem neuvedl, pro fungování programu je samozřejmě nezbytné je doplnit, případně pracovat s úplnou specifikací tříd. Po kompilaci hotový program obvyklým způsobem spustíme: java MyDlg. Měl by se zobrazit dialog s tlačítkem. Po stisku tlačítka (nebo uzavíracího tlačítka) okna se dialog zavře a program skončí. Stavební kameny I když z uvedeného příkladu jednoduchého GUI programu je zcela zřejmé, že nejde o nic těžkého, přesto se vyplatí dobře poznat některé třídy a rozhraní. Příští díl bych chtěl věnovat těmto základním "stavebním kamenů" a jejich použití. O praktické příklady nebude nouze. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1184 Java (25) - základní grafické třídy Při vývoji GUI pro javovské aplikace se vyplatí dobře znát některé základní třídy. I v případě, kdy za nás "černou práci" dělá grafický návrhář v IDE, pomůže znalost těchto tříd výrazně zefektivnit vývoj a snadno nalézat případné chyby. 8.6.2006 09:00 | Lukáš Jelínek | přečteno 36944× Bez čeho se nelze obejít Přehled důležitých tříd začnu tím úplně nejzákladnějším, co bude každého doprovázet po celou dobu vývoje GUI a grafiky obecně. Dobrá práce s těmito třídami je první nutnou (nikoli postačující) podmínkou k efektivnímu vývoji kvalitních aplikací. Měříme Ať už pracujeme s jakýmkoli grafickým objektem, často potřebujeme vědět, jak je daný objekt velký a kde se nachází. Proto potřebujeme rozumný způsob, jak s těmito informacemi pracovat. Začneme s rozměry. Máme abstraktní třídu Dimension2D, která pracuje s rozměry naprosto obecně, a proto používá datový typ double. To sice budeme později potřebovat (a ještě se k tomu vrátíme), ale při práci s GUI nám obvykle postačí celočíselné hodnoty, protože pracujeme s celými pixely. A k tomu slouží třída Dimension, která je potomkem zmíněné abstraktní třídy. Třída neobsahuje (po datové stránce) nic jiného, než právě tyto celočíselné rozměry. Instance třídy vrací některé metody, nejčastěji využijeme getSize() definovanou již v java.awt.Component (a všech potomcích, kam patří i naprostá většina tříd frameworku Swing). Podívejme se na příklad: JLabel lab = new JLabel("text"); ... Dimension d = lab.getSize(); ... d = lab.getSize(d); V příkladu se nejprve vytvoří instance objektu JLabel (popisek), s ní se provedou nějaké operace a pak se zjišťuje jeho velikost. Metoda vytvoří novou instanci třídy Dimension a tu vrátí. V dalším
případě ovšem metodě předáme již vytvořenou instanci - ušetříme tím zbytečné volání konstruktoru objektu a tím jak alokaci další paměti, tak i čas procesoru. Při častém volání se to může výrazně projevit, proto vždy, když to jde, opakovaně využíváme vytvořený objekt. Když už jsme u toho šetření, každý grafický objekt má také metody getWidth() a getHeight() pokud potřebujeme pouze jeden rozměr, namísto getSize() voláme raději tyto metody (vrací přímo hodnotu jako typ int). Podobně při nastavování rozměrů volíme přednostně setSize(int, int) oproti setSize(Dimension), pokud máme k dispozici jednotlivé hodnoty. Ještě připomenu, že hodnoty rozměrů mohou být i záporné (nekontrolují se), ale chování objektů se zápornými rozměry obvykle není definováno. U pozice objektů je to podobné. Je tu abstraktní třída Point2D, která má kromě potomka Point také vnořené implementace Point2D.Float a Point2D.Double (opět ponechávám na později). Záporné hodnoty už zde ale samozřejmě mají smysl a lze je normálně používat. Krátký příklad: Dimension d = new Dimension(100, 20); JButton but1 = new JButton("button 1"); JButton but2 = new JButton("button 2"); ... but1.setSize(d); but2.setSize(d); ... Příklad ukazuje použití třídy Dimension pro nastavení určitých rozměrů většímu počtu objektů. Mohli bychom samozřejmě pracovat i s jednotlivými rozměry, ale v tomto případě by se většinou mnoho neušetřilo. Třídy Dimension a Point najdeme v balíku java.awt, zmíněné abstraktní třídy pak v balíku java.awt.geom. Události Při obsluze událostí získáme v metodě vždy objekt představující příslušnou událost. Ten nám poskytuje cenné informace o tom, co, kde a jak se stalo. Všechny třídy pro události vycházejí z jedné společné, a to java.util.EventObject. Tato nejvyšší třída má jen jednu specifickou metodu getSource(). Ta vrací objekt, kde se daná událost stala. Pokud např. stiskneme tlačítko, vytvoří se událost, kde bude zdrojem toto tlačítko. Většina grafických událostí je odvozena od třídy AWTEvent, potomka výše uvedené třídy. Zde nás zajímá především metoda setSource(). Umožňuje změnit zdroj události, což se hodí např. při generování událostí ve složených grafických komponentách. Událost postupně "probublává" skrz jednotlivé objekty až k tomu zastřešujícímu a cestou se její zdroj mění. Příjemci události se pak jeví, jako kdyby událost vygenerovala tato komponenta, přestože to bylo úplně jinak. Nyní se ještě zmíním o jedné specifické třídě, která se často používá. Je to ActionEvent (pro "akce", např. stisk tlačítka). Nejzajímavější metodou je getActionCommand(), umožňující rozlišovat události podle příkazu, který je spjat s generujícím objektem. Můžeme tak rozlišovat události podle příkazů, i když pocházejí z různých zdrojů nebo může tentýž zdroj (podle svého stavu) generovat různé události. Viz příklad: JButton but1 = new JButton("Konec"); but1.setActionCommand("quit");
JMenuItem item1 = new JMenuItem("Konec"); item1.setActionCommand("quit"); ... public void actionPerformed(ActionEvent e) { if (e.getActionCommand().equals("quit")) { ... } } Uvedený příklad ukazuje, jak by to vypadalo, kdyby se stejná akce generovala tlačítkem a položkou v menu. Rozlišovalo by se jen a pouze podle řetězce příkazu. "Bedny nástrojů" Začneme třídou, která se objevila již v minulém dílu seriálu. Ze třídy SwingUtilities jsme použili metodu invokeLater(), která zajistí vykonání kódu ve "správném" vlákně. Jenže tato třída obsahuje spoustu dalších metod, které výrazně usnadňují některé operace. Jsou tu např. metody na přepočet souřadných systémů (convertPoint(), convertPointFromScreen(), convertPointToScreen() atd.), operace s obdélníkovými oblastmi (calculateInnerArea(), computeDifference(), computeIntersection() ad.), výpočet rozměrů textu (computeStringWidth()), zkoumání kompozitních grafických komponent (getRoot(), getDeepestComponentAt()) a různé další. Jsou mezi nimi i speciální operace (jako např. paintComponent()), které při nesprávném použití dokáží nadělat pěknou paseku. Před používáním metod třídy SwingUtilities doporučuji důkladné přečtení dokumentace - ale rozhodně tyto metody nezavrhujte, jsou velice užitečné. Zajímavé a důležité je rozhraní SwingConstants. Implementuje ho celá řada swingovských tříd a obsahem tohoto rozhraní jsou konstanty pro určení polohy a orientace komponent. Rozhraní mimochodem porušuje důležitou zásadu - do rozhraní by neměla patřit žádná data (ani konstanty). Nicméně někdo to takto - zřejmě z pohodlnosti - navrhl, proto jeho existenci berme jako anomálii, ze které bychom si ale neměli brát příklad. Grafický kontext Pojem grafického kontextu úzce souvisí hlavně se samotným kreslením, ale i v GUI je velice důležitý - hlavně v případě, kdy tvoříme nějaké vlastní komponenty. Lze si ho představit jako abstraktní kreslítko, kdy sice nevíme, jak a kam se kreslí, ale zajímá nás, co se kreslí. Tedy například kružnice, obdélník, rastrový obrázek nebo text. Obvykle se setkáme s třídou Graphics (i když se většinou jedná o instanci jejího potomka Graphics2D), poskytující celou řadu metod pro kreslení. Instance tříd grafického kontextu běžně nevytváříme (kromě dvou případů; buď si od existujícího kontextu odvozujeme nový, s jiným počátkem souřadnic a jinou ořezovou oblastí, anebo tehdy, potřebujeme-li kontext zkopírovat kvůli ochraně před nežádoucími změnami), pouze je používáme ke kreslení. Jak takové kreslení vypadá, ukazuje příklad. Opět se nejedná o nic složitého, použití metod je velice snadné: Graphics g = ... // odněkud získáme grafický kontext g.setColor(new Color(100, 100, 100)); // tmavě šedá barva g.fillRect(0, 0, 200, 100); // šedý vyplněný obdélník g.setColor(Color.RED); // nastavíme červenou barvu
g.drawOval(10, 10, 180, 80); // červená elipsa Z příkladu jsou zřejmé i základy práce s barvami (zde je v jednom případě použita barevná konstanta, ve druhém vytvoření barvy ze složek RGB). Blíže se na barvy podíváme později. Grafické komponenty Nyní se zaměříme na třídu javax.swing.JComponent. Z ní jsou totiž odvozeny téměř všechny třídy grafických komponent, které se v prostředí Swing používají. Všimněte si, že třída JComponent je odvozena od třídy java.awt.Container, a proto dědí její metody (a metody nadtříd), kterých je požehnaně. Některé z nich jsou už zastaralé a u nových programů se nepoužívají - ale i tak jich zbývá dost. Nejprve to nejjednodušší. S některými metodami jsme pracovali již minule. Máme tu dvojice setVisible()/isVisible(), setEnabled()/isEnabled() a známé metody na práci s pozicí a rozměry (viz výše). Uvedu velice krátký příklad, není to žádná velká věda: JComponent c = ... // odněkud získáme komponentu if (c.isVisible()) // pokud je komponenta viditelná, c.setEnabled(false); // deaktivujeme ji Snad je vše dostatečně zřejmé. Jen připomenu, že neaktivní komponenta nepřijímá žádné uživatelské vstupy. Ještě by se asi slušelo zmínit, že až na výjimky (na které později zvlášť upozorním) jsou ve výchozím stavu všechny komponenty aktivní a viditelné (zobrazované). Kreslení a tisk Na kreslení se teď podíváme z té druhé strany - teď už nebudeme tedy přímo kreslit, ale zajímá nás, jak to vypadá zvenku. Těžiště spočívá v metodě paint(), kterou třída JComponent obsahuje. Tuto metodu GUI volá k překreslení komponenty. Standardní implementace volá metody paintComponent(), paintBorder() a paintChildren() - nejprve se tedy překreslí komponenta samotná, pak okraje a nakonec potomci (myšleno komponenty vložené uvnitř). Pokud potřebujeme v komponentě kreslit něco vlastního, máme několik možností. První je předefinovat metodu paint() - v ní dostaneme k dispozici grafický kontext a můžeme kreslit dle libosti. Tento postup asi použijeme nejčastěji. Příklad ukazuje, jak to vypadá: public void paint(Graphics g) { Dimension d = getSize(); g.setColor(Color.LIGHT_GRAY); g.fill3DRect(0, 0, d.width, d.height, true); g.setColor(Color.BLACK); String s = ... // odněkud získáme řetězec Rectangle2D r = g.getFontMetrics().getStringBounds(s, g); g.drawString(s, (int) ((d.width - r.getWidth()) / 2), (int) ((d.height - r.getHeight()) / 2)); } V příkladu se nejprve nakreslí "vystouplý" obdélník. Pak se na něj umístí text obsažený v odněkud získaném řetězci. Text je vodorovně i svisle vycentrován (centruje se opsaný obdélník, proto může text v některých případech vypadat podivně) - uvedené řešení je jen jedno z možných, lze to provést i jinak. Uvedené řešení také nijak nepočítá s okraji.
Druhou možností je předefinovat paintComponent(). Ve výchozí implementaci tato metoda, pokud existuje definice vzhledu UI (budeme se tomu věnovat v některé z dalších kapitol), volá metodu UI, na níž se kreslení deleguje. Pokud takovou delegaci nebudeme používat nebo potřebujeme něco nakreslit nezávisle, můžeme metodu předefinovat. Pozor však, abychom po skončení kreslení neponechali grafický kontext modifikovaný - mělo by to dopady na kreslení okrajů a případných potomků. Proto buď (po změnách) vrátíme kontext do původního stavu, nebo na něj vůbec nesaháme a pracujeme s kopií (metoda create() třídy Graphics). I když v naprosté většině případů nemáme žádný důvod explicitně vyžadovat překreslení nějaké komponenty, výjimečně taková potřeba může nastat. K tomu slouží dvě metody - repaint() a revalidate(). Liší se tím, že zatímco první vyvolá pouze překreslení komponenty jako takové (v podstatě naplánuje zavolání metody paint()), druhá způsobí aktualizaci zobrazení od kořene stromu komponent (nejbližší nadřazená komponenta, pro kterou zavolání metody isValidateRoot() vrací true) a případné překreslení všech komponent, které to potřebují. Obě metody lze volat z libovolného vlákna (omezení, o kterých jsem se zmiňoval, zde tedy neplatí) a s nutností jejich použití se setkáme skutečně jen výjimečně. Ještě pár slov k tisku. Komponenty mají metody print(), printAll(), printComponent(), printBorder() a printChildren(). Výchozí implementace funguje tak, že jednotlivé metody volají své paintXXX() protějšky a tisková podoba GUI je tedy shodná s tou, jakou vidíme na obrazovce. V případě potřeby můžeme metody předefinovat. To se ale netýká metod print() a printAll(), které by se měnit neměly - k jejich použití se dostaneme později, v souvislosti s problematikou tisku. Rozměry Kromě aktuálních rozměrů komponenty, se kterými se pracuje pomocí setSize() a dalších metod, tu máme ještě trojici dalších parametrů - minimální, maximální a preferovanou velikost. Tyto hodnoty mají smysl při používání tzv. správců rozložení (layout managers), které podle nich umísťují komponenty. K práci s těmito rozměry slouží metody setMinimumSize(), setMaximumSize() a setPreferredSize(), resp. jejich varianty getXXX(). Všechny pracují pouze s objektem třídy Dimension, používání primitivního typu int není možné. Až se zanedlouho dostaneme k layout managerům, uvedu příklad i pro tyto parametry. Zatím bych to ponechal pouze na této teoretické úrovni. Události Když jsem popisoval mechanismus zpracování událostí, možná někoho napadlo, jak se takové události generují. Nepočítám-li nízkoúrovňové události od okenního systému, generují většinu událostí přímo grafické komponenty - a také zajišťují distribuci přihlášeným odběratelům. Často poskytují přímo mechanismus, jak lze událost vygenerovat prostým zavoláním metody s patřičnými parametry. Tyto metody se snadno poznají podle toho, že začínají fireXXX(). Např. JComponent disponuje metodami firePropertyChange() a fireVetoableChange(). Podobně mohou jiné třídy poskytovat různé jiné metody tohoto typu. Metody se volají většinou v potomcích třídy (výjimečně i z jiných tříd) k vygenerování události. Např. zmíněná (přetížená) metoda firePropertyChange() se zavolá poté, co se v kódu změnila nějaká pojmenovaná vlastnost. Podobně jsou třeba v tabulce metody pro generování událostí při změnách v buňkách, přidání nebo odebrání řádků a podobně. Příklad opět odložím na později, až to bude mít větší význam. Časovač Poslední třídou, na kterou se dnes dostane, je třída pro časovač - javax.swing.Timer. Hodí se pro všechny časově řízené operace v rámci GUI. Kromě tohoto časovače máme ještě druhý, java.util.Timer. Ten však při "alarmu" vytvoří nové vlákno, čímž se jeho použití v GUI stává
komplikovanější. Proto pro GUI použijeme vždy javax.swing.Timer, který vše provádí v eventdispatching threadu. Časovač pracuje tak, že v okamžiku vypršení prodlevy (nastal čas něco provést) přidá do fronty patřičnou událost (v aktuální implementaci stejným způsobem, jako kdybychom zavolali invokeLater()), která nakonec vyústí v instanci třídy ActionEvent. Na časovač lze "navěsit" libovolný počet odběratelů této události. Jak se s časovačem pracuje, ukazuje příklad: class MyLabel extends JLabel implements ActionListener { javax.swing.Timer t = null; public MyLabel() { super(); t = new javax.swing.Timer(10000, this); t.start(); } public void actionPerformed(ActionEvent e) { setText(""); } } Jedná se o rozšíření třídy JLabel o zvláštní vlastnost - každých 10 sekund se obsah vymaže. K implementaci není co dodat, snad je vše dostatečně zřejmé. Jen bych doporučil u třídy Timer uvádět úplnou specifikaci, aby třeba někdy později nedošlo k případné kolizi se stejnojmennou třídou z balíku java.util. Z čeho stavět? Tak to by bylo z oblasti "základních stavebních kamenů" všechno. Příště nás čekají složitější třídy frameworku Swing. Budeme z nich stavět složitější celky, podíváme se taktéž na některá úskalí, která tato oblast skýtá. Je to problematika velice zajímavá, proto doufám, že nejsem sám, kdo se na to těší. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1234 Java (26) - tvorba GUI Při návrhu a implementaci grafického uživatelského rozhraní se lze vydat dvěma hlavními cestami: buď tvořit ručně (přímým psaním kódu), nebo využít grafické návrhové prostředí. Na obě se nyní podíváme, samozřejmě i včetně kombinace obou přístupů. Při tom se dostanou ke slovu některé více či méně zajímavé objektové třídy. 24.7.2006 09:00 | Lukáš Jelínek | přečteno 48781× Okna, dialogy, rámy Prvky grafického rozhraní obvykle neplavou "ve vzduchu" (tedy jen tak na ploše obrazovky), i když to v některých případech bývá. Mnohem častěji je umísťujeme do různých oken. Před návrhem GUI nějaké aplikace se vyplatí dobře znát vlastnosti základních druhů oken, se kterými se ve frameworku Swing pracuje. Okno Základním prvkem je "obyčejné" okno, reprezentované třídou JWindow. Nemá žádný rámeček, titulkovou lištu ani ovládací tlačítka. Přímo ho využijeme málokdy, uvádím ho hlavně proto, že má
většinu vlastností, které souvisejí s vkládáním komponent GUI. Okno je přímo navázáno na nativní objekt (např. v X Window Systemu). Každé okno obsahuje určité plochy, které mají svůj význam pro fungování okna a s každou z nich se pracuje specifickým způsobem: Kořenová plocha (rootPane) - instance třídy JRootPane, obsahuje další plochy, žádný zvláštní význam nemá. "Skleněná" plocha (glassPane) - překrývá shora celou oblast okna, umožňuje sledovat pohyb kurzoru myši přes okno. Může být tvořena prakticky libovolnou grafickou komponentou, lze do ní tedy i kreslit. Za normálních okolností je skrytá. "Vrstvená" plocha (layeredPane) - reprezentována instancí JLayeredPane. Má důležitý význam, a to hloubkové (souřadnice z) uspořádání komponent v různých situacích. Týká se to např. plovoucích panelů, vyskakovacích oken nebo přetahování komponent. Obsahová plocha (contentPane) - sem se vkládají běžné komponenty GUI. S touto plochou jsme se již setkali v příkladu na jednoduchý program s GUI. Tato plocha je vložena do vrstvené plochy a opět ji může tvořit jakákoli komponenta. Nabídková lišta (menuBar) - tvořena instancí JMenuBar. Používá se jen v případě potřeby. Je součástí vrstvené plochy. Zdaleka nejčastěji pracujeme s obsahovou plochou. Buď použijeme tu, která je v okně již obsažena (ale pak předem neznáme její vlastnosti, resp. ani třídu), nebo nastavíme komponentu vlastní. Vhodnými kandidáty bývají třídy JPanel (pro okna pevné velikosti), JScrollPane, JSplitPane apod. Někdy využijeme také nabídkovou lištu. Práce s ní je triviální, ještě o tom bude řeč. Dialog Smysl této komponenty (představované třídou JDialog) je jasný - používá se pro různé dialogy a pevná okna. Dialog má titulkovou lištu a ovládací tlačítka, může být modální. Podobně jako okno, i dialog je na obrazovce tvořen nativním grafickým objektem. Rám Pod tímto nepříliš výstižným termínem se skrývá okno s proměnnou velikostí. Opět je to nativní objekt okenního systému. Ve Swingu ho reprezentuje třída JFrame. Vnitřní rám Od předchozího se liší tím, že je to čistě objekt Swingu (není reprezentován nativně) a může existovat jen uvnitř jiné swingové komponenty. Typicky se používá pro dokumentová okna v MDI programech. Jedná se o třídu JInternalFrame. Applet Uvádím ho jen pro úplnost. Applet bývá umístěn na webové stránce a plní podobné úkoly jako běžné okno. Třída má název JApplet. Sestavení GUI aplikace Vytváříme-li GUI ručně (bez pomoci grafického návrháře ve vývojovém prostředí), je dobré si vše předem nakreslit na papír, nejlépe milimetrový. Takto si připravíme rozvržení aplikace a pak už jen implementujeme chování grafických komponent. Pokud nepotřebujeme pracovat s pevným rozmístěním komponent (a používáme správce rozložení, layout managery - bude o nich řeč v jednom z příštích dílů), je to ještě jednodušší a ruční práce je velice efektivní. Můžeme přímo používat již existující třídy nebo si od nich vytvářet potomky - druhá možnost je lepší v případech, kdy potřebujeme nějak zásadněji změnit chování některé komponenty nebo tehdy,
chceme-li nějakou upravenou komponentu používat opakovaně. Příklad ruční tvorby Pusťme se nyní do tvorby aplikace. Na příkladu bude nejlépe vidět, jak se dá s GUI pracovat a jak to celé funguje. Budeme vytvářet jednoduchý textový editor - bude umět založit nový text, otevřít existující soubor a uložit data do zvoleného souboru. Kdo by chtěl nějakou funkci navíc (např. udržování názvu souboru v programu, automatické ukládání, dotaz na zahození neuložených dat, automatické zalamování řádků a podobně), jistě snadno přijde na to, jak to udělat. Třída editoru bude odvozena od třídy JFrame, o níž byla řeč výše. Lze to samozřejmě udělat i v samostatné třídě a do JFrame nesahat. Začněme tedy: public class Editor extends JFrame implements ActionListener { private JScrollPane sp = null; private JTextArea ta = null; private JMenuBar mb = null; private String sep = null; Deklarujeme členské proměnné hlavních komponent. Není to nutné, ale pro pozdější přístup se to hodí. Některé můžeme rovnou plně inicializovat, ovšem pro lepší orientaci to ponechám na později. Ještě upozorním na proměnnou sep, která bude obsahovat oddělovač řádků (brzy vysvětlím). Chvíli bych se zdržel u třídy JTextArea. Protože je ve Swingu důkladně využita hierarchie tříd, projevuje se to i zde. Máme abstraktní třídu JTextComponent, která obsahuje základní funkcionalitu pro práci s textem. Neřeší však implementační detaily, zejména způsob komunikace s uživatelem. Umožňuje jak primitivní práci s textem (jako je to i v tomto příkladu), tak možnost použít dokumentový aparát Swingu s mnohem rozsáhlejšími možnosti (složitější editace, undo/redo, logické členění apod.). JTextArea je jednou z konkrétních implementací JTextComponent, další je např. třída JTextField (jednořádkové textové pole), která má sama o sobě ještě další potomky (např. JPasswordField pro zadávání hesel). public Editor() { super(); init(); } Konstruktor je jednoduchý a volá nejprve konstruktor předka a potom inicializační metodu. Do té je vhodné umístit všechno, co se týká inicializace komponenty. Můžeme pak mít více konstruktorů a z každého tuto metodu volat. public void init() { sep = System.getProperty("line.separator"); ta = new JTextArea(); Font f = Font.decode("Monospaced"); if (f != null) ta.setFont(f); sp = new JScrollPane(ta, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
setContentPane(sp); První část inicializační metody nejprve nastaví již zmíněný oddělovač řádků do podoby, jaká odpovídá platformě (tedy na GNU/Linuxu to bude "\n") - oddělovač budeme potřebovat při načítání souboru. Pak se vytvoří komponenta pro editaci textu. Protože je vhodnější pracovat s neproporcionálním písmem, zkusíme ho nastavit (pokud se to nepovede, zůstává původní písmo). A konečně poslední část kódu vytvoří plochu s posuvníky (budou zobrazovány jen v případě potřeby) a textovou oblast do ní vloží. Plocha se pak nastaví jako obsahová plocha rámu. Protože nezasahujeme do nastavení správců rozložení, uplatní se výchozí stav, který nám zajistí, že velikost textové oblasti bude odpovídat textu uvnitř. JMenu menu= new JMenu("Soubor"); menu.setMnemonicv(KeyEvent.VK_S); JMenuItem mi = new JMenuItem("Nový", KeyEvent.VK_N); mi.setActionCommand("new"); mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, KeyEvent.CTRL_MASK)); mi.addActionListener(this); menu.add(mi); mi = new JMenuItem("Otevřít...", KeyEvent.VK_O); vmi.setActionCommand("open"); mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, KeyEvent.CTRL_MASK)); mi.addActionListener(this); menu.add(mi); mi = new JMenuItem("Uložit...", KeyEvent.VK_U); mi.setActionCommand("save"); mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_MASK)); mi.addActionListener(this); menu.add(mi); mi = new JMenuItem("Konec", KeyEvent.VK_K); mi.setActionCommand("quit"); mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, KeyEvent.CTRL_MASK)); mi.addActionListener(this); menu.add(mi); mb = new JMenuBar(); mb.add(menu); setJMenuBar(mb); Tento poněkud delší úsek inicializátoru se zabývá přípravou nabídkové lišty. Vytvoříme nabídku "Soubor" (žádné jiné nebudou), přidáme do ní potřebné položky pro různé operace, a na závěr vytvoříme samotnou lištu a vložíme do ní menu. Všimněte si několika věcí. Nabídka se bude otvírat zvolenou klávesou (samozřejmě v kombinaci s Alt), každá položka nabídky má svoji klávesu (která se použije, je-li nabídka otevřená) a také klávesovou zkratku použitelnou kdykoli. Optimální je, pokud si klávesa položky a aplikační klávesová zkratka odpovídají (pokud to lze), ale často je to docela problém (máme zažité zkratky, např. Ctrl-S pro uložení, a to se slovem "Uložit" moc dohromady nejde). Mohli bychom ještě např. nastavit bublinovou nápovědu položek
(setTooltipText()) apod. Opět malé zdržení - a to u třídy JMenuItem a jejích potomků. Tato třída reprezentuje položku v menu a je rozšířením třídy AbstractButton, podobně jako třeba JButton. Může mít pouhý text nebo i ikonu, ostatně jako každá implementace abstraktního tlačíka (AbstractButton). Potomkem třídy JMenuItem je i třída JMenu, což znamená, že pokud se do menu vloží jiné menu (namísto obyčejné položky), prostě se tím vytvoří další úroveň. Dále jsou tu také potomci JCheckBoxMenuItem a JRadioButtonMenuItem, představující zaškrtávátko, resp. přepínač v menu. Nejsme tedy omezeni na obyčejné položky, ale lze pracovat i s tímto. Občas je v menu potřeba oddělovač - můžeme buď vložit instanci třídy JSeparator nebo zavolat addSeparator(), obě cesty jsou rovnocenné. Zvolené řešení reakce na výběr položek menu je jen jedno z mnoha. Kromě rozlišení příkazu (action command) můžeme operace rozlišovat také podle zdroje události. Jinou možností je vytvořit ke každé položce anonymní třídu (implementující ActionListener) a odtud pak volat metody operací. Každé řešení má své pro i proti, u jednoduchých GUI na zvoleném postupu ale víceméně nezáleží. setTitle("Editor"); setDefaultCloseOperation(DISPOSE_ON_CLOSE); setLocation(100, 100); setSize(400, 300); } Tyto příkazy by měl již každý znát. Nastavují titulek, výchozí zavírací operaci, polohu a velikost okna. Tím je inicializace dokončena. public void actionPerformed(ActionEvent e) { String s = e.getActionCommand(); if (s.equals("new")) clear(); else if (s.equals("open")) load(); else if (s.equals("save")) save(); else if (s.equals("quit")) dispose(); } Obsluha událostí od položek menu. Je to snad zřejmé na první pohled, porovnává se řetězec příkazu s definovanými hodnotami. Pro delší seznam by to bylo operačně náročné (a bylo by lepší použít jiný způsob rozlišení), zde nám to ale problémy nedělá. public void clear() { ta.setText(""); } Vytvoření "nového souboru". Spočívá prostě v tom, že se textová oblast vyprázdní. public void load() { JFileChooser fc = new JFileChooser(); fc.setDialogType(JFileChooser.OPEN_DIALOG); if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { File f = fc.getSelectedFile();
ta.setText(""); try { BufferedReader br = new BufferedReader(new FileReader(f)); StringBuilder sb = new StringBuilder(); String s = ""; boolean fl = false; while ((s = br.readLine()) != null) { if (!fl) sb.append(sep); else fl = true; sb.append(s); } br.close(); ta.setText(sb.toString()); ta.setCaretPosition(0); } catch (IOException e) { JOptionPane.showMessageDialog(this, "Soubor nelze otevřít.", "Chyba", JOptionPane.ERROR_MESSAGE); } } } Načtení textu ze souboru. Swing má svoji implementaci dialogu pro práci se soubory. Dialog toho umí mnohem víc, než se zde používá (např. filtraci souborů, vícečetné výběry), ale nám stačí základní operace. Pokud výběr souboru proběhl správně (nedošlo k žádné chybě a uživatel potvrdil výběr), načteme soubor. Možností je opět více. Zvolil jsem čtení pomocí textového bufferovaného streamu po řádcích. Protože metoda readLine() konce řádků odřezává, opět je přidáme - vedlejším efektem (někdy vítaným, někdy ne) bude, že se všechny konce řádků změní tak, že to odpovídá platformě. K sestavení textu se použije třída StringBuilder. Po vložení řetězce do textové oblasti se přesune kurzor na začátek (jinak by zůstal na konci). Nyní nastal čas upozornit na velice zajímavou a důležitou třídu - JOptionPane. Ta slouží pro práci s jednoduchými dialogy. Kromě toho, že s ní lze pracovat obvyklým způsobem a vytvářet si dialogy podle potřeby, má také řadu statických metod pro zobrazování primitivních informativních a potvrzovacích dialogů. To je užitečné právě v takových případech, jako je tento - k oznamování chyb, informování o ukončení časově náročných operací, dotazům typu ano/ne(/zrušit), vložení jediné hodnoty atd. Doporučuji vydatně používat. public void save() { JFileChooser fc = new JFileChooser(); fc.setDialogType(JFileChooser.SAVE_DIALOG); if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { File f = fc.getSelectedFile(); try { BufferedWriter bw = new BufferedWriter(new FileWriter(f)); bw.write(ta.getText()); bw.close(); } catch (IOException e) { JOptionPane.showMessageDialog(this, "Soubor nelze uložit.", "Chyba", JOptionPane.ERROR_MESSAGE);
} } } K tomu snad není potřeba nic dodat. Od metody k načtení dat se liší prakticky pouze tím, že se celý textový obsah uloží zavoláním jediné metody. Nyní už chybí pouze hlavní metoda pro spuštění programu: public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { Editor m = new Editor(); m.setVisible(true); } }); } Importy jsem jako obvykle vynechal. Po kompilaci a spuštění by se mělo objevit okno, které bude mít nahoře nabídkovou lištu. Ta bude fungovat obvyklým způsobem, pro ovládání půjde používat i klávesové zkratky. Tvoříme s pomocníkem Toto byla ukázka kompletně ruční tvorby aplikace. Hlavně u větších programů s GUI je mnohdy efektivnější aspoň částečně využít grafické prostředí, ve kterém si GUI vytvoříme "klikacím způsobem". V následujících odstavcích budu hovořit o tvorbě téže aplikace v IDE NetBeans verze 5, jiná prostředí se používají podobně. Popis nebude ve stylu, aby to podle toho udělala cvičená opice - spíše vyzdvihnu důležité body. Předpokládám již založený projekt a v něm nějaký balík. Začneme vytvořením potomka JFrame např. z kontextové nabídky balíku: New -> JFrame Form... IDE se automaticky přepne do vizuálního návrháře GUI a můžeme začít "klikat". Na plochu rámu vložíme komponentu JScrollPane a do ní pak JTextFrame. Pro každý objekt nastavíme potřebné vlastnosti na kartě vlastností (Properties). Pak se přepneme do zobrazení zdrojového kódu (Source). Vygenerovaný kód je barevně označen, je sbalený a nelze do něj přímo zasahovat. Do souboru můžeme nyní vložit metody clear(), load() a save(). Není to ovšem úplně nutné, hned uvedu proč. Ve vizuálním editoru pak přidáme nabídkovou lištu, nabídku a její položky (stejné jako ručně psaného editoru). Klávesy položek a klávesové zkratky se dají nastavit přímo na kartě vlastností. Následující obrázky ukazují výslednou logickou strukturu GUI a panel s kartou vlastností: Zbývá už jen nastavit spouštění jednotlivých operací. Nejjednodušší je pro každou položku přes kontextovou nabídku zvolit Events -> Action -> actionPerformed. Tím se do kódu vygeneruje kostra metody, kam se zapíše reakce na danou událost (může to být přímo i výkonný kód, proto se můžeme obejít bez samostatných metod pro jednotlivé operace). Kdo si rozbalí generovaný blok kódu, zjistí, jak je to řešeno. IDE pro každý zdroj událostí vytvoří anonymní třídu, v níž implementuje příslušné rozhraní. Odtud pak volá metodu v naší hlavní třídě. Je to sice jednoduché a elegantní, ale u větších GUI budeme mít úplnou záplavu anonymních tříd, což není úplně nejvhodnější řešení. Pak je lepší postupovat jinak (např. si obsluhu událostí napsat ručně), ale zde se s tím plně spokojíme.
Po dopsání implementace vygenerovaných metod je aplikace hotova (metoda main() se generuje automaticky). Bylo-li vše provedeno správně, měla by se navenek chovat úplně stejně, jako ta ručně napsaná. Jak je vidět, psaní kódu (souvisejícího přímo s GUI) lze omezit na minimum a současně se k fungující aplikaci dopracujeme velice rychle. Na druhou stranu, výhodou je možnost obě cesty kombinovat a využívat je podle potřeby. Seznamy, tabulky a stromy Dosud jsme při tvorbě GUI využívali pouze jednoduché grafické komponenty. Mnoho programů ale vyžaduje používání různých seznamů (ať už obyčejných či rozbalovacích), stromů a tabulek. Zde se opět pořádně projeví výhody frameworku Swing, protože nám poskytuje velmi příjemné mechanismy pro práci s těmito grafickými komponentami a s daty, nad nimiž pracují. Příští díl seriálu bude kompletně věnován této oblasti, protože ta si takovou pozornost jednoznačně zaslouží. Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1284 Java (27) - seznamy, stromy, tabulky Prakticky žádná větší aplikace se neobejde bez zobrazování a modifikace větších datových celků. Obvykle bývají reprezentovány jako různé seznamy, stromy a tabulky. Framework Swing nám nabízí v tomto ohledu velice užitečný a snadno použitelný soubor prostředků. 31.8.2006 06:00 | Lukáš Jelínek | přečteno 32090× Modely a jejich smysl Než přejdeme ke konkrétním objektovým třídám, dovolil bych si nejprve představit obecný mechanismus práce s daty, který je odděluje od vlastního GUI. Jsou to tzv. modely. Model není nic jiného než implementace rozhraní, přes které grafická komponenta přistupuje k datům pro jejich zobrazení a úpravy. Komponenta tedy nemusí vůbec znát detaily implementace, vystačí si s rozhraním. K čemu je to dobré? Použití modelů výrazně zlepšuje variabilitu implementace a umožňuje důsledné oddělení GUI od backendu. Máme-li například tabulku, mohou být data uložena jak přímo v aplikaci (v kolekcích apod.), tak třeba i v databázi, klidně i s přímým přístupem (bez mezičlánku v podobě úložiště v aplikaci). Současně lze manipulace nad daty provádět také jinak než přes GUI, aniž by se na způsobu uložení dat cokoli měnilo. Praktická realizace modelů ve Swingu je řešena tak, že máme jednak základní rozhraní (např. TableModel), pak abstraktní implementaci (obsahuje často používanou funkcionalitu pro práci s daty - např. AbstractTableModel), a nakonec výchozí implementaci modelu (např. DefaultTableModel - používá se v případech, kdy nepoužijeme implementaci vlastní). Někdy je hierarchie složitější (obsahuje např. neproměnné a proměnné modely), komponenty navíc pracují s více modely, protože kromě uložení dat lze obdobně využít modely i pro další účely (třeba pro vlastnosti sloupců nebo pro výběry). Důležitou vlastností modelů je oznamování událostí. Dojde-li ke změně v datech, model to oznámí všech přihlášeným odběratelům. Již jsem to zmiňoval někdy dříve, ale opět to připomínám. Využívá se standardní mechanismus práce s událostmi (tedy vytvoření instance objektu události a její předání v argumentu metody pro obsluhu dané události). Seznamy ve Swingu Můžeme v zásadě rozlišovat dva druhy seznamů: obyčejné (v okénku je nějaký počet prvků,
některé z nich mohou být "vybrané") a rozbalovací, tzv. combo boxy (normálně je zobrazen jen jediný prvek, při rozbalení se teprve zobrazí další). Každý z těchto seznamů má svoji třídu a svůj model. Podívejme se na ně blíže. Na úvod bych chtěl říci, že ačkoli seznamy obsahují metody pro operace nad daty, pokud máme přímo k dispozici referenci k modelu, je lepší volat metody modelu. Tím spíš, že model obecně nemusí podporovat všechny operace, které lze prostřednictvím seznamu volat. Obyčejný seznam Je reprezentován třídou JList a jako model používá rozhraní ListModel. Rozhraní modelu má pouze 4 metody, pro přidání/odebrání odběratele událostí, pro zjištění délky seznamu a pro získání určitého prvku seznamu. Text prvku se získává pomocí metody toString() příslušného prvku, v seznamu mohou bez problémů být i prvky různých typů. Co se týká samotného vykreslování, seznam nemá vlastní posuvníky (je to podobné jako např. u JTextArea). Proto ho téměř vždy umísťujeme do komponenty JScrollPane, která posuvníky poskytne. Třída seznamu disponuje obrovským množstvím metod (což dokazuje, jak velké pole působnosti zde máme) - z prostorových důvodů se ovšem podíváme jen na několik málo z nich. Zde je krátký příklad, jak se seznamem pracovat: DefaultListModel m = new DefaultListModel(); m.addElement("Prvek 1"); m.addElement("Prvek 2"); m.addElement("Prvek 3"); JList list = new JList(m); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); JScrollPane sp = new JScrollPane(list, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); ... Object o = list.getSelectedValue(); V příkladu nejprve vytvoříme instanci výchozího datového modelu a vložíme do ní prvky. Pak zkonstruujeme seznam (s použitím daného modelu), tomu nastavíme režim výběru (pouze jednotlivé položky) a vložíme ho do komponenty s posuvníky. V reálné aplikaci bychom případně pak nastavovali velikost, pozici apod. Poslední řádek příkladu pak ukazuje, jak zjistit, který prvek seznamu je vybrán. Tato metoda vrací přímo příslušný prvek (případně null); pokud potřebujeme index, použije se getSelectedIndex(). Jsou k dispozici i metody pro vícečetné výběry, ale to v tomto příkladu nemá význam. Co by se stalo, pokud bychom přidali do modelu nějaký prvek? Výchozí implementace na to reaguje zavoláním metody fireIntervalAdded() z abstraktní třídy AbstractListModel, což vyústí v distribuci objektu události. Událost obdrží též instance třídy JList a proto překreslí seznam. Podobně by se to chovalo při odebrání jednoho nebo více prvků anebo při jejich změně. Pokud se tato změna děje uvnitř objektu prvku, není ji ovšem tento model schopen detekovat. Ještě upřesním, že DefaultListModel je vlastně obal na třídu Vector, se vším, co k tomu patří - proto je mnohdy lepší použít vlastní implementaci modelu, hlavně při častých změnách uvnitř seznamu. Rozbalovací seznam
Tuto komponentu potřebujeme možná ještě častěji než normální seznam. Je tvořena třídou JComboBox a modelem ComboBoxModel (potomek rozhraní ListModel - obsahuje navíc podporu pro výběr prvku, který je zobrazen v nerozbaleném stavu). Potomkem ComboBoxModel je MutableComboBoxModel, umožňující přidávání a odebírání prvků. Combo box se z hlediska použití příliš neliší od běžného seznamu. Protože ovšem zobrazuje (v nerozbaleném stavu) jen jednu položku, do GUI se začleňuje podobným způsobem jako textové pole. Rozbalený seznam má v případě potřeby posuvník, není třeba se o to starat. Výběr může samozřejmě obsahovat pouze jediný prvek. Rozlišujeme dva druhy rozbalovacích seznamů - editovatelné a needitovatelné. U needitovatelných se pracuje pouze s výběrem prvku. Editovatelný seznam umožňuje upravit vybranou položku, která však implicitně zůstává mimo vlastní seznam - v modelu se tedy neobjeví a metoda getSelectedIndex() vrací -1. Potřebujeme-li, aby se upravený prvek objevil v seznamu, musíme se o to postarat vlastními silami. Následující příklad ukazuje základy práce s rozbalovacím seznamem: String sa[] = { "Prvek 1", "Prvek 2", "Prvek 3", "Prvek 4" }; DefaultComboBoxModel m = new DefaultComboBoxModel(sa); JComboBox cb = new JComboBox(m); cb.setEditable(true); cb.addItemListener(new ItemListener() { public void itemStateChanged(ItemEvent e) { if (e.getStateChange() == ItemEvent.SELECTED) { ... } } }); Nejdřív opět vytvoříme výchozí model. Tento příklad ukazuje jiný způsob než minule konstruktoru se předává připravené pole prvků. Pak zkonstruujeme seznam a označíme ho jako editovatelný. Další úsek kódu slouží k reakci na změnu výběru. Když uživatel vybere prvek ze seznamu (nebo upraví ten, který je vybraný), provedeme zvolenou akci. Reagovat lze samozřejmě i na zrušení výběru (odvybrání). Stromy Zatímco seznamy byly ve své podstatě velmi jednoduché, stromy jsou o něco složitější. Stromové datové struktury asi každý zná, stromové zobrazení v GUI rovněž. Proto by snad nikdo neměl mít problémy s pochopením toho, jak swingovská implementace stromů funguje. Začneme modelem. Základem je rozhraní TreeModel, obsahující (kromě známých metod pro správu odběratelů událostí) metody pro zjištění kořene stromu, počtu potomků určitého uzlu, přístup k potomkovi přes index, zjištění indexu potomka a dotaz, zda je uzel listem. Dále je tu také metoda valueForPathChanged(), volaná při změně hodnoty uzlu ve stromě. Často používáme výchozí implementaci, DefaultTreeModel, obsahující ještě řadu dalších metod. Nelze opomenout třídu TreePath. Představuje cestu, kterou musíme projít od kořene k nějakému uzlu. Je např. předávána do výše uvedené metody valueForPathChanged(), ale hodí se i v jiných
případech. Ačkoli rozhraní TreeModel pracuje s obecnými objekty, ve třídě DefaultTreeModel se používají specializované třídy - implementace rozhraní TreeNode. Má to své důvody. Pokud bychom totiž neuchovávali informace o stromové hierarchii v objektech, musel by to dělat model a to není příliš systémové. Rozhraní TreeNode má potřebné metody, které umožňují zjistit informace o rodičovi a případných potomcích. Často se používá rozhraní MutableTreeNode, které přidává navíc ještě manipulační operace. K dispozici je též výchozí implementace, DefaultMutableTreeNode. V mnoha případech si s ní vystačíme, tím ovšem vyvstává problém, jak tam napojit data. Jednoduše - pomocí odkazu na uživatelský objekt. Pokud je odkaz nastaven, přebírá se zobrazovaný text z tohoto objektu (metoda toString()). Třída JTree, představující grafickou komponentu pro kreslení stromu, patří mezi nejsložitější třídy ve Swingu. Má obrovské množství metod, umožňujících provádět se stromem všemožné operace. Popisovat je nemá cenu, raději si na příkladu ukážeme, jak se se stromem pracuje: void addFiles(File dir, DefaultMutableTreeNode parent) { File fa[] = dir.listFiles(); for (int i=0; i
adresářem, metoda se na něj opět zavolá. Při samotném použití pak zjistíme domovský adresář (získáme ho z vlastností systému), vytvoříme pro něj kořenový uzel a zavoláme výše popsanou metodu. Až je logický strom vytvořen, zkonstruujeme pro něj model a ten předáme konstruktoru grafické komponenty stromu. Zbývá už jen vložit komponentu do oblasti s posuvníky. Když už jsem zmínil to postupné načítání při rozbalování, naznačím ještě, jak by se to dělalo. Existuje rozhraní TreeWillExpandListener, které slouží ke zpracování událostí generovaných stromem před tím, než se rozbalí nebo sbalí větev pod uzlem. Vytvoříme si tedy implementaci rozhraní a zaregistrujeme ji u stromu: tree.addTreeWillExpandListener(new TreeWillExpandListener() { public void treeWillExpand(TreeExpansionEvent e) throws ExpandVetoException { DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.getPath().getLastPathComponent(); ... } public void treeWillCollapse(TreeExpansionEvent e) throws ExpandVetoException {} }); Příklad pracuje s anonymní třídou. Implementovat musíme samozřejmě obě metody rozhraní, i když používáme jen jednu z nich. Objekt události obsahuje cestu k uzlu, který bude rozbalen. Lze snadno získat přímo tento uzel, jak je v příkladu ukázáno. Pokud bychom ale takto implementovali načítání adresářového stromu, takto jednoduché by to nebylo. Buď bychom museli jako uživatelský objekt stromu použít něco jiného než obyčejný řetězec s název souboru (objekt File použíte nejde ve stromě by se zobrazovaly celé cesty), anebo cestu k načítanému adresáři sestavit z komponent cesty k uzlu (TreePath). Tabulky Posledním objektem v tomto dílu seriálu bude tabulka, představovaná třídou JTable. Nemusím snad připomínat, že základem budou opět modely, jeden pro samotnou tabulku, druhý pro sloupce. Model tabulky má metody pro zjištění počtu řádků a sloupců, získání a uložení hodnoty buňky, zjištění názvu a třídy sloupce (viz dále), a zjištění editovatelnosti buňky. Třída sloupce má zásadní význam - ovlivňuje totiž zobrazování a případné úpravy buněk v daném sloupci (různé např. pro textové řetězce a pravdivostní hodnoty). Sloupcový model umožňuje velice detailní ovlivnění toho, jak se bude se sloupci tabulky zobrazovat. Bohužel na tu dostatečný popis není dost prostoru, proto ho musím vynechat. Pro případy, kdy si vytváříme vlastní model pro tabulková data, je nejlepší použít abstraktní třídu AbstractTableModel. Povinně se implementují pouze tři metody (getRowCount(), getColumnCount() a getValueAt()) - pokud si vystačíme s read-only režimem, nepotřebujeme pro základní funkcionalitu nic dalšího. Pro editovatelné buňky musíme předefinovat metody setValueAt() a isCellEditable(). Obvykle je ale vhodné předefinovat i další metody (názvy sloupců, jejich třídy atd.). Uložení dat je plně v naší režii, při jejich změně se musíme postarat o zavolání příslušných metod fireXXX(), aby byli informováni odběratelé událostí. K implementaci vlastního tabulkového modelu se ještě vrátíme později, až se dostaneme k problematice databází.
V jednodušších případech si lze vystačit s výchozí implementací modelu, třídou DefaultTableModel. Ta používá pro tabulkové řádky kolekce typu Vector, a ukládá je opět do instance Vector. Totéž platí i pro názvy sloupců. Model automaticky generuje události při změnách v datech, změny uvnitř datových objektů však samozřejmě není schopen detekovat. Všechny buňky jsou v tomto modelu editovatelné. Pro metody třídy JTable platí totéž, co jsem uvedl u předchozích grafických komponent - je jich mnoho a popisovat je nemá smysl. Proto bude lepší si je opět ukázat na příkladu: DefaultTableModel m = new DefaultTableModel(); m.addColumn("Název"); m.addColumn("Hodnota"); Properties p = System.getProperties(); Iterator