Kihagyás

I/O

Az előadás videója elérhető a itt.

A Java I/O rendszer

Ahogy azt láttuk a C nyelvnél, maga az input/output kezelés nem szerves része a nyelvnek. Az, hogy ezt hogy oldják meg, azonban nagyban függ a nyelv lehetőségeitől is. A Javaban számtalan megvalósítással találkozunk, ami az I/O kezelését teszi lehetővé.

A kezdetek kezdetén a Java 1.0-ás verziónál byte alapú volt az I/O adatok kezelése, amit az InputStream/OutputStream osztályokkal valósítottak meg. Ezt egészítették ki a karakter-orientált Reader/Writer osztályok, amelyek ráadásul már unicode karaktereket is támogattak. Ahhoz, hogy valóban értsük, hogyan is valósul meg a Java I/O feldolgozása egy csomó függvénykönyvtár működését meg kell értenünk és ismernünk.

A File osztály

Mielőtt ténylegesen megismerkednénk azokkal az osztályokkal, amik az adatfolyamokat írják/olvassák, nézzünk meg egy olyan osztályt, ami segít a fájl kezelési feladatok megoldásában. A File osztállyal fogunk most foglalkozni, aminek a neve egy kicsit ugyan félrevezető, mert nem egyértelműen csak fájlokkal foglalkozik, inkább az elérési útvonalakkal: definiálhat ez egyetlen fájlt is, vagy fájlok nevének egy halmazát egy adott könyvtárban. De nézzük inkább a példán keresztül:

import java.io.*;
import java.util.*;

class DirFilter implements FilenameFilter {
  String s;
  DirFilter(String s) {this.s = s;}
  // callback függvény:
  public boolean accept(File dir, String name) {
    // path torlese:
    String f = new File(name).getName();
    // resz-string?
    return f.indexOf(s) != -1;
  }
}

public class DirList {
  public static void main(String[] args) {
    File path = new File(".");
    String[] list;
    if (args.length == 0)
      list = path.list();
    else 
      list = path.list(new DirFilter(args[0]));
    for(int i = 0; i < list.length; i++)
      System.out.println(list[i]);
  }
}

Ha a DirList programot parancssori argumentum nélkül futtatjuk, akkor az kilistázza az aktuális könyvtár tartalmát:

Kimenet

DirFilter.class
DirList.class
DirList.java

Magát a File objektum konstruktorát a "." sztring paraméterrel hívjuk, amely az aktuális könyvtárra hivatkozik. A létrehozott File objektum list() metódusa pedig visszaadja egy String tömbbe az aktuális könyvtár elemeinek nevét.

Ha a parancssori argumentumban megadunk egy szöveget (mondjuk egy kiterjesztést), akkor csak azok az állományai lesznek kilistázva az adott könyvtárnak, amelyek tartalmazzák a parancssori argumentumban adott sztringet. Tehát ha pl. a java DirList .class paranccsal futtatjuk, akkor a kimenet a következő:

Kimenet

DirFilter.class
DirList.class

Ehhez a list metódusnak egy olyan objektumot kellett átadni paraméterként, amelynek osztálya implementálja a FilenameFilter interface-t, aminek az accept metódusa mondja meg, hogy mely elemeket kell listázni. Itt épp azokat, amik tartalmazzák azt a sztringet, amivel meghívtuk a DirFilter konstruktorát. A list metódus ezt az accept metódust fogja "vissza hívni" (call back módon hívni), és eldönteni, hogy a könyvtár mely elemeire kíváncsi a felhasználó.

A File objektumok segítségével azonban nemcsak listázni tudjuk egy-egy könyvtár tartalmát, de akár törölni, vagy létrehozni is tudunk állományokat, illetve mindenféle hasznos információt is elérhetünk a tartalmakról.

import java.io.*;

public class MakeDirectories {
  public static void main(String[] args) {
    if (args.length < 1) System.exit(1);
    File f = new File(args[0]);
    System.out.println(
      "Abszolut utv.:\t" + f.getAbsolutePath() +
      "\nNev:\t\t" + f.getName() +
      "\nHossz:\t\t" + f.length() +
      "\nModositva:\t" + f.lastModified());
    if (f.isFile()) System.out.println("fajl");
    else if(f.isDirectory()) System.out.println("konyvt.");
    if (f.exists()) {
      System.out.println(f + " letezik, toroljuk");
      f.delete();
    } else {
      System.out.println("letrehozas... " + f);
      f.mkdirs();
    }
  }
}

