Tällää tiedostoon rivitkin
virtaan viskaile tavuset
sinne sullo saatavaksi
muitten metsästettäväksi.
Rivit riivi tiedostosta
ime virrasta tavuset
sieltä sieppaa saataville
levyltä lue lukuset.
Pistele rivit paloiksi
kunnolla katko kummajaiset
sanat sieltä sommittele
numerotkin napsi niistä.
Mitä tässä luvussa käsitellään?
· Tiedostojen käsittely Javan tietovirroilla
· Tiedostot joissa rivillä monta kenttää
Syntaksi:
Tied. avaaminen luku: f = new BufferedReader(new FileReader(nimi));
kirj. f = new PrintWriter(new FileWriter(nimi))
jatk. f = new PrintWriter(new FileWriter(nimi,true))
Stream kirj: f = new PrintStream(new FileOutputStream(nimi));
jatk: f = new PrintStream(new FileOutputStream(nimi),true); Lukeminen s = f.readLine();
Kirjoittaminen f.println(s);
Sulkeminen f.close(); // aina finally lohkossa!
Luvun esimerkkikoodit:
Pyrimme seuraavaksi lisäämään kerho–ohjelmaamme tiedostosta lukemisen ja tiedostoon tallettamisen. Tätä varten tutustumme ensin lukemiseen mahdollisesti liittyviin ongelmiin.
Tiedostojen käsittely ei eroa päätesyötöstä ja tulostuksesta, siis tiedostojen käyttöä ei ole mitenkään syytä vierastaa! Itse asiassa päätesyöttö ja tulostus ovat vain stdin ja stdout –nimisten tiedostojen käsittelyä.
Tiedostoja on kahta päätyyppiä: tekstitiedostot ja binääritiedostot. Tekstitiedostojen etu on siinä, että ne käyttäytyvät täsmälleen samoin kuin päätesyöttökin. Binääritiedoston etu on taas siinä, että talletus saattaa viedä vähemmän tilaa (erityisesti numeerista muotoa olevat tyypit) ja suorasaannin käyttö on järkevämpää.
Keskitymme aluksi tekstitiedostoihin, koska niitä voidaan tarvittaessa editoida tavallisella editorilla. Näin ohjelman testaaminen helpottuu, kun tiedosto voidaan rakentaa valmiiksi ennen kuin ohjelmassa edes on päätesyöttöä lukevaa osaa.
Muutamme hieman alkuperäistä suunnitelmaamme jäsenrekisteritiedoston sisällöstä:
Kelmien kerho ry
100
; Kenttien järjestys tiedostossa on seuraava:
id| nimi |sotu |katuosoite |postinumero|postiosoite|kotipuhelin...
1|Ankka Aku |010245–123U|Ankkakuja 6 |12345 |ANKKALINNA |12–12324 ...
2|Susi Sepe |020347–123T| |12555 |Perämetsä | ...
3|Ponteva Veli |030455–3333| |12555 |Perämetsä | ...
Olemme lisänneet rivin, jossa kerrotaan tiedoston maksimikoko. Tätähän tarvittiin jäsenlistan luomisessa. Nyt kokoa voidaan tarvittaessa muuttaa tiedostosta tekstieditorilla tarvitsematta tietää ohjelmoinnista mitään.
Tiedoston sisällössä on kuitenkin pieni ongelma: siinä on sekaisin sekä puhtaita merkkijonoja, numeroita että tietuetyyppisiä rivejä. Vaikka kielessä onkin työkalut sekä numeeristen tietojen lukemiseksi tiedostosta, että merkkijonojen lukemiseen, nämä työkalut eivät välttämättä toimi yksiin. Siksi usein kannattaa käyttää lukemiseen vain yhtä työkalua, joka useimmiten on kokonaisen tiedoston rivin lukeminen.
Javan IO-systeemi on varsin monimutkainen. Erilaisia tietovirtoja on yli 60 kappaletta. Alimman tason virta-luokat ovat abstrakteja luokkia määräten vain virtojen rajapinnan. Ylemmällä tasolla hoidetaan fyysistä lukemista ja kirjoittamista. Fyysinen lukeminen ja kirjoittaminen voi tarkoittaa levyn käyttöä, verkon käyttöä tai muiden IO-porttien käyttöä. Seuraavaksi ylemmällä tasolla tarjotaan yksinkertaisempaa rajapintaa esimerkiksi rivien käsittelyyn. Siksi virtoja käytettäessä niitä pitää kerrostaa.
Kun perustoimet on saatu tehtyä, on tiedostojen käsittely Javassa esimerkiksi System.in ja System.out –tietovirtoja vastaavien tietovirtojen käsittelyä.
Olkoon meillä tiedosto nimeltä luvut.dat:
13.4
23.6
kissa
1.9
<EOF> <– ei aina välttämättä mikään merkki
Kirjoitetaan esimerkkitiedoston luvut lukeva ohjelma Javan tietovirroilla. Tarkoitus on hylätä ne rivit, joilla ei ole pelkästään reaalilukua:
import java.io.*;
import fi.jyu.mit.ohj2.Mjonot;
/**
* Lukujen lukeminen tiedostosta
* @author Vesa Lappalainen
* @version 1.0, 07.03.2003
*/
public class Tied_ka {
public static void main(String[] args) {
BufferedReader fi;
try { // Avataan tiedosto lukemista varten
fi = new BufferedReader(new FileReader("luvut.dat"));
} catch (FileNotFoundException ex) {
System.out.println("Tiedosto ei aukea!");
return;
}
double summa=0;
int n=0;
try {
String s; double luku;
while ( ( s = fi.readLine() ) != null ) {
try {
luku = Double.parseDouble(s);
} catch (NumberFormatException ex) {
continue;
}
summa += luku;
n++;
}
} catch (IOException ex) {
System.out.println("Virhe tiedostoa luettaessa!");
} finally { // Aina ehdottomasti finally:ssa resurssien vapautus
try {
fi.close(); // tiedoston sulkeminen heti kun sitä ei enää tarvita
} catch (IOException ex) {
System.out.println("Tiedostoa ei saa suljettua!");
}
}
double ka = 0;
if ( n > 0 ) ka = summa/n;
System.out.println("Lukuja oli " + n + " kappaletta.");
System.out.println("Niiden summa oli " + Mjonot.fmt(summa,4,2));
System.out.println("ja keskiarvo oli " + Mjonot.fmt(ka,4,2));
}
}
Tehtävä 17.1 Tiedoston lukujen summa
1. Muuta tiedoston Tied_ka.java –ohjelmaa siten, että väärän rivin kohdalla tulostetaan väärä rivi ja lopetetaan koko ohjelma.
2. Muuta edelleen ohjelmaa siten, että väärät rivit tulostetaan näyttöön:
Tiedostossa oli seuraavat laittomat rivit:
kissa
Lukuja oli...
Ilmoitusta ei tietenkään tule, mikäli tiedostossa ei ole laittomia merkkejä. Tyhjää riviä ei tulkita vääräksi riviksi.
Tiedosto voidaan siis avata heti kun tiedostoa vastaava tietovirta luodaan:
new FileReader("luvut.dat")
Parametri ”luvut.dat” on tiedoston nimi levyllä. Nimi voi sisältää myös hakemistopolun, mutta tätä kannattaa välttää, koska hakemistot eivät välttämättä ole samanlaisia kaikkien käyttäjien koneissa. Jos hakemistopolkuja käyttää, niin erottimena kannattaa käyttää /-merkkiä. Samoin kannattaa olla tarkkana isojen ja pienien kirjainten kanssa, sillä useissa käyttöjärjestelmissä kirjainten koolla on väliä.
Lukemista varten avattaessa tiedoston täytyy olla olemassa tai avaus epäonnistuu. Avauksen epäonnistumisesta heitetään FileNotFoundException-poikkeus.
Tiedostosta lukeminen on jälleen analogista päätesyötön kanssa:
s = fi.readLine();
Mikäli tiedosto on loppu, saa s null-arvon.
Helpoin ratkaisu on perustaa lukusilmukka siihen, että yritetään lukea kokonainen tiedoston rivi ja jos tämä epäonnistuu, on tiedostokin todennäköisesti loppu.
while ( ( s = fi.readLine() ) != null ) {
... käsittele jonoa s
}
Vastaavasti kirjoittamista varten avattuun tiedostoon kirjoitettaisiin
PrintStream fo;
...
fo = new PrintStream(new FileOutputStream("taulu.txt"));
// avataan tiedosto kirjoittamista varten
// avauksessa vanha tiedosto tuhoutuu
Mikäli avattaessa tiedostoa kirjoittamista varten, ei haluta tuhota vanhaa sisältöä, vaan kirjoittaa vanhan perään, käytetään avauksessa toista parametria, jolla kerrotaan halutaanko kirjoittaa edellisen tiedoston perään (append):
fo = new PrintStream(new FileOutputStream("taulu.txt",true));
// avataan perään kirjoittamista varten
Tiedoston jatkaminen on erittäin kätevä esimerkiksi virhelogitiedostoja kirjoitettaessa.
import java.io.*;
/**
* Ohjelmalla tulostetaan kertotaulu tiedostoon. Jos tiedosto on
* olemassa, jatketaan vanhan tiedoston perään.
* @author Vesa Lappalainen
* @version 1.0, 21.02.2003
*/
public class Kertotaulu {
public static void main(String[] args) {
PrintStream fo = null;
try {
fo = new PrintStream(new FileOutputStream("taulu.txt",true));
} catch (FileNotFoundException ex) {
System.out.println("Tiedosto ei aukea"); return;
}
int kerroin = 5;
try {
for (int i=0; i<10; i++)
fo.println( i + "*" + kerroin + " = " + i*kerroin);
} finally {
fo.close();
}
}
}
Edellä voisi käyttää PrintStream virran sijasta PrintWriter-luokkaa, joka olisi yhteensopivampi Reader-luokan kanssa:
PrintWriter fo;
...
fo = new PrintWriter(new FileWriter(nimi,true))
Kuitenkin PrintStream on taas yhteensopiva System.out:in kanssa, joten joissakin tapauksissa tämä puolustaa PrintStream-luokan käyttämistä.
Useimmiten kannattaa kaikki näyttöön tulostavat aliohjelmat/metodit kirjoittaa sellaiseksi, että niille viedään parametrinä se tietovirta, johon tulostetaan. Näin samalla aliohjelmalla voidaan helposti tulostaa sitten näyttöön tai tiedostoon tai jopa kirjoittimelle (joka on vain yksi tietovirta muiden joukossa, esim. Windowsissa PRN-niminen tiedosto).
Avattu tiedosto on aina lukemisen tai kirjoittamisen lopuksi syytä sulkea. Tiedoston käsittely on usein puskuroitua, eli esimerkiksi kirjoittaminen tapahtuu ensin apupuskuriin, josta se kirjoittuu fyysisesti levylle vain puskurin täyttyessä tai tiedoston sulkemisen yhteydessä. Käyttöjärjestelmä päivittää tiedoston koon levylle usein vasta sulkemisen jälkeen. Sulkemattoman tiedoston koko saattaa näyttää 0 tavua.
Javassa tiedoston sulkeminen pitää aina varmistaa try-finally-lohkolla:
... avaa tiedosto
try {
... käsittele tiedostoa
} finally { // Aina ehdottomasti finally:ssa resurssien vapautus
try {
fi.close(); // tiedoston sulkeminen heti kun sitä ei enää tarvita
} catch (IOException ex) {
... toimenpiteet jos tiedostoa ei saada suljettua
}
}
Tiedosto kannattaa sulkea heti kun sen käyttö on loppu.
Tehtävä 17.2 Kommentit näytölle
Kirjoita ohjelma, joka kysyy tiedoston nimen ja tämän jälkeen tulostaa tiedostosta rivien /******* ja––––––*/välisen osan näytölle.
Jäsenrekisterissä on tiedoston yhdellä rivillä useita kenttiä. Kentät saattavat olla myös eri tyyppisiä. Miten lukeminen hoidetaan varmimmin?
Olkoon meillä vaikkapa seuraavanlainen tiedosto:
Volvo | 12300 | 1
Audi | 55700 | 2
Saab | 1500 | 4
Volvo | 123400 | 1<EOF>
Tiedostoa voitaisiin periaatteessa niin että luetaan ensin yksi merkkijono, sitten tolppa, sitten reaaliluku, tolppa ja lopuksi kokonaisluku.
Ratkaisussa on kuitenkin seuraavia huonoja puolia:
· mikäli tiedoston loppu ei olekaan viimeisen rivin lopussa, tulee ”ylimääräisen” rivin käsittelystä ongelmia
· mikäli jokin rivi on väärää muotoa, menee ohjelma varsin sekaisin
Tehtävä 17.3 Ohjelman "sekoaminen"
Jos esimerkin hahmotellussa ratkaisussa olisi silmukka, joka tulostaa tiedot kunkin lukemisen jälkeen, niin mitä tulostuisi seuraavasta tiedostosta:
Volvo | 12300 | 1
Audi 55700 | 2
Saab | 1500 | 4
Volvo | 123400 | 1
<EOF>
Tutkitaanpa ongelmaa tarkemmin. Tiedostosta on siis luettu rivi, joka on muotoa
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ │V│o│l│v│o│ │|│ │ │1│2│3│0│0│ │|│ │1│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
Jos saisimme erotettua tästä 3 merkkijonoa:
pala1 pala2 pala3
┌─┬─┬─┬─┬─┬─┬─┬ ┬─┬─┬─┬─┬─┬─┬─┬─┬ ┬─┬─┬
│ │V│o│l│v│o│ │ │ │ │1│2│3│0│0│ │ │ │1│
└─┴─┴─┴─┴─┴─┴─┴ ┴─┴─┴─┴─┴─┴─┴─┴─┴ ┴─┴─┴
voisimme kustakin palasesta erikseen ottaa haluamme tiedot. Esimerkiksi 1. palasesta saadaan tuotteen nimi, kun siitä poistetaan turhat välilyönnit. Hinta saataisiin 2. palasesta kutsulla
sscanf(pala2.c_str(),"%lf",&hinta);
Merkkijono pitää varsin usein muuttaa reaaliluvuksi tai kokonaisluvuksi. Java tarjoaa luokissa Integer ja Double mahdollisuuden muuttaa merkkijono vastaavaksi lukutyypiksi:
double d = Double.parseDouble(jono);
int i = Integer.parseInt(jono);
Edellän mainitut metodit heittävät poikkeuksen jos jono sisältää mitä tahansa muuta kuin pelkkiä lukuun kuuluvia merkkejä.
Siksi kirjoitammekin luokkaan Mjonot kaksi funktiota erotaDouble ja erotaInt:
public static double erotaDouble(String jono, double oletus) ...
public static double erotaInt(String jono, int oletus) ...
Jos funktio ei löydä merkkijonosta lukua, se palauttaa oletuksen. Nämä funktiot toimivat oikein myös seuraavien jonojen kanssa:
12.34 e => 12.34
14 kpl => 14
Tehdään myös yleiskäyttöinen funktio erota, jonka tehtävä on ottaa merkkijonon alkuosa valittuun merkkiin saakka, poistaa valittu merkki ja palauttaa sitten funktion tuloksena tämä alkuosa. Itse merkkijonoon jää jäljelle ensimmäisen merkin jälkeinen osa. Funktio on kirjoitettu tiedostoon Mjonot.java:
public static String erota(StringBuffer jono, char merkki,
boolean etsitakaperin) {
if ( jono == null ) return "";
int p;
if ( !etsitakaperin ) p = jono.indexOf(""+merkki);
else p = jono.lastIndexOf(""+merkki);
String alku;
if ( p < 0 ) {
alku = jono.toString();
jono.delete(0,jono.length());
return alku;
}
alku = jono.substring(0,p);
jono.delete(0,p+1);
return alku;
}
}
Kirjoitetaan lyhyt esimerkki, jolla demonstroidaan funktion käyttöä:
import fi.jyu.mit.ohj2.Mjonot;
/**
* Ohjelmalla demonstroidaan erota-funktion toimintaa
* @author Vesa Lappalainen
* @version 1.0, 21.02.2003
*/
public class ErotaEsim {
private static void tulosta(int n,String pala, StringBuffer jono)
{
int valeja = 10-pala.length();
System.out.println(n + ": pala = '" + Mjonot.fmt(pala + "'",-10) +
" jono = '" + jono + "'");
}
public static void main(String[] args) {
StringBuffer jono = new StringBuffer(" Volvo | 12300 | 1");
String pala=""; tulosta(0,pala,jono);
pala = Mjonot.erota(jono,'|'); tulosta(1,pala,jono);
pala = Mjonot.erota(jono,'|'); tulosta(2,pala,jono);
pala = Mjonot.erota(jono,'|'); tulosta(3,pala,jono);
pala = Mjonot.erota(jono,'|'); tulosta(4,pala,jono);
}
}
Ohjelma tulostaa:
0: pala = '' jono = ' Volvo | 12300 | 1'
1: pala = ' Volvo ' jono = ' 12300 | 1'
2: pala = ' 12300 ' jono = ' 1'
3: pala = ' 1' jono = ''
4: pala = '' jono = ''
Ennen ensimmäistä kutsua tilanne on seuraava:
pala jono
┌┐ ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
││ │ │V│o│l│v│o│ │|│ │ │1│2│3│0│0│ │|│ │1│
└┘ └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
0 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8
Ensimmäisessä kutsussa erota-funktio löytää etsittävän | -merkin paikasta 7. Merkit 0-6 kopioidaan funktion paluuarvoksi ja sitten jonosta tuhotaan merkit 0-7. Funktion paluuarvo sijoitetaan muuttujaan pala:
pala jono
┌─┬─┬─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ │V│o│l│v│o│ │ │ │ │1│2│3│0│0│ │|│ │1│
└─┴─┴─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
0 1 2 3 4 5 6 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8
Seuraavalla kutsulla (kerta 2) |-merkki löytyy jonosta paikasta 8. Nyt merkit jonon merkit 0-7 kopioidaan funktion paluuarvoon ja merkki 8 tuhotaan. Kutsun jälkeen tilanne on:
pala jono
┌─┬─┬─┬─┬─┬─┬─┬─┐ ┌─┬─┐
│ │ │1│2│3│0│0│ │ │ │1│
└─┴─┴─┴─┴─┴─┴─┴─┘ └─┴─┘
0 1 2 3 4 5 6 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8
Kolmannessa kutsussa merkkiä | ei enää löydy jonosta. Tämä ilmenee siitä, että find-metodi palauttaa arvon string::npos (no position), eli ei paikkaa. Näin koko jono kopioidaan funktion paluuarvoksi ja kutsun jälkeen tilanne on:
pala jono
┌─┬─┐ ┌┐
│ │1│ ││
└─┴─┘ └┘
0 1 2 3 4 5 6 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8
Vastaava toistuu neljännessä kutsussa, eli koko jono sitten kopioidaan paluuarvoksi ja tilanne on neljännen kutsun jälkeen:
pala jono
┌┐ ┌┐
││ ││
└┘ └┘
0 1 2 3 4 5 6 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8
Tämän jälkeen tilanne pysyy samana vaikka erota-funktiota kutsuttaisiin kuinka monta kertaa tahansa. Tästä saadaan se etu, että erota-funktiota voidaan turvallisesti kutsua kuinka monta kertaa tahansa, vaikkei jonosta enää palasia saataisikaan. Jos kutsua tehdään silmukassa, voidaan silmukan lopetusehdoksi kirjoittaa
while ( !jono.equals("") ) {
pala = erota(jono,'|');
System.out.println(pala);
}
Usein samassa jonossa on sekaisin lukuja ja merkkijonoja. Jotta käsittely saataisiin symmetrisemmäksi eri tyyppien välillä, niin luokkaan Mjonot on kirjoitettu myös polymorfiset funktiot:
StringBuffer jono = new StringBuffer(" Volvo 145 | 2000 e | 3 kpl ")
String s = "";
double d = 0.0;
int kpl = 0;
s = erota(jono,'|',s); // "Volvo 145"
d = erota(jono,'|',d); // 2000
kpl = erota(jono,'|',kpl); // 3
Idea on siinä, että jos myöhemmin huomataan vaikka että kpl pitäisi olla tyypiltään double eikä int, riittää vain muuttujan kpl tyypin vaihtaminen.
Jos halutaan käsitellä tilanteet, joissa joku kenttä onkin virheellistä muotoa, on edellisestä myös poikkeuksen heittävät muodot:
try {
s = erotaEx(jono,'|',s); // "Volvo 145"
d = erotaEx(jono,'|',d); // 2000
kpl = erotaEx(jono,'|',kpl); // 3
} catch ( NumberFormatException ex ) {
System.out.println(ex.getMessage());
}
Nyt voimme toteuttaa "tuotetiedoston" lukevan ohjelman Javan tietovirroilla ja funktioiden erotaEx avulla:
import fi.jyu.mit.ohj2.*;
import java.io.*;
/**
* Ohjelmalla luetaan tuotetiedosto ja tulostetaan tuotteet
* @author Vesa Lappalainen
* @version 1.0, 21.02.2003
*/
public class LueTuote {
public static boolean tulosta_tuotteet() {
String srivi,pala;
String nimike; double hinta; int kpl;
BufferedReader fi = Tiedosto.avaa_lukemista_varten("tuotteet.dat");
if ( fi == null ) return false;
System.out.println(); System.out.println(); System.out.println();
System.out.println("-------------------------------------------");
try {
while ( ( srivi = fi.readLine() ) != null ) {
StringBuffer rivi = new StringBuffer(srivi);
try {
nimike = Mjonot.erotaEx(rivi,'|',"");
hinta = Mjonot.erotaEx(rivi,'|',0.0);
kpl = Mjonot.erotaEx(rivi,'|',0);
} catch (NumberFormatException ex) {
System.out.println("Virhe: " + ex.getMessage());
continue;
}
System.out.println(Mjonot.fmt(nimike,-20) +" " + Mjonot.fmt(hinta,7,0) +
Mjonot.fmt(kpl,4));
}
} catch (IOException ex) {
System.out.println("Vikaa tiedostoa luettaessa");
} finally {
try {
fi.close();
} catch (IOException ex) {
System.out.println("Ongelmia tiedoston sulkemisessa");
}
}
System.out.println("-------------------------------------------");
System.out.println(); System.out.println(); System.out.println();
return true;
}
public static void main(String[] args) {
if ( !tulosta_tuotteet() ) System.out.println("Tuotteita ei saada luetuksi");
}
}
Ohjelma tulostaa:
-------------------------------------------
Volvo 12300 1
Audi 55700 2
Saab 1500 4
Volvo 123400 1
-------------------------------------------
Muutetaan vielä tuotteiden lukua oliomaisemmaksi, eli annetaan tuotteelle kuuluvat tehtävät kokonaan Tuote–luokan vastuulle, samalla lisätään Tuotteet–luokka.
import java.io.*;
import fi.jyu.mit.ohj2.*;
/**
* Esimerkki oliosta joka käsittelee tiedostoa
* @author Vesa Lappalainen
* @version 1.0, 09.03.2003
*/
public class LueRek {
static public class Tuote {
private String nimike = "";
private double hinta = 0.0;
private int kpl = 0;
public Tuote() {}
public Tuote(String rivi) { parse(rivi); }
public void parse(String s) throws NumberFormatException {
StringBuffer sb = new StringBuffer(s);
nimike = Mjonot.erotaEx(sb,'|',nimike);
hinta = Mjonot.erotaEx(sb,'|',hinta);
kpl = Mjonot.erotaEx(sb,'|',kpl);
}
public String toPrintString() {
return Mjonot.fmt(nimike,-20) + " " + Mjonot.fmt(hinta,7,0) +
Mjonot.fmt(kpl,4);
}
}
static public class Tuotteet {
private String nimi = "";
public Tuotteet(String nimi) { this.nimi = nimi; }
public boolean tulosta(OutputStream os) {
PrintStream out = Tiedosto.getPrintStream(os);
BufferedReader fi = Tiedosto.avaa_lukemista_varten("tuotteet.dat");
if ( fi == null ) return false;
out.println(); out.println(); out.println();
out.println("-------------------------------------------");
try {
String rivi; Tuote tuote;
while ( ( rivi = fi.readLine() ) != null ) {
try {
tuote = new Tuote(rivi);
} catch (NumberFormatException ex) {
System.err.println("Virhe: " + ex.getMessage());
continue;
}
out.println(tuote.toPrintString());
}
} catch (IOException ex) {
System.err.println("Vikaa tiedostoa luettaessa");
} finally {
try {
fi.close();
} catch (IOException ex) {
System.err.println("Ongelmia tiedoston sulkemisessa");
}
}
out.println("-------------------------------------------");
out.println(); System.out.println(); System.out.println();
return true;
}
}
public static void main(String[] args) {
Tuotteet tuotteet = new Tuotteet("tuotteet.dat");
if ( !tuotteet.tulosta(System.out) ) {
System.err.println("Tuotteita ei saada luetuksi");
}
}
}
Seuraavaksi kirjoitamme ohjelman, jossa tulee esiin varsin yleinen ongelma: tietueen etsiminen joukosta. Kirjoitamme edellisiä esimerkkejä vastaavan ohjelman, jossa tavallisen tulostuksen sijasta tulostetaan kunkin tuoteluokan yhteistilanne.
import java.io.*;
import fi.jyu.mit.ohj2.*;
/**
* Ohjelma lukee tiedostoa tuotteet.dat, joka on muotoa:
* <pre>
* Volvo | 12300 | 1
* Audi | 55700 | 2
* Saab | 1500 | 4
* Volvo | 123400 | 1
* </pre>
* Ohjelma tulostaa kuhunkin tuoteluokkaan kuuluvien tuotteiden
* yhteishinnat ja kappalemäärät sekä koko varaston yhteishinnan
* ja kappalemäärän. Eli em. tiedostosta tulostetaan:
* <pre>
* -------------------------------------------
* Volvo 135700 2
* Audi 111400 2
* Saab 6000 4
* -------------------------------------------
* Yhteensä 253100 8
* -------------------------------------------
*</pre>
* @author Vesa Lappalainen
* @version 1.0, 09.03.2003
*/
public class LueTRek {
/****************************************************************************/
/**
* Luokka yhden tuotteen tiedoille.
*/
static public class Tuote {
private String nimike = "";
private double hinta = 0.0;
private int kpl = 0;
public Tuote() {}
public Tuote(String rivi) {
try {
parse(rivi);
} catch (NumberFormatException ex) {
}
}
public void parse(String s) throws NumberFormatException {
StringBuffer sb = new StringBuffer(s);
nimike = Mjonot.erotaEx(sb,'|',nimike);
hinta = Mjonot.erotaEx(sb,'|',hinta);
kpl = Mjonot.erotaEx(sb,'|',kpl);
}
public String toPrintString() {
return Mjonot.fmt(nimike,-20) + " " + Mjonot.fmt(hinta,7,0) +
Mjonot.fmt(kpl,4);
}
public void ynnaa(Tuote tuote) {
hinta += tuote.hinta * tuote.kpl;
kpl += tuote.kpl;
}
public String getNimike() { return nimike; }
public void setNimike(String nimike) { this.nimike = nimike; }
}
/****************************************************************************/
/**
* Luokka joka säilyttää kunkin ero tuotteen yhteissumman ja lukumäär'n
* sekä kaikkien tuotteiden yhteissumman ja lukumäärän
*/
static public class Tuotteet {
private String nimi = "";
private int lkm;
private Tuote alkiot[];
private Tuote yhteensa = new Tuote("Yhteensä");
public Tuotteet(String nimi) {
this.nimi = nimi;
alkiot = new Tuote[10];
}
public int etsi(String tnimi) {
for (int i=0; i<lkm; i++)
if ( alkiot[i].getNimike().equalsIgnoreCase(tnimi) ) return i;
return -1;
}
public int lisaa(String tnimi) {
if ( alkiot.length <= lkm ) return -1;
alkiot[lkm] = new Tuote(tnimi);
return lkm++;
}
public boolean ynnaa(Tuote tuote) {
if ( tuote.getNimike().equals("") ) return false;
int i = etsi(tuote.getNimike());
if ( i < 0 ) i = lisaa(tuote.getNimike());
if ( i < 0 ) return false;
alkiot[i].ynnaa(tuote);
yhteensa.ynnaa(tuote);
return true;
}
public boolean lue() {
BufferedReader fi = Tiedosto.avaa_lukemista_varten("tuotteet.dat");
if ( fi == null ) return false;
try {
String rivi; Tuote tuote = new Tuote();
while ( ( rivi = fi.readLine() ) != null ) {
try {
tuote.parse(rivi);
ynnaa(tuote);
} catch (NumberFormatException ex) {
System.err.println("Rivillä jotakin pielessä " + rivi + " " +
ex.getMessage());
continue;
}
}
} catch (IOException ex) {
System.err.println("Vikaa tiedostoa luettaessa");
} finally {
try {
fi.close();
} catch (IOException ex) {
System.err.println("Ongelmia tiedoston sulkemisessa");
}
}
return true;
}
public void tulosta(OutputStream os) {
PrintStream out = Tiedosto.getPrintStream(os);
out.println(); out.println(); out.println();
out.println("-------------------------------------------");
for (int i=0; i<lkm; i++)
out.println(alkiot[i].toPrintString());
out.println("-------------------------------------------");
out.println(yhteensa.toPrintString());
out.println("-------------------------------------------");
out.println(); System.out.println(); System.out.println();
}
}
/****************************************************************************/
public static void main(String[] args) {
Tuotteet varasto = new Tuotteet("tuotteet.dat");
if ( !varasto.lue() ) {
System.err.println("Tuotteita ei saada luetuksi");
return;
}
varasto.tulosta(System.out);
}
}
Tehtävä 17.4 Tietorakenne
Piirrä kuva Tuotteet –luokan tietorakenteesta.
Tehtävä 17.5 Perintä
Miten voisit perinnän avulla saada tiedoston luerek.cpp luokasta Tuote tiedoston LueTRek.java vastaavan luokan (tietysti eri nimelle, esim. RekTuote). Mitä muutoksia olisi hyvä tehdä alkuperäisessä Tuote–luokassa.
Tehtävä 17.6 Tunnistenumero
Lisää LueTRek.java–ohjelmaan tunnistenumeron käsittely mahdollista tulevaa relaatiokäyttöä varten.
Tehtävä 17.7 Mittakaava
Kirjoita mittakaavaohjelma, jossa on vakiotaulukko
yks |
mm |
mm cm dm m inch |
1.0 10.0 100.0 1000.0 25.4 |
ja jonka toiminta näyttäisi seuraavalta:
...
Mittakaava ja matka>1:10000 10 cm[RET]
Matka maastossa on 1.00 km.
Mittakaava ja matka>1:200000 20[RET]
Matka maastossa on 4.00 km.
Mittakaava ja matka>loppu[RET]
Kiitos!
Muuta ohjelmaa siten, että yksiköiden muunnostaulukko luetaan ohjelman aluksi tiedostosta muunnos.dat.
Muuta ohjelmaa vielä siten, että mikäli mittakaava jätetään antamatta, käytetään edellisellä kerralla annettua mittakaavaa ja ensimmäinen luku onkin matka.
...
Mittakaava ja matka>1:10000 10 cm[RET]
Matka maastossa on 1.00 km.
Mittakaava ja matka>0.20 dm[RET]
Matka maastossa on 0.20 km.
Mittakaava ja matka>loppu[RET]
Kiitos!