2.x. io nio
io nio
Материалы
Java предоставляет две системы ввода-вывода: классический IO (пакет java.io, с Java 1.0) и NIO (пакеты java.nio.*, с Java 1.4/1.7). Они решают одни задачи разными способами и часто используются совместно.
Обзор и сравнение
| Аспект | IO (java.io) | NIO (java.nio) |
|---|---|---|
| Модель | Потоковая (Stream) | Буферная (Buffer + Channel) |
| Направление | Однонаправленные потоки | Двунаправленные каналы |
| Блокировка | Блокирующий | Блокирующий / неблокирующий |
| Подход | Байт за байтом | Блоками данных |
| Файловая система | File | Path + Files (NIO.2) |
| Появление | Java 1.0 | Java 1.4 (NIO), Java 7 (NIO.2) |
Часть 1: Классический IO (java.io)
Иерархия потоков
IO построен на четырёх абстрактных классах:
Байтовые потоки:
InputStream ─────► OutputStream
│ │
FileInputStream FileOutputStream
BufferedInputStream BufferedOutputStream
DataInputStream DataOutputStream
ByteArrayInputStream ByteArrayOutputStream
Символьные потоки:
Reader ─────────► Writer
│ │
FileReader FileWriter
BufferedReader BufferedWriter
InputStreamReader OutputStreamWriter
StringReader StringWriter
Байтовые потоки (InputStream / OutputStream)
Работают с сырыми байтами — для бинарных данных, изображений, архивов:
// Чтение файла побайтово
try (InputStream in = new FileInputStream("image.png")) {
int byteValue;
while ((byteValue = in.read()) != -1) {
// byteValue содержит значение 0-255
process(byteValue);
}
}
// Чтение блоками — эффективнее
try (InputStream in = new FileInputStream("data.bin")) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
// bytesRead — сколько байт прочитано в buffer
process(buffer, 0, bytesRead);
}
}
// Запись в файл
try (OutputStream out = new FileOutputStream("output.bin")) {
out.write(65); // Один байт
out.write(new byte[]{66, 67, 68}); // Массив байтов
}
Символьные потоки (Reader / Writer)
Работают с символами Unicode — для текста:
// Чтение текстового файла
try (Reader reader = new FileReader("text.txt", StandardCharsets.UTF_8)) {
int charValue;
while ((charValue = reader.read()) != -1) {
char c = (char) charValue;
System.out.print(c);
}
}
// Запись текста
try (Writer writer = new FileWriter("output.txt", StandardCharsets.UTF_8)) {
writer.write("Привет, мир!");
writer.write('\n');
writer.write(new char[]{'A', 'B', 'C'});
}
Буферизация
Буферизованные потоки значительно повышают производительность, снижая количество обращений к ОС:
// Буферизованное чтение текста построчно
try (BufferedReader reader = new BufferedReader(
new FileReader("log.txt", StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
// Буферизованная запись
try (BufferedWriter writer = new BufferedWriter(
new FileWriter("output.txt", StandardCharsets.UTF_8))) {
writer.write("Первая строка");
writer.newLine(); // Платформозависимый перевод строки
writer.write("Вторая строка");
}
// Буферизованные байтовые потоки
try (BufferedInputStream in = new BufferedInputStream(
new FileInputStream("large.bin"), 65536)) { // 64KB буфер
// Чтение теперь идёт из буфера в памяти
}
Совет: Всегда оборачивайте файловые потоки в буферизованные. Размер буфера по умолчанию — 8192 байта, но для больших файлов можно увеличить.
Мост между байтами и символами
InputStreamReader и OutputStreamWriter преобразуют байты в символы и обратно:
// Чтение из байтового потока как текст
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("data.txt"),
StandardCharsets.UTF_8))) {
String line = reader.readLine();
}
// Запись текста в байтовый поток
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("data.txt"),
StandardCharsets.UTF_8))) {
writer.write("Текст в UTF-8");
}
// Чтение из System.in (стандартный ввод)
BufferedReader console = new BufferedReader(
new InputStreamReader(System.in, Charset.defaultCharset()));
String userInput = console.readLine();
Потоки данных (DataInputStream / DataOutputStream)
Для чтения/записи примитивных типов в бинарном формате:
// Запись примитивов
try (DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
out.writeInt(42);
out.writeDouble(3.14159);
out.writeUTF("Hello"); // Modified UTF-8
out.writeBoolean(true);
}
// Чтение в том же порядке
try (DataInputStream in = new DataInputStream(
new BufferedInputStream(new FileInputStream("data.bin")))) {
int i = in.readInt();
double d = in.readDouble();
String s = in.readUTF();
boolean b = in.readBoolean();
}
PrintStream и PrintWriter
Удобные методы для форматированного вывода:
// PrintStream — для байтовых потоков (System.out — это PrintStream)
try (PrintStream ps = new PrintStream("output.txt", StandardCharsets.UTF_8)) {
ps.println("Строка");
ps.printf("Число: %d, Дробь: %.2f%n", 42, 3.14);
ps.print(true);
}
// PrintWriter — для символьных потоков
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(new FileWriter("output.txt")))) {
pw.println("Строка");
pw.printf("Дата: %tF%n", LocalDate.now());
}
Важно:
PrintStreamиPrintWriterне бросаютIOException— они подавляют исключения. ПроверяйтеcheckError()если нужна надёжность.
Класс File
Представление пути к файлу или директории:
File file = new File("data/config.txt");
File dir = new File("/home/user/documents");
// Информация о файле
boolean exists = file.exists();
boolean isFile = file.isFile();
boolean isDir = file.isDirectory();
long size = file.length();
long modified = file.lastModified();
String name = file.getName(); // "config.txt"
String path = file.getPath(); // "data/config.txt"
String absPath = file.getAbsolutePath();
// Операции с файлами
file.createNewFile();
file.delete();
file.renameTo(new File("new-name.txt"));
// Работа с директориями
dir.mkdir(); // Создать одну директорию
dir.mkdirs(); // Создать все директории в пути
String[] names = dir.list(); // Имена файлов
File[] files = dir.listFiles(); // Объекты File
File[] filtered = dir.listFiles(f -> f.getName().endsWith(".txt"));
Класс RandomAccessFile
Произвольный доступ к файлу — чтение и запись в любой позиции:
try (RandomAccessFile raf = new RandomAccessFile("data.bin", "rw")) {
// "r" — только чтение, "rw" — чтение и запись
// Позиция в файле
long position = raf.getFilePointer(); // Текущая позиция
raf.seek(100); // Перейти к позиции 100
// Чтение
int value = raf.readInt();
// Запись
raf.seek(0);
raf.writeInt(42);
// Размер файла
long length = raf.length();
raf.setLength(1024); // Установить размер
}
Часть 2: NIO — Каналы и буферы
NIO (New I/O) использует другую модель: каналы читают/пишут в буферы.
Buffer — контейнер данных
Буфер — это массив примитивов с метаданными для управления позицией:
┌─────────────────────────────────────────────────────────────┐
│ 0 1 2 3 4 5 6 7 8 9 ... │
│ [A] [B] [C] [D] [E] [ ] [ ] [ ] [ ] [ ] ... capacity=100 │
│ ▲ ▲ ▲ │
│ position=4 limit=6 capacity=100 │
└─────────────────────────────────────────────────────────────┘
Ключевые свойства:
- capacity — максимальный размер, неизменен
- limit — текущая граница для чтения/записи
- position — текущая позиция
- mark — отметка для возврата
Инвариант: 0 ≤ mark ≤ position ≤ limit ≤ capacity
// Создание буферов
ByteBuffer heapBuffer = ByteBuffer.allocate(1024); // В куче
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024); // В нативной памяти
ByteBuffer wrapped = ByteBuffer.wrap(new byte[1024]); // Обёртка массива
// Специализированные буферы
CharBuffer charBuf = CharBuffer.allocate(100);
IntBuffer intBuf = IntBuffer.allocate(100);
DoubleBuffer doubleBuf = DoubleBuffer.allocate(100);
Операции с буфером
ByteBuffer buffer = ByteBuffer.allocate(10);
// position=0, limit=10, capacity=10
// === ЗАПИСЬ В БУФЕР ===
buffer.put((byte) 'H');
buffer.put((byte) 'i');
// position=2, limit=10
buffer.put(new byte[]{'!', '!'});
// position=4, limit=10
// === ПЕРЕКЛЮЧЕНИЕ В РЕЖИМ ЧТЕНИЯ ===
buffer.flip();
// position=0, limit=4 (limit стал равен бывшему position)
// === ЧТЕНИЕ ИЗ БУФЕРА ===
while (buffer.hasRemaining()) {
byte b = buffer.get();
System.out.print((char) b); // Выведет: Hi!!
}
// position=4, limit=4
// === ПОДГОТОВКА К ПОВТОРНОЙ ЗАПИСИ ===
buffer.clear(); // position=0, limit=capacity — очистить полностью
// или
buffer.compact(); // Сдвинуть непрочитанное в начало, position после данных
Ключевые методы:
| Метод | Действие | Результат |
|---|---|---|
flip() | Запись → Чтение | limit=position, position=0 |
clear() | Сброс | position=0, limit=capacity |
compact() | Сдвиг непрочитанного | Копирует остаток в начало |
rewind() | Перечитать | position=0, limit не меняется |
mark() / reset() | Закладка | Запомнить/вернуться к позиции |
Прямые буферы (Direct Buffers)
// Обычный буфер — в куче Java
ByteBuffer heapBuf = ByteBuffer.allocate(1024);
// Прямой буфер — в нативной памяти
ByteBuffer directBuf = ByteBuffer.allocateDirect(1024);
| Аспект | Heap Buffer | Direct Buffer |
|---|---|---|
| Расположение | Java heap | Нативная память |
| Аллокация | Быстрая | Медленная |
| GC | Управляется GC | Только ссылка в GC |
| I/O операции | Требует копирования | Прямой доступ ОС |
| Когда использовать | Короткоживущие | Долгоживущие, большие |
Совет: Используйте прямые буферы для долгоживущих буферов с интенсивным I/O. Для временных операций heap-буферы эффективнее.
Channel — двунаправленный канал
Каналы соединяют буферы с источниками/приёмниками данных:
// FileChannel из потоков
FileInputStream fis = new FileInputStream("input.txt");
FileChannel readChannel = fis.getChannel();
FileOutputStream fos = new FileOutputStream("output.txt");
FileChannel writeChannel = fos.getChannel();
// Или напрямую (предпочтительно)
try (FileChannel channel = FileChannel.open(
Path.of("data.txt"),
StandardOpenOption.READ,
StandardOpenOption.WRITE,
StandardOpenOption.CREATE)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Чтение
int bytesRead = channel.read(buffer);
buffer.flip();
// Запись
channel.write(buffer);
}
Копирование файла через NIO
public static void copyFile(Path source, Path target) throws IOException {
try (FileChannel srcChannel = FileChannel.open(source, StandardOpenOption.READ);
FileChannel dstChannel = FileChannel.open(target,
StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
// Способ 1: через буфер
ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
while (srcChannel.read(buffer) != -1 || buffer.position() > 0) {
buffer.flip();
dstChannel.write(buffer);
buffer.compact();
}
// Способ 2: transferTo (эффективнее, использует DMA)
// srcChannel.transferTo(0, srcChannel.size(), dstChannel);
}
}
Memory-Mapped Files
Отображение файла в память — очень эффективно для больших файлов:
try (FileChannel channel = FileChannel.open(
Path.of("large-file.dat"), StandardOpenOption.READ)) {
// Отобразить весь файл в память
MappedByteBuffer mappedBuffer = channel.map(
FileChannel.MapMode.READ_ONLY,
0,
channel.size()
);
// Теперь файл доступен как массив байтов
while (mappedBuffer.hasRemaining()) {
byte b = mappedBuffer.get();
}
// Для записи
// MapMode.READ_WRITE — изменения пишутся в файл
// MapMode.PRIVATE — copy-on-write, изменения не сохраняются
}
Часть 3: NIO.2 — Files и Path (Java 7+)
NIO.2 — современный API для работы с файловой системой.
Path — замена File
// Создание Path
Path path1 = Path.of("data", "config.txt"); // Java 11+
Path path2 = Paths.get("data", "config.txt"); // Java 7+
Path path3 = Path.of("/home/user/documents");
// Информация о пути
String fileName = path1.getFileName().toString(); // "config.txt"
Path parent = path1.getParent(); // "data"
Path root = path3.getRoot(); // "/"
int nameCount = path1.getNameCount(); // 2
// Манипуляции с путями
Path resolved = path3.resolve("file.txt"); // /home/user/documents/file.txt
Path sibling = path1.resolveSibling("other.txt"); // data/other.txt
Path relative = path3.relativize(Path.of("/home/user/other")); // ../other
Path normalized = Path.of("data/../config/./app.txt").normalize(); // config/app.txt
Path absolute = path1.toAbsolutePath();
// Сравнение
boolean same = path1.equals(path2);
boolean startsWith = path3.startsWith("/home");
boolean endsWith = path1.endsWith("config.txt");
Files — утилиты для файловых операций
Path path = Path.of("example.txt");
// === ПРОВЕРКИ ===
boolean exists = Files.exists(path);
boolean isRegular = Files.isRegularFile(path);
boolean isDir = Files.isDirectory(path);
boolean isReadable = Files.isReadable(path);
boolean isWritable = Files.isWritable(path);
boolean isHidden = Files.isHidden(path);
// === ЧТЕНИЕ И ЗАПИСЬ (простые случаи) ===
// Чтение всего файла
String content = Files.readString(path, StandardCharsets.UTF_8); // Java 11+
byte[] bytes = Files.readAllBytes(path);
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
// Запись
Files.writeString(path, "Hello, World!", StandardCharsets.UTF_8); // Java 11+
Files.write(path, bytes);
Files.write(path, lines, StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
// === ПОТОКОВОЕ ЧТЕНИЕ ===
try (Stream<String> stream = Files.lines(path, StandardCharsets.UTF_8)) {
stream.filter(line -> !line.isBlank())
.forEach(System.out::println);
}
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
// ...
}
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
// ...
}
Операции с файлами и директориями
// === СОЗДАНИЕ ===
Files.createFile(Path.of("new-file.txt"));
Files.createDirectory(Path.of("new-dir"));
Files.createDirectories(Path.of("path/to/nested/dir")); // Все промежуточные
Path tempFile = Files.createTempFile("prefix", ".tmp");
Path tempDir = Files.createTempDirectory("prefix");
// === КОПИРОВАНИЕ ===
Files.copy(source, target);
Files.copy(source, target,
StandardCopyOption.REPLACE_EXISTING, // Перезаписать
StandardCopyOption.COPY_ATTRIBUTES); // Копировать атрибуты
// Копирование из/в поток
Files.copy(inputStream, targetPath);
Files.copy(sourcePath, outputStream);
// === ПЕРЕМЕЩЕНИЕ ===
Files.move(source, target);
Files.move(source, target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE); // Атомарно (если поддерживается)
// === УДАЛЕНИЕ ===
Files.delete(path); // Бросает исключение если не существует
Files.deleteIfExists(path); // Возвращает boolean
// === АТРИБУТЫ ===
long size = Files.size(path);
FileTime modified = Files.getLastModifiedTime(path);
Files.setLastModifiedTime(path, FileTime.from(Instant.now()));
// Детальные атрибуты
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
FileTime created = attrs.creationTime();
boolean isSymlink = attrs.isSymbolicLink();
Обход дерева директорий
Path startDir = Path.of("/home/user/projects");
// === Files.list() — только содержимое директории ===
try (Stream<Path> stream = Files.list(startDir)) {
stream.filter(Files::isRegularFile)
.forEach(System.out::println);
}
// === Files.walk() — рекурсивный обход ===
try (Stream<Path> stream = Files.walk(startDir)) {
List<Path> javaFiles = stream
.filter(p -> p.toString().endsWith(".java"))
.toList();
}
// С ограничением глубины
try (Stream<Path> stream = Files.walk(startDir, 2)) { // maxDepth=2
// ...
}
// === Files.find() — обход с фильтром по атрибутам ===
try (Stream<Path> stream = Files.find(startDir, Integer.MAX_VALUE,
(path, attrs) -> attrs.isRegularFile() &&
path.toString().endsWith(".log") &&
attrs.size() > 1024 * 1024)) { // > 1MB
stream.forEach(System.out::println);
}
// === Files.walkFileTree() — полный контроль ===
Files.walkFileTree(startDir, new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("Entering: " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("File: " + file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Failed: " + file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
return FileVisitResult.CONTINUE;
}
});
FileVisitResult:
CONTINUE— продолжить обходSKIP_SUBTREE— пропустить поддиректориюSKIP_SIBLINGS— пропустить оставшиеся файлы в директорииTERMINATE— прекратить обход
Рекурсивное удаление директории
public static void deleteRecursively(Path dir) throws IOException {
Files.walkFileTree(dir, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
WatchService — мониторинг файловой системы
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
Path dir = Path.of("/home/user/watched");
dir.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE);
while (true) {
WatchKey key = watchService.take(); // Блокирует до события
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW) {
continue; // События были потеряны
}
@SuppressWarnings("unchecked")
WatchEvent<Path> pathEvent = (WatchEvent<Path>) event;
Path fileName = pathEvent.context();
System.out.printf("%s: %s%n", kind.name(), fileName);
}
boolean valid = key.reset(); // Сбросить для следующих событий
if (!valid) {
break; // Директория больше недоступна
}
}
}
Часть 4: Неблокирующий и асинхронный I/O
Selector и неблокирующие каналы (NIO)
Позволяют одному потоку обрабатывать множество соединений:
try (Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // Неблокирующий режим
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // Блокирует до готовности каналов
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
keys.remove();
if (key.isAcceptable()) {
// Новое входящее соединение
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// Данные готовы для чтения
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = client.read(buffer);
if (bytesRead == -1) {
client.close();
} else {
buffer.flip();
// Обработка данных...
key.interestOps(SelectionKey.OP_WRITE);
}
} else if (key.isWritable()) {
// Канал готов к записи
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
client.write(buffer);
if (!buffer.hasRemaining()) {
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
Асинхронные каналы (NIO.2)
Операции возвращают Future или вызывают CompletionHandler:
// === С Future ===
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(
Path.of("data.txt"), StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
Future<Integer> future = channel.read(buffer, 0);
// Делаем что-то другое пока идёт чтение...
int bytesRead = future.get(); // Блокирует до завершения
buffer.flip();
}
// === С CompletionHandler ===
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
Path.of("data.txt"), StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer bytesRead, ByteBuffer attachment) {
attachment.flip();
System.out.println("Прочитано: " + bytesRead + " байт");
// Обработка данных...
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.err.println("Ошибка: " + exc.getMessage());
}
});
Асинхронный сервер
AsynchronousServerSocketChannel server =
AsynchronousServerSocketChannel.open()
.bind(new InetSocketAddress(8080));
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel client, Void attachment) {
// Принять следующее соединение
server.accept(null, this);
// Обработать текущее
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer bytesRead, ByteBuffer buf) {
buf.flip();
// Обработка и ответ...
}
@Override
public void failed(Throwable exc, ByteBuffer buf) {
// Ошибка чтения
}
});
}
@Override
public void failed(Throwable exc, Void attachment) {
// Ошибка accept
}
});
Сравнение подходов
| Подход | Поток на соединение | Производительность | Сложность |
|---|---|---|---|
| Blocking IO | Да | Низкая при многих соединениях | Простая |
| NIO + Selector | Нет (мультиплексирование) | Высокая | Средняя |
| NIO.2 Async | Пул потоков | Высокая | Высокая |
Практический пример: файловый процессор
public class LogAnalyzer {
public static void main(String[] args) throws IOException {
Path logsDir = Path.of("logs");
// Найти все .log файлы, прочитать и проанализировать
Map<String, Long> errorCounts = new ConcurrentHashMap<>();
try (Stream<Path> logFiles = Files.walk(logsDir)
.filter(p -> p.toString().endsWith(".log"))) {
logFiles.parallel().forEach(logFile -> {
try (Stream<String> lines = Files.lines(logFile, StandardCharsets.UTF_8)) {
lines.filter(line -> line.contains("ERROR"))
.map(LogAnalyzer::extractErrorType)
.forEach(errorType ->
errorCounts.merge(errorType, 1L, Long::sum));
} catch (IOException e) {
System.err.println("Ошибка чтения: " + logFile);
}
});
}
// Вывод результатов
Path report = Path.of("error-report.txt");
List<String> reportLines = errorCounts.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.map(e -> String.format("%s: %d", e.getKey(), e.getValue()))
.toList();
Files.write(report, reportLines, StandardCharsets.UTF_8);
System.out.println("Отчёт сохранён: " + report.toAbsolutePath());
}
private static String extractErrorType(String line) {
// Извлечь тип ошибки из строки лога
int start = line.indexOf("ERROR") + 6;
int end = line.indexOf(":", start);
return end > start ? line.substring(start, end).trim() : "Unknown";
}
}
Резюме
Когда использовать что:
| Задача | Рекомендуемый API |
|---|---|
| Простое чтение/запись текста | Files.readString() / Files.writeString() |
| Построчное чтение | Files.lines() или BufferedReader |
| Бинарные данные | Files.readAllBytes() или FileChannel |
| Большие файлы | FileChannel + ByteBuffer или Memory-mapped |
| Много соединений | NIO Selector или NIO.2 Async |
| Обход директорий | Files.walk() или Files.walkFileTree() |
| Мониторинг изменений | WatchService |
Ключевые принципы:
- Всегда используйте try-with-resources для потоков и каналов
- Указывайте кодировку явно (
StandardCharsets.UTF_8) - Буферизуйте файловые потоки
- Для современного кода предпочитайте
Path/FilesнадFile - Выбирайте NIO для высоконагруженных сценариев