Gyakorlati alapok II.
Bemenet - kimenet
Csatorna-műveletek nemcsak közegészségügyiek részére
Az előző, A csatorna (stream) fejezetben a legfontosabb tulajdonságaik alapján katalogizáltuk a csatornákat. 4 olyan alaposztályt határozhatunk meg, amelyek képesek önálló input-output-műveletek elvégzésére:
-
InputStream - bemeneti bájtcsatorna,
-
OutputStream - kimeneti bájtcsatorna,
-
Reader - bemeneti karaktercsatorna,
-
Writer - kimeneti karaktercsatorna.
Mindenekelőtt kiindulópontként ismételjük meg egyik, már publikált Java-kódot, amely szöveget olvas be egy fájlból:
import java.io.*;
public class Main {
public static void main(String[] args) {
FileReader reader = null;
try {
reader = new FileReader("D:\\Kertész leszek.txt");
BufferedReader br = new BufferedReader(reader);
String line = br.readLine();
while(line != null) {
System.out.println(line);
line = br.readLine();
}
} catch (FileNotFoundException fnfe) {
System.out.println("A fájl nem található!");
fnfe.printStackTrace();
} catch (IOException ioe) {
ioe.printStackTrace();
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
}
Végeredmény:
Kertész leszek, fát nevelek,
kelő nappal én is kelek,
nem törődök semmi mással,
csak a beojtott virággal.
Minden beojtott virágom
kedvesem lesz virágáron,
ha csalán lesz, azt se bánom,
igaz lesz majd a virágom.
Tejet iszok és pipázok,
jóhíremre jól vigyázok,
nem ér engem veszedelem,
magamat is elültetem.
Kell ez nagyon, igen nagyon,
napkeleten, napnyugaton -
ha már elpusztul a világ,
legyen a sírjára virág.
Több szempontból is nagyon tanulságos a példa:
-
már említett módon az input-output-műveletek elég kényes, nagy hibaszázalékú műveletek, ezért többségüket biztonsági okokból kötelező valamilyen szintű kivételkezelés alá vonni.
-
Az input-output-rutinokat importálnunk kell (import java.io.*;).
-
A csatornát önálló szabványos eszközökkel objektumként először létre kell hozni (reader = new FileReader("D:\\Kertész leszek.txt");), csakis ezután érhetők el metódusai.
-
A csatornát használata után kötelező lezárni (reader.close();), hogy felszabaduljanak a lefoglalt erőforrások.
-
Kimeneti csatornák esetén a close() művelet egyúttal meghívja a flush() metódust is, amely a maradék, a memóriában még bufferelt (ideiglenesen tárolt) adatot is kiírja a csatornára.
-
A close() művelet meghívása után a csatorna megszűnik. Bármilyen további, rá történő hivatkozás IOException kivételt vált ki.
Ha nem kívánunk bajlódni kivételkezeléssel, akkor már említett módon a kivételt "továbbdobhatjuk" a JVM vagy valami (valaki) más felé (throws IOException). Ez nyilvánvalóan leegyszerűsíti a kódot, ám egyúttal el is veszítjük a hiba lekezelésének lehetőségét:
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
FileReader reader = null;
reader = new FileReader("D:\\Kertész leszek.txt");
BufferedReader br = new BufferedReader(reader);
String line = br.readLine();
while(line != null) {
System.out.println(line);
line = br.readLine();
}
reader.close();
}
}
Végeredmény:
Kertész leszek, fát nevelek,
kelő nappal én is kelek,
nem törődök semmi mással,
csak a beojtott virággal.
Minden beojtott virágom
kedvesem lesz virágáron,
ha csalán lesz, azt se bánom,
igaz lesz majd a virágom.
Tejet iszok és pipázok,
jóhíremre jól vigyázok,
nem ér engem veszedelem,
magamat is elültetem.
Kell ez nagyon, igen nagyon,
napkeleten, napnyugaton -
ha már elpusztul a világ,
legyen a sírjára virág.
Az író-olvasó műveletek -függetlenül adattípusuktól, azaz, hogy bájt-, vagy karakterszervezésűk-, belső működésük szerint háromfélék lehetnek:
-
a művelet 1 db adategységet olvas be vagy ír ki (1 bájt vagy 1 karakter),
-
a művelet 1 tömbnyi adategységet olvas be vagy ír ki,
-
a művelet 1 résztömbnyi adategységet olvas be vagy ír ki, amelynek 3 bemeneti paramétere van: tömb, résztömb kezdőpozíciója és a résztömb hossza.
Bájtszervezésű Reader-csatorna esetén:
-
read()
-
read(byte b[])
-
read(byte b[], int pozicio, int hossz)
Bájtszervezésű Writer-csatorna esetén:
-
write()
-
write(byte b[])
-
write(byte b[], int pozicio, int hossz)
Karakterszervezésű Reader-csatorna esetén:
-
read()
-
read(char b[])
-
read(char b[], int pozicio, int hossz)
Karakterszervezésű Writer-csatorna esetén:
-
write()
-
write(char b[])
-
write(char b[], int pozicio, int hossz)
A Java-API-ban a fent ismertetett Reader-Writer-tömbműveletek a következő hivatalos formátumban állnak rendelkezésre, például:
-
read(byte b[], int off, int len),
-
write(byte b[], int off, int len).
Hibás paraméterezés esetén a Reader-tömbműveletek a rájuk jellemző kivételeket válthatják ki, amelyek tipikusan vagy az IndexOutOfBoundsException vagy a NullPointerException, Writer-műveletek pedig az IndexOutOfBoundsException nevű kivételt.
A Reader-műveleteknél meg kell különböztetnünk üres és véget ért csatornákat. Az üres csatorna nem tartalmaz adatot, de még vár újabb adatok érkezésére. Ezzel ellentétben a véget ért csatorna már elküldte a lezáró vezérjelet, amely jelzi, hogy több adat nem fog érkezni.
Most nézzük meg egy ellentétes irányú, de szintén karakter típusú adatcsatorna működését (Writer). Az alábbi futtatható Java-kód PrintWriter objektuma (writer) a dedikált helyen (itt a D: meghajtó) létrehoz egy Hello.txt nevű fájlt és feltölti a "Helló Világ!" szöveggel:
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
PrintWriter writer = new PrintWriter(new FileWriter("D:\\Hello.txt"));
writer.println("Helló Világ!");
writer.close();
}
}
Végeredmény:
Hello.txt nevű fájl "Helló Világ!" szöveggel a D: meghajtón
Most pedig egyesítsük a Reader-, és Writer-műveleteket egy rövid, futtatható Java-kódban! Az alábbi kód átmásolja a Kertész leszek.txt nevű fájl tartalmát a létrehozott üres Hello.txt nevű fájlba:
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
FileReader reader = null;
reader = new FileReader("D:\\Kertész leszek.txt");
PrintWriter writer = new PrintWriter(new FileWriter("D:\\Hello.txt"));
BufferedReader br = new BufferedReader(reader);
String line = br.readLine();
while(line != null) {
writer.println(line);
line = br.readLine();
}
writer.close();
reader.close();
}
}
Végeredmény:
Szövegtartalom a Hello.txt nevű fájlban a D: meghajtón:
Kertész leszek, fát nevelek,
kelő nappal én is kelek,
nem törődök semmi mással,
csak a beojtott virággal.
Minden beojtott virágom
kedvesem lesz virágáron,
ha csalán lesz, azt se bánom,
igaz lesz majd a virágom.
Tejet iszok és pipázok,
jóhíremre jól vigyázok,
nem ér engem veszedelem,
magamat is elültetem.
Kell ez nagyon, igen nagyon,
napkeleten, napnyugaton -
ha már elpusztul a világ,
legyen a sírjára virág.
Most pedig teszteljük le az egyik Writer-tömbműveletet! A fenti kódot úgy módosítjuk, hogy 25 karakteres blokkméretenként másolja át a szövegtartalmat. Ehhez az egyik beépített függvényt használjuk fel (read(karakterTomb, 0 blokkMeret). A függvény visszatérési értéke a karakterTombben tárolt karaktermennyiség, -1 ha üres (nincs benne karakter). A látható végeredményhez azonban (itt 25 karakteres sorhosszak) a Kertész leszek című fájlt úgy kell módosítanunk, hogy ne legyen benne semmilyen új sor vagy sortörés:
Kertész leszek, fát nevelek,kelő nappal én is kelek,nem törődök semmi mással,csak a beojtott virággal.Minden beojtott virágomkedvesem lesz virágáron,ha csalán lesz, azt se bánom,igaz lesz majd a virágom.Tejet iszok és pipázok,jóhíremre jól vigyázok,nem ér engem veszedelem,magamat is elültetem.Kell ez nagyon, igen nagyon,napkeleten, napnyugaton -ha már elpusztul a világ,legyen a sírjára virág.
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
int blokkMeret = 25;
int hossz = blokkMeret;
char[] karakterTomb = new char[blokkMeret];
FileReader reader = null;
reader = new FileReader("D:\\Kertész leszek.txt");
PrintWriter writer = new PrintWriter(new FileWriter("D:\\Hello.txt"));
BufferedReader br = new BufferedReader(reader);
while(hossz != -1){
hossz = br.read(karakterTomb, 0, blokkMeret);
if(hossz == -1){
break;
}
if (hossz == blokkMeret) {
System.out.println(karakterTomb);
writer.println(karakterTomb);
} else {
for(int i = 0; i < hossz; i++) {
System.out.print(karakterTomb[i]);
writer.print(karakterTomb[i]);
}
}
}
writer.close();
reader.close();
}
}
Végeredmény:
Kertész leszek, fát nevel
ek,kelő nappal én is kele
k,nem törődök semmi mássa
l,csak a beojtott virágga
l.Minden beojtott virágom
kedvesem lesz virágáron,h
a csalán lesz, azt se bán
om,igaz lesz majd a virág
om.Tejet iszok és pipázok
,jóhíremre jól vigyázok,n
em ér engem veszedelem,ma
gamat is elültetem.Kell e
z nagyon, igen nagyon,nap
keleten, napnyugaton -ha
már elpusztul a világ,leg
yen a sírjára virág.
Az utolsó sor menedzselését nem túl elegáns, bár hatékony módon oldottuk meg: mivel az utolsó sor hossza nagy valószínűséggel kisebb lesz 25-nél, ahhoz, hogy a karakterTomb-ből csak a hasznos karaktereket kaphassuk vissza (egyébként ilyen sorokkal szembesülnénk)...
yen a sírjára virág.g,leg
...ezért for ciklusba téve külön kell foglalkoznunk vele:
for(int i = 0; i < hossz; i++) {
System.out.print(karakterTomb[i]);
writer.print(karakterTomb[i]);
}
}
A felmerült probléma beépített függvények segítségével másképpen is megoldható, ekkor az Arrays osztály egyik beépített metódusát használjuk fel az utolsó sor másolásához (Arrays.copyOf(karakterTomb, hossz)). A függvény használatát kötelező importálás előzi meg (import java.util.Arrays;):
import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
int blokkMeret = 25;
int hossz = blokkMeret;
char[] karakterTomb = new char[blokkMeret];
FileReader reader = null;
reader = new FileReader("D:\\Kertész leszek.txt");
PrintWriter writer = new PrintWriter(new FileWriter("D:\\Hello.txt"));
BufferedReader br = new BufferedReader(reader);
while(hossz != -1){
hossz = br.read(karakterTomb, 0, blokkMeret);
if(hossz == -1){
break;
}
if (hossz == blokkMeret) {
System.out.println(karakterTomb);
writer.println(karakterTomb);
} else {
for(int i = 0; i < hossz; i++) {
System.out.println(Arrays.copyOf(karakterTomb, hossz));
writer.print(Arrays.copyOf(karakterTomb, hossz));
}
}
}
writer.close();
reader.close();
}
}
Végeredmény:
Kertész leszek, fát nevel
ek,kelő nappal én is kele
k,nem törődök semmi mássa
l,csak a beojtott virágga
l.Minden beojtott virágom
kedvesem lesz virágáron,h
a csalán lesz, azt se bán
om,igaz lesz majd a virág
om.Tejet iszok és pipázok
,jóhíremre jól vigyázok,n
em ér engem veszedelem,ma
gamat is elültetem.Kell e
z nagyon, igen nagyon,nap
keleten, napnyugaton -ha
már elpusztul a világ,leg
yen a sírjára virág.
A fenti példákban a bemeneti és kimeneti adatok azért egyeznek, mert a bemeneti és kimeneti karakterkódolás azonos volt. Ez az azonosság az alapértelmezés, bár ne lepődjünk meg, ha néha ilyen jellegű hibákba futunk bele: a magam részéről már többször kellett karakterkódolási hibaelhárítással foglalkoznom az Eclipse környezeten belül. Most nézzünk egy érdekes példát a különböző input-output karakterkódolási ütközésekre!
Kis elméleti bevezető:
A java.io csomagon belül az InputStreamReader és OutputStreamWriter osztályok olyan különleges karaktercsatornák, amelyek "legalul" bájtcsatornát vesznek igénybe karaktereik továbbítására. A bájtinformáció továbbítása után a karakterek automatikusan Unicode-szabvány szerint konvertálódnak vissza karakterekké. Az is már említésre került a Karakter című fejezetben, hogy a Java-rendszeren belül a karakterek int típusú számok formájában vannak tárolva, ebből következően lehetőségünk van a karaktereket int formátumban is megadni.
Az alábbi futtatható Java-kód ezt fogja bebizonyítani, illetve azt is megmutatja, hogy a különböző kódtábla-beállítások garantáltan zavart okoznak a karakterkódolásban. A ÷ karakter az ISO Latin-1 kódtáblában (8859_1) 247-es kódú, ám az ISO Latin/héber kódtáblában (8859_8) már 186-os:
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
char karakter = '÷';
int karakterKod = (int) karakter;
System.out.println(karakter);
System.out.println(karakterKod);
PipedInputStream inputStream = new PipedInputStream();
PipedOutputStream outputStream = new
PipedOutputStream(inputStream);
Reader reader = new InputStreamReader(inputStream, "8859_1");
Writer writer = new OutputStreamWriter(outputStream,
"8859_8");
writer.write(karakter);
writer.flush();
System.out.println(reader.read());
}
}
Végeredmény:
÷
247
186
A különböző kódtáblák voltaképpen az Unikód-tábla egy-egy nyelvspecifikus részhalmaza, például latin/görög, latin/héber kódtábla. Bár az Unikód-tábla az összes lehetséges betűt tartalmazza, ám a legtöbb kódtábla-hivatkozás (például: (inputStream, "8859_1");) annak csak egy részhalmazát határozza meg. Így lehetséges az, hogy hibával szembesülünk, ha a kódtábla által nem definiált betűt használunk fel. Az alábbi futtatható Java-kódban a magyar ő betűt használjuk fel, amit a pontatlan kódtábla-definíció miatt a rendszer nem tud értelmezni:
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
String szo = "piperkőc";
System.out.println(szo);
Writer writer = new OutputStreamWriter(System.out, "8859_1");
writer.write(szo);
writer.close();
}
}
Végeredmény:
piperkőc
piperk?c
A megoldás a helyes kódtábla beállítása (itt 8859_2 használata):
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
String szo = "piperkőc";
System.out.println(szo);
Writer writer = new OutputStreamWriter(System.out, "8859_2");
writer.write(szo);
writer.close();
}
}
Végeredmény:
piperkőc
piperkőc
Összességében megállapíthatjuk, hogy a csatornák adatátviteli útvonalat biztosítanak A pontból B pontba. A fenti példákban egy dedikált fájl éppúgy lehetett adatforrás, mint adatnyelő. Ám az adatátvitelnek van egy további fontos, szabályozó aspektusa is: jogosultság adatforrás-adatnyelő elérésére, módosítására. Ezzel java.io csomagon belül a FilePermission osztály foglalkozik.