C, PHP, VB, .NET

Дневникът на Филип Петров


* Вход и изход – работа с файлове

Публикувано на 14 септември 2009 в раздел ПИК3 Java.

Вече се запознахме със стандартните вход и изход като байтови потоци. Те всъщност са наследници на абстрактните класове InputStream и OutputStream. Двата класа FileInputStream и FileOutputStream също ги наследяват. Нека разгледаме един пример за програма, която чете информация от един файл и я записва в друг:

    java.io.FileInputStream in = null;
    java.io.FileOutputStream out = null;
    try {
      in = new java.io.FileInputStream("input.txt");
      out = new java.io.FileOutputStream("output.txt");
      int readbyte;

      while ((readbyte = in.read()) != -1) {
        out.write(readbyte);
      }
    }
    catch(java.io.IOException e){
      System.err.println("Error reading file");
    }
    finally{
      try{
        if (in != null) in.close();
        if (out != null) out.close();
      }
      catch(java.io.IOException e){}
    }

FileInputStream и FileOutputStream може да се използват за четене и запис на всякакви видове файлове. Въпреки това по конвенция е добре да ги използваме само за бинарни файлове. За четене и запис на символни файлове използваме FileReader и FileWriter (наследници на абстрактните Reader и Writer), които работят със символи. Що се отнася до текстови файлове – те работят по един и същи начин. Ето горния пример реализиран чрез FileReader и FileWriter:

    java.io.FileReader in = null;
    java.io.FileWriter out = null;
    try {
      in = new java.io.FileReader("input.txt");
      out = new java.io.FileWriter("output.txt");
      int readbyte;

      while ((readbyte = in.read()) != -1) {
        out.write(readbyte);
      }
    }
    catch(java.io.IOException e){
      System.err.println("Error reading file");
    }
    finally{
      try{
        if (in != null) in.close();
        if (out != null) out.close();
      }
      catch(java.io.IOException e){}
    }

По конвенция когато работим с текстови файлове ще използваме FileReader и FileWriter. На практика символните потоци използват байтови и един вид ги „надграждат“. Основна разлика между тях е, че InputStream и OutputStream четат/пишат по 8 бита (числа от -128 до 127), а Reader и Writer четат/пишат char (до 16 бита). Това не означава, че Reader и Writer работят с по два байта едновременно. Всичко зависи от кодировката на файла.

Има смесени типове потоци. Това са InputStreamReader и OutputStreamWriter. Те се използват за специални случаи като например текстови файлове с нестандартна кодировка. Също така се използват предимно при правене на мрежови връзки, които пренасят смесена информация. Засега няма да ги разглеждаме подробно.

Примерите от по-горе се наричат „небуферирани“ потоци. Това означава, че използват директен достъп до информацията така, както е поднесена от операционната система. Това е изключително неефективно особено при честа работа с информацията, защото предизвиква много дискови операции. Затова по-често ще използваме т.нар. „буферирани“ потоци. При тях се заделя място в паметта, в което се записват големи части от информацията, която четем. По този начин ние намаляваме обръщенията към диска. Ето горния пример със символните потоци написан чрез буфериран поток:

    java.io.BufferedReader in = null;
    java.io.BufferedWriter out = null;
    try {
      in = new java.io.BufferedReader(new java.io.FileReader("input.txt"));
      out = new java.io.BufferedWriter(new java.io.FileWriter("output.txt"));
      int readbyte;

      while ((readbyte = in.read()) != -1) {
        out.write(readbyte);
      }
    }
    catch(java.io.IOException e){
      System.err.println("Error reading file");
    }
    finally{
      try{
        if (in != null) in.close();
        if (out != null) out.close();
      }
      catch(java.io.IOException e){
        System.err.println("System error occured");
        return;
      }
    }

Аналогично за байтови потоци можете да използвате BufferedInputStream и BufferedOutputStream. Няма да даваме същия пример с тях, защото е напълно аналогичен.

Накрая за файлови потоци ще посочим т.нар. „потоци от данни“ или „Data Streams“. Това са бинарни потоци от данни, които се използват за записване на различни типове информация. Изключително удобни са когато искаме да четем и пишем комбинирана информация от числа и символи. Ето един пример как можем да запишем един int, последван от double и текст:

    java.io.DataOutputStream out = null;
    try{
      out = new java.io.DataOutputStream(
                          new java.io.BufferedOutputStream(
                             new java.io.FileOutputStream("output.txt")
                          )
                       );
      int i = 5;
      double d = 5.5;
      String str = "Hello";

      out.writeInt(i);
      out.writeDouble(d);
      out.writeUTF(str);
    }
    catch(java.io.IOException e){
      System.err.print("Error writing to file");
    }
    finally{
      try{
         if (out != null) out.close();
      }
      catch(java.io.IOException e){}
    }