Futtassuk ezt a programot úgy, hogy kiválasztunk az aktuális könyvtárunkban egy olyan állományt, amit nem sajnálunk törölni! Pl.: java MakeDirectories proba.java

Ekkor a program a következő kimenetet adja:

Kimenet

Abszolut utv.: D:\prog1\proba.java
Nev: proba.java
Hossz: 667
Modositva: 1080123982328
fajl
proba.java letezik, toroljuk

Most futtassuk valami olyan argumentummal, ami nem létezik az aktuális könyvtárunkban! Pl.: java MakeDirectories proba

Ekkor a kimenet:

Kimenet

Abszolut utv.: D:\prog1\proba
Nev: proba
Hossz: 0
Modositva: 0
letrehozas... proba

És futtassuk le mégegyszer ugyanezt a parancsot!

Kimenet

Abszolut utv.: D:\prog1\proba
Nev: proba
Hossz: 0
Modositva: 1080124219296
konyvt.
proba letezik, toroljuk

Input és output

Az I/O könyvtárak olyan objektumokat, streameket, használnak, amelyek valamilyen adatforrást (input), vagy adatnyelőt (output) jelképeznek. Maga a stream elrejti azokat a részleteket, hogy mi is történik az adatokkal az I/O eszközön belül.

A Java I/O osztályokat (is) tehát két nagy részre bonthatjuk, bejövő és kimenő adatokat kezelő részekre. Öröklődés szempontjából minden az InputStream vagy Reader, valamint az OutputStream vagy Writer osztályokból származik, amelyek read, illetve write metódusait használjuk arra, hogy byte-okat, vagy byte-ok tömbjét beolvassuk, kiírjuk. Persze általában nem fogjuk ezeket az osztályokat direktben használni, lesznek más osztályok, amelyek kényelmesebb interface-t biztosítanak a használatukhoz. Így általában a kényelmes I/O kezeléshez több objektumot kell majd egymásba ágyazva használni, ami talán egy kicsit kusza lehet az egész I/O folyamat megértése során.

InputStream típusai (adatforrások)

A bejövő adatok különböző típusúak lehetnek, és ez meghatározhatja azt a konkrét InputStreamet, ami fel tudja ezeket dolgozni:

Input adat fajtája Feldolgozó InputStream
bájtok tömbje ByteArrayInputStream
string objektum StringBufferInputStream
fájl FileInputStream
cső (pipe) PipedInputStream
más adatforrás folyamok sorozata (összegyűjtése) SequenceInputStream

Érdekes kivétel az InputStream osztályok gyerek osztályai között a FilterInputStream. Ez a különböző InputStreameket dekoráló osztályok őse lesz, amely különböző egyéb attribútumokat, vagy hasznos interface-eket képes az input streamekhez kapcsolni majd. A dekorátor tervezési minta megismerésénél ennek szerepe is érthetőbbé válik majd.

OutputStream típusai (adatnyelők)

Hasonlóan a kimeneti adatokat is különböző adatnyelő osztályok kezelik le a kimenet típusától függően:

Output adat fajtája Feldolgozó OutputStream
bájtok tömbje ByteArrayOutputStream
string objektum --
fájl FileOutputStream
cső (pipe) PipedOutputStream

A FilterOutputStream pedig az OutputStream dekorátor ősosztály lesz.

Filter input stream

Önmagában a InputStream csak egyszerű read metódusokat tartalmaz, amik byte-okat, byte tömböket tud olvasni. Azonban a kényelem azt igényli, hogy adott esetben tudjunk egy egészet, vagy egy akármilyen számot, vagy egy szringet, stb... olvasni. És ez persze a sokféle InputStream osztály valamennyiénél lehet igényünk. Ha létre akarnánk hozni speciális alosztályokat, amik ezeket az igényeket külön-külön kielégítenék, akkor nagyon sok kombinációt kellene végigjárnunk, ami nagyon sok alosztályhoz vezetne. Ilyen helyzetekben lehet jó a dekorátor tervezési minta használata.

A FilterInputStream osztály egy olyan ősosztályt ad, ami az InputStreamek dekorálására szolgál. Mint ilyen, származnia kell az InputStream osztályból. A speciális esetei pedig olyan megoldásokat fognak nyújtani, amik megkönnyítik a különböző típusú adatok beolvasását.

Speciális FilterInputStream Cél
DataInputStream primitív adattípusok olvasása (String is), pl. readByte(),readFloat(), stb.
BufferedInputStream pufferelt adatfolyam
LineNumberInputStream figyeli a beolvasott sorok számát (getLineNumber, setLineNumber)
PushbackInputStream a legutóbb olvasott karaktert vissza lehet tenni az adatfolyamba

Filter output stream

A FilterInputStream mintájára a kimenetek dekorálására is adott egy dekorátor ősosztályunk, a FilterOutputStream. Ennek a dekorátor tervezési mintából adódóan pedig származnia kell az OutputStream osztályból. Alesetei:

Speciális FilterOutputStream Cél
DataOutputStream primitív adattípusok írása (String is), pl. writeByte(),
BufferedOutputStream pufferelt adatfolyam (flush())
PrintStream formázott kimenet (print(), println())

Reader/Writer osztályok

A Java 1.1 szignifikáns módosításokat hozott az alap I/O stream könyvtárakba. A Java 1.1-ben bevezetett Reader és Writer osztályok a byte alapú olvasást/írást karakter alapúval egészíti ki, és ami még fontosabb, támogatva az unicode karaktereket.

A Java 1.1 azonban nem cseréli le a korábbi megoldást, inkább kiegészíti. Ehhez a két megoldás közé konvertáló osztályokat hoz be, mint amilyen az InputStreamReader, OutputStreamWriter.

Megfeleltetés

Majdnem minden eredeti I/O stream osztálynak megvan a megfelelő Reader, illetve Writer osztálya, ezeket foglalja össze az alábbi táblázat

Java 1.0 osztályok Java 1.1 osztályok
InputStream Reader
OutputStream Writer
FileInputStream FileReader
FileOutputStream FileWriter
StringBufferInputStream StringReader
ByteArrayInputStream CharArrayReader
ByteArrayOutputStream CharArrayWriter
PipedInputStream PipedReader
PipedOutputStream PipedWriter
FilterInputStream FilterReader
FilterOutputStream FilterWriter
BufferedInputStream BufferedReader
BufferedOutputStream BufferedWriter
DataInputStream ---
PrintStream PrintWriter
LineNumberInputStream LineNumberReader
PushBackInputStream PushBackReader

A RandomAccessFile osztály

A RandomAccessFile egyike azoknak a Java 1.0 megoldásoknak, amik nem változtak meg a Java 1.1 újításaival. Ez a fájl nem része az adatfolyam öröklési hierarchiának, mivel kicsit olyan, mintha a két ág (input/output) keveréke lenne. A RandomAccessFile osztályt olyan fájlok kezelésére használjuk, amelyek tartalmának szerkezetét ismerjük, azaz tudjuk, hogy benne az egyes adatok (rekordok) hol kezdődnek, mekkorák. Ezeken ugrálva tetszőleges adatot elérhetünk ezekben a fájlokban, amiket aztán kiolvashatunk, vagy módosíthatunk. Akár visszafelé is haladhatunk a fájlban. Ezeket a műveleteket a getFilePointer(), seek() és length() metódusok segítik.

Példák

import java.io.*;

public class IOStreamDemo {
  public static void main(String[] args) throws IOException {
    System.out.println("-- fajlbeolvasas soronkent --");
    BufferedReader brf =
      new BufferedReader(
        new FileReader("src/IOStreamDemo.java"));
    String s;
    StringBuilder f = new StringBuilder();
    while((s = brf.readLine())!= null) {
      f.append(s).append("\n");
    }
    brf.close();

    System.out.println("-- input memoriabol --");
    StringReader sr = new StringReader(f.toString());
    int c;
    while ((c = sr.read()) != -1) {
      System.out.print((char)c);
    }

    System.out.println("-- formatalt input memoriabol --");
    try {
      DataInputStream dis =
        new DataInputStream(
          new ByteArrayInputStream(f.toString().getBytes()));
      while (true) {
        System.out.print((char)dis.readByte());
      }
    } catch (EOFException e) {
      System.err.println("Adatfolyam vege");
    }

    System.out.println("-- fajl output --");
    BufferedReader brs =
      new BufferedReader(
        new StringReader(f.toString()));
    PrintWriter pw =
      new PrintWriter(
        new BufferedWriter(
          new FileWriter("IOStreamDemo.out")));
    int lineCount = 1;
    while ((s = brs.readLine()) != null) {
      pw.println(lineCount++ + ": " + s);
    }
    pw.close();

    System.out.println("-- random access fajl irasa/olvasasa --");
    RandomAccessFile raf =
      new RandomAccessFile("IOStreamDemo.dat", "rw");
    for(int i = 0; i < 5; i++) {
      raf.writeDouble(i*1.414);
    }
    raf.seek(3*8);
    raf.writeDouble(47.0001);
    raf.seek(0);
    for(int i = 0; i < 5; i++) {
      System.out.println(i + ". ertek: " + raf.readDouble());
    }
    raf.close();
  }
}

A példa lépésről lépésre bemutatja, hogyan tudunk adatokat beolvasni, illetve kiírni. Mivel a forráskód igen nagy, így darabokban haladva nézzük meg, mit is csinálnak az egyes részek!

5-14. sorok: Olvasásra megnyitunk egy fájl, aminek tartalmát soronként beolvassuk, és a tartalmát eltároljuk egy StringBuilder objektumban. A beolvasáshoz egy BufferedReader-t alkalmazunk (dekorátor) egy FileReader objektummal párosítva. Vagy máshogy megfogalmazva egy fájlt olvasunk be pufferelt módon.

16-21. sorok: a memóriában eltárolt szöveget kiírjuk a standard outputra. Gyakorlatilag kiírjuk az IOStreamDemo.java tartalmát.

23-33. sorok: a memóriában tárolt adatot egy DataInputStream dekorátor objektumba tesszük, majd ezt íratjuk ki bájtonként a standard outputra.

35-47. sorok: IOStreamDemo.out-ba bekerül a forrás állomány úgy, hogy a sorok sorszámozva vannak.

49-61.sorok: egy RandomAccessFile-ba 5 darab double értéket írunk ki, majd egyik értéket (4. értéket) átírjuk. Ehhez a megfelelő helyre kell pozícionáljuk magunkat a fájlba. Ezután a fájl elejéről olvasva kiírjuk az 5 bináris értéket úgy, hogy egyszerűen csak double értékeket kell beolvasni az egyébként bináris fájlból.

Standard I/O

A standard I/O kifejezés a unix fogalomra épül. A bemenetet a standard input adja, amelyet Javaban a System osztály in nevű mezőjén keresztül érjük el, amely egy raw InputStream objektum, azaz hogy arról ténylegesen olvasni tudjuk, a megfelelő dekorátor osztálynak át kell adjuk. A kimenetek, amiket a System out és err attribútumain keresztül érünk el egy-egy PrintStream objektum, amelyekre már közvetlen írhatunk.

Példa standard inputra

Az alábbi példa bemutatja, hagyományosan hogyan tudunk olvasni a standard inputról.

import java.io.*;

public class Echo {
  public static void main(String[] args) throws IOException {
    BufferedReader in =
        new BufferedReader(
          new InputStreamReader(System.in));
    String s;
    // ures sorra kilep
    while ((s = in.readLine()).length() != 0) {
      System.out.println(s);
    }
  }
}

Kimenet

haliho
haliho
hehe
hehe
papagaj
papagaj

Scanner osztály

Ha valakinek ez túl sok volt, akkor nem kell elkeserednie. A Java függvénykönyvtárak számos olyan megoldást kínálnak, amelyek becsomagolják ezeket a lépéseket, és egy kényelmesen használható interface-t biztosítanak a standard input olvasására. Ilyen a Scanner osztály is, ami a primitív típusokat és sztringeket tudja felismerni reguláris kifejezésekkel. Magát az inputot darabokra szedi (alapértelmezetten a szóköz választja el a szavakat), majd az eredmény tokeneknek megfelelő típusú nextTipus hívással a megfelelő típusra konvertálja azokat.

import java.util.Scanner;
import java.io.File;

public class MyScanner {
  public static void main(String args[]) {
     try {
       Scanner sc = new Scanner(new File(args[0]));
       while (sc.hasNextLong()) {
          long number = sc.nextLong();
          System.out.println("Long: " + number); 
       }
       if (sc.hasNext()) {
         System.out.println("Next: " + sc.next());
       }
     } catch (Exception e) {} 
  }
}

Formatált kiíratás

A sztringek tárgyalásakor már szó volt arról, hogy lehetőség van Javaban is olyan format sztringek létrehozására, mint amilyet a C-ben megismertünk. Ezek használata sokszor praktikus lehet a sok sztring konkatenációja helyett.

import java.util.*;

public class Printf {
  public static void main(String[] args) {
    double pi = Math.PI;

    System.out.format("%f%n", pi);       // -->  "3,141593"
    System.out.format("%.3f%n", pi);     // -->  "3,142"
    System.out.printf("%10.3f%n", pi);   // -->  "     3,142"
    System.out.printf(Locale.US, "%10.4f%n", pi); // -->  "    3.1416"

    Calendar c = Calendar.getInstance();
    System.out.format("%tB %te, %tY%n", c, c, c); // -->  "március 19, 2018"
    System.out.format("%tl:%tM %tp%n", c, c, c);  // -->  "12:16 du."
    System.out.format("%tD%n", c);    // -->  "03/19/18"
  }
}

Objektumok szerializálása, mentése

Egy-egy program futása során elképzelhető, hogy szeretnénk a program állapotát, azaz a programban jelen levő objektumok állapotát lementeni, vagy átadni más objektumok számámra. Erre való a szerializálás, amikor az objektumokat bájtok sorozatával ábrázoljuk, és amely bájtsorozatot akár ki is írhatunk.

Ahhoz, hogy egy objektum szerializálható lehessen, implementálnia kell a java.io.Serializable interface-t. Ha van olyan mezője, amit nem kell, nem lehet szerializálni, az a transient jelzővel kell ellátni. Ha az osztályunk gyakran változik még, akkor érdemes az osztálynak egy serialVersionUID nevű mezőt felvenni, és valami egyedi értékkel inicializálni, mely értéket minden változtatás során módosítunk. Ezzel adott esetben tudjuk ellenőrizni, hogy serializált objektumunk betöltéskor kompatibilis-e a rendszerrel.

Legyen adott egy Student osztályunk, melynek objektumait szerializálni szeretnénk:

public class Student implements java.io.Serializable {
  private String name;
  private String address;
  private transient int age;
  public Student(String name, String address, int age) {
    this.name = name;
    this.address = address;
    this.age = age;
  }
  public String toString() {
    return "Student " + name + " " + address +  " " + age;
  }
}

A SerializeDemo programban egy Student objektumot hozunk létre, és írunk ki szerializálva a megfelelő output streammel:

import java.io.*;

public class SerializeDemo {
  public static void main(String [] args) {
    Student e = new Student("Kelemen Ede", "Szeged", 25);
    try {
      ObjectOutputStream out =
        new ObjectOutputStream(
          new FileOutputStream("student.ser"));
      out.writeObject(e);
      out.close();
      System.out.printf("Szerializalva ide: student.ser");
    } catch (IOException i) {
      i.printStackTrace();
    }
  }
}

A kimentet, szerializált objektumot pedig beolvassuk a DeserializeDemo osztályban:

import java.io.*;

public class DeserializeDemo {
  public static void main(String [] args) {
    Student e = null;
    try {
      ObjectInputStream in =
        new ObjectInputStream(
          new FileInputStream("student.ser"));
      e = (Student)in.readObject();
      in.close();
      System.out.println("Deszerializalt Student..." + e);
    } catch (IOException i) {
      i.printStackTrace();
    } catch (ClassNotFoundException c) {
      System.out.println("Student class not found");
      c.printStackTrace();
    }
  }
}

Ha összehasonlítjuk az eredeti, és a betöltött objektum adatait, akkor látjuk, hogy a transient mezőn kívül minden mező eredeti tartalmát sikerült visszaállítani.


Utolsó frissítés: 2024-04-11 07:54:27