Файлове
Проблем
Програмата забравя
Пишеш програма за разходите си. Въвеждаш ги вечер, затваряш я, а на другия ден тя не знае нищо за вчера. Ето най-краткият пример:
int visits = 0;visits++;System.out.println("Пусната " + visits + " път.");Пусната 1 път.Колкото пъти и да я пуснеш, казва „1 път“. Променливите живеят в паметта, докато програмата работи. Когато свърши, паметта се освобождава и всичко изчезва.
За да помни, програмата трябва да запише данните там, където остават и след нея: във файл на диска.
Идеята
Път и файл
Ето цяла програма, която записва бележка във файл и я чете обратно:
import java.io.IOException;import java.nio.file.Files;import java.nio.file.Path;
public class Main { public static void main(String[] args) { Path notes = Path.of("notes.txt"); try { Files.writeString(notes, "Купи мляко\nПлати тока до 15-ти\n"); String text = Files.readString(notes); System.out.print(text); } catch (IOException e) { System.out.println("Проблем с файла: " + e.getMessage()); } }}Купи млякоПлати тока до 15-тиPathе пътят до файла: „къде е“.Path.of("notes.txt")е файл с това име в папката, от която е пусната програмата.Filesима статични методи за всичко с файлове.writeStringзаписва текст (и създава файла, ако го няма),readStringго чете целия.IOExceptionе проверено изключение от модула за изключенията. Дискът може да е пълен, файлът да е заключен или да го няма, и компилаторът задължава да кажеш какво да стане тогава.
Пусни програмата, после отвори папката: notes.txt е там и може да се отвори с всеки текстов редактор. Следващото пускане ще го намери.
В примерите по-нататък import-ите не се показват. Всичко е от java.nio.file и java.io, а List е от java.util.
Идеята
Добави и прочети редовете
Разходите се добавят един по един. StandardOpenOption.APPEND дописва в края, вместо да записва файла наново. readAllLines чете файла като редове:
Path expenses = Path.of("expenses.txt");try { Files.writeString(expenses, "Обяд;12.50\n"); Files.writeString(expenses, "Такси;8.20\n", StandardOpenOption.APPEND); Files.writeString(expenses, "Книга;18.90\n", StandardOpenOption.APPEND); List<String> lines = Files.readAllLines(expenses); double total = 0; for (String line : lines) { String[] parts = line.split(";"); total += Double.parseDouble(parts[1]); System.out.println(parts[0] + ": " + parts[1] + " €"); } System.out.printf("%d разхода, общо %.2f €%n", lines.size(), total);} catch (IOException e) { System.out.println("Проблем с файла: " + e.getMessage());}Обяд: 12.50 €Такси: 8.20 €Книга: 18.90 €3 разхода, общо 39.60 €readAllLines връща List<String>: списък от текстове, по един за всеки ред. Списъците са в следващия модул, „Колекции“. Засега ти трябват само две неща: обхождане с for-each, както масив, и size() за броя на редовете.
По-точно, за любопитните
APPENDсам не създава файла. Ако го няма, получавашNoSuchFileException. За „създай или допиши“ подай двете:StandardOpenOption.CREATE, StandardOpenOption.APPEND.- От Java 18 текстът във файловете е по подразбиране в UTF-8 и кирилицата се чете и пише правилно. В по-стари версии зависеше от настройките на компютъра. Ако четеш файл от друга система, кодировката може да се даде изрично:
Files.readString(path, StandardCharsets.UTF_8). readAllLinesиreadStringчетат целия файл в паметта. За файл от няколко гигабайта е по-добре да четеш ред по ред. Виж картата заBufferedReader.
Пробвай
Две смени
Какво ще отпечата програмата?
Path shift = Path.of("shift.txt");try { Files.writeString(shift, "Първа смяна: Иво\n"); Files.writeString(shift, "Втора смяна: Мила\n"); System.out.print(Files.readString(shift));} catch (IOException e) { System.out.println(e.getMessage());}writeString без APPEND изтрива старото съдържание и записва новото. Първият ред е изчезнал още преди четенето. Това е най-честата причина „файлът ми се изтри“.
Идеята
Когато файла го няма
Настройките на програмата са във config.txt, а някой го е изтрил. Ако никой не хване изключението:
import java.io.IOException;import java.nio.file.Files;import java.nio.file.Path;
public class Main { static String readConfig() throws IOException { return Files.readString(Path.of("config.txt")); }
public static void main(String[] args) throws IOException { System.out.println(readConfig()); }}Exception in thread "main" java.nio.file.NoSuchFileException: config.txt at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92) ... at java.base/java.nio.file.Files.readString(Files.java:3318) at Main.readConfig(Main.java:7) at Main.main(Main.java:11)Редовете от java.base са вътрешността на Java (тук са съкратени с „...“). Твоят код е най-отдолу: ред 7 в readConfig, извикан от ред 11 в main. main също е обявил throws IOException, затова изключението стига до JVM и програмата спира.
NoSuchFileException е наследник на IOException. Хвани го отделно, преди общия случай:
static String readConfig() throws IOException { return Files.readString(Path.of("config.txt"));}
try { System.out.println(readConfig());} catch (NoSuchFileException e) { System.out.println("Няма файл " + e.getMessage() + ". Ползвам настройките по подразбиране.");} catch (IOException e) { System.out.println("Не мога да прочета настройките: " + e.getMessage());}System.out.println(Files.exists(Path.of("config.txt")));Няма файл config.txt. Ползвам настройките по подразбиране.falseFiles.exists(path) проверява дали файлът съществува. Проверката не замества catch: файлът може да изчезне между проверката и четенето, а и има други причини четенето да не стане.
Идеята
Ред по ред и try-with-resources
За голям файл или за писане на части има BufferedWriter и BufferedReader. Те държат файла отворен, докато работиш с него, и затова трябва да се затворят накрая:
Path report = Path.of("report.txt");try (BufferedWriter out = Files.newBufferedWriter(report)) { for (int day = 1; day <= 3; day++) { out.write("Ден " + day + ": " + day * 120 + " €"); out.newLine(); }} catch (IOException e) { System.out.println("Не мога да запиша: " + e.getMessage());}
try (BufferedReader in = Files.newBufferedReader(report)) { String line; while ((line = in.readLine()) != null) { System.out.println(line); }} catch (IOException e) { System.out.println("Не мога да прочета: " + e.getMessage());}Ден 1: 120 €Ден 2: 240 €Ден 3: 360 €writeзаписва текст,newLine()минава на нов ред.readLine()връща следващия ред илиnull, когато файлът свърши.try (...)със създаване на ресурс в скобите е try-with-resources. Когато блокът свърши, по какъвто и да е начин, ресурсът се затваря сам.
Затварянето не е формалност: докато BufferedWriter е отворен, текстът може още да е в буфера му, а не на диска. За да е кодът кратък, тук main е с throws IOException:
Path report = Path.of("report.txt");try (BufferedWriter out = Files.newBufferedWriter(report)) { for (int day = 1; day <= 3; day++) { out.write("Ден " + day + ": " + day * 120 + " €"); out.newLine(); } System.out.println("В блока: " + Files.size(report) + " байта");}System.out.println("След блока: " + Files.size(report) + " байта");for (String line : Files.readAllLines(report)) { System.out.println(line);}Стъпка по стъпка, с думи
- Path е само адресът на файла. Отляво е паметта на програмата, отдясно е дискът.
- newBufferedWriter създава празния файл report.txt и обект с буфер в паметта.
- write и newLine пишат в буфера. До диска още не стига нищо.
- Файлът на диска все още е 0 байта.
- Блокът свършва и try вика close(). Буферът отива на диска: файлът става 54 байта.
- 54 байта за 39 знака с новите редове: в UTF-8 всяка буква на кирилица е 2 байта, а € е 3.
- readAllLines чете трите реда обратно от диска.
Обещахме го в урока за finally. Ето същото записване с finally:
BufferedWriter out = null;try { out = Files.newBufferedWriter(report); out.write("Ден 1: 120 €");} catch (IOException e) { System.out.println("Не мога да запиша: " + e.getMessage());} finally { if (out != null) { try { out.close(); } catch (IOException e) { System.out.println("Не мога да затворя: " + e.getMessage()); } }}Петнайсет реда за едно записване, а close() също може да хвърли IOException. try (...) прави точно това вместо теб.
По-точно, за любопитните
- В скобите може да има няколко ресурса, разделени с
;. Затварят се в обратен ред: последният отворен се затваря първи. - Буфер.
BufferedWriterсъбира текста в паметта и го праща към диска на големи парчета. Всяко отделно писане на диска е бавно, а така са малко. По същия начинBufferedReaderчете по много наведнъж. - Байтове и знаци. Отдолу файлът е поредица от байтове.
InputStreamиOutputStream(напримерFiles.newInputStream) четат и пишат байтове: за снимки, архиви, всичко, което не е текст.ReaderиWriterпревръщат байтовете в знаци по кодировката.BufferedReaderеReaderс буфер. - По-старият начин. В чужд код ще видиш
new BufferedReader(new FileReader("report.txt"))иnew Scanner(new File("report.txt")). Работят, ноFilesе по-новото и по-удобно.Scannerчете и от файл, с познатите тиhasNextLineиnextLine:
try (Scanner in = new Scanner(Path.of("report.txt"))) { while (in.hasNextLine()) { System.out.println(in.nextLine()); }}Пробвай
Кога се затваря?
Всеки клас, който имплементира интерфейса AutoCloseable (метод close()), може да стои в try (...). Ето принтерът на касата от урока за finally, като ресурс:
Какво ще отпечата програмата?
class Printer implements AutoCloseable { Printer() { System.out.println("Принтерът е зает."); }
public void close() { System.out.println("Принтерът е свободен."); }}
try (Printer printer = new Printer()) { System.out.println("Печатам бележката..."); int qty = Integer.parseInt("три"); System.out.println("Количество: " + qty);} catch (NumberFormatException e) { System.out.println("Грешно количество.");}parseInt("три") хвърля и редът „Количество“ се прескача. На излизане от try ресурсът се затваря веднага, още преди catch. Затова „свободен“ е преди „Грешно количество.“ Когато catch се изпълнява, принтерът вече е освободен.Пробвай
Намери грешката
0. На кой ред е грешката?Избери реда с грешката.
BufferedWriter никога не се затваря. Текстът е още в буфера в паметта и не е стигнал до файла, затова файлът е празен. Програмата не дава грешка, а тихо губи данни. Отвори го в try (BufferedWriter out = ...) { ... } и прочети файла след блока: тогава той е затворен и редът е там.Идеята
Папки
Резервните копия на касата отиват в папка, по едно на ден:
Path backup = Path.of("backup");try { Files.createDirectories(backup.resolve("old")); Files.writeString(backup.resolve("2026-10-01.csv"), "Кафе,2.40\n"); Files.writeString(backup.resolve("2026-10-02.csv"), "Кафе,2.40\nЧай,1.80\n"); String[] names = backup.toFile().list(); Arrays.sort(names); for (String name : names) { Path p = backup.resolve(name); if (Files.isDirectory(p)) { System.out.println(name + "/"); } else { System.out.println(name + " (" + Files.size(p) + " байта)"); } } Files.delete(backup.resolve("2026-10-01.csv")); System.out.println(Files.exists(backup.resolve("2026-10-01.csv")));} catch (IOException e) { System.out.println(e.getMessage());}2026-10-01.csv (14 байта)2026-10-02.csv (26 байта)old/false| Метод | Какво прави |
|---|---|
Files.createDirectories(p) |
създава папката и всички папки над нея, ако ги няма |
dir.resolve("име") |
пътят до име вътре в dir |
Files.isDirectory(p), Files.exists(p) |
папка ли е, има ли го |
Files.size(p) |
размерът в байтове |
Files.delete(p) |
изтрива файл или празна папка |
dir.toFile().list() |
масив с имената на всичко в папката |
Размерите са 14 и 26 байта, макар че символите са по-малко: всяка буква на кирилица е 2 байта в UTF-8.
list() дава имената в произволен ред, затова ги подреждаме с java.util.Arrays.sort.По-точно, за любопитните
Files.deleteхвърляDirectoryNotEmptyException, ако папката не е празна, иNoSuchFileException, ако я няма.Files.deleteIfExistsне се сърди, ако я няма.Files.list(dir)е новият начин за съдържанието на папка, но връщаStream, който е в модул „Модерна Java“.Files.copyиFiles.moveкопират и местят.- В по-стар код ще видиш класа
java.io.File:new File("notes.txt")с методиexists(),length(),delete(),renameTo(...),createNewFile()иgetAbsolutePath(). Той е предшественикът наPathиFiles, аpath.toFile()иfile.toPath()превръщат единия в другия. Там добавянето в края еnew FileWriter("output.txt", true). - Пътят
"notes.txt"е относителен: спрямо папката, от която е пусната програмата.Path.of("/home/ana/notes.txt")е абсолютен.path.toAbsolutePath()показва къде точно е файлът.
Упражнение
Програми, които помнят
Реши задачите в редактора си. Във всяка задача програмата първо сама създава входния файл с Files.writeString, за да можеш да я пробваш веднага.
1. Конфигурационен файл. Създай server.conf с това съдържание, после го прочети и отпечатай всяка настройка като ключ: стойност. Празните редове и редовете, които започват с #, се пропускат. Подсказка: split("=", 2) разделя само на първото =.
# настройки на сървъраmax_connections=500log_file_path=/var/log/server.log
db_timeout=30max_connections: 500log_file_path: /var/log/server.logdb_timeout: 302. Филтър на лог файл. Създай app.log с четири реда (два INFO и два ERROR). Прочети от клавиатурата ниво, например ERROR, и запиши редовете с това ниво в errors.log с BufferedReader и BufferedWriter в едно try (...). Отпечатай errors.log и колко реда си записал.
ERRORERROR: Failed to load configurationERROR: Null pointer exceptionЗаписах 2 реда в errors.log3. Съдържание на папка. Създай папка project с файловете document.txt, image.jpg, script.js и папката backup. Запиши имената им, подредени по азбучен ред, в directory_contents.txt, като след имената на папките сложиш /. Отпечатай файла.
backup/document.txtimage.jpgscript.js4. Индексен файл. Създай text.txt с трите реда по-долу. Прочети дума от клавиатурата и запиши в index.txt всяка нейна поява във вида ред:позиция: номерът на реда (от 1) и индексът в реда (от 0). Ако думата е два пъти на един ред, това са два записа.
hello worldworld hellohello again, hellohello1:02:63:03:13Още задачи
- Брояч на пусканията. Поправи програмата от първата карта: брой пусканията във
visits.txt. Ако файлът го няма, започни от 0. - Намери и замени във файл. Задачата от урока „Търсене и замяна“: прочети файл, замени „старо“ с „ново“ и запиши резултата в нов файл.
- Дати от файл. Задачата за дати от урока „Регулярни изрази“: чети датите от файл, по една на ред.
- Копие с номерата на редовете. Прочети файл и запиши копие, в което всеки ред започва с номера си:
1: ...,2: ....
Тест