Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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)
НаправлениеОднонаправленные потокиДвунаправленные каналы
БлокировкаБлокирующийБлокирующий / неблокирующий
ПодходБайт за байтомБлоками данных
Файловая системаFilePath + Files (NIO.2)
ПоявлениеJava 1.0Java 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 BufferDirect 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

Ключевые принципы:

  1. Всегда используйте try-with-resources для потоков и каналов
  2. Указывайте кодировку явно (StandardCharsets.UTF_8)
  3. Буферизуйте файловые потоки
  4. Для современного кода предпочитайте Path/Files над File
  5. Выбирайте NIO для высоконагруженных сценариев