Работа с файлове. Четене и писане. Обработка и употреба на файлове.

Всички операции, които са свързани с достъпване на ресурси извън програмата, се наричат входно-изходни операции(I/O Operations). Освен четене от клавиатура, писане в конзола и тн, четене/писане във файл също е такава операция.

Причината за това е, че файлът обикновено се намира някъде на дисковото пространство на нашия компютър. За да го достъпим ползваме неговия URL адрес. Например : C://Folder/myFile.doc

За да отворим този файл, за да достъпим клавиатурата или конзолата, или каквото и да е друго входно/изходно устройство ни трябва интерфейс, през който да го направим. Улавяме входно/изходния поток от/към него и четем/пишем от/в него.  При такава работа с външни ресурси, най-естествено може да се породи грешка- прекъснат кабел, някой е изтрил или преместил файла, повреда или други проблеми и комуникацията на нашата програма с файла(ресурса) да се наруши. Това обаче не би следвало да наруши работата на програмата и тя да просто да спре да работи. Напротив- трябва тази грешка да се прихване и да се обработи – да се изведе подходящо съобщение на потребителя, или да се предприеме към друга стъпка и тн. Тогава всичко ще бъде наред. Как Java се справя с този проблем?

Решението е Exception Handling. Това е технологията, с която Java прихваща, обработва или просто „изхвърля“ такива събития, които ще наречем „изключения“(exceptions). Ще започнем с декларирането на try-catch блок.

try {
//some code here

} catch (Exception nameOfExc){

//something to be done if exception occurs.

}

Когато даден код може да предизвика изключение, то той се поставя в такъв try-catch блок. Ако по време на изпълнение се предизвика грешка, автоматично се преминава в catch-блока, където е заложена логиката, с която програмата ще продължи. Какви изключения ще може да прихваща този блок зависи от това какво е посочено в скобите след catch. Обикновено е клас, който е или Exception(общо изключение), или някой негов наследник- т.нар. частни изключения. Повече за йерархията на изключенията, за механизмите на обработване и изхвърляне, както и как да създавате собствени изключения, вече прочетохте в предна статия. За настоящата цел, ще трябва да обработваме изключения от тип IOException, SecurityException, NoSuchFileException и FileNotFoundException. Когато не сте сигурни кое изключение да прихванете, винаги може да прихванете изключение от най-общия тип – Exception, макар и да не е най-добра практика.

Създаване на файл:

Един вариант да създадем файл е като работим с класа File. Той съдържа няколко метода за достъп и работа с файлове. Интересува ни конкретно createNewFile(). Той създава нов празен файл, ако такъв не съществува и връща true, ако всичко е наред при създаването. Нека да създадем един файл:

import java.io.File;
import java.io.IOException;
public class CreateFileExample {
public static void main( String[] args ){
   try {
      File file = new File("C:\\newfile.txt");
      boolean isCreated = file.createNewFile();
    if (isCreated){
        System.out.println("File has been created successfully");
      }
   else{
System.out.println("File already present at the specified location");
        }
       } catch (IOException e) {
System.out.println("Exception Occurred:");
e.printStackTrace();
   }
 }
}

Както виждате ако нещо се обърка при създаването на файла, то ще възникне IOException. Извикването на метода printStackTrace() ще доведе до печатане на пълния път на изключението в конзолата.

След като успешно сме създали файл, трябва да видим как се пише и чете от него. Нека се заемем с първата операция. Какво трябва да се направи, за да се пише във файл? Трябва да се отвори изходен поток, след което да се инициализира „писец“ до файла и да се запише желаното в него чрез методите, които писецът предлага.

pik1

Потоците от данни и класовете за работа с тях са дадени по-долу:

pik2

Байтови потоци(Byte Streams):

Байтовите потоци се използват, за да трансферират байтове от данни(8-bit bytes). Най-често се използват FileInputStream и FileOutputStream. Обикновено, за да се инициализира такъв поток, му е нужно име на файл. Те позволяват директно четене и писане във файла.

FileInputStream in = new FileInputStream("input.txt");

