RandomAccessFile, научимся работать с указателем файла и создавать настоящие базы данных в файлах.
Преподаватель: Ваш преподаватель ОАиП
Сайт: gadget59.ru
Прежде чем говорить о прямом доступе, давайте вспомним базовые понятия. На предыдущих лекциях мы уже работали с файлами, поэтому многое вам будет знакомо.
Файл (file) — это именованная область данных на диске компьютера. Файл имеет имя (например, data.txt), расширение (.txt, .dat, .bin) и расположение (путь, например C:\Users\student\Documents\data.txt).
Файлы нужны для того, чтобы:
На лекции 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 (начало файла). При чтении указатель автоматически сдвигается вперёд. Но при прямом доступе мы можем вручную переместить указатель на любую позицию!
Чтобы лучше понять разницу между последовательным и прямым доступом, давайте рассмотрим три носителя информации из истории:
Последовательный доступ
Чтобы послушать песню №5, нужно перемотать плёнку через первые четыре. Нельзя сразу «прыгнуть» на нужную песню. Это медленно и неудобно.
🔹 Аналог в Java: BufferedReader, FileReader, Scanner
Прямой доступ
Лазерный луч может мгновенно переместиться на любую дорожку. Хочешь песню №5? Пожалуйста, она заиграет сразу.
🔹 Аналог в Java: RandomAccessFile
Есть ещё один интересный пример — виниловая пластинка. Она занимает промежуточное положение: игла может быть установлена на любую дорожку (прямой доступ), но внутри одной дорожки чтение идёт последовательно.
В Java прямой доступ к файлам реализован через класс java.io.RandomAccessFile. Этот класс находится в пакете java.io и позволяет как читать, так и записывать данные в произвольные позиции файла.
RandomAccessFile (дословно «файл с произвольным доступом») — это класс, который открывает файл и даёт возможность:
Важно понимать, что RandomAccessFile — это не наследник ни InputStream, ни OutputStream. Это отдельный класс, который реализует интерфейсы DataInput и DataOutput. Благодаря этому он может читать и записывать данные всех примитивных типов Java: int, double, boolean, char и так далее.
RandomAccessFile работает только с файлами, но не с другими источниками данных (например, с сетевыми потоками). Если вам нужен произвольный доступ к данным в памяти, используйте класс ByteArrayInputStream или ByteBuffer.
Когда мы создаём объект RandomAccessFile, мы обязательно должны указать режим открытия. Режим определяет, что мы можем делать с файлом: только читать, или и читать, и писать.
Файл открывается только для чтения. Любая попытка записать данные вызовет ошибку IOException. Этот режим полезен, когда мы уверены, что будем только читать файл, и хотим случайно его не испортить.
// Открываем файл только для чтения
RandomAccessFile file = new RandomAccessFile("data.dat", "r");
// ✅ Можно читать
int value = file.readInt();
// ❌ Нельзя писать — будет ошибка!
// file.writeInt(123); // IOException: Stream closed
Файл открывается и для чтения, и для записи. Если файла не существует, он будет создан автоматически. Это самый распространённый режим.
// Открываем файл для чтения И записи
RandomAccessFile file = new RandomAccessFile("data.dat", "rw");
// ✅ Можно и читать, и писать
file.writeInt(42);
file.seek(0); // Возвращаемся в начало
int value = file.readInt(); // Читаем 42
"rws" и "rwd" — это «rw» с гарантированной синхронизацией с диском. Они медленнее, но надёжнее: данные точно будут сохранены на физический диск, а не останутся в оперативной памяти. Используются в критически важных приложениях (банковские системы, базы данных).
Указатель файла (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, оно «обрежется». Будьте внимательны с типами!
Вот здесь и начинается магия прямого доступа! У нас есть три ключевых метода для работы с указателем:
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
getFilePointer() — узнать текущую позициюЭтот метод возвращает текущую позицию указателя в байтах.
file.seek(100);
long pos = file.getFilePointer(); // pos = 100
length() — узнать размер файлаЭтот метод возвращает размер файла в байтах.
long fileSize = file.length(); // Например, 1024 байт
seek() — это «прыжок» в нужное место файла. getFilePointer() — это вопрос «где я сейчас?». length() — это вопрос «какой размер файла?».
Давайте решим реальную задачу: создадим файл, в котором будут храниться данные о студентах. Каждый студент будет «записью» фиксированного размера.
Запись (record) — это структура данных, состоящая из нескольких полей разного типа. В нашем случае запись о студенте будет содержать:
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 — это гарантирует, что файл будет закрыт автоматически.
Теперь самое интересное! Представьте, что нам нужно прочитать данные только второго студента. При последовательном доступе пришлось бы читать первого, потом второго. При прямом доступе — просто «прыгаем» на нужную позицию!
Но есть проблема: строки 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);
}
}
Ещё одно мощное преимущество прямого доступа — возможность изменять данные в файле без его полного перезаписывания. Представьте: у нас есть файл с миллионом студентов, и нам нужно изменить средний балл только одного. При последовательном доступе пришлось бы переписать весь файл. При прямом — просто «прыгаем» на нужную позицию и перезаписываем одно поле!
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, имя и возраст, и записали новое значение только в поле «средний балл».
Удаление записи из файла с прямым доступом — задача не такая простая, как кажется. Нельзя просто «вырезать» запись из середины файла, потому что все последующие записи сдвинутся, и их позиции изменятся.
Есть два подхода:
Мы не удаляем запись физически, а просто помечаем её как удалённую. Например, записываем в поле 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
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, обновление записи |
try-with-resources, файл останется открытым, и другие программы не смогут его использовать. Всегда закрывайте файлы!readUTF() и readLine(): RandomAccessFile не имеет метода readLine() в привычном смысле. Для чтения строк используйте readUTF(), но помните, что это специальный формат.IOException. Если нужно писать — открывайте в режиме «rw».EOFException (End Of File — конец файла). Всегда проверяйте getFilePointer() < length().int — это 4 байта, double — 8 байт. Если вы записали int, а читаете как double, получите «мусор».writeUTF() использует модифицированный формат UTF (Universal Text Format — универсальный текстовый формат), который отличается от обычного UTF-8. Если вы записали строку через writeUTF(), читать её нужно только через readUTF(). Иначе получите «мусор» или ошибку.IOException. Обязательно используйте try-catch или try-with-resources для обработки ошибок.seek() и skipBytes(): seek() перемещает указатель в абсолютную позицию (от начала файла), а skipBytes(n) пропускает n байтов относительно текущей позиции. Это разные вещи!IOException. Проверяйте режим открытия!RandomAccessFile, всегда рисуйте на бумаге структуру вашей записи: сколько байтов занимает каждое поле, в каком порядке они идут, какой общий размер записи. Это избавит вас от 90% ошибок.
А теперь давайте закрепим материал практикой. Вот несколько задач разного уровня сложности — от простых до продвинутых. Попробуйте решить их самостоятельно, а если что-то не получится — вернитесь к предыдущим слайдам.
Создайте файл, в котором хранится одно целое число — счётчик посещений сайта. Напишите программу, которая при каждом запуске читает это число, увеличивает его на 1 и записывает обратно. Так вы сможете отслеживать, сколько раз запускалась программа.
Создайте файл, в котором хранятся 10 целых чисел (от 1 до 10). Напишите программу, которая спрашивает у пользователя номер записи (от 1 до 10) и выводит значение этой записи, используя прямой доступ (без чтения предыдущих чисел).
Создайте файл «телефонный справочник», в котором хранятся записи фиксированной длины. Каждая запись содержит: имя (20 символов), номер телефона (10 символов). Реализуйте функции: добавить запись, найти запись по имени, изменить номер телефона, удалить запись (пометить как удалённую).
Создайте файл, в котором хранятся оценки студентов. Каждая запись: ID студента (int), имя (30 символов), оценка по математике (int), оценка по физике (int), оценка по информатике (int). Напишите программу, которая позволяет: добавить студента, изменить оценку по конкретному предмету, вычислить средний балл студента, найти студента с максимальным средним баллом.
Создайте полноценную «базу данных» в файле. Структура: ID записи (4 байта), статус (1 байт: 0 — активна, 1 — удалена), данные (100 байт). Реализуйте: вставку новой записи (найти первую удалённую или добавить в конец), обновление записи по ID, удаление записи по ID (пометка), поиск записи по ID, вывод всех активных записей.
Давайте подведём итоги нашей большой лекции о файлах прямого доступа. Мы разобрали очень важную тему, которая открывает перед вами новые возможности в программировании.
RandomAccessFile — это основной инструмент прямого доступа в Java. Он позволяет читать и записывать данные всех примитивных типов."r" — только чтение, "rw" — чтение и запись, "rws" и "rwd" — с гарантированной синхронизацией с диском.seek(), getFilePointer(), length() позволяют управлять указателем.Тема файлов — одна из самых обширных в программировании. На следующих лекциях мы продолжим углубляться в работу с данными и структурами:
Вопросы?
Если что-то осталось непонятным — не стесняйтесь спрашивать. Тема важная, и лучше разобраться в ней сейчас, чем столкнуться с проблемами в будущих проектах.