Към съдържанието
wannacode

Файлове

Проблем

Програмата забравя

Пишеш програма за разходите си. Въвеждаш ги вечер, затваряш я, а на другия ден тя не знае нищо за вчера. Ето най-краткият пример:

int visits = 0;
visits++;
System.out.println("Пусната " + visits + " път.");
Изход
Пусната 1 път.

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

За да помни, програмата трябва да запише данните там, където остават и след нея: във файл на диска.

Идеята

Път и файл

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

Main.java
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());
}
Отговор

Идеята

Когато файла го няма

Настройките на програмата са във config.txt, а някой го е изтрил. Ако никой не хване изключението:

Main.java
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. Ползвам настройките по подразбиране.
false

Files.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);
}
Стъпка по стъпка, с думи
  1. Path е само адресът на файла. Отляво е паметта на програмата, отдясно е дискът.
  2. newBufferedWriter създава празния файл report.txt и обект с буфер в паметта.
  3. write и newLine пишат в буфера. До диска още не стига нищо.
  4. Файлът на диска все още е 0 байта.
  5. Блокът свършва и try вика close(). Буферът отива на диска: файлът става 54 байта.
  6. 54 байта за 39 знака с новите редове: в UTF-8 всяка буква на кирилица е 2 байта, а € е 3.
  7. 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("Грешно количество.");
}
Отговор

Пробвай

Намери грешката

Програмата записва поръчка и брои редовете във файла. Отпечатва 0. На кой ред е грешката?

Избери реда с грешката.

Идеята

Папки

Резервните копия на касата отиват в папка, по едно на ден:

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) разделя само на първото =.

server.conf
# настройки на сървъра
max_connections=500
log_file_path=/var/log/server.log
db_timeout=30
Изход
max_connections: 500
log_file_path: /var/log/server.log
db_timeout: 30

2. Филтър на лог файл. Създай app.log с четири реда (два INFO и два ERROR). Прочети от клавиатурата ниво, например ERROR, и запиши редовете с това ниво в errors.log с BufferedReader и BufferedWriter в едно try (...). Отпечатай errors.log и колко реда си записал.

Вход
ERROR
Изход
ERROR: Failed to load configuration
ERROR: Null pointer exception
Записах 2 реда в errors.log

3. Съдържание на папка. Създай папка project с файловете document.txt, image.jpg, script.js и папката backup. Запиши имената им, подредени по азбучен ред, в directory_contents.txt, като след имената на папките сложиш /. Отпечатай файла.

directory_contents.txt
backup/
document.txt
image.jpg
script.js

4. Индексен файл. Създай text.txt с трите реда по-долу. Прочети дума от клавиатурата и запиши в index.txt всяка нейна поява във вида ред:позиция: номерът на реда (от 1) и индексът в реда (от 0). Ако думата е два пъти на един ред, това са два записа.

text.txt
hello world
world hello
hello again, hello
Вход
hello
index.txt
1:0
2:6
3:0
3:13

Още задачи

  • Брояч на пусканията. Поправи програмата от първата карта: брой пусканията във visits.txt. Ако файлът го няма, започни от 0.
  • Намери и замени във файл. Задачата от урока „Търсене и замяна“: прочети файл, замени „старо“ с „ново“ и запиши резултата в нов файл.
  • Дати от файл. Задачата за дати от урока „Регулярни изрази“: чети датите от файл, по една на ред.
  • Копие с номерата на редовете. Прочети файл и запиши копие, в което всеки ред започва с номера си: 1: ..., 2: ....

Тест

Провери се

1. Защо компилаторът иска try/catch около Files.readString?
2. Files.writeString(file, "А\n"); после Files.writeString(file, "Б\n"); Какво има във файла?
3. Кога try-with-resources вика close()?
4. Какво връща in.readLine(), когато файлът свърши?
5. Записваш с BufferedWriter, но не го затваряш. Какво може да има във файла?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·