Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 1. Классический Java I/O (java.io)
SequenceInputStream – объединение потоков. Последовательное чтение и управление памятью
Ключевое архитектурное решение — ленивое переключение между потоками.
Конструкторы и внутреннее устройство
Конструктор с двумя потоками создает временный
Создание
Механика чтения и путь данных
Посимвольное чтение
Путь данных для
Критически важно:
Блочное чтение
Блочное чтение более эффективно: данные копируются напрямую из underlying потока в предоставленный массив без промежуточных буферов
#Java #для_новичков #beginner #IO #NIO #SequenceInputStream
Глава 1. Классический Java I/O (java.io)
SequenceInputStream – объединение потоков. Последовательное чтение и управление памятью
SequenceInputStream — декоратор над одним или несколькими InputStream, который представляет их как единый непрерывный поток. Данные читаются последовательно: сначала из первого потока до его исчерпания, затем из второго, и так далее. С точки зрения вызывающего кода это обычный InputStream — методы read(), available(), close() работают прозрачно, скрывая факт наличия нескольких underlying источников.Ключевое архитектурное решение — ленивое переключение между потоками.
SequenceInputStream не читает данные заранее и не буферизует их. Он делегирует вызовы текущему активному потоку, и только при достижении конца текущего потока (возврат -1) переключается на следующий. Это минимизирует потребление памяти: данные не дублируются, а передаются напрямую из underlying потоков в вызывающий код.Конструкторы и внутреннее устройство
// Упрощенная структура из OpenJDK
public class SequenceInputStream extends InputStream {
// Текущий активный поток
private InputStream in;
// Перечисление оставшихся потоков
private Enumeration<InputStream> e;
// Конструктор для двух потоков
public SequenceInputStream(InputStream s1, InputStream s2) {
Vector<InputStream> v = new Vector<>(2);
v.addElement(s1);
v.addElement(s2);
e = v.elements();
nextStream(); // Установка первого потока
}
// Конструктор для произвольного количества потоков
public SequenceInputStream(Enumeration<InputStream> e) {
this.e = e;
nextStream(); // Установка первого потока
}
// Переключение на следующий поток
private void nextStream() {
if (in != null) {
in.close(); // Закрытие исчерпанного потока
}
in = e.hasMoreElements() ? e.nextElement() : null;
}
}
Конструктор с двумя потоками создает временный
Vector — устаревшая synchronized коллекция. Это исторический артефакт: SequenceInputStream существует с Java 1.0, и Vector был стандартом того времени. Создание
Vector в heap — незначительная аллокация, но в современном коде предпочтительнее использовать конструктор с Enumeration, построенным из Iterator или Collections.enumeration().Механика чтения и путь данных
Посимвольное чтение
public int read() throws IOException {
if (in == null) {
return -1; // Все потоки исчерпаны
}
int c = in.read(); // Делегирование текущему потоку
if (c == -1) {
nextStream(); // Переключение на следующий
return read(); // Рекурсивный вызов для нового потока
}
return c;
}Путь данных для
read():[Java: sis.read()]
-> [SequenceInputStream: in != null?]
-> [in.read()] // Делегирование текущему потоку
-> [Например, FileInputStream.read()]
-> [Системный вызов read()]
-> [Page Cache -> нативный буфер -> byte в heap]
-> [c == -1?] // Конец текущего потока?
-> [Нет: возврат c]
-> [Да: nextStream()]
-> [in.close()] // Закрытие исчерпанного потока
-> [in = e.nextElement()] // Следующий поток
-> [Рекурсивный read()] // Чтение из нового потока
Критически важно:
nextStream() автоматически закрывает исчерпанный поток. Это предотвращает утечку файловых дескрипторов. Однако если underlying потоки не поддерживают повторное открытие (как FileInputStream), они становятся недоступны после закрытия.Блочное чтение
public int read(byte[] b, int off, int len) throws IOException {
if (in == null) {
return -1;
}
if (b == null) {
throw new NullPointerException();
}
int n = in.read(b, off, len); // Попытка чтения из текущего потока
if (n > 0) {
return n; // Данные прочитаны
}
if (n == -1) {
nextStream(); // Конец потока — переключение
return read(b, off, len); // Рекурсивный вызов
}
// n == 0: поток вернул 0 байт, но не EOF
return 0;
}Блочное чтение более эффективно: данные копируются напрямую из underlying потока в предоставленный массив без промежуточных буферов
SequenceInputStream. Это минимизирует копирование в памяти.#Java #для_новичков #beginner #IO #NIO #SequenceInputStream
👍3
Роль garbage collector
Жизненный цикл объектов
Анализ памяти:
При закрытии
Проблема: утечка при исключении
Если
Практический пример: склеивание текстовых файлов
Путь данных при склеивании текстовых файлов
Ограничения и альтернативы
Невозможность произвольного доступа
Отсутствие буферизации
Современная альтернатива: Stream API Java 8
#Java #для_новичков #beginner #IO #NIO #SequenceInputStream
Жизненный цикл объектов
public void concatenateFiles(String outputPath, String... inputPaths)
throws IOException {
// Создание Enumeration из списка путей
Vector<InputStream> streams = new Vector<>();
for (String path : inputPaths) {
streams.add(new FileInputStream(path)); // Каждый поток — объект в heap
}
// SequenceInputStream создается в heap
// Внутренний Vector — объект в heap
try (SequenceInputStream sis = new SequenceInputStream(streams.elements());
FileOutputStream fos = new FileOutputStream(outputPath)) {
byte[] buffer = new byte[8192]; // Буфер в heap
int bytesRead;
// Цикл без аллокаций — GC не вовлечен
while ((bytesRead = sis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
} // sis.close() закрывает текущий поток и все оставшиеся в Enumeration
// Vector, FileInputStream'ы, buffer — недостижимы, GC собирает
}
Анализ памяти:
Vector<InputStream> — коллекция в heap, содержит ссылки на все потокиFileInputStream для каждого файла — объекты в heap с файловыми дескрипторамиSequenceInputStream — объект в heap с ссылкой на Vector и текущий потокbuffer[8192] — массив в heap, переиспользуется в циклеПри закрытии
SequenceInputStream через try-with-resources вызывается close(), который закрывает текущий поток и все оставшиеся потоки в Enumeration. Это гарантирует освобождение файловых дескрипторов.Проблема: утечка при исключении
// Антипаттерн: утечка при исключении в конструкторе
Vector<InputStream> streams = new Vector<>();
for (String path : paths) {
streams.add(new FileInputStream(path)); // Если здесь исключение — предыдущие потоки не закрыты
}
SequenceInputStream sis = new SequenceInputStream(streams.elements());
Если
new FileInputStream() выбрасывает FileNotFoundException на N-м файле, предыдущие N-1 потоков остаются открытыми. Решение — явное управление ресурсами:// Правильно: гарантированное закрытие при исключении
List<InputStream> streams = new ArrayList<>();
try {
for (String path : paths) {
streams.add(new FileInputStream(path));
}
Enumeration<InputStream> en = Collections.enumeration(streams);
try (SequenceInputStream sis = new SequenceInputStream(en)) {
// Использование
}
} catch (IOException e) {
// Закрытие всех созданных потоков при ошибке
for (InputStream is : streams) {
try { is.close(); } catch (IOException ignored) {}
}
throw e;
}
Практический пример: склеивание текстовых файлов
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class FileConcatenator {
// Объединение файлов с автоматическим закрытием
public void concatenate(String outputPath, String... inputPaths)
throws IOException {
// Создание списка потоков с гарантированным закрытием при ошибках
List<InputStream> streams = new ArrayList<>();
boolean success = false;
try {
for (String path : inputPaths) {
streams.add(new FileInputStream(path));
}
// SequenceInputStream объединяет все потоки
Enumeration<InputStream> en = Collections.enumeration(streams);
try (SequenceInputStream sis = new SequenceInputStream(en);
// Декодирование байтов в символы через UTF-8
InputStreamReader isr = new InputStreamReader(sis, StandardCharsets.UTF_8);
// Буферизация символьного ввода
BufferedReader reader = new BufferedReader(isr);
// Запись в выходной файл
FileOutputStream fos = new FileOutputStream(outputPath);
OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
BufferedWriter writer = new BufferedWriter(osw)) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
success = true;
}
} finally {
// Закрытие всех потоков, если try-with-resources не сработал
if (!success) {
for (InputStream is : streams) {
try { is.close(); } catch (IOException ignored) {}
}
}
}
}
// Альтернатива: объединение только двух файлов
public void concatenateTwo(String outputPath, String path1, String path2)
throws IOException {
try (SequenceInputStream sis = new SequenceInputStream(
new FileInputStream(path1),
new FileInputStream(path2));
FileOutputStream fos = new FileOutputStream(outputPath)) {
// Прямое блочное копирование без промежуточных преобразований
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = sis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
// Оба FileInputStream закрыты автоматически:
// - первый при исчерпании (внутри nextStream)
// - второй при закрытии SequenceInputStream
}
}
Путь данных при склеивании текстовых файлов
[Файл 1 на диске: байты UTF-8]
-> [FileInputStream 1: системный вызов read()]
-> [SequenceInputStream: делегирование FileInputStream 1]
-> [InputStreamReader: декодирование байт -> char]
-> [BufferedReader: накопление в cb[8192], поиск \n]
-> [readLine(): создание String]
-> [BufferedWriter.write(String)]
-> [OutputStreamWriter: кодирование char -> байты UTF-8]
-> [FileOutputStream: системный вызов write()]
-> [Page Cache -> диск]
[Конец файла 1: FileInputStream 1.read() возвращает -1]
-> [SequenceInputStream.nextStream()]
-> [FileInputStream 1.close()] // Автоматическое закрытие
-> [FileInputStream 2 становится активным]
[Файл 2 на диске: байты UTF-8]
-> [FileInputStream 2: системный вызов read()]
-> [SequenceInputStream: делегирование FileInputStream 2]
-> [Тот же путь через Reader/Writer]
Ограничения и альтернативы
Невозможность произвольного доступа
SequenceInputStream — строго последовательный. После переключения на следующий поток вернуться к предыдущему невозможно — он закрыт. Это отличает его от ConcatenationInputStream в некоторых библиотеках или от ручного управления массивом потоков.Отсутствие буферизации
SequenceInputStream не имеет собственного буфера. Если underlying потоки не буферизованы, каждый read() инициирует системный вызов. Рекомендуется оборачивать underlying потоки в BufferedInputStream:// Правильно: буферизация каждого потока
Vector<InputStream> streams = new Vector<>();
for (String path : paths) {
streams.add(new BufferedInputStream(new FileInputStream(path)));
}
SequenceInputStream sis = new SequenceInputStream(streams.elements());
Современная альтернатива: Stream API Java 8
// Объединение через Stream API — более гибко, но с другой семантикой
public void concatenateModern(String outputPath, String... inputPaths)
throws IOException {
try (OutputStream out = new FileOutputStream(outputPath)) {
for (String path : inputPaths) {
Files.copy(Path.of(path), out); // Копирование файла в общий поток
}
}
}
Files.copy() использует transferTo() — оптимизированный механизм копирования через FileChannel, потенциально с zero-copy на уровне ОС. Это более эффективно, чем SequenceInputStream с промежуточным буфером.#Java #для_новичков #beginner #IO #NIO #SequenceInputStream
👍3
Что выведет код?
#Tasks
import java.io.*;
public class Task010726 {
public static void main(String[] args) throws IOException {
ByteArrayInputStream bais1 = new ByteArrayInputStream(new byte[]{1, 2, 3});
ByteArrayInputStream bais2 = new ByteArrayInputStream(new byte[]{4, 5, 6});
SequenceInputStream sis = new SequenceInputStream(bais1, bais2);
byte[] buffer = new byte[5];
int len = sis.read(buffer, 0, 5);
System.out.println(len);
for (int i = 0; i < len; i++) {
System.out.print(buffer[i] + " ");
}
}
}
#Tasks
👍3
👍3
Водопад Корбу
🔥11
Что такое 🤓
Ответ:
В Java 11 добавлены удобные методы для чтения и записи строк из/в файл:
Files.readString(Path path) — читает весь файл в строку (используя UTF-8).
Files.writeString(Path path, CharSequence csq) — записывает строку в файл.
Поддерживают опции StandardOpenOption. Это избавляет от рутины с BufferedReader и FileWriter для простых сценариев.
Пример: String content = Files.readString(Path.of("file.txt"));. Подходят для небольших файлов (для больших использовать стримы).
#собеседование
Files.readString() и writeString()? Ответ:
Files.readString(Path path) — читает весь файл в строку (используя UTF-8).
Files.writeString(Path path, CharSequence csq) — записывает строку в файл.
Поддерживают опции StandardOpenOption. Это избавляет от рутины с BufferedReader и FileWriter для простых сценариев.
Пример: String content = Files.readString(Path.of("file.txt"));. Подходят для небольших файлов (для больших использовать стримы).
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
История технологии сегодня — 2 июля
ℹ️ Кто родился в этот день
Па́вел Дми́триевич Кузьми́нский (20 июня [2 июля] 1840, Усть-Лабинская, Кубанская область — 7 [20] апреля 1900, Санкт-Петербург) — русский инженер, изобретатель газовой турбины.
Ханс А́льбрехт Бе́те (нем. Hans Albrecht Bethe; 2 июля 1906 года, Страсбург, Германия — 6 марта 2005 года, Итака, Нью-Йорк) — американский астрофизик и физик-ядерщик, лауреат Нобелевской премии по физике (1967). В сферу его интересов также входили квантовая электродинамика и физика твёрдого тела.
🌐 Знаковые события
1948 — первый полёт летающей лодки Бе-6.
#Biography #Birth_Date #Events #02июля
Па́вел Дми́триевич Кузьми́нский (20 июня [2 июля] 1840, Усть-Лабинская, Кубанская область — 7 [20] апреля 1900, Санкт-Петербург) — русский инженер, изобретатель газовой турбины.
Ханс А́льбрехт Бе́те (нем. Hans Albrecht Bethe; 2 июля 1906 года, Страсбург, Германия — 6 марта 2005 года, Итака, Нью-Йорк) — американский астрофизик и физик-ядерщик, лауреат Нобелевской премии по физике (1967). В сферу его интересов также входили квантовая электродинамика и физика твёрдого тела.
1948 — первый полёт летающей лодки Бе-6.
#Biography #Birth_Date #Events #02июля
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
[Совет по Java #056]
Тема:
Проблема:
Однако он не предоставляет сами результаты. Разработчики часто ошибочно полагают, что результирующий фьючер содержит список значений, и пытаются его обработать, сталкиваясь с тем, что нет доступа к данным. Приходится либо повторно обращаться к исходным фьючерам через
Решение: После
Важно помнить, что
Объяснение:
Передавая массив фьючеров, мы получаем
После того как
#Java #советы
Тема:
CompletableFuture.allOf не возвращает результаты.Проблема:
CompletableFuture.allOf(CompletableFuture<?>... cfs) возвращает CompletableFuture<Void>, который завершается, когда все переданные фьючеры завершены (успешно или с исключением). Однако он не предоставляет сами результаты. Разработчики часто ошибочно полагают, что результирующий фьючер содержит список значений, и пытаются его обработать, сталкиваясь с тем, что нет доступа к данным. Приходится либо повторно обращаться к исходным фьючерам через
join(), либо использовать громоздкие конструкции. Это ведет к дублированию кода и потенциальным ошибкам, если какой-то фьючер завершился исключительно.Решение: После
allOf примените thenApply или thenRun, чтобы объединить результаты из исходного списка. Самый чистый способ — обернуть allOf в цепочку, которая собирает значения через Stream и map(CompletableFuture::join). Если нужна обработка каждого результата, используйте thenApply с преобразованием в коллекцию. Для асинхронного продолжения можно применить thenCompose или thenCombine, но они сложнее. Важно помнить, что
join() на завершенном фьючере не блокирует (или блокирует очень коротко), поэтому безопасно вызывать его после allOf.import java.util.concurrent.CompletableFuture;
import java.util.List;
import java.util.stream.Collectors;
public class AllOfResults {
public static void main(String[] args) {
List<CompletableFuture<String>> futures = List.of(
CompletableFuture.supplyAsync(() -> "Result 1"),
CompletableFuture.supplyAsync(() -> "Result 2"),
CompletableFuture.supplyAsync(() -> "Result 3")
);
//allOf возвращает Void — результаты не доступны
CompletableFuture<Void> all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
all.join(); // ждем завершения, но результатов нет
//Собираем результаты вручную через thenApply
CompletableFuture<List<String>> combined = CompletableFuture
.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList()));
List<String> results = combined.join();
System.out.println(results); // [Result 1, Result 2, Result 3]
// ✅ Альтернатива: использование thenAccept для побочного эффекта
CompletableFuture<Void> log = CompletableFuture
.allOf(futures.toArray(new CompletableFuture[0]))
.thenAccept(v -> futures.forEach(f -> System.out.println(f.join())));
log.join();
//Сборка с преобразованием типов
CompletableFuture<Integer> totalLength = CompletableFuture
.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join)
.mapToInt(String::length)
.sum());
System.out.println("Total length: " + totalLength.join());
}
}
Объяснение:
allOf — это синхронизатор, а не коллектор. Его предназначение — дождаться завершения всех задач, чтобы затем предпринять действие. Передавая массив фьючеров, мы получаем
Void, потому что при проектировании API было решено не навязывать типовые ограничения на разнородные результаты. Поэтому сбор результатов остается на совести разработчика. После того как
allOf завершился, все фьючеры гарантированно находятся в состоянии "завершено" (completed), поэтому вызов join() на каждом из них не блокирует и безопасен. Это позволяет строить цепочки, где thenApply получает Void и возвращает нужную коллекцию. Для обработки ошибок используйте handle вместо thenApply. #Java #советы
👍5
Что выведет код?
#Tasks
import java.util.concurrent.CompletableFuture;
public class Task020726 {
public static void main(String[] args) throws Exception {
CompletableFuture<Integer> f1 = CompletableFuture.completedFuture(42);
CompletableFuture<String> f2 = CompletableFuture.completedFuture("hello");
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2);
System.out.println(all.get());
}
}
#Tasks
👍4