4. praktikum (Sõnetöötlus ja tekstiline sisend/väljund)

Teemad

Klassid String ja StringBuilder, tekstifailidega suhtlemine.

Pärast selle praktikumi läbimist üliõpilane

Sõned

Erinevalt arvutüüpidest (täisarvutüübid int, byte, short, long ja reaalarvutüübid double, float), loogilisest tüübist (boolean) ja sümboltüübist (char) pole sõnetüüp keeles Java algtüüp, vaid iga sõne on klassi String isend. Kui vaataksime näiteks Java API-st, siis näeme, et klassi String isendi loomiseks on võimalik kasutada üle kümne erineva konstruktori. Näiteks on nende hulgas konstruktor, mis nõuab argumendiks sümbolite järjendit.

char[] sümboliteJärjend = {'T','e','r','e',' ','h','o','m','m','i','k','u','s','t'};
String teade = new String(sümboliteJärjend);

System.out.println(teade);

On aga ka lihtsam viis klassi String isendi loomiseks, nimelt kiirloome sõneliteraali abil:
String teade = "Tere hommikust";

(Literaal on konkreetse väärtuse üleskirjutus programmis. Väärtuse tüüp on määratud kirjakujuga, nt. 15 on int-tüüpi, 15L aga long-tüüpi. Sõneliteraali tähistavad jutumärgid. Kui sõneliteraali sees on vaja jutumärke kasutada, siis saab seda teha langkriipsu abil, nt  "\"".)

Edasi, kui klassi String isend on loodud, saab kasutada klassis String defineeritud isendimeetodeid, näiteks length, toLowerCase jne. Nagu eelmises praktikumis õppisime, kasutatakse isendimeetodit  koos konkreetse isendiga. Isendi nimi ja meetodi nimi ühendatakse punktiga, näiteks sõne teade pikkus leitakse järgmiselt:

teade.length()


Järgnev näiteprogramm demonstreerib klassi String meetodeid. Pange tähele, et sõne sümbolite nummerdamine algab nullist (analoogiliselt massiiviindeksiga). Leidke nende meetodite täpsed kirjeldused Java API-st. Pöörake tähelepanu meetodi signatuurile (eriti parameetrite arvule ja nende tüüpidele) ning tagastustüübile. Tutvuge API-s ka teiste klassi String meetoditega.

class TestString {
    public static void main(String[] args) {
        String nimi = "Mart Mardikas";
        System.out.println("Sõne pikkus on: " + nimi.length()); // 13
        System.out.println(nimi.startsWith("Mart")); // true
        System.out.println(nimi.endsWith("kas")); // true
        System.out.println(nimi.endsWith("Mart")); // false
        System.out.println("\'a\' esimene positsioon: " +
        nimi.indexOf('a')); // 1
        int rIndex = nimi.indexOf('r');
        System.out.println("\'r\' esimene positsioon: " + rIndex); // 2
        System.out.println("\'r\' jargmine positsioon: " +
        nimi.indexOf('r', rIndex + 1)); // 7
        int aIndex = nimi.lastIndexOf('a');
        System.out.println("\'a\' viimane positsioon: " + aIndex); // 11
        System.out.println("Alamsõne \'Mardi\' algus: " +
        nimi.indexOf("Mardi")); // 5
        System.out.println("4. täht on "+nimi.charAt(3)); // 't'
        //Täpne võrdsuse kontroll:
        System.out.println(nimi.equals("Mart Mardikas")); // true

        System.out.println(nimi.equals("mart mardikas")); // false
        //Suuri-väikesi tähti mitteeristav võrdsuse kontroll:
        System.out.println(nimi.equalsIgnoreCase("mart mardikas"));  // true
        //Leksikograafiline võrdlemine:
        System.out.println(nimi.compareTo("Jaan Jaaniste")); // >0
        System.out.println(nimi.compareTo("Peeter Paan")); // <0
        System.out.println(nimi.compareTo("Mart Mardikas")); // =0
        System.out.println(nimi.replace('M', 'P')); // 'Part Pardikas'
        System.out.println(nimi.toUpperCase()); // 'MART MARDIKAS'
    }
}