Нека да прочетем съдържанието на един файл и да го запишем в друг:

import java.io.*;
public class CopyFile {
public static void main(String args[]) throws IOException   {  
FileInputStream in = null;
FileOutputStream out = null;
 try {
   in = new FileInputStream("input.txt");
   out = new FileOutputStream("output.txt");
   int c;
   while ((c = in.read()) != -1) {
     out.write(c); 
          }
      }
finally {
if (in != null) {
    in.close();        
   }
if (out != null) {
   out.close();
   }
  }
 }
}

Отварянето на поток и ползването му е рискова операция- може да предизвика изключение, затова се поставя в try-catch блок. Ако даден поток е отворен, то след като приключим с него е редно да го затворим. Затова try-catch блока обикновено се разширява с finally –блок, непосредствено след catch. В него се извиква метод close() на отворения по-рано поток.

Знакови потоци(Character Streams):

Тези потоци се използват за предаване на 16-битови данни във формат Unicode. Най-популярните сред тях са FileReader I FileWriter. Вътрешно FileReader-а , за да получи достъп до файла отваря поток с FileInputStream, но главната разлика е, че FileReader и FileWriter чете/пише по 2 байта наведнъж, за разлика от байтовите потоци. Можете да пренапишете горния пример като замените просто използваните потоци.

FileInputStream и FileOutputStream

Можем да създадем поток за четене на файл, като използваме конструктор, приемащ като аргумент стринг- името на файла.

InputStream f = new FileInputStream("C:/java/hello");

Друг начин да отворим файла и да четем от него  е като създадем обект File и после го подадем на конструктора FileInpuStream(File f), който приема като аргумент файл.

File f = new File("C:/java/hello");
InputStream f = new FileInputStream(f);

Нека разгледаме един пример:

import java.io.*;

public class FileExample{

   public static void main(String args[]){
   
   try{
      byte[] writeArr = {1,2,3,4,5};
      OutputStream os = new FileOutputStream("test.txt");
      for(int i = 0; i < writeArr.length ; i++){
         os.write( writeArr[i] ); 
      }
      os.close();
     
      InputStream is = new FileInputStream("test.txt");
      int size = is.available();

      for(int i=0; i< size; i++){
         System.out.print((char)is.read() + "  ");
      }
      is.close();
   }catch(IOException e){
      System.out.print("Exception");
   }	
   }
}

Буферирани и небуферирани потоци:

Буферираните потоци позволят, както подсказва името им, да се буферират операциите на Reader-а, който изпозлваме. Това означава, че няма да се чете символ по символ директно от дестинацията, а по-големи порции данни на един път. Тези порции се записват във временна памет и после се чете не от оригиналния ресурс, а от буфера. Това значително забързва IO операциите.

Четене от файл с BufferedReader:

Java предоставя различни имплементации за реализация на буферирано четене и писане. Класът BufferedReader е почти подобен на класът BufferedInputStream. Първият чете символи – текст, а вторият чете байтове. За направим един Reader буфериран, просто трябва да го “обвием” в BufferedReader.

BufferedReader buffReader = new BufferedReader(new FileReader("D:\\someFolder\\someFile.txt"));

В горния пример buffReader-а чете блок от символи от FileReader-а, обикновено от char[]. Когато извикваме метод read(), всеки знак се чете не директно от файла, а от този масив(буфер). Когато този масив се изчете напълно, той се пълни с нова порция информация и тн.

Можем да променим размера на буфера, от който четем. За целта класът предоставя конструктор, с подаден допълнителен параметър за размер на буфера. Нека покажем как става това:

int buffSize = 5 * 1024;

BufferedReader buffReader= new BufferedReader(new FileReader("D:\\someFolder\\someFile.txt"),
                      bufferSize);

С горния пример дефинирахме buffReader, който е с размер 5 KB. Препоръчително е размерът на буфера да е число, кратно на 1024.

Работата с BufferedReader не е по-сложна от тази с Reader. Първият има добавен метод readLine(), който връща целия текст от всеки ред(докато намери знак за край на реда- \n). Методът чете ред по ред и като стигне край, връща null.