Ако искаме да прочетем обратно тази информация, то използваме „обратния обект“ за четене:

    java.io.DataInputStream in = null;
    try{
      in = new java.io.DataInputStream(
                          new java.io.BufferedInputStream(
                             new java.io.FileInputStream("output.txt")
                          )
                       );
      System.out.print(in.readInt());
      System.out.print(in.readDouble());
      System.out.print(in.readUTF());
    }
    catch(java.io.IOException e){
      System.err.print("Error reading file");
    }
    finally{
      try{
         if (in != null) in.close();
      }
      catch(java.io.IOException e){}
    }

Специално внимание трябва да обърнем на още един обект за писане във файл. Това е PrintStream(). Чрез него можем да използваме функциите print() и println(), които имат абсолютно същата функционалност както при System.out:

    java.io.PrintStream out = null;
    try {
      out = new java.io.PrintStream(new java.io.FileOutputStream("output.txt"));
      int i = 123;
      out.print("Hello");
      out.println(" world "+i);
    }
    catch (java.io.FileNotFoundException e) {
            System.err.println("FileNotFound");
            return;
    }
    finally {
       if (out!=null) out.close();
    }

Вече сме готови да стигнем до същината на идеята на абстракциите на потоци в Java и да покажем как можем да предефинираме стандартния вход/изход. Ето един пример – пренасочваме System.out.* вместо да изкарва информация на екрана да я записва във файл:

    try {
      System.setOut(new java.io.PrintStream(
                         new java.io.FileOutputStream("output.txt")
                   ));
    }
    catch (java.io.FileNotFoundException e) {
            System.err.println("FileNotFound");
            return;
    }
    System.out.println("Tozi text shte se zapishe vav file");
    System.out.close();

Редно е да се спомене, че System.out и System.err всъщност са обекти от тип PrintStream. Именно поради тази причина можем да ги заменим по показания начин. Същото можем да направим и за стандартния вход. Помните ли какво говорихме за „буфера на клавиатурата“ в миналата статия? Е, след като вече знаете какво е буфериран поток, то можете да се досетите, че System.in може да бъде пренасочен към BufferedInputStream():

    try {
      System.setIn(new java.io.BufferedInputStream(
                       new java.io.FileInputStream("input.txt")
                  ));
      int c = System.in.read();
      while (c!=-1){
        System.out.print((char)c);
        c = System.in.read();
      }
    }
    catch (java.io.IOException e) {
            System.err.println("FileNotFound");
            return;
    }
    finally{
      try{
        if (System.in != null) System.in.close();
      }
      catch (java.io.IOException e){}
    }

Виждате, че на практика четенето и писането в конзолата не е по-различно от това във файлове. По-нататък ще разгледаме и мрежови връзки (sockets) – те също не се различават съществено. Методиката на работа е една и съща.

 



7 коментара


  1. Константин каза:

    Може ли да обясните какво прави следното:
    while ((readbyte = in.read()) != -1) {
    out.write(readbyte);
    За начинаещ въобще не е очевидно. Аз до колкото го разбирам, съдържанието на инпут.тхт се чете байт по байт и се записва в аутпут.тхт… Обаче въобще не съм сигурен че го разбирам правилно?

  2. Точно това прави…

  3. Gas каза:

    Здравейте.
    Следното не мога да разбера логически:
    if (in != null) in.close();
    if (out != null) out.close();

    До колкото разбирам това е : затвори, ако в in има нещо и затвори, ако в out има нещо. И ми се струва, че това противоречи на функцията на програмата. Къде бъркам ?

  4. Това са команди, които в общия случай не се изпълняват въобще. Логиката е „ако нещо е станало и все пак файла не се е затворил – затвори го“.

  5. Kris каза:

    System.setIn(new java.io.BufferedInputStream(
    new java.io.FileInputStream(„input.txt“)
    )); -> след като променим изходния поток, след това го затворим. Как да го възтановим и да ни изкарва на екрана?

  6. Kris каза:

    объркал съм кода –> System.setOut(new java.io.PrintStream(
    new java.io.FileOutputStream(„output.txt“)
    ));

  7. Направете следното – в началото на програмата си запишете оригиналния входен поток:

    InputStream stdInput = System.in;

    След това може да го промените. Когато желаете да го възстановите направете:

    System.setIn(stdInput);

    Не знам за друг начин. При System.out е аналогично.

Добави коментар

Адресът на електронната поща няма да се публикува


*