Ülesanne 1

Kompileerige ja käivitage klass TestString.

Sõnede tükeldamine

Üsna sageli on vaja sõnesid tükeldada. Selleks on mitu võimalust (nt. klass StringTokenizer). Siin praktikumis kasutame tükeldamiseks klassi String isendimeetodit split. Selle meetodi tagastustüübiks on String[], mis tähendab, et tulemuseks saadakse sõnede massiiv. Argumendiks on regulaaravaldis, mis määrab, milliseid sümboleid lugeda eraldajateks. Järgmises näites on eraldajaks tühik

String[] tükid = nimi.split(" ");
for (int i = 0; i<tükid.length; i++)
    System.out.println(tükid[i]);

Antud juhul saaksime siis ekraanile

Mart
Mardikas

Kui aga määrata eraldajaks täht "a"

String[] tükid = nimi.split("a");,

siis saame ekraanile

M
rt M
rdik
s

Regulaaravaldis võib olla ka keerulisem, näiteks "[art]" korral loetakse eraldajaks nii tähte "a" kui ka tähti "r" ja "t".

Ülesanne 2

Võtke sõnena kasutusele üks lause, milles on vähemalt 7 sõna. Koostage programm, mis leiab kõik lauses esinevad sõnad ja mõõdab, kui pikad need on.  Ekraanile peab väljastatama alglause ning sõnad ja nende pikkused.

Unicode

Java kasutab tekstitöötlusel kooditabelit Unicode, mis sisaldab tähti jm. sümboleid erinevate tähestike jaoks. Iga sümbol on Javas esitatud 16-bitise märgita täisarvuna, seega võimaldab kooditabel esitada 216 = 65 536 erinevat sümbolit. Paljud teised programmeerimiskeeled kasutavad tekstitöötlusel ASCII kooditabelit, milles sümbolite kodeerimiseks kasutatakse 7-bitiseid arve (128 sümbolit). Samas on ASCII Unicode-i alamhulk – kooditabeli Unicode esimesed 128 sümbolit. Esimesed 256 moodustavad aga kooditabeli ISO-Latin-1 extended ASCII. Sõnede võrdlemisel, näiteks meetodiga compareTo võrreldakse tegelikult paarikaupa nende sümbolite arvulisi koode antud kooditabelis.

Sõnede võrdlus

Ülaltoodud meetodite hulgas on mitmeid, mis kaht klassi String isendit omavahel võrdlevad. Võrduse kontrolliks saab kasutada meetodit equals. Miks ei võiks kahe isendi võrdust aga kontrollida märgipaari  == abil nagu arvude võrduse kontrollimiseks? Tegelikult ongi märgipaar == täiesti lubatud ja kompileerimisel veateadet ei tule. Mure allikas on selles, et võrdusmärkide puhul võrreldakse isendite viitasid, aga meetod equals arvestab isendite sisu. Olukorra selgitamiseks loome kolm sõne, esimese sümbolijärjendi ja kaks ülejäänut sõneliteraali abil.

char[] sümboliteJärjend = {'T','e','r','e'};
String teade1 = new String(sümboliteJärjend);
String teade2="Tere";
String teade3="Tere";

Püüdke ennustada, millised väärtused väljastatakse, kui kasutame järgmisi võrdlusi.     


System.out.println(teade1.equals(teade2));
System.out.println(teade2.equals(teade3));
System.out.println(teade1==teade2);
System.out.println(teade2==teade3);

Ülesanne 3

Proovige nüüd eeltoodud võrdlemise näited läbi ja kontrollige oma ennustusi.

Täpsustavalt saab öelda, et kuna sõne on Javas muutmatu ja sageli kasutatav, siis JVM (Java virtuaalmasin) säästab mälu ja kasvatab jõudlust sellega, et paneb kiirloome abil sama sõneliteraaliga loodud klassi String isendid ühte. Seda sõne nimetatakse kanooniliseks sõneks. Nii ongi viitade mõttes teade2 ja teade3 võrdsed, aga teade1 nendega mitte. Sisu mõttes on aga kõik kolm võrdsed. Kuna tavaliselt on meil just sisu mõttes võrdsust vaja kontrollida, siis kasutamegi meetodit equals.

