📁 ОАиП. Лекция 42 — Файлы прямого доступа в Java

📚 Тема лекции: Сегодня мы изучим один из самых мощных способов работы с файлами — прямой доступ. В отличие от последовательного чтения, где нужно читать файл с начала до конца, прямой доступ позволяет «прыгать» в любое место файла и читать или изменять данные мгновенно. Мы разберём класс RandomAccessFile, научимся работать с указателем файла и создавать настоящие базы данных в файлах.

Преподаватель: Ваш преподаватель ОАиП
Сайт: gadget59.ru

📋 План лекции

  1. Повторение: что такое файл и зачем он нужен
  2. Последовательный доступ: как мы читали файлы раньше
  3. Прямой доступ: новая парадигма работы с файлами
  4. Аналогия из жизни: кассета vs виниловая пластинка vs CD
  5. Класс RandomAccessFile: знакомство
  6. Режимы открытия файла: «r» и «rw»
  7. Указатель файла (file pointer) — сердце прямого доступа
  8. Методы чтения данных из файла
  9. Методы записи данных в файл
  10. Методы перемещения указателя
  11. Получение размера файла
  12. Практика 1: создание файла с записями фиксированной длины
  13. Практика 2: чтение конкретной записи по номеру
  14. Практика 3: изменение существующей записи
  15. Практика 4: удаление записи из файла
  16. Сравнение последовательного и прямого доступа
  17. Когда какой доступ использовать
  18. Типичные ошибки при работе с RandomAccessFile
  19. Практика: задачи для самостоятельного решения
  20. Итоги и что дальше

📂 Повторение: что такое файл?

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

Файл (file) — это именованная область данных на диске компьютера. Файл имеет имя (например, data.txt), расширение (.txt, .dat, .bin) и расположение (путь, например C:\Users\student\Documents\data.txt).

Файлы нужны для того, чтобы:

  • 💾 Сохранять данные между запусками программы. Если вы написали программу-калькулятор и посчитали что-то, результат исчезнет, когда программа закроется. Чтобы сохранить его навсегда, нужно записать в файл.
  • 📤 Передавать данные между программами. Одна программа записывает данные в файл, другая — читает из него.
  • 📊 Хранить большие объёмы информации. В памяти компьютера (оперативной памяти, ОЗУ) данные хранятся только пока компьютер включён. Файлы же живут на жёстком диске и сохраняются годами.
  • 🔐 Обеспечивать долговременное хранение. Базы данных, логи, конфигурации — всё это хранится в файлах.
💡 Важно помнить: Файл — это просто последовательность байтов на диске. Байт — это минимальная единица информации, которая может принимать значения от 0 до 255. Всё, что мы видим на экране (текст, картинки, видео), в файле хранится как последовательность байтов.

📖 Последовательный доступ: как мы читали файлы раньше

На лекции 20 (oaip20) мы разбирали работу с файлами через классы FileReader, BufferedReader, FileWriter, FileInputStream, FileOutputStream. Все они работают по принципу последовательного доступа.

Последовательный доступ (sequential access) — это способ чтения файла, при котором данные читаются строго по порядку: сначала первый байт, потом второй, потом третий, и так далее до конца файла. Нельзя «перепрыгнуть» через несколько байтов и прочитать, например, сразу сотый.

