Всички операции, които са свързани с достъпване на ресурси извън програмата, се наричат входно-изходни операции(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() ще доведе до печатане на пълния път на изключението в конзолата.
След като успешно сме създали файл, трябва да видим как се пише и чете от него. Нека се заемем с първата операция. Какво трябва да се направи, за да се пише във файл? Трябва да се отвори изходен поток, след което да се инициализира „писец“ до файла и да се запише желаното в него чрез методите, които писецът предлага.
Потоците от данни и класовете за работа с тях са дадени по-долу:
Байтови потоци(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(); } } } } }
Явор Томов, Даниел Джолев
Много добра статия, поздравления! Но в един от примерите откривам логическа грешка. Става въпрос за “public class BuffCloseExample”,
в try блока декларираме един стринг line, в който стринг слагаме прочетения ред от съответния файл. По- късно използваме същия стринг за
проверка на входа на while цикъла за четене, но не следва принтиране, а взимаме следващия ред и го задаваме на line, след това следва принтиране.
Дотук сме принтирали само втория ред на файла, а сме прочели първите два. По- добрият вариант е да се принтира първо, а след това да се взима следващия ред.