Klass StringBuilder

Eespool nägime palju meetodeid, mida saab rakendada klassi String isendi puhul. Tähtis on aga silmas pidada, et need meetodid ei muuda isendit ennast. Sõne on Javas muutumatu  tema sisu ei saa muuta. Ka näiteks read

String s = "Soome"
s = "Poola";

ei muuda sõne s sisu. Esimene rida loob isendi, mille sisuks on "Soome" ja omistab selle viida s-ile. Teine rida loob isendi, mille sisuks on "Poola" ja omistab selle viida s-ile. Esimene objekt jääb tegelikult alles (ega muutu), aga tema poole ei saa enam pöörduda.

Võime öelda, et klass String käsitleb sõne staatiliselt. Kui aga tahame sõne dünaamiliselt käsitleda (nt. muuta), siis on sobiv klass StringBuilder.
Klassis StringBuilder (vt. API) on neli konstruktorit, millest meie vaatleme kolme. Parameetriteta konstuktori abil moodustub klassi
StringBuilder isend, milles on kohti 16 sümboli jaoks, aga ühtegi sümbolit (esialgu) pole. Kui isendi loomisel anda ette täisarv, siis tekibki kohti niipaljude sümbolite jaoks. Kui argumendiks on sõne, siis  vastava sõne sümbolid "puhvrisse" pannaksegi.

StringBuilder()
StringBuilder(int)
StringBuilder(String)

Meetodeid on mõnikümmend, mitmed neist on korduva nimega. Meie jaoks olulisemad on
Toomegi mõningad näited nende kasutamisest. Kõigepealt mõned meetodid, mille väärtuseks on täisarv ja mis veel isendi sisu ei muuda.

StringBuilder sb = new StringBuilder("Suusalumi-suusalumi sadas õhtust");
System.out.println(sb.capacity());
System.out.println(sb.length());
System.out.println(sb.indexOf("sada"));

Järgmine meetod tagastab char-tüüpi väärtuse.

System.out.println(sb.charAt(10));

Järgmised meetodid on void-tüüpi ja muudavad vastavalt sisu ja pikkust.

sb.setCharAt(10, 'p');
sb.setLength(20);
System.out.println(sb);

Järgmisedki meetodid muudavad sisu, aga nad tagastavad ka viida isendile endale. Neid saab kasutada kahel moel. Kui me pole tagastatavast viidast huvitatud, siis võime neid meetodeid ilma omistamata kasutada.


sb.append(" ei sadanud");
sb.delete(10,18);
sb.insert(10, "lum");
sb.replace(0, 5, "Uisu");
sb.reverse();

Seda omadust saab kasutada näiteks nii:

StringBuilder tervitusSB = new StringBuilder();
String tervitatav = "Andrus";
tervitusSB.append("Tere, ").append(tervitatav).append("!");

Ülesanne 4

Katsetage klassi StringBuilder meetodeid ja vahetulemuste nägemiseks lisage sobivatesse kohtadesse rida System.out.println(sb);

Tekstiline sisend/väljund (I/O)

Päris mitmes praktikumis oleme põgusalt vaadanud, kuidas saada kasutajalt programmi töö alustamisel või käigus informatsiooni. Vaatluse all on olnud käsurealt programmi käivitamisel sõnejärjendina saadavad argumendid (2. praktikum), klassi Scanner abil klaviatuurilt saadavad väärtused (1. praktikum) ja klassi JOptionPane abil korralduv dialoog (1. rühmatöö juhend). Käesolevas lõigus püüame andmed saada kätte tekstifailist ja tekstifaili ka kirjutada.

Failidega suhtlemise saab Javas korraldada mitmeti. Siinkohal toome vaid ühe võimaluse. Kõigepealt püüame määrata, millise failiga suhtlemine käib. Selleks loome klassi java.io.File isendi.