Вот типичный код последовательного чтения:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class SequentialRead {
    public static void main(String[] args) {
        try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
            String line;
            // Читаем файл ПОСТРОЧНО, с начала до конца
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Этот код читает файл data.txt строка за строкой, начиная с самой первой. Если нам нужна только десятая строка, нам всё равно придётся прочитать первые девять. Это как читать книгу: чтобы добраться до 100-й страницы, нужно пролистать первые 99.

🎯 Прямой доступ: новая парадигма

Прямой доступ (direct access, random access) — это способ работы с файлом, при котором программа может обратиться к любому байту файла напрямую, без необходимости читать всё, что было до него.

Представьте, что у вас есть файл размером 1 гигабайт (миллиард байтов). При последовательном доступе, чтобы прочитать последний байт, нужно прочитать все предыдущие 999 999 999 байтов. Это долго! При прямом доступе можно мгновенно «прыгнуть» на позицию 999 999 999 и прочитать именно этот байт.

Как это работает технически? У каждого файла есть указатель (pointer) — специальная метка, которая показывает, в каком месте файла мы сейчас находимся. Когда мы открываем файл, указатель стоит на позиции 0 (начало файла). При чтении указатель автоматически сдвигается вперёд. Но при прямом доступе мы можем вручную переместить указатель на любую позицию!

💡 Аналогия с книгой: Представьте, что у книги есть закладки. При последовательном доступе вы читаете книгу с первой страницы. При прямом доступе вы можете открыть книгу на любой странице — просто переставив закладку. В Java указатель файла — это и есть такая «закладка».
🔑 Главное преимущество прямого доступа: Скорость! Если вам нужно прочитать одну конкретную запись из файла с миллионом записей, прямой доступ сделает это мгновенно, а последовательный будет читать всё до нужной записи.

🎵 Аналогия из жизни: кассета vs пластинка vs CD

Чтобы лучше понять разницу между последовательным и прямым доступом, давайте рассмотрим три носителя информации из истории:

📼 Аудиокассета

Последовательный доступ

Чтобы послушать песню №5, нужно перемотать плёнку через первые четыре. Нельзя сразу «прыгнуть» на нужную песню. Это медленно и неудобно.

🔹 Аналог в Java: BufferedReader, FileReader, Scanner

💿 Компакт-диск (CD)

Прямой доступ

Лазерный луч может мгновенно переместиться на любую дорожку. Хочешь песню №5? Пожалуйста, она заиграет сразу.

🔹 Аналог в Java: RandomAccessFile

Есть ещё один интересный пример — виниловая пластинка. Она занимает промежуточное положение: игла может быть установлена на любую дорожку (прямой доступ), но внутри одной дорожки чтение идёт последовательно.

✅ Вывод: Прямой доступ — это как CD: можно мгновенно перейти к любому месту. Последовательный доступ — это как кассета: нужно перематывать. В современных компьютерах жёсткие диски и SSD поддерживают прямой доступ, поэтому мы можем использовать эту мощную технологию.

🔧 Класс RandomAccessFile: знакомство

В Java прямой доступ к файлам реализован через класс java.io.RandomAccessFile. Этот класс находится в пакете java.io и позволяет как читать, так и записывать данные в произвольные позиции файла.

RandomAccessFile (дословно «файл с произвольным доступом») — это класс, который открывает файл и даёт возможность:

  • 📖 Читать данные из любой позиции файла
  • ✏️ Записывать данные в любую позицию файла
  • 🔀 Перемещаться по файлу вперёд и назад
  • 📏 Узнавать размер файла
  • 📍 Управлять указателем файла (file pointer)

Важно понимать, что RandomAccessFile — это не наследник ни InputStream, ни OutputStream. Это отдельный класс, который реализует интерфейсы DataInput и DataOutput. Благодаря этому он может читать и записывать данные всех примитивных типов Java: int, double, boolean, char и так далее.

⚠️ Важно: RandomAccessFile работает только с файлами, но не с другими источниками данных (например, с сетевыми потоками). Если вам нужен произвольный доступ к данным в памяти, используйте класс ByteArrayInputStream или ByteBuffer.

🔓 Режимы открытия файла: «r» и «rw»

Когда мы создаём объект RandomAccessFile, мы обязательно должны указать режим открытия. Режим определяет, что мы можем делать с файлом: только читать, или и читать, и писать.

Режим «r» (read — чтение)

Файл открывается только для чтения. Любая попытка записать данные вызовет ошибку IOException. Этот режим полезен, когда мы уверены, что будем только читать файл, и хотим случайно его не испортить.

// Открываем файл только для чтения
RandomAccessFile file = new RandomAccessFile("data.dat", "r");

// ✅ Можно читать
int value = file.readInt();

// ❌ Нельзя писать — будет ошибка!
// file.writeInt(123); // IOException: Stream closed

Режим «rw» (read-write — чтение и запись)

Файл открывается и для чтения, и для записи. Если файла не существует, он будет создан автоматически. Это самый распространённый режим.

// Открываем файл для чтения И записи
RandomAccessFile file = new RandomAccessFile("data.dat", "rw");

// ✅ Можно и читать, и писать
file.writeInt(42);
file.seek(0); // Возвращаемся в начало
int value = file.readInt(); // Читаем 42
💡 Есть ещё режимы: "rws" и "rwd" — это «rw» с гарантированной синхронизацией с диском. Они медленнее, но надёжнее: данные точно будут сохранены на физический диск, а не останутся в оперативной памяти. Используются в критически важных приложениях (банковские системы, базы данных).

📍 Указатель файла (file pointer)

Указатель файла (file pointer) — это, пожалуй, самая важная концепция прямого доступа. Это число, которое показывает, в каком месте файла мы сейчас находимся. Указатель измеряется в байтах от начала файла.

Когда файл только что открыт, указатель стоит на позиции 0 (самое начало файла). Когда мы читаем или записываем данные, указатель автоматически сдвигается вперёд на количество прочитанных/записанных байтов.

Давайте посмотрим на конкретном примере. Представим файл, в котором записаны три целых числа (int). Каждое число int в Java занимает 4 байта:

// Файл содержит три числа: 100, 200, 300
// В байтах это выглядит так:
// Позиция:  0  1  2  3  4  5  6  7  8  9  10 11
// Байты:   [100 в виде 4 байт] [200] [300]

RandomAccessFile file = new RandomAccessFile("numbers.dat", "rw");
file.writeInt(100);  // Указатель сдвигается на 4 → теперь на позиции 4
file.writeInt(200);  // Указатель сдвигается на 4 → теперь на позиции 8
file.writeInt(300);  // Указатель сдвигается на 4 → теперь на позиции 12

System.out.println(file.getFilePointer()); // 12 — конец файла
💡 Ключевая идея: Указатель файла — это как курсор в текстовом редакторе. Он показывает, где вы сейчас находитесь. Когда вы печатаете, курсор двигается вперёд. Когда вы читаете текст, ваши глаза тоже двигаются. Указатель файла работает точно так же.

📖 Методы чтения данных

Класс RandomAccessFile предоставляет методы для чтения данных всех примитивных типов Java. Каждый метод читает данные начиная с текущей позиции указателя и сдвигает указатель на количество прочитанных байтов.

Метод Что читает Размер Пример
readByte() Один байт 1 байт byte b = file.readByte();
readShort() Короткое целое 2 байта short s = file.readShort();
readInt() Целое число 4 байта int i = file.readInt();
readLong() Длинное целое 8 байт long l = file.readLong();
readFloat() Вещественное число 4 байта float f = file.readFloat();
readDouble() Двойной точности 8 байт double d = file.readDouble();
readBoolean() Логическое значение 1 байт boolean b = file.readBoolean();
readChar() Символ 2 байта char c = file.readChar();
readUTF() Строка в UTF переменный String s = file.readUTF();
⚠️ Важно: Метод readUTF() читает строки в специальном формате UTF (Universal Text Format — универсальный текстовый формат). Это не то же самое, что обычные строки в Java. Если вы записали строку через writeUTF(), читать её нужно через readUTF(). Иначе получите «мусор».

✏️ Методы записи данных

Для записи данных в файл используются методы, симметричные методам чтения. Каждый метод записывает данные начиная с текущей позиции указателя и сдвигает указатель вперёд.

Метод Что записывает Размер Пример
writeByte(int) Один байт 1 байт file.writeByte(65);
writeShort(int) Короткое целое 2 байта file.writeShort(1000);
writeInt(int) Целое число 4 байта file.writeInt(12345);
writeLong(long) Длинное целое 8 байт file.writeLong(1234567890L);
writeFloat(float) Вещественное число 4 байта file.writeFloat(3.14f);
writeDouble(double) Двойной точности 8 байт file.writeDouble(3.14159);
writeBoolean(boolean) Логическое значение 1 байт file.writeBoolean(true);
writeChar(int) Символ 2 байта file.writeChar('A');
writeUTF(String) Строка в UTF переменный file.writeUTF("Привет");
💡 Совет: Обратите внимание, что методы записи принимают параметры определённых типов. Например, writeByte() принимает int, но запишет только младший байт. Если вы передадите число больше 255 или меньше -128, оно «обрежется». Будьте внимательны с типами!

🔀 Перемещение указателя файла

Вот здесь и начинается магия прямого доступа! У нас есть три ключевых метода для работы с указателем:

1. seek(long position) — переместить указатель

Этот метод перемещает указатель файла на указанную позицию (в байтах от начала файла). Позиция 0 — это начало файла.

RandomAccessFile file = new RandomAccessFile("data.dat", "rw");

// Записываем три числа
file.writeInt(100);  // Позиция 0-3
file.writeInt(200);  // Позиция 4-7
file.writeInt(300);  // Позиция 8-11

// Перемещаемся на позицию 4 (начало второго числа)
file.seek(4);
int second = file.readInt();  // Читаем 200

// Перемещаемся в самое начало
file.seek(0);
int first = file.readInt();   // Читаем 100

// Перемещаемся на позицию 8 (начало третьего числа)
file.seek(8);
int third = file.readInt();   // Читаем 300

2. getFilePointer() — узнать текущую позицию

Этот метод возвращает текущую позицию указателя в байтах.

file.seek(100);
long pos = file.getFilePointer();  // pos = 100

3. length() — узнать размер файла

Этот метод возвращает размер файла в байтах.

long fileSize = file.length();  // Например, 1024 байт
💡 Запомните: seek() — это «прыжок» в нужное место файла. getFilePointer() — это вопрос «где я сейчас?». length() — это вопрос «какой размер файла?».

💼 Практика 1: создание файла с записями

Давайте решим реальную задачу: создадим файл, в котором будут храниться данные о студентах. Каждый студент будет «записью» фиксированного размера.

Запись (record) — это структура данных, состоящая из нескольких полей разного типа. В нашем случае запись о студенте будет содержать:

  • 🆔 ID студента (целое число, 4 байта)
  • 👤 Имя (строка в UTF, переменный размер)
  • 🎂 Возраст (целое число, 4 байта)
  • 📊 Средний балл (вещественное число, 8 байт)
import java.io.IOException;
import java.io.RandomAccessFile;

public class CreateStudentFile {
    public static void main(String[] args) {
        try (RandomAccessFile file = new RandomAccessFile("students.dat", "rw")) {
            // Записываем первого студента
            file.writeInt(1);              // ID = 1
            file.writeUTF("Иванов Иван");  // Имя
            file.writeInt(20);             // Возраст
            file.writeDouble(4.5);         // Средний балл
            
            // Записываем второго студента
            file.writeInt(2);              // ID = 2
            file.writeUTF("Петрова Анна"); // Имя
            file.writeInt(19);             // Возраст
            file.writeDouble(4.8);         // Средний балл
            
            // Записываем третьего студента
            file.writeInt(3);              // ID = 3
            file.writeUTF("Сидоров Пётр"); // Имя
            file.writeInt(21);             // Возраст
            file.writeDouble(3.9);         // Средний балл
            
            System.out.println("Файл создан! Размер: " + file.length() + " байт");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
💡 Что произошло: Мы создали файл students.dat и записали в него три записи о студентах. Каждая запись содержит ID, имя, возраст и средний балл. Обратите внимание, что мы используем try-with-resources — это гарантирует, что файл будет закрыт автоматически.

🔍 Практика 2: чтение конкретной записи

Теперь самое интересное! Представьте, что нам нужно прочитать данные только второго студента. При последовательном доступе пришлось бы читать первого, потом второго. При прямом доступе — просто «прыгаем» на нужную позицию!

Но есть проблема: строки writeUTF() имеют переменный размер. Имя «Иванов Иван» занимает больше байт, чем «Петрова Анна». Поэтому мы не можем просто вычислить позицию второй записи.

Решение: будем хранить записи фиксированной длины! Для этого имена будем дополнять пробелами до одинаковой длины.

import java.io.IOException;
import java.io.RandomAccessFile;

public class ReadStudentRecord {
    // Фиксированная длина имени в байтах
    private static final int NAME_LENGTH = 50;
    
    public static void main(String[] args) {
        try (RandomAccessFile file = new RandomAccessFile("students_fixed.dat", "rw")) {
            // Записываем студентов с именами фиксированной длины
            writeStudent(file, 1, "Иванов Иван", 20, 4.5);
            writeStudent(file, 2, "Петрова Анна", 19, 4.8);
            writeStudent(file, 3, "Сидоров Пётр", 21, 3.9);
            
            // Читаем ВТОРОГО студента (номер записи = 2)
            int recordNumber = 2;
            readStudent(file, recordNumber);
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    // Метод для записи студента
    private static void writeStudent(RandomAccessFile file, int id, 
                                      String name, int age, double gpa) 
                                      throws IOException {
        file.writeInt(id);
        
        // Дополняем имя пробелами до фиксированной длины
        StringBuilder sb = new StringBuilder(name);
        while (sb.length() < NAME_LENGTH) {
            sb.append(' ');
        }
        file.writeUTF(sb.toString());
        
        file.writeInt(age);
        file.writeDouble(gpa);
    }
    
    // Метод для чтения студента по номеру записи
    private static void readStudent(RandomAccessFile file, int recordNumber) 
                                    throws IOException {
        // Вычисляем размер одной записи
        // 4 (int) + 2 (длина UTF) + NAME_LENGTH (само имя) + 4 (int) + 8 (double)
        int recordSize = 4 + 2 + NAME_LENGTH + 4 + 8;
        
        // Перемещаемся на начало нужной записи
        long position = (recordNumber - 1) * recordSize;
        file.seek(position);
        
        // Читаем данные
        int id = file.readInt();
        String name = file.readUTF().trim(); // Убираем лишние пробелы
        int age = file.readInt();
        double gpa = file.readDouble();
        
        System.out.println("Студент #" + recordNumber + ":");
        System.out.println("  ID: " + id);
        System.out.println("  Имя: " + name);
        System.out.println("  Возраст: " + age);
        System.out.println("  Средний балл: " + gpa);
    }
}
✅ Готово! Теперь мы можем прочитать любого студента по его номеру мгновенно, без чтения предыдущих записей. Это и есть прямой доступ в действии!

✏️ Практика 3: изменение существующей записи

Ещё одно мощное преимущество прямого доступа — возможность изменять данные в файле без его полного перезаписывания. Представьте: у нас есть файл с миллионом студентов, и нам нужно изменить средний балл только одного. При последовательном доступе пришлось бы переписать весь файл. При прямом — просто «прыгаем» на нужную позицию и перезаписываем одно поле!

import java.io.IOException;
import java.io.RandomAccessFile;

public class UpdateStudentRecord {
    private static final int NAME_LENGTH = 50;
    
    public static void main(String[] args) {
        try (RandomAccessFile file = new RandomAccessFile("students_fixed.dat", "rw")) {
            // Допустим, студент №2 сдал дополнительный экзамен
            // и его средний балл вырос с 4.8 до 4.9
            
            int recordNumber = 2;
            double newGpa = 4.9;
            
            // Вычисляем размер одной записи
            int recordSize = 4 + 2 + NAME_LENGTH + 4 + 8;
            
            // Перемещаемся на начало нужной записи
            long position = (recordNumber - 1) * recordSize;
            file.seek(position);
            
            // Пропускаем поля, которые не меняем:
            // ID (4 байта) + длина UTF (2 байта) + имя (NAME_LENGTH байт) + возраст (4 байта)
            file.skipBytes(4 + 2 + NAME_LENGTH + 4);
            
            // Перезаписываем только средний балл
            file.writeDouble(newGpa);
            
            System.out.println("Средний балл студента №2 обновлён!");
            
            // Проверяем, что всё записалось правильно
            file.seek(position);
            int id = file.readInt();
            String name = file.readUTF().trim();
            int age = file.readInt();
            double gpa = file.readDouble();
            
            System.out.println("Проверка:");
            System.out.println("  ID: " + id);
            System.out.println("  Имя: " + name);
            System.out.println("  Возраст: " + age);
            System.out.println("  Средний балл: " + gpa); // Должно быть 4.9
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
💡 Ключевой момент: Мы использовали метод skipBytes(n), чтобы пропустить n байтов. Это очень удобно, когда нужно изменить не всю запись, а только одно поле. Мы «перепрыгнули» через ID, имя и возраст, и записали новое значение только в поле «средний балл».

🗑️ Практика 4: удаление записи

Удаление записи из файла с прямым доступом — задача не такая простая, как кажется. Нельзя просто «вырезать» запись из середины файла, потому что все последующие записи сдвинутся, и их позиции изменятся.

Есть два подхода:

Подход 1: Пометка «удалено» (мягкое удаление)

Мы не удаляем запись физически, а просто помечаем её как удалённую. Например, записываем в поле ID специальное значение (например, -1). При чтении файла мы просто игнорируем записи с ID = -1.

// Помечаем студента №2 как удалённого
int recordNumber = 2;
int recordSize = 4 + 2 + NAME_LENGTH + 4 + 8;
long position = (recordNumber - 1) * recordSize;

file.seek(position);
file.writeInt(-1);  // Помечаем как удалённого (ID = -1)

// При чтении игнорируем записи с ID = -1
file.seek(position);
int id = file.readInt();
if (id == -1) {
    System.out.println("Запись удалена");
} else {
    // Читаем остальные поля
}

Подход 2: Полное удаление (сдвиг записей)

Мы читаем все записи после удаляемой, сдвигаем их на одну позицию назад, и уменьшаем размер файла. Это сложнее, но файл остаётся компактным.

// Упрощённый пример: удаляем запись №2
int recordNumber = 2;
int recordSize = 4 + 2 + NAME_LENGTH + 4 + 8;

// Сдвигаем все последующие записи на одну позицию назад
for (long i = recordNumber; i * recordSize < file.length(); i++) {
    // Читаем запись i
    file.seek(i * recordSize);
    byte[] buffer = new byte[recordSize];
    file.readFully(buffer);
    
    // Записываем её на позицию i-1
    file.seek((i - 1) * recordSize);
    file.write(buffer);
}

// Уменьшаем размер файла
file.setLength(file.length() - recordSize);
💡 Совет: В реальных базах данных чаще используется первый подход (пометка «удалено»), потому что он быстрее. А потом, когда «удалённых» записей становится слишком много, выполняется «уплотнение» файла — специальная процедура, которая удаляет все помеченные записи сразу.

⚖️ Сравнение последовательного и прямого доступа

Характеристика 📼 Последовательный доступ 🎯 Прямой доступ
Классы в Java FileReader, BufferedReader, FileInputStream RandomAccessFile
Скорость чтения конкретной записи 🐢 Медленно (нужно читать всё до неё) 🚀 Мгновенно (прыгаем сразу на позицию)
Возможность изменения данных ❌ Только перезапись всего файла ✅ Можно изменить любую часть
Простота реализации ✅ Очень просто ⚠️ Требует вычисления позиций
Требования к данным Любые данные Записи должны быть фиксированной длины
Подходит для Логов, текстовых файлов, потоковой обработки Баз данных, индексных файлов, справочников
Пример использования Чтение конфигурации, логирование Поиск студента по ID, обновление записи
✅ Вывод: Прямой доступ — это мощный инструмент для работы со структурированными данными фиксированного размера. Если вам нужно часто искать, изменять или удалять конкретные записи — используйте прямой доступ. Если же вы просто читаете файл с начала до конца — последовательный доступ проще и удобнее.

🎯 Когда какой доступ использовать?

Используйте последовательный доступ, когда:

  • 📖 Вы читаете файл с начала до конца (например, выводите весь текст на экран)
  • 📝 Вы записываете данные по мере их появления (например, логирование событий)
  • 📊 Данные имеют переменный размер (например, строки разной длины)
  • 🔄 Вы обрабатываете данные потоком (например, видео или аудио)
  • 💾 Файл очень большой, и вы не можете загрузить его целиком в память

Используйте прямой доступ, когда:

  • 🔍 Вам нужно быстро находить конкретные записи (например, студента по ID)
  • ✏️ Вы часто изменяете данные в файле (например, обновляете баланс пользователя)
  • 🗑️ Вам нужно удалять записи из середины файла
  • 📦 Записи имеют фиксированный размер (например, все студенты занимают одинаковое количество байт)
  • 🏢 Вы создаёте простую базу данных без использования СУБД (системы управления базами данных)
💡 Правило большого пальца: Если вы не знаете, какой доступ выбрать — начните с последовательного. Он проще и покрывает 90% задач. Переходите на прямой доступ только тогда, когда понимаете, что последовательный слишком медленный для вашей задачи.

⚠️ Типичные ошибки при работе с RandomAccessFile

  1. Забывают закрыть файл: Если не использовать try-with-resources, файл останется открытым, и другие программы не смогут его использовать. Всегда закрывайте файлы!
  2. Путают readUTF() и readLine(): RandomAccessFile не имеет метода readLine() в привычном смысле. Для чтения строк используйте readUTF(), но помните, что это специальный формат.
  3. Неправильно вычисляют позиции: Если записи имеют переменный размер (например, строки разной длины), невозможно просто умножить номер записи на размер. Используйте записи фиксированной длины или храните индексы позиций.
  4. Открывают файл в режиме «r» и пытаются писать: Будет ошибка IOException. Если нужно писать — открывайте в режиме «rw».
  5. Читают за пределами файла: Если указатель находится в конце файла и вы пытаетесь прочитать, будет ошибка EOFException (End Of File — конец файла). Всегда проверяйте getFilePointer() < length().
  6. Не учитывают размер типов данных: int — это 4 байта, double — 8 байт. Если вы записали int, а читаете как double, получите «мусор».
  7. Забывают про кодировку строк: writeUTF() использует модифицированный формат UTF (Universal Text Format — универсальный текстовый формат), который отличается от обычного UTF-8. Если вы записали строку через writeUTF(), читать её нужно только через readUTF(). Иначе получите «мусор» или ошибку.
  8. Не обрабатывают исключения: Все операции с файлами могут выбрасывать IOException. Обязательно используйте try-catch или try-with-resources для обработки ошибок.
  9. Путают seek() и skipBytes(): seek() перемещает указатель в абсолютную позицию (от начала файла), а skipBytes(n) пропускает n байтов относительно текущей позиции. Это разные вещи!
  10. Записывают в режим «r»: Если файл открыт в режиме «r» (только чтение), любая попытка записи вызовет IOException. Проверяйте режим открытия!
  11. Не учитывают порядок байтов: Java хранит числа в формате «от старшего байта к младшему» (big-endian). Если вы читаете файл, созданный другой программой (например, на C++), порядок байтов может быть другим, и числа будут неверными.
⚠️ Золотое правило: Перед тем как писать код для работы с RandomAccessFile, всегда рисуйте на бумаге структуру вашей записи: сколько байтов занимает каждое поле, в каком порядке они идут, какой общий размер записи. Это избавит вас от 90% ошибок.

💪 Задачи для самостоятельного решения

А теперь давайте закрепим материал практикой. Вот несколько задач разного уровня сложности — от простых до продвинутых. Попробуйте решить их самостоятельно, а если что-то не получится — вернитесь к предыдущим слайдам.

🟢 Задача 1 (простая): Счётчик посещений

Создайте файл, в котором хранится одно целое число — счётчик посещений сайта. Напишите программу, которая при каждом запуске читает это число, увеличивает его на 1 и записывает обратно. Так вы сможете отслеживать, сколько раз запускалась программа.

🟢 Задача 2 (простая): Чтение конкретной записи

Создайте файл, в котором хранятся 10 целых чисел (от 1 до 10). Напишите программу, которая спрашивает у пользователя номер записи (от 1 до 10) и выводит значение этой записи, используя прямой доступ (без чтения предыдущих чисел).

🟡 Задача 3 (средняя): Телефонный справочник

Создайте файл «телефонный справочник», в котором хранятся записи фиксированной длины. Каждая запись содержит: имя (20 символов), номер телефона (10 символов). Реализуйте функции: добавить запись, найти запись по имени, изменить номер телефона, удалить запись (пометить как удалённую).

🟡 Задача 4 (средняя): Журнал оценок

Создайте файл, в котором хранятся оценки студентов. Каждая запись: ID студента (int), имя (30 символов), оценка по математике (int), оценка по физике (int), оценка по информатике (int). Напишите программу, которая позволяет: добавить студента, изменить оценку по конкретному предмету, вычислить средний балл студента, найти студента с максимальным средним баллом.

🔴 Задача 5 (сложная): Простая база данных

Создайте полноценную «базу данных» в файле. Структура: ID записи (4 байта), статус (1 байт: 0 — активна, 1 — удалена), данные (100 байт). Реализуйте: вставку новой записи (найти первую удалённую или добавить в конец), обновление записи по ID, удаление записи по ID (пометка), поиск записи по ID, вывод всех активных записей.

✅ Совет: Начните с первой задачи и постепенно переходите к более сложным. Если застряли — не стесняйтесь спрашивать. Работа с файлами — одна из самых важных тем в программировании, и её нужно понять основательно.

✅ Итоги лекции

Давайте подведём итоги нашей большой лекции о файлах прямого доступа. Мы разобрали очень важную тему, которая открывает перед вами новые возможности в программировании.

  • 📂 Файлы — это именованные области данных на диске, которые позволяют сохранять информацию между запусками программы.
  • 📖 Последовательный доступ — это чтение файла с начала до конца, байт за байтом. Подходит для логов, текстов, потоковой обработки.
  • 🎯 Прямой доступ — это возможность «прыгнуть» в любое место файла и прочитать или изменить данные мгновенно. Подходит для баз данных, справочников, индексных файлов.
  • 🔧 Класс RandomAccessFile — это основной инструмент прямого доступа в Java. Он позволяет читать и записывать данные всех примитивных типов.
  • 🔓 Режимы открытия: "r" — только чтение, "rw" — чтение и запись, "rws" и "rwd" — с гарантированной синхронизацией с диском.
  • 📍 Указатель файла (file pointer) — это число, показывающее текущую позицию в файле в байтах. Методы seek(), getFilePointer(), length() позволяют управлять указателем.
  • 📦 Записи фиксированной длины — это ключ к эффективному прямому доступу. Если все записи одинакового размера, можно мгновенно вычислить позицию любой записи.
  • ⚖️ Выбор подхода: Если не знаете, что выбрать — используйте последовательный доступ. Переходите на прямой, когда нужна скорость и возможность изменения конкретных записей.
💡 Главная мысль: Файлы прямого доступа — это основа для создания простых баз данных. Поняв эту тему, вы сможете создавать программы, которые работают с данными так же эффективно, как настоящие СУБД (системы управления базами данных).

🚀 Что дальше?

Тема файлов — одна из самых обширных в программировании. На следующих лекциях мы продолжим углубляться в работу с данными и структурами:

  • 🔗 Связные списки (LinkedList) — структура данных, основанная на указателях. Мы разберём, как устроены односвязные и двусвязные списки «под капотом», и научимся создавать свои собственные реализации.
  • 📚 Стек (Stack) и очередь (Queue) — две важнейшие структуры данных с разными принципами работы. Стек работает по принципу LIFO (последний пришёл — первый вышел), очередь — по принципу FIFO (первый пришёл — первый вышел).
  • 🗺️ Коллекции Java: Map — словари, отображения «ключ-значение». Мы изучим HashMap, TreeMap и научимся эффективно хранить данные с быстрым поиском по ключу.
  • 📊 Сравнение коллекций — когда что использовать: List, Set, Map? Мы разберём сильные и слабые стороны каждой структуры данных и научимся делать правильный выбор.
  • 🔍 Алгоритмы поиска и сортировки — как эффективно находить данные в больших массивах и упорядочивать их. Линейный поиск, бинарный поиск, сортировка пузырьком, быстрая сортировка.
🎉 Поздравляю! Вы освоили одну из самых мощных тем в программировании. Файлы прямого доступа — это мост между простыми программами и настоящими базами данных. Практикуйтесь, решайте задачи, и soon вы сможете создавать собственные системы хранения данных!

📚 Спасибо за внимание!

Вопросы?

Если что-то осталось непонятным — не стесняйтесь спрашивать. Тема важная, и лучше разобраться в ней сейчас, чем столкнуться с проблемами в будущих проектах.

← Вернуться на главную страницу

1 / 23