Пример:

public class BuffExample{
public static void main(String[] args) {
   BufferedReader br = null;
   try {

      String currentLine;

      br = new BufferedReader(new FileReader("D:\\someFile.txt"));

      while ((currentLine = br.readLine()) != null) {
       System.out.println(currentLine);
      }

    } catch (IOException e) {
         e.printStackTrace();
    } finally {
        try {
            if (br != null)br.close();
         } catch (IOException ex) {
            ex.printStackTrace();
     }
   }
  
 }
}

Затваряне на BufferedReader.

С пускането на Java 7 се въвежда т.нар. try-with-resources statement. Това е try- блок, който  приема като аргумент resource. Resource е такъв обект, който след като програмата завърши изпълнението си, трябва да бъде затворен. Всички класове, чиито инстанци могат да бъдат затваряни, имплементират интерфейса  AutoCloseable . BufferedReader-a е от класовете, които имплементират интерфейса AutoCloseable, така че можем да го използваме и без изрично да пишем finally блок, в който да го затваряме. Нека демонстрираме с пример:

public class BuffCloseExample{
 public static void main(String[] args) throws IOException {

  Reader reader = new FileReader("D://someFile.txt");

   try(BufferedReader buffReader =
           new BufferedReader(reader)){

     String line = buffReader.readLine();
     while(line != null) {
         line = buffReader.readLine();
         System.out.println(line);
     }
   }
 }
}

Затварянето на BufferedReader-a е съпроводено със затваряне и на Reader-а.

Писане във файл с BufferedWriter:

Аналогично на класа BufferReader, BufferWriter предоставя възможност за буфериране на Writer и забързване на IO операциите, като вместо символ по символ, ще записва цели парчета символи на един път в диска или в друго пространство памет. Ето едно примерно инстанциране на BufferWriter, като показваме, че той също има конструктор  с допълнителен параметър за заделяне на памет за буфера му.

int bufferSize = 5 * 1024;
BufferedWriter buffWriter = new BufferedWriter(new FileWriter("D:\\someFolder\\someFile.txt"),
            bufferSize);

Като функционалност, предоставя почти същите методи като на Writer, но и добавя нови – например newLine(), който, както подсказва името му,  добавя знак за нов ред. За да сме абсолютно сигурни, че всички записани в буфера символи ще се запишат в дестинацията, за която са предназначени, то можем да извикваме метод flush().

Пример:

 

public class BuffWriterExample {
    public static void main(String[] args) {
      BufferedWriter buffWriter= null;
      FileWriter fileWriter = null;
      try {
         String textToBeWritten = "Some text..";
         File file = new File("D://someNewFile.txt");
         fileWriter = new FileWriter(file.getAbsoluteFile());
         buffWriter= new BufferedWriter(fileWriter);
         buffWriter.write(textToBeWritten);
         buffWriter.flush();
         System.out.println("Ready!");
      } catch (IOException e) {
           e.printStackTrace();
      }finally{
        if(buffWriter!=null ){
            try {
                buffWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
               }
        }if(fileWriter != null){
            try {
               fileWriter.close();
            } catch (IOException e) {
               e.printStackTrace();
            }
         }
       }
   }
}


 

Явор Томов,  Даниел Джолев

 

One thought on “Работа с файлове. Четене и писане. Обработка и употреба на файлове.”

  1. Много добра статия, поздравления! Но в един от примерите откривам логическа грешка. Става въпрос за “public class BuffCloseExample”,
    в try блока декларираме един стринг line, в който стринг слагаме прочетения ред от съответния файл. По- късно използваме същия стринг за
    проверка на входа на while цикъла за четене, но не следва принтиране, а взимаме следващия ред и го задаваме на line, след това следва принтиране.
    Дотук сме принтирали само втория ред на файла, а сме прочели първите два. По- добрият вариант е да се принтира първо, а след това да се взима следващия ред.

Leave a Reply

Your email address will not be published. Required fields are marked *