BufferedInputStream: примеры (JAVA)

Рабочие подходы к BufferedInputStream в Java
Раздел: Потоки ввода-вывода (Streams) байтовые
BufferedInputStream(InputStream in)

Описание и сигнатуры

Класс BufferedInputStream из пакета java.io применяется для добавления буферизации к любому InputStream. Буферизация повышает производительность при множественных мелких операциях чтения и позволяет использовать возможности, такие как mark и reset, без непосредственного обращения к более медленному источнику данных.

Основные конструкторы:

  • BufferedInputStream(InputStream in) - создаёт буфер по умолчанию (обычно 8192 байт) вокруг указанного входного потока.
  • BufferedInputStream(InputStream in, int size) - создаёт буфер указанного размера. Если size <= 0, будет выброшено IllegalArgumentException.

Главные методы и их поведение:

  • int read() - читает один байт и возвращает значение в диапазоне 0–255; при достижении конца возвращает -1. Может выбросить IOException.
  • int read(byte[] b) - пытается прочитать до b.length байт; возвращает количество реально прочитанных байт или -1 при EOF. Если b.length == 0, возвращает 0.
  • int read(byte[] b, int off, int len) - читает до len байт в массив с указанного смещения; возвращает количество прочитанных байт или -1 при EOF. Не гарантируется чтение ровно len байт за один вызов.
  • long skip(long n) - пропускает до n байт, возвращает фактическое число пропущенных. Может возвращать 0, если EOF или если поток не поддерживает пропуск.
  • int available() - возвращает оценку количества байт, доступных для немедленного чтения без блокирования. Значение может быть 0 и не гарантируется точность для всех типов потоков.
  • void close() - закрывает сам поток и оборачиваемый поток, освобождая ресурсы.
  • void mark(int readlimit) - устанавливает метку, позволяющую вернуться методом reset() пока не будет прочитано более readlimit байт после метки. Для BufferedInputStream поддержка метки реализована и управляется размером внутреннего буфера: если после метки было прочитано более размера буфера, метка может стать недействительной.
  • void reset() - возвращает позицию чтения к последней метке; если метка недействительна, бросает IOException.
  • boolean markSupported() - возвращает trueBufferedInputStream поддержка метки доступна).

Особенности использования:

  • Буферизация полезна при чтении с диска, сети или других медленных источников.
  • Размер буфера выбирается в зависимости от профиля доступа: для последовательного чтения можно использовать небольшой буфер; для множества мелких операций - увеличить размер для снижения числа системных вызовов.
  • Метод available() даёт приблизительную оценку и не предназначен для определения конца потока.

Короткие примеры использования

Пример 1. Чтение всех байт из потока (ByteArrayInputStream используется для демонстрации):

import java.io.*;

public class Example1 {
    public static void main(String[] args) throws Exception {
        byte[] data = "Привет".getBytes("UTF-8");
        try (BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(data))) {
            int b;
            while ((b = bis.read()) != -1) {
                System.out.print((char) b);
            }
        }
    }
}
Результат:
Привет

Пример 2. Чтение в массив с обработкой частичного чтения:

import java.io.*;

public class Example2 {
    public static void main(String[] args) throws Exception {
        byte[] data = "Hello BufferedInputStream".getBytes("UTF-8");
        try (BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(data), 8)) {
            byte[] buf = new byte[10];
            int read = bis.read(buf); // может прочитать менее 10 байт
            System.out.println("Прочитано: " + read);
            System.out.println(new String(buf, 0, read, "UTF-8"));
        }
    }
}
Результат:
Прочитано: 10
Hello Buff

Пример 3. mark/reset внутри размера буфера:

import java.io.*;

public class Example3 {
    public static void main(String[] args) throws Exception {
        byte[] data = "ABCDE".getBytes("UTF-8");
        try (BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(data), 4)) {
            System.out.print((char) bis.read()); // A
            bis.mark(4);
            System.out.print((char) bis.read()); // B
            System.out.print((char) bis.read()); // C
            bis.reset();
            System.out.print((char) bis.read()); // B again
        }
    }
}
Результат:
ABC B (в потоке выводится: AB C B без пробелов, здесь для наглядности показаны шаги)

Похожие классы в Java и их особенности

  • BufferedReader - аналог для символьных потоков (Reader). Предпочтителен при работе с текстом и кодировками, предоставляет метод readLine().
  • ByteArrayInputStream - поток поверх массива байт, быстрый для тестов и небольших данных, не использует дополнительной буферизации.
  • PushbackInputStream - позволяет «вернуть» прочитанные байты обратно в поток; пригоден для простого парсинга при необходимости отката чтения.
  • FileInputStream - базовый поток чтения из файла. Часто оборачивается в BufferedInputStream для повышения скорости.
  • Channels (NIO) и java.nio.file.Files.newInputStream - альтернативы на базе NIO, дают более гибкий контрол над операциями ввода-вывода и могут лучше масштабироваться в многопоточных сценариях.

Выбор: для побайтного или поблочного чтения бинарных данных удобен BufferedInputStream. Для текста с кодировками лучше BufferedReader. Для высокопроизводительных серверов и неблокирующих операций стоит рассмотреть NIO-решения.

Аналоги в других языках

  • Python (io.BufferedReader)
    import io
    buf = io.BufferedReader(io.BytesIO(b"Hello"))
    print(buf.read())
    Результат:
    b'Hello'
    Отличие: Python явно разделяет двоичные и текстовые потоки, API удобен для чтения и доступен в стандартной библиотеке.
  • C# (System.IO.BufferedStream)
    using System;
    using System.IO;
    
    class P { static void Main(){
      byte[] data = System.Text.Encoding.UTF8.GetBytes("Hi");
      using(var ms = new MemoryStream(data))
      using(var bs = new BufferedStream(ms)){
        int b = bs.ReadByte();
        Console.WriteLine((char)b);
      }
    }}
    
    Результат:
    H
    Отличие: BufferedStream служит оболочкой и в целом схож с BufferedInputStream.
  • Go (bufio.Reader)
    package main
    import (
      "bufio"
      "bytes"
      "fmt"
    )
    func main(){
      r := bufio.NewReader(bytes.NewBufferString("Hello"))
      s, _ := r.ReadString('o')
      fmt.Println(s)
    }
    
    Результат:
    Hello
    Отличие: встроенная модель чтения с удобными методами (ReadString, Peek) и эффективной реализацией буфера.
  • JavaScript (Node.js, stream + Buffer)
    const fs = require('fs');
    const rs = fs.createReadStream('file.bin', { highWaterMark: 8192 });
    rs.on('data', chunk => console.log('chunk.length=', chunk.length));
    
    Результат:
    chunk.length= 8192
    ...
    Отличие: в Node.js поток сам буферизует данные, highWaterMark управляет размером буфера.
  • PHP
    $fp = fopen('php://memory', 'r+');
    fwrite($fp, "data");
    rewind($fp);
    echo stream_get_contents($fp);
    
    Результат:
    data
    Отличие: PHP использует стандартные C-стайловые буферы и функции для чтения; явной оболочки для InputStream как в Java нет.
  • Kotlin - использует те же Java-классы (BufferedInputStream) с синтаксическим сокращением; поведение идентичное.
  • Lua - стандартная библиотека предоставляет file:read, где буферизация обрабатывается библиотекой С, явной оболочки нет.

Типичные ошибки и демонстрации

1) Ожидание, что read(byte[]) всегда заполнит массив полностью:

// Неправильное предположение
byte[] buf = new byte[1024];
int n = bis.read(buf);
// если нужно точно n байт, нужно вызывать в цикле
Проблема:
Метод может вернуть меньше байт, чем размер массива, особенно при чтении с сетевого потока или в конце данных.

2) Неправильное использование mark/reset за пределами буфера:

byte[] data = "1234567890".getBytes();
try (BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(data), 4)){
    bis.mark(4);
    bis.read(new byte[6]); // прочитано больше, чем размер буфера
    bis.reset(); // IOException - метка недействительна
}
Результат:
java.io.IOException: Resetting to invalid mark

3) Передача неположительного размера буфера:

new BufferedInputStream(new FileInputStream("file"), 0);
Результат:
java.lang.IllegalArgumentException: Buffer size <= 0

4) Чтение после закрытия потока:

BufferedInputStream bis = new BufferedInputStream(...);
bis.close();
bis.read();
Результат:
java.io.IOException: Stream closed

5) Зависимость от available() как от точного размера оставшихся данных:

int avail = bis.available();
byte[] b = new byte[avail];
bis.read(b); // может не прочитать все данные, доступные позже
Проблема:
Для сетевых или потоковых источников available() даёт только оценку и не заменяет корректного цикла чтения.

Изменения в реализации и версиях JDK

Класс BufferedInputStream стабилен и не претерпевал значительных API-изменений в недавних релизах. Основные моменты:

  • Внутренние оптимизации в реализации JDK могли меняться между версиями для улучшения производительности и безопасности, но публичный API остался прежним.
  • Развитие платформы сместило акцент в сторону NIO и асинхронных API для высоконагруженных приложений; тем не менее BufferedInputStream остаётся валидным решением для синхронного чтения.

Для точных изменений между конкретными версиями можно свериться с заметками к релизам JDK и исходниками в OpenJDK.

Расширенные и нетипичные сценарии

1) Комбинация с CipherInputStream для чтения зашифрованных данных:

Пример java
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;

// Псевдокод: читает зашифрованный файл через буфер
byte[] key = new byte[16]; // ключ для примера
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"));
try (InputStream fis = new FileInputStream("enc.bin");
     BufferedInputStream bis = new BufferedInputStream(fis);
     CipherInputStream cis = new CipherInputStream(bis, cipher)) {
    byte[] buf = new byte[4096];
    int r;
    while ((r = cis.read(buf)) != -1) {
        // обработка расшифрованных данных
    }
}
Результат:
(в процессе выполнения читаются и расшифровываются байты из файла)

2) Использование с GZIPInputStream для распаковки большого файла по частям:

Пример java
import java.util.zip.GZIPInputStream;
// Чтение и распаковка
try (InputStream fis = new FileInputStream("file.gz");
     BufferedInputStream bis = new BufferedInputStream(fis, 16384);
     GZIPInputStream gis = new GZIPInputStream(bis)) {
    byte[] buf = new byte[8192];
    int n;
    while ((n = gis.read(buf)) != -1) {
        // запись в целевой поток
    }
}
Результат:
Файл распакован и прочитан блоками по 8192 байта

3) Эффективное чтение фиксированного количества байт (readFully):

Пример java
public static int readFully(InputStream in, byte[] buf, int off, int len) throws IOException {
    int total = 0;
    while (total < len) {
        int r = in.read(buf, off + total, len - total);
        if (r == -1) break;
        total += r;
    }
    return total; // может быть меньше len при EOF
}

// Использование с BufferedInputStream для производительности
Результат:
Гарантированное чтение до len байт или меньше при EOF

4) Кастомизация размера буфера для сетевых приложений и тесты производительности:

Пример java
// Тесты показывают оптимальный размер буфера для конкретной платформы и сети
int[] sizes = {1024, 4096, 8192, 65536};
for (int s : sizes) {
    long t0 = System.nanoTime();
    try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("big.dat"), s)){
        while (bis.read() != -1) {}
    }
    long t1 = System.nanoTime();
    System.out.println(s + ": " + (t1 - t0)/1_000_000 + " ms");
}
Результат:
Выдаёт время чтения для каждого размера буфера; помогает выбрать лучший размер в конкретной среде

5) Реюз внутреннего буфера через обёртку с пулом буферов (снижение аллокаций в горячих путях):

Пример java
// Идея: переиспользовать массивы байт при создании BufferedInputStream через фабричный метод
// Концептуальный пример, реализация требует аккуратной синхронизации и контроля времени жизни
Результат:
Снижение числа выделений памяти в интенсивных сценариях ввода/вывода

6) Совместное чтение и потоковая обработка: сочетание BufferedInputStream и java.util.zip.ZipInputStream позволяет эффективно находить и читать элементы в ZIP-архивах, избегая множественных малых чтений.

джава BufferedInputStream function comments

En
BufferedInputStream Adds buffering to an input stream, improving performance