java.io.File fail = new java.io.File("c:/temp/marsruut.txt");

Kasutada saab mitmesuguseid meetodeid (vt. API), millest hetkel vaatleme meetodit exists, mille abil saame teada, kas antud fail eksisteerib või mitte.

if (fail.exists())
     System.out.println("Fail on juba olemas");
else
     System.out.println("Faili ei ole olemas");

Püüame nüüd sinna faili midagi kirjutada. Selleks loome klassi java.io.PrintWriter isendi. Selles klassis on mitmeid konstruktoreid (vt. API), meie kasutame seda, mille argumendiks on fail. (Võite proovida ka varianti, kus argumendiks on failinimi sõnena.)

java.io.PrintWriter pw = new java.io.PrintWriter(fail); 

Selle rea lisamisel tekib aga veateade, mis räägib käsitlemata erindist (unhandled exception). Erinditest tuleb hiljem eraldi praktikum, siinkohal lahendame olukorra lihtsalt peameetodi päisele kahe sõna lisamisega.

public static void main(String[] args) throws Exception

Klassis PrintWriter on mitmeid meetodeid, sealhulgas ka meile tuttavad print ja println, mida nüüd kasutamegi.

pw.print("Karl Ernst von Baeri ");
pw.println("tänav");
pw.print("Johann Wilhelm Friedrich Hezeli ");
pw.println("tänav");
pw.print("Juhan Liivi ");
pw.println("tänav");


Kui me enam juurde kirjutada ei taha, siis sulgeme voo.

pw.close();

Kas teate, kus need tänavad Tartus asuvad? Hezeli tänav sai oma nime alles 22. veebruaril 2012.

Ülesanne 5

Püüdke ülaltoodud programmilõikude abil vastav fail luua. Vaadake mingi tekstiredaktoriga, kas fail tõesti loodetud kujul täitus.


Failist lugemiseks kasutame klassi Scanner (kasutasime seda tegelikult juba ka 1. praktikumi klaviatuurilt lugemiseks). Isendi konstrueerimisel saab ette anda faili.

java.util.Scanner sc = new java.util.Scanner(fail);   

Kasutades meetodeid  hasNextLine (mis kontrollib, kas on veel võtta ridu) ja nextLine (mis võtab järgmise rea) loeb järgmine programmilõik failist andmeid ja väljastab need ekraanil nii, et eesnimed jäävad ära.

while (sc.hasNextLine()) {
    String rida = sc.nextLine();
    String[] tükid = rida.split(" ");
    System.out.print(tükid[tükid.length-2]+" "+tükid[tükid.length-1]);
    System.out.println();
}


Analoogilisi meetodeid on teisigi. Näiteks lekseemide (token) jaoks on hasNext (mis kontrollib, kas on veel võtta lekseeme) ja next (mis võtab järgmise lekseemi). Vt. ka
API.

Pakettide nimede igakordse mainimise asemel võib ka vastavad paketid importida, kirjutades programmi algusesse nt.

import java.util.Scanner;

java.io.PrintWriter;
java.io.File

Siis võib
java.util.Scanner sc = new java.util.Scanner(fail);
asemel kirjutada
Scanner sc = new Scanner(fail); 

Sulgemiseks saab jälle kasutada meetodit close.

Ülesanne 6 (kontroll)

Failis konverentsid.txt on konverentside nimed, näiteks

International Conference on Computer Games 2007
Dark Side of the Moon 2004
Juulikuise lume uurimise konverents 1998

Eeldame, et iga nimi koosneb sõnadest, mille järel on neljakohaline aastaarv. Failis võib olla 1 kuni 30 konverentsinime.

Kirjutada programm, mis väljastab ekraanile kõigi konverentsinimede lühendid, võttes  igast sõnast esitähe (suurtähtena) ja aastaarvu kaks viimast numbrit ning asetades nende vahele ülakoma.

Näiteks ülaltoodud faili puhul ilmub ekraanile

ICOCG'07
DSOTM'04
JLUK'98