Глава 15. Текстови файлове

Автор

Данаил Алексиев

В тази тема...

В настоящата тема ще се запознаем с основните похвати при работа с текстови файлове в Java. Ще разясним какво е това поток, за какво служи и как се ползва. Ще обясним какво е текстов файл и как се чете и пише в текстови файлове. Ще демонстрираме и обясним добрите практики за прихващане и обра­ботка на изключения, възникващи при работата с файлове. Разбира се, всичко това ще онагледим и демонстрираме на практика с много примери.

Съдържание


Потоци

Потоците (streams) са важна част от всяка входно-изходна библиотека. Те намират своето приложение, когато програмата трябва да "прочете" или "запише" данни от или във външен източник на данни – файл, други компютри, сървъри и т.н.

Преди да продължим е важно да уточним, че терминът вход (input) се асоциира с четенето на информация, а терминът изход (output) – със записването на информация.

Какво представляват потоците?

Потокът е наредена последователност от байтове, които се изпращат от едно приложение или входно устройство и се получават в друго прило­жение или изходно устройство. Тези байтове се изпращат и получават един след друг и винаги пристигат в същия ред, в който са били изпратени. Потоците са абстракция на комуникационен канал за данни, който свързва две устройства или програми.

Потоците са основното средство за обмяна на информация в компютърния свят. Чрез тях различни програми достъпват файловете на компютъра, чрез тях се осъществява и мрежова комуникация между отдалечени ком­пютри. За да прочетем или запишем нещо от или във файл, единственото, което трябва да направим, е да отворим поток към дадения файл. Това ни позволява да достъпим данните, записани във файла, и да извършим необходимата ни операция (вход или изход).

Модерните сайтове в Интернет не могат без потоци и така наречения streaming (произлиза от stream - поток), който представлява достъпване на големите мултимедийни файлове по уеб страниците чрез поток. Това позволява техния преглед да започне преди цялостното им сваляне, което повишава бързодействието на страницата.

Основни неща, които трябва да знаем за потоците

Потоците се използват, за да четем и записваме данни от и на различни устройства. Те улесняват комуникацията между програма и файл, прог­рама и отдалечен компютър и т.н.

Потоците са подредени серии от байтове. Не случайно наблягаме на думата подредени. От огромна важност е да се запомни, че потоците са строго подредени и организирани. По никакъв начин не можем да си позволим да влияем на подредбата на информацията в потока, защото по този начин ще я направим неизползваема.

Потоците позволяват последователен достъп до данните си. Отново е важно да се вникне в значението на думата последователен. Може да манипулираме данните само в реда, в който те пристигат от потока. Това е тясно свързано с горното свойство.  Имайте това предвид, когато създа­вате собствени програми. Не можете да вземете първия байт, след това осмия, третия, тринадесетия и така нататък. Потоците не предоставят произволен достъп до данните си, а само последователен. Ако ви се струва по-лесно, може да мислим за потоците като за свързан списък от байтове, в който те имат строга последователност.

За различните ситуации има различни видове потоци. Едни служат за работа с текстови файлове, други – за работа с бинарни (двоични) файлове, трети пък – за работа със символни низове. Различни са и потоците, които се използват при мрежова комуникация. Голямото изобилие от потоци ни улеснява в различните ситуации, но също така и ни затруднява, защото трябва да сме запознати със спецификата на всеки отделен тип, преди да го използваме в приложението си.

Потоците се отварят преди началото на работата с тях и се затварят след като е приключило използването им. Това е нещо абсолютно задължи­телно и не може да се пропусне, поради риск от загуба на данни, повреждане на файла, към който е отворен потока и т.н. – все неприятни неща, които не трябва да допускаме да стават в програмите ни.

Потоците можем да оприличим на тръби, свързващи две точки:

clip_image002

От едната страна "наливаме" данни, а от другата данните "изтичат". Този, който налива данните не се интересува как те се пренасят, но е сигурен, че каквото е налял, такова ще излезе от другата страна на тръбата. Тези, които ползват потоците не се интересуват как данните стигат до тях. Те знаят, че ако някой нещо налее от другата страна, то ще пристигне при тях. Потоците са транспортен канал за данни, както и тръбите.

Потоци в Java – основни класове

В Java класовете за работа с потоци се намират в пакета java.io. Сега ще се концентрираме върху тяхната йерархия и организация.

Можем да отличим два основни типа потоци – такива, които работят с двоични данни и такива, които работят с текстови данни. Ще се спрем на основните характеристики на тези два вида след малко.

Общото между тях е организацията и структурирането им. На върха на йерархията стоят абстрактни класове съответно за вход и изход. Те няма как да бъдат инстанцирани, но дефинират основната функционалност, която притежават всички останали потоци. Съществуват и буферирани потоци, които не добавят никаква допълнителна функционалност, но позволяват работата с буфер при четене и записване на информацията, което значително повишава бързодействието. Буферираните потоци няма да се разглеждат в тази глава, тъй като ние се концентрираме върху обработката на текстови файлове. Ако имате желание, може да се допитате до богатата документация, достъпна в Интернет, или към някой учебник за по-напреднали в програмирането.

Основните класове в пакета java.iо са InputStream, OutputStream, BufferedInputStream, BufferedOutputStream, DataInputStream, DataOutputStream, Reader, Writer, BufferedReader, BufferedWriter, PrintWriter и PrintStream. Сега ще се спрем по-обстойно на тях, разделяйки ги по основния им признак – типа данни, с които работят.

В тази глава в примерите за писане в текстов файл ще ползваме само PrintStream, защото е идеален за работа с текстови файлове и с него се борави изключително лесно.

Всички потоци в Java си приличат и по едно основно нещо – задължи­телно е да ги затворим, след като сме приключили работа с тях. В противен случай рискуваме да навредим на данните в потока или файла, към който сме го отворили. Това ни води и до първото основно правило, което винаги трябва да помним при работа с потоци:

clip_image003

Винаги затваряйте потоците и файловете, с които рабо­тите! Оставянето на отворен поток или файл води до загуба на ресурси и може да блокира работата на други потребители или процеси във вашата система.

Двоични и текстови потоци

Както споменахме по-рано, можем да разделим потоците на две големи групи в съответствие с типа данни, с който боравят, а именно – двоични потоци и текстови потоци.

Двоични потоци

От името им личи, че работят с двоични данни. Сами се досещате, че това ги прави универсални и тях може да ползваме за четене на информация от всякакви файлове (картинки, музикални и мултимедийни файлове, текстови файлове и т.н.). Ще ги разгледаме накратко, защото за момента се ограничаваме до работа с текстови файлове.

Двата основни абстрактни класа, които седят в дъното на йерархията на двоичните потоци са съответно InputStream за вход и OutputStream за изход. Както казахме по-горе, те не могат да се инстанцират, но дефи­нират основната функционалност и се ползват при създаването на другите двоични потоци.

InputStream ни предлага различни методи за четене (четене само на един байт, четене на произволен брой байтове и записването им в масив и т.н.), пропускане на определен брой байтове, проверяване на броя достъпни байтове и, разбира се, метод за затваряне на потока. Обект от този клас може да получим от източник, извиквай съответния му метод.

OutputStream също е абстрактен клас. Той съдържа базовата функцио­налност за записване на информация в даден източник. Основните му методи ни позволяват да записваме различен брой байтове в източника, да прочистваме буфера на потока и, отново, да затваряме потока.

Другите класове за работа с бинарни потоци са BufferedInputStream, BufferedOutputStream, DataInputStream, DataOutputStream. За да можем да създадем обекти от тях, се нуждаем от обект от InputStream или, съответно, OutputStream.

Текстови потоци

Текстовите потоци са много подобни на двоичните, но работят само с текстови данни или, по-точно казано, с данни от символен тип (char). Идеални са за работа с текстови файлове. От друга страна това ги прави неизползваеми при работа с каквито и да е бинарни файлове.

Основните класове за работа с текстови потоци са Reader и Writer. Те са аналогични на основните класове от двоичните потоци. Методите им са същите, но вместо аргументи от тип byte приемат char. Както знаете, символите в Java са Unicode символи, но потоците могат да работят освен с Unicode и с други кодирания.

Съществуват и буферираните варианти BufferedReader и BufferedWriter, които се отличават с по-голямото си бързодействие.

Важно място има и класа PrintWriter, но той не е във фокуса на тази глава. Ако имате желание, може да погледнете документацията на Java API-то или източници в Интернет.

Класът, на когото ще обърнем най-голямо внимание е PrintStream. Той в голяма част се припокрива с PrintWriter, но има някой специфични особености. За да създадем обект от PrintStream класа ни е нужен файл или символен низ с име и път до файла. Той има много полезни методи, като например добре познатите print() и println(). Всъщност System.out не е нищо повече от обект от тип PrintStream. Ето защо боравейки с този клас ще можем да използваме всички методи, с които вече сме добре запознати от работата ни с конзолата. Това, с което PrintStream класа се отличава, е, че скрито от нас той превръща текста в байтове преди да ги запише на желаното място. Леснотата, с която се работи с него и големите му възможности го правят идеален за изпол­зване в примерите, които ще последват по-напред в тази глава.

Четене от текстов файл

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

Java платформата предоставя множество начини за четене от файлове, но не всички са много лесни и интуитивни за използване. Ето защо се спираме на нещо познато за вас – класът java.util.Scanner. Сигурно до сега стотици пъти ви се е налагало да го ползвате за всевъзможни операции. Именно за това считаме, че ще е идеален за случая, защото е най-лесния начин за четене на текстов файл и същевременно сте имали много шансове да го усвоите до съвършенство.

Класът java.util.Scanner за четене на текстов файл

В момента сигурно сте малко объркани. До тук казахме, че четенето и записването в текстови файлове става само и изключително с потоци, а, същевременно, java.util.Scanner не се появи никъде в изброените по-горе потоци и не сте сигурни дали въобще е поток. Наистина, java.util.Scanner не е поток, но може да работи с потоци. Той предоставя най-лесния и разбираем начин за четене от текстов файл като се има предвид, че често до сега сте го използвали за четене на различни неща от конзолата.

Едно от големите предимства на java.util.Scanner е, че не е нужно да има поток, за да бъде създаден. Може да го създадем просто от файл, което значително ни улеснява и намалява вариантите за грешка. При създаването можем да уточним и кодирането. Ето пример как може да бъде създаден обект от класа java.util.Scanner:

// Link the File variable to a file on the computer

File file = new File("test.txt");

 

// Create a Scanner connected to a file and specify encoding

Scanner fileReader = new Scanner(file, "windows-1251");

 

// Read file here...

 

// Close the resource after you've finished using it

fileReader.close();

Първото, което трябва да направим е да създадем променлива от тип java.io.File, която да свържем с конкретен файл от нашия компютър. За целта е нужно само да подадем като параметър в конструктора му името на желания файл. Имайте предвид, че ако файлът се намира в папката на проекта, то можем да подадем само конкретното му име. В противен случай трябва да подадем пълния път до файла.

clip_image003[1]

Не забравяйте при подаване на пълния път до даден файл да направите escaping на наклонените черти, които се използват за разделяне на папките ("C:\\Temp\\test.txt", а не "C:\Temp\test.txt"). По възможност избягвайте пълни пътища и работете с релативни!

Използването на пълен път до даден файл (примерно C:\Temp\test.txt) е лоша практика, защото прави програмата ви зависима от средата и непреносима. Ако я пренесете на друг компютър, ще трябва да коригирате пътищата до файловете, които тя търси. Ако използвате релативен (относителен) път спрямо текущата директория (това е директорията на проекта), вашата програма ще е лесно преносима.

Вече можем да създадем и нашия java.util.Scanner. Като параметри този път подаваме новосъздадената файлова променлива и име на encoding (като String), който желаем да ползваме при прочитането на файла (в този случай използваме windows-1251). Така можем да го ползваме за прочитане на желаната информация. Ако не укажем изрично кодиране, Java използва кодирането по подразбиране в операционната система (което може да е различно на различни компютри).

Ще забележите, че при създаването на нашия Scanner, че Eclipse ви предупреждава за неприхваната изключителна ситуация. За сега изберете опцията просто да добавите throws декларация. За прихващането и обработването на изключителни ситуации при работа с файлове ще стане дума малко по-късно в тази глава, в секцията "Обработка на грешки".

Четене на текстов файл ред по ред – пример

След като се научихме как да създаваме Scanner вече можем да се опитаме да направим нещо по-сложно: да прочетем цял текстов файл ред по ред и да печатаме прочетеното на конзолата. Моят съвет е да използваме възможността на Eclipse за създаване на текстови файлове (десен бутон на мишката върху проекта -> New -> File; за име пишем нещо, което завършва на .txt), за да създадем нашия текстов файл. Така той ще се създаде в самия проект и няма да се налага да подаваме пълния път до него при създаването на файлова променлива. Нека нашият файл изглежда така:

sample.txt

This is our first line.

This is our second line.

This is our third line.

This is our fourth line.

This is our fifth line.

Имаме текстов файл, от който да четем. Сега трябва да създадем файлова променлива, свързана с него, да създадем и Scanner и да прочетем и отпечатаме всички редове. Това можем да направим по следния начин:

FileReader.java

// Link the File variable to a file on the computer

File file = new File("sample.txt");

 

// Next line may throw an exception!

Scanner fileReader = new Scanner(file);

 

int lineNumber = 0;

 

// Read file

while (fileReader.hasNextLine()) {

    lineNumber++;

    System.out.printf("Line %d: %s%n",

        lineNumber, fileReader.nextLine());

}

 

// Close the resource after you've finished using it

fileReader.close();

Сами се убеждавате, че няма нищо трудно в четенето на текстови файлове. Първата част на програмата вече ни е добре позната – създаваме файловата променлива, а след това и Scanner. След това създаваме и една променлива – брояч, чиято цел е да брои и показва на кой ред от файла се намираме в текущия момент.

За същинската част – прочитането на файла ред по ред, while цикъл. За условие за изпълнение на цикъла използваме метода на класа Scanner hasNextLine(). Той проверява дали има следващ достъпен ред или е достигнат края на файла и връща резултата от тип boolean. Съществуват подобни методи за много от Java типовете. В тялото на цикъла задачата ни се свежда до това да увеличим стойността на променливата – брояч с единица и след това да отпечатаме текущия ред от файла в желания от нас формат. За целта използваме един метод, който ни е отлично познат от задачите, в които се е изисквало да се прочете нещо от конзолата – nextLine().

След като сме прочели нужното ни от файла, отново не бива да забравяме да затворим Scanner обекта, за да избегнем загубата на ресурси. За това ползваме метода close().

clip_image003[2]

Винаги затваряйте инстанциите на Scanner след като приключите работа с тях. В противен случай рискувате да загубите системни ресурси. За затваряне използвайте метода close().

Резултатът от изпълнението на програмата би трябвало да изглежда така:

Line 1: This is our first line.

Line 2: This is our second line.

Line 3: This is our third line.

Line 4: This is our fourth line.

Line 5: This is our fifth line.

Кодиране на файловете. Четене на кирилица

Нека сега разгледаме проблемите, които се появяват при четене с неко­ректно кодиране, например при четене на файл на кирилица.

Кодиране (encoding)

Добре знаете, че в паметта на компютрите всичко се запазва в двоичен вид. Фактически, това означава, че се налага и текстовите файлове да се представят цифрово, за да могат да бъдат съхранени в паметта. Този процес се нарича кодиране на файловете.

Кодирането се състои в заместването на текстовите символи (цифри, букви, препинателни знаци и т.н.) с точно определени числови стой­ности. Може грубо да си го представите като голяма таблица, в която срещу всеки символ стои определена стойност (пореден номер).

Пример за кодираща схема (encoding или charset) е например ISO 8859-1, windows-1251, UTF-8, KOI8-R и т.н. Това е една таблица със символи и техните номера, но може да съдържа и специални правила. Например символът "ударение" (U+0300) е специален и се залепя за последния символ, който го предхожда.

Четене на кирилица

Вероятно вече се досещате, че ако искаме да четем от файл, който съдържа символи от кирилицата, трябва да използваме точния encoding, който "разбира" тези специални символи. Такъв именно е windows-1251. С него спокойно можем да четем текстови файлове, съдържащи кирилица. Единственото, което трябва да направим, е да го определим като encoding на потока, който ще обработваме с нашия Scanner (погледнете отново примера за създаване на Scanner).

Ако не укажем изрично кодиращата схема (encoding) за четене от файла, ще бъде използван системният encoding, който е различен на всеки един компютър. В такъв случай програмата може и да работи коректно, но може и да не работи коректно. Може да се случи и нещо по-лошо: при нас програмата да работи коректно, а като я занесем при клиента, за който е предназначена, там да се счупи кирилицата.

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

-     Ако ползваме само латиница, всичко ще работи нормално.

-     Ако ползваме кирилица и четем с грешен encoding, ще прочетем т. нар. каракацили (познати още като джуджуфлечки или маймуняци). Това са случайни безсмислени символи.

-     Ако записваме кирилица в кодиране, което не поддържа кирилската азбука, буквите от кирилицата ще бъдат заменени безвъзвратно със символа "?" (въпросителна).

При всички случаи това са неприятни проблеми, които можем да не забележим веднага, а чак след време.

clip_image003[3]

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

Стандартът Unicode. Четене на Unicode

Unicode представлява индустриален стандарт, който позволява на ком­пютри и други електронни устройства винаги да представят и манипу­лират по един и същи начин текст, написан на повечето от световните писмености. Той се състои от дефиниции на над 100 000 символа, както и разнообразни стандартни кодиращи схеми (encodings). Обединението на различните символи, което ни предлага Unicode, води до голямото му разпространение. Както знаете, символите в Java (типовете char и String) също се представят в Unicode.

За да прочетем символи, записани в Unicode, трябва да използваме някоя от поддържаните в този стандарт кодиращи схеми (encodings). Най-известен и широко използван е UTF-8. Той представя стандартните ASCII символи с 1 байт, а всички останали – с до 4 байта. Можем да го определим за encoding по вече познатия ни начин (погледнете отново примера за създаване на Scanner):

File file = new File("sample.txt");

Scanner scanner = new Scanner(file, "UTF-8");

Ако се чудите дали за четене на текстов файл на кирилица да ползвате кодиране windows-1251 или UTF-8, на този отговор няма ясен отговор. И двата стандарта масово се ползват за записване на текстове на български език. И двете кодиращи схеми за позволени и може да ги срещнете.

Писане в текстов файл

Писането в текстови файлове е много удобен способ за съхранение на различни видове информация. Например, можем да записваме резулта­тите от изпълнението на дадена програма. Също така можем да ги полз­ваме, примерно и за да направим нещо като дневник на програмата – удобен начин за следене кога се е стартирала, отбелязване на различни грешки при изпълнението и т.н.

Отново както и при четенето текстов файл, и при писането ще използваме един познат ни от работата с конзолата клас, въпреки че този път това не е толкова явно. Вярвам, че сте добре запознати с System.out. Това не е нищо повече от инстанция на класа, който ще използваме за писане в текстови файлове, а именно java.io.PrintStream.

Класът java.io.PrintStream

Както вече няколкократно споменахме, класът PrintStream е част от пакета java.io и се използва изключително и само за работа с текстови данни. За разлика от другите текстови потоци, преди да запише данните на желаното място, той ги превръща в байтове. PrintStream ни дава възможност при създаването си да определим желания от нас encoding. Можем да създадем инстанция на класа по следния начин:

PrintStream fileWriter = new PrintStream(

    "test.txt", "windows-1251");

Като параметри на конструктора трябва да подадем файл/име на файл и ако искаме, желаният от нас encoding. Този ред код отново може да предизвикат появата на грешка. За сега просто добавете throws декла­рация в сигнатурата на текущия метод. Скоро ще обърнем внимание и на обработката на грешки при работа във файлове.

Отпечатване на числата от 1 до 20 в текстов файл – пример

След като вече можем да създаваме PrintStream, ще го използваме по предназначение. Целта ни ще е да запишем в един текстов файл числата от 1 до 20, като всяко число е на отделен ред. Можем да го направим по следния начин:

// Create a PrintStream instance

PrintStream fileWriter = new PrintStream("numbers.txt");

       

// Loop through the numbers from 1 to 20 and write them

for (int num = 1; num <= 20; num++) {

    fileWriter.println(num);

}

       

// Close the stream when you are done using it

fileWriter.close();

Започваме като създаваме инстанция на PrintStream по вече познатия ни от примера начин.

За да вземем числата от 1 до 20 ще използваме един for-цикъл. В тялото на цикъла ще използваме метода println(…), който отново познаваме от работата ни с конзолата, за да запишем текущото число на нов ред във файла. Не бива да се притеснявате, ако файлът, чието име сте дали не съществува. Ако случаят е такъв, той ще бъде автоматично създаден в папката на проекта, а ако вече съществува, ще бъде презаписан (ще изгубите старото му съдържание). Той ще има следния вид:

numbers.txt

1

2

3

20

В края на програмата затваряме потоците, които сме използвали.

clip_image003[4]

Не пропускайте да затворите потока след като приключите с използването му! За затваряне използвайте метода close().

Когато искате да печатате кирилица и се чудите кое кодиране да ползвате, предпочитайте кодирането UTF-8. То е универсално и поддържа не само кирилица, но и всички широкоразпространени световни азбуки: гръцки, арабски, китайски, японски и др.

Обработка на грешки

Докато сте правили примерите до тук, сигурно сте забелязали, че при доста от операциите, свързани с файлове, могат да възникнат изключи­телни ситуации. Основните принципи и подходи за тяхното прихващане и обработка вече са ви познати от предишните глави и най-вече от главата "Обработка на изключения". Сега ще се спрем малко на специфичните грешки при работа с файлове и най-добрите практики, за тяхната обра­ботка.

Прихващане на изключения при работа с файлове

Може би най-често срещаната грешка при работа с файлове е FileNotFoundException (от името и личи, че индикира, че желаният файл не е намерен). Тя може да възникне, когато използваме този файл за създаването на Scanner или PrintStream.

Когато избираме и типа encoding при създаване на Scanner и PrintStream може да възникне и UnsupportedEncodingException. Това значи, че избраният от нас encoding не е поддържан.

Друга често срещана грешка е  IOException. Това е клас, който е базов за всички входно-изходни грешки при работа с потоци.

Стандартният подход при обработване на изключения при работа с файлове е следният: декларираме променливите от клас Scanner и/или PrintStream преди try-catch-finally блока, като ги инициализираме със стойност null. В самия блок ги инициализираме с нужните ни стойности и прихващаме и обработваме потенциалните грешки по подходящ начин. За по-специална цел ще ползваме finally частта. За да онагледим казаното до тук, ще дадем пример.

Прихващане на грешка при отваряне на файл – пример

Ето как можем да прихванем изключенията, настъпващи при работа с файлове:

String fileName = "sample.txt";

Scanner fileReader = null;

int lineNumber = 0;

try {

    fileReader = new Scanner(new File(fileName));

    System.out.println("File " + fileName + " opened.");

           

    while (fileReader.hasNextLine()) {

        lineNumber++;

        System.out.printf("Line %d:%s%n",

            lineNumber, fileReader.nextLine());

    }

} catch (FileNotFoundException fnf) {

    System.out.println("File " + fileName + " not found.");

} catch (NullPointerException npe) {

    System.out.println("File " + fileName + " not found.");

} finally {

    // Close the scanner in the finally block

    if (fileReader != null) {

        fileReader.close();

    }

    System.out.println("Scanner closed.");

}

Това е същият пример за четене на файл ред по ред. Този път сме прихванали и обработили по подходящ начин възможните извънредни ситуации. Обърнете внимание как използваме finally блока. Тъй като той се изпълнява винаги, независимо дали са възникнали грешки или не, той е идеалното място където да направим задължителното затваряне на използваните ресурси (в случай, че не са вече затворени). За подсигу­ряване сме добавили и catch блок за NullPointerException.

Текстови файлове – още примери

Надяваме се теоретичните обяснения и примерите до сега да са успели да ви помогнат да навлезете в тънкостите при работа с текстови файлове. Сега ще разгледаме още няколко по-комплексни примери с цел да затвърдим получените до тук знания и да онагледим как да ги ползваме в практически задачи.

Брой срещания на дума във файл – пример

Ето как може да реализираме проста програма, която брои колко пъти се среща дума в даден текстов файл (за дума считаме всеки подниз от текста). В примера, нека текстът изглежда така:

sample.txt

This is our "Intro to Programming in Java" book.

In it you will learn the basics of Java programming.

You will find out how nice Java is.

Броенето можем да направим така: ще прочитаме файла ред по ред и всеки път, когато срещнем търсената от нас дума, ще увеличаваме стой­ността на една променлива (брояч). Ще обработим възможните изключи­телни ситуации, за да може потребителят да получава адекватна инфор­мация при появата на грешки. Ето и примерна реализация:

CountWordOccurrences.java

String fileName = "sample.txt";

Scanner fileReader = null;

int occurrences = 0;

String word = "Java";

try {

    fileReader = new Scanner(new File(fileName));

    System.out.println("File " + fileName + " opened.");

 

    while (fileReader.hasNextLine()) {

        String line = fileReader.nextLine();

        int index = line.indexOf(word);

        while (index != -1) {

            occurrences++;

            index = line.indexOf(word, (index + 1));

        }

    }

} catch (FileNotFoundException fnf) {

    System.out.println("File " + fileName + " not found.");

} catch (NullPointerException npe) {

    System.out.println("File " + fileName + " not found.");

} finally {

    if (fileReader != null) {

        fileReader.close();

    }

    System.out.println("Scanner closed.");       

}

 

System.out.printf("The word %s occurs %d times",

    word, occurrences);

За леснота в примера думата, която търсим е твърдо кодирана (hardcoded). Вие може да реализирате програмата така, че да търси дума, въведена от потребителя.

Виждате, че примерът не се различава много от предишните. Отново инициализираме променливите извън try-catch-finally блока. Пак използваме while-цикъл, за да прочитаме редовете на текстовия файл един по един. Вътре в тялото му има още един while-цикъл, с който преброяваме колко пъти се среща думата в дадения ред и увеличаваме брояча на срещанията. Това става като използваме метода от класа String indexOf(…) (припомнете си какво прави той в случай, че сте забравили). Не пропускаме да затворим Scanner обекта. Единственото, което после ни остава да направим е да изведем резултата на конзолата.

За нашия пример резултатът е следният:

File sample.txt opened.

Scanner closed.

The word Java occurs 3 times

Коригиране на файл със субтитри – пример

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

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

GORA.sub

{1029}{1122}{Y:i}Капитане, системите са|във шибана готовност.

{1123}{1270}{Y:i}Шибаното налягане е стабилно.|- Пригответе се за шибаното кацане.

{1343}{1468}{Y:i}Моля, затегнете коланите|и се настанете по местата си.

{1509}{1610}{Y:i}Координати 5.6|- Пет, пет, шест, точка ком.

{1632}{1718}{Y:i}Къде се дянаха|шибаните координати?

{1756}{1820}Командир Логар,|всички говорят на английски.

{1821}{1938}Не може ли да преминем|на турски още от началото?

{1942}{1992}Може!

{3104}{3228}{Y:b}Г.О.Р.А.|филм за космоса

...

За да го коригираме, просто трябва да нанесем корекция във времето за показване на субтитрите. Такава корекция може да бъде отместване (до­бавяне или изваждане на някаква константа) или промяна на ско­ростта (умножаване по някакви число, примерно 1.05).

Ето и приме­рен код, с който може да реализираме такава програма:

FixingSubtitles.java

import java.io.File;

import java.io.FileNotFoundException;

import java.io.PrintStream;

import java.io.UnsupportedEncodingException;

import java.util.Scanner;

 

public class FixingSubtitles {

    private static final int COEFFICIENT = 2;

    private static final int ADDITION = 5000;

    private static final String INPUT_FILE = "GORA.sub";

    private static final String OUTPUT_FILE = "fixed.sub";

 

    public static void main(String[] args) {

        Scanner fileInput = null;

        PrintStream fileOutput = null;

        try {

            // Create scanner with the Cyrillic encoding

            fileInput = new Scanner(

                new File(INPUT_FILE), "windows-1251");

            // Create PrintWriter with the Cyrillic encoding

            fileOutput = new PrintStream(

                OUTPUT_FILE, "windows-1251");

            String line;

            while (fileInput.hasNextLine()) {

                line = fileInput.nextLine();

                String fixedLine = fixLine(line);

                fileOutput.println(fixedLine);

            }

        } catch (FileNotFoundException fnfe) {

            System.err.println(fnfe.getMessage());

        } catch (UnsupportedEncodingException uee) {

            System.err.println(uee.getMessage());

        } finally {

            if (null != fileInput) {

                fileInput.close();

            }

            if (null != fileOutput) {

                fileOutput.close();

            }

        }

    }

 

    private static String fixLine(String line) {

        // Find closing brace

        int bracketFromIndex = line.indexOf('}');

 

        // Extract 'from' time

        String fromTime = line.substring(1, bracketFromIndex);

 

        // Calculate new 'from' time

        int newFromTime =

            Integer.parseInt(fromTime) * COEFFICIENT + ADDITION;

 

        // Find the following closing brace

        int bracketToIndex = line.indexOf('}', bracketFromIndex+1);

 

        // Extract 'to' time

        String toTime =

            line.substring(bracketFromIndex + 2, bracketToIndex);

 

        // Calculate new 'to' time

        int newToTime =

            Integer.parseInt(toTime) * COEFFICIENT + ADDITION;

 

        // Create a new line using the new 'from' and 'to' times

        String fixedLine = "{" + newFromTime + "}" + "{" +

            newToTime + "}" + line.substring(bracketToIndex + 1);

 

        return fixedLine;

    }

}

Тук създаваме Scanner и PrintStream и задаваме да използват encoding "windows-1251", защото ще работим с файлове, съдържащи кирилица. Това значи, че не трябва да забравяме да добавим catch блок, за да прихванем UnsupportedEncodingException. Отново използваме вече поз­натия ни начин за четене на файл ред по ред. Различното този път е, че в тялото на цикъла записваме всеки ред във файла с вече коригирани субтитри, след като го поправим в метода fixLine(String) (този метод не е обект на нашата дискусия, тъй като може да бъде имплементиран по много и различни начини в зависимост какво точно искаме да кориги­раме). Важно е да на забравим да затворим потоците във finally блока.

Упражнения

1.    Напишете програма, която чете от текстов файл и принтира нечетните му редове на конзолата.

2.    Напишете програма, която чете списък от имена от един текстов файл, сортира ги по азбучен ред и ги запазва в друг файл. Имената да са с латински букви. На всеки ред от файла, където са записани имената, има точно по едно име. На всеки ред от файла с резултата също трябва да има само по едно име.

3.    Напишете програма, която чете от файл квадратна матрица от цели числа и намира подматрицата с размери 2 х 2 с най-голяма сума и записва тази сума в отделен текстов файл. Първия ред на входния файл съдържа големината на записаната матрица (N). Следващите N реда съдържат по N числа, разделени с интервал. 

4.    Напишете програма, която заменя всяко срещане на подниза "start" с "finish" в текстов файл. Можете ли да пренапишете програмата така, че да заменя само цели думи?

5.    Напишете програма, която прочита списък от думи от файл, наречен words.txt, преброява колко пъти всяка от тези думи се среща в друг файл text.txt и записва резултата в трети файл – result.txt, като преди това ги сортира по брой на срещане в намаляващ ред.

Решения и упътвания

1.    Използвайте примерите, които разгледахме в настоящата глава.

2.    Записвайте всяко прочетено име в масив и след това го сортирайте по подходящ начин.

3.    Прочетете първия ред от файла и създайте матрица с получения размер. След това четете останалите редове един по един и отделяйте числата. След това ги записвайте на съответния ред в матрицата.

4.    Четете файла ред по ред и използвайте методите на класа String.

5.    Създайте хеш таблица с ключ думите от words.txt и стойност броя срещания на всяка дума. Четете ред по ред text.txt и разделяйте всеки ред на думи. Проверете дали някоя от получените при разде­лянето думи се среща в хеш таблицата и при нужда прибавяте 1 към броя на срещанията й.

Дискусионен форум

Коментирайте книгата и задачите в нея във: форума на софтуерната академия.

Share

Коментирай