Если byte[] представляет собой встроенный в язык тип, представляющий собой данные в памяти компьютера (ОЗУ==RAM), то InputStream / OutputStream представляют собой классы стандартной библиотеки (JDK) для ввода/вывода (чтения/записи) байтов.
Основные отличия byte[] от InputStream/OutputStream:
1) у byte[] есть размер (byte[].length), а у IS/OS - нет, что позволяет читать как неограниченные по размеру объемы данных (length - имеет тип int, и максимальное значение порядка 2.000.000.000) - 10, 100, 1000 Gb, так и данные неизвестного заранее размера - поток видеоданных с сервера при видео-конференции.
2) byte[] - один тип И для чтения И для записи, IS/OS - два разных.
3) byte[] - целиком лежит в памяти и, значит, убирается автоматически (GC), IS/OS - могут содержать компоненты вне памяти, которые надо явно закрывать методом close().
4) byte[] - данные с произвольным доступом, IS/OS - с последовательным.
Обратите внимание, InputStream / OutputStream содержат метод close(). Необходимо обеспечить из вызов по окончанию работы с потоком (успешным или безуспешным). С потоком ввода/вывода возможно связаны ресурсы за пределами Java heap.
Пример: ByteArrayInputStream / ByteArrayOutputStream - не связаны, все уберет GC.
Пример: FileInputStream / FileOutputStream - скорее всего связаны "магические структуры" операционной системы, файловые дескрипторы.
Пример: работа с интернетом - точно связаны классы JDK - java.net.Socket + "магические структуры" нашей операционной системы = сокеты (socket) + какие-то ресурсы сетевой карты (порты, прерывания, память) + сокет на второй стороне (на сервере).
При вызове close() эти ресурсы освобождаются.
InputStream
Замечание: далее везде ниже в файле "c:/tmp/text.txt" лежит строчка "Hello World!" (без кавычек - "). Файл занимает 12 байт.
Два улучшение предыдущего примера:
1. Корректно закрываем поток ввода.
2. "Укороченная форма" чтения из InputStream.
Справка (упрощенное описание, может не совпадать с точным от авторов компиляторов):
Statement - все, после чего можно поставить точку с запятой. Пример: Math.sin(1); int x; int y = 1;
Expression - все, что имеет значение, т.е. может стоять в правой части оператора присваивания. Пример: Math.sin(1), 123, "Hello World!", 2 * (2 + 30 / 2).
Некоторые части кода являются И Statement И Expression. Пример: Math.sin(1). Присвоение является И тем И другим. Это позволяет писать такое:
int x, y, z;
z = 1;
y = (z = 2);
x = (y = (z = 3));
byte b1 = -10;
int x1 = ... что-то сделать с b1;
byte b2 = ... что-то сделать с x2;
int x3 = 210;
byte b3 = ... что-то сделать с x3;
"Укороченная форма" чтения буфером:
Общая практика завершения работы с потоком вывода заключается в вызове flush() + close(). Для некоторых потоков эти методы могут ничего не делать, как у ByteArrayOutputStream. Но никто еще не был уволен за вызов этой комбинации. close() стоит вызывать в finally секции, в close() могут освобождаться важные системные ресурсы (FileOutputStream - может вызывать освобождение ресурсов операционной системы, связанных с файлом).
Запись всего массива байтов производится методом write(byte[]).
Запись диапазона из массива байтов производится методом write(byte[], int, int).Обратите внимание: второй аргумент - это индекс левого конца, но третий аргумент - это длина диапазона, а не индекс правого конца.
Заметьте, что запись массива и диапазона - безусловны (т.е. либо будут записаны все данные, либо будет инициирована исключительная ситуация - обе ситуации могут произойти после задержки/"залипания"/блокирования метода). Чтение из OutputStream (read(byte[]), read(byte[], int, int)) может не заполнить весь массив данными.
close() - производит "закрытие" потока вывода и освобождения связанных ресурсов.
ПОЛНАЯ ВЕРСИЯ:
Демонстрируем, что ввод/вывод часто сопряжен с блокирующими операциями (операциями выполняющимися неопределенно долго). Заметьте - время отмеряем в НАНОСЕКУНДАХ, выводим только случаи задержек. Подсказка - данные приходят в виде IP-пакетов, ждем данных пакета долго (миллисекунды), потом очень быстро их вычитываем .потом ждем следующего пакета:
>> Elapsed time = 33ms
>> Elapsed time = 31ms
>> Elapsed time = 16ms
>> Elapsed time = 16ms
>> Elapsed time = 3ms
>> Elapsed time = 13ms
>> Elapsed time = 3ms
>> Elapsed time = 14ms
>> Elapsed time = 2ms
>> Elapsed time = 14ms
>> Elapsed time = 4ms
>> Elapsed time = 3ms
>> Elapsed time = 2ms
>> Elapsed time = 4ms
>> Elapsed time = 7ms
Основные отличия byte[] от InputStream/OutputStream:
1) у byte[] есть размер (byte[].length), а у IS/OS - нет, что позволяет читать как неограниченные по размеру объемы данных (length - имеет тип int, и максимальное значение порядка 2.000.000.000) - 10, 100, 1000 Gb, так и данные неизвестного заранее размера - поток видеоданных с сервера при видео-конференции.
2) byte[] - один тип И для чтения И для записи, IS/OS - два разных.
3) byte[] - целиком лежит в памяти и, значит, убирается автоматически (GC), IS/OS - могут содержать компоненты вне памяти, которые надо явно закрывать методом close().
4) byte[] - данные с произвольным доступом, IS/OS - с последовательным.
Обратите внимание, InputStream / OutputStream содержат метод close(). Необходимо обеспечить из вызов по окончанию работы с потоком (успешным или безуспешным). С потоком ввода/вывода возможно связаны ресурсы за пределами Java heap.
Пример: ByteArrayInputStream / ByteArrayOutputStream - не связаны, все уберет GC.
Пример: FileInputStream / FileOutputStream - скорее всего связаны "магические структуры" операционной системы, файловые дескрипторы.
Пример: работа с интернетом - точно связаны классы JDK - java.net.Socket + "магические структуры" нашей операционной системы = сокеты (socket) + какие-то ресурсы сетевой карты (порты, прерывания, память) + сокет на второй стороне (на сервере).
При вызове close() эти ресурсы освобождаются.
InputStream
Замечание: далее везде ниже в файле "c:/tmp/text.txt" лежит строчка "Hello World!" (без кавычек - "). Файл занимает 12 байт.
Этот пример кода показывает, что разработчики JDK положили в основу получения байтовых данных из различных источников (файл, интернет, массив) один специальный тип - InputStream. Можно сказать, что этот тип представляет собой Pure Fabrication (Чистая Синтетика) - абстракция, не соответствующая чему-либо конкретному из предметной области
import java.io.*;
import java.net.URL;
/**
* BAD! You MUST close InputStreams and OutputStreams always!
*/
public class _0_ISTest {
public static void main(String[] args) throws IOException {
InputStream inFile
= new FileInputStream("c:/tmp/text.txt");
readFullyByByte(inFile);
System.out.print("\n\n\n");
InputStream inUrl
= new URL("http://google.com").openStream();
readFullyByByte(inUrl);
System.out.print("\n\n\n");
InputStream inArray
= new ByteArrayInputStream(new byte[] {65, 66, 67, 68, 69});
readFullyByByte(inArray);
System.out.print("\n\n\n");
}
public static void readFullyByByte(InputStream in) throws IOException {
while (true) {
int oneByte = in.read();
if (oneByte != -1) {
System.out.print((char) oneByte);
} else {
System.out.print("\n" + "end");
break;
}
}
}
}
Два улучшение предыдущего примера:
1. Корректно закрываем поток ввода.
2. "Укороченная форма" чтения из InputStream.
Справка (упрощенное описание, может не совпадать с точным от авторов компиляторов):
Statement - все, после чего можно поставить точку с запятой. Пример: Math.sin(1); int x; int y = 1;
Expression - все, что имеет значение, т.е. может стоять в правой части оператора присваивания. Пример: Math.sin(1), 123, "Hello World!", 2 * (2 + 30 / 2).
Некоторые части кода являются И Statement И Expression. Пример: Math.sin(1). Присвоение является И тем И другим. Это позволяет писать такое:
int x, y, z;
z = 1;
y = (z = 2);
x = (y = (z = 3));
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class _1_ISTest_close_short {
public static void main(String[] args) throws IOException {
String fileName = "c:/tmp/text.txt";
InputStream inFile = null;
try {
inFile = new FileInputStream(fileName);
readFullyByByte(inFile);
} catch (IOException e) {
throw new IOException("Exception when open+read file " + fileName, e);
} finally {
if (inFile != null) {
try {
inFile.close();
} catch (IOException ignore) {
/*NOP*/
}
}
}
}
public static void readFullyByByte(InputStream in) throws IOException {
int oneByte;
while ((oneByte = in.read()) != -1) {
System.out.print((char) oneByte);
}
}
}
Вопросы по примеру:
1) зачем нужна проверка "if (inFile != null) {...}" ?
2) зачем приведение типа перед выводом на консоль "(char) oneByte"?
3) разберитесь, как именно преобразуется byte в int при чтении методом "int read()". Напишите такое преобразование самостоятельно (помните, byte лежит в диапазоне [-128, 127], а соответствующий int в диапазоне [0, 255]):
byte b0 = 10;
int x0 = ... что-то сделать с b0;
int x1 = ... что-то сделать с b1;
int x2 = 10;
int x3 = 210;
byte b3 = ... что-то сделать с x3;
Более оптимально (меньше обращений к программно-аппаратным (винту, сетевой карте, драйверам, модулям операционной системы) устройствам ввода/вывода) вычитывать данные не побайтно, а диапазонами байт (в буфер):
import java.io.*;
import java.util.Arrays;
public class _2_ISTest_array {
public static void main(String[] args) throws IOException {
String fileName = "c:/tmp/text.txt";
InputStream inFile = null;
try {
inFile = new FileInputStream(fileName);
readFullyByArray(inFile);
} catch (IOException e) {
throw new IOException("Exception when open+read file " + fileName, e);
} finally {
closeQuietly(inFile);
}
}
public static void readFullyByArray(InputStream in) throws IOException {
byte[] buff = new byte[5];
while (true) {
int count = in.read(buff);
if (count != -1) {
System.out.println("count = " + count
+ ", buff = " + Arrays.toString(buff)
+ ", str = " + new String(buff, 0, count, "UTF8"));
} else {
break;
}
}
}
private static void closeQuietly(InputStream inFile) {
if (inFile != null) {
try {
inFile.close();
} catch (IOException ignore) {
/*NOP*/
}
}
}
}
Примечание: обратите внимание на различие в смысле int, который возвращают методы "int read()" и "int read(byte[])". Если вернули -1, то смысл одинаков - больше в потоке нет данных. Но если вернули не -1, то "int read()" возвращает прочитанный байт, преобразованные к типу int, т.е. сами данные, а "int read(byte[])" возвращает количество данных в буфере.
"Укороченная форма" чтения буфером:
import java.io.*;
public class _3_ISTest_array_short {
public static void main(String[] args) throws IOException {
String fileName = "c:/tmp/text.txt";
InputStream inFile = null;
try {
inFile = new FileInputStream(fileName);
readFullyByArray(inFile);
} catch (IOException e) {
throw new IOException("Exception when open+read file " + fileName, e);
} finally {
closeQuietly(inFile);
}
}
public static void readFullyByArray(InputStream in) throws IOException {
byte[] buff = new byte[5];
int count;
while ((count = in.read(buff)) != -1) {
System.out.println(new String(buff, 0, count, "UTF8"));
}
}
private static void closeQuietly(InputStream inFile) {
if (inFile != null) {
try {
inFile.close();
} catch (IOException ignore) {
/*NOP*/
}
}
}
}
OutputStream
Абстрактный класс OutputStream имеет 5 методов, объединенных в 2 группы:
Запись: write(int), write(byte[]), write(byte[], int, int)
Завершение: flush(), close().
Надо понимать, что есть контракт класса OutputStream, а есть контракт конкретного наследника. Они могут не совпадать, но контракты наследников не могут противоречить контракту предка! Т.е. код, написанный для работы с предком, должен корректно работать с любым потомком.
Запись: write(int), write(byte[]), write(byte[], int, int)
Завершение: flush(), close().
Надо понимать, что есть контракт класса OutputStream, а есть контракт конкретного наследника. Они могут не совпадать, но контракты наследников не могут противоречить контракту предка! Т.е. код, написанный для работы с предком, должен корректно работать с любым потомком.
В общем случае, предполагается, что у потока вывода есть два состояние - "готов к записи" и "закрыт" (по факту, может быть всего единственное состояние, как у ByteArrayOutputStream -"Closing a ByteArrayOutputStream has no effect", flush() - унаследован от OutputStream, тоже ничего не делает). У потока вывода нет состояния "новый", это выражается в том, что нет методов init()/open()/connect()/start()/... Сразу же после создания, поток готов к записи. После вызова close() - поток закрыт. Невозможно повторно открыть закрытый поток.
Общая практика завершения работы с потоком вывода заключается в вызове flush() + close(). Для некоторых потоков эти методы могут ничего не делать, как у ByteArrayOutputStream. Но никто еще не был уволен за вызов этой комбинации. close() стоит вызывать в finally секции, в close() могут освобождаться важные системные ресурсы (FileOutputStream - может вызывать освобождение ресурсов операционной системы, связанных с файлом).
"Пишущие" методы
Запись одного байта производится методом write(int). По ряду причин (TODO: каких?)сигнатура метода имеет параметр типа int, а не типа byte. Тут происходит нетривиальное(TODO: какое?) преобразование - байт лежит в диапазоне [-128,127], а преобразуется в значение int в диапазоне [0, 255].Запись всего массива байтов производится методом write(byte[]).
Запись диапазона из массива байтов производится методом write(byte[], int, int).Обратите внимание: второй аргумент - это индекс левого конца, но третий аргумент - это длина диапазона, а не индекс правого конца.
Заметьте, что запись массива и диапазона - безусловны (т.е. либо будут записаны все данные, либо будет инициирована исключительная ситуация - обе ситуации могут произойти после задержки/"залипания"/блокирования метода). Чтение из OutputStream (read(byte[]), read(byte[], int, int)) может не заполнить весь массив данными.
"Завершающий" методы
flush() - производит "сбрасывание" данных, если таковые "застряли" в потоке вывода. Основной пример "застревания" - буферизация. Можно вызывать много раз подряд. После вызова поток вывода готов к продолжению записи данных.close() - производит "закрытие" потока вывода и освобождения связанных ресурсов.
УПРОЩЕННАЯ ВЕРСИЯ:
package java.io;
public class OutputStream {
public void write(int b) throws IOException;
public void write(byte b[]) throws IOException;
public void write(byte b[], int off, int len) throws IOException;
public void flush() throws IOException {}
public void close() throws IOException {}
}
ПОЛНАЯ ВЕРСИЯ:
package java.io;
public abstract class OutputStream implements Closeable, Flushable {
public abstract void write(int b) throws IOException;
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
public void write(byte b[], int off, int len) throws IOException {
...
for (int i = 0 ; i < len ; i++) {
write(b[off + i]);
}
}
public void flush() throws IOException {}
public void close() throws IOException {}
}
Читаем побайтно файл через InputStream (FileInputInputStream), пишем побайтно в OutputStream (ByteArrayOutputStream)
import java.io.*;
public class _4_OSTest {
public static void main(String[] args) throws IOException {
String fileName = "c:/tmp/text.txt";
InputStream inFile = null;
try {
inFile = new FileInputStream(fileName);
byte[] data = readFullyByByte(inFile);
System.out.println(new String(data, "UTF8"));
} catch (IOException e) {
throw new IOException("Exception when open+read file " + fileName, e);
} finally {
closeQuietly(inFile);
}
}
public static byte[] readFullyByByte(InputStream in) throws IOException {
int oneByte;
ByteArrayOutputStream out = new ByteArrayOutputStream();
while ((oneByte = in.read()) != -1) {
out.write(oneByte);
}
return out.toByteArray();
}
private static void closeQuietly(InputStream inFile) {
if (inFile != null) {
try {
inFile.close();
} catch (IOException ignore) {
/*NOP*/
}
}
}
}
Читаем массивами файл через InputStream (FileInputInputStream), пишем массивами в OutputStream (ByteArrayOutputStream)
import java.io.*;
public class _5_OSTest_array {
public static void main(String[] args) throws IOException {
String fileName = "c:/tmp/text.txt";
InputStream inFile = null;
try {
inFile = new FileInputStream(fileName);
byte[] data = readFullyByByte(inFile);
System.out.println(new String(data, "UTF8"));
} catch (IOException e) {
throw new IOException("Exception when open and read file " + fileName, e);
} finally {
closeQuietly(inFile);
}
}
public static byte[] readFullyByByte(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buff = new byte[5];
int count;
while ((count = in.read(buff)) != -1) {
out.write(buff, 0, count);
}
return out.toByteArray();
}
private static void closeQuietly(InputStream inFile) {
if (inFile != null) {
try {
inFile.close();
} catch (IOException ignore) {
/*NOP*/
}
}
}
}
Демонстрируем огромную разницу времени побайтного чтения и чтения буферами (у меня "c:/tmp/image0.png" - это картинка 500Кб):
import java.io.*;
public class _6_OSTest_different_buffers {
public static void main(String[] args) throws IOException {
String fileFromName = "c:/tmp/image0.png";
String fileToName = "c:/tmp/image1.png";
for (int k = 1; k < 64 * 1024; k *= 2) {
InputStream in = null;
OutputStream out = null;
try {
in = new FileInputStream(fileFromName);
out = new FileOutputStream(fileToName);
long startTime = System.currentTimeMillis();
copy(in, out, k);
long stopTime = System.currentTimeMillis();
System.out.println("Elapsed time = " + (stopTime - startTime));
} catch (IOException e) {
throw new IOException("Exception when copy from '" + fileFromName + "' to file '" + fileToName + "'", e);
} finally {
closeQuietly(in);
closeAndFlushQuietly(out);
}
}
}
public static void copy(InputStream in, OutputStream out, int bufferSize) throws IOException {
byte[] buff = new byte[bufferSize];
int count;
while ((count = in.read(buff)) != -1) {
out.write(buff, 0, count);
}
}
private static void closeQuietly(InputStream in) {
if (in != null) {
try {
in.close();
} catch (IOException ignore) {/*NOP*/}
}
}
private static void closeAndFlushQuietly(OutputStream out) {
if (out != null) {
try {
out.flush();
} catch (IOException ignore) {/*NOP*/}
try {
out.close();
} catch (IOException ignore) {/*NOP*/}
}
}
}
>> Elapsed time = 2290
>> Elapsed time = 1201
>> Elapsed time = 1022
>> Elapsed time = 512
>> Elapsed time = 262
>> Elapsed time = 127
>> Elapsed time = 63
>> Elapsed time = 34
>> Elapsed time = 16
>> Elapsed time = 9
>> Elapsed time = 5
>> Elapsed time = 3
>> Elapsed time = 2
>> Elapsed time = 1
>> Elapsed time = 1
>> Elapsed time = 1
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
public class _7_ISTest_blocked_operations {
public static void main(String[] args) throws IOException {
InputStream is = new URL("http://lenta.ru").openStream();
long startTime = System.nanoTime();
while (is.read() != -1) {
long stopTime = System.nanoTime();
long dT = stopTime - startTime;
if (dT > 1000000) {
System.out.println("Elapsed time = " + dT/1000000 + "ms");
}
startTime = stopTime;
}
}
}
>> Elapsed time = 33ms
>> Elapsed time = 31ms
>> Elapsed time = 16ms
>> Elapsed time = 16ms
>> Elapsed time = 3ms
>> Elapsed time = 13ms
>> Elapsed time = 3ms
>> Elapsed time = 14ms
>> Elapsed time = 2ms
>> Elapsed time = 14ms
>> Elapsed time = 4ms
>> Elapsed time = 3ms
>> Elapsed time = 2ms
>> Elapsed time = 4ms
>> Elapsed time = 7ms
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
x
Лабораторные
io.streams.remove_zero
import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Arrays; public class BAOSTest { public static void main(String[] args) throws IOException { ByteArrayOutputStream buff = new ByteArrayOutputStream(); buff.write(0); buff.write(1); buff.write(2); byte[] arr = buff.toByteArray(); System.out.println(Arrays.toString(arr)); } }
Написать программу, которая вычитывает файл и записывает файл, удаляя все байты равные 0.
а) добавьте в текущую программу корректное завершение работы c потоками(flush(), close()) + гарантированные вызов close() ОБОИХ потоков даже в случае IOException.
б) добавьте буферизацию (BufferedInputStream + BufferedOutputStream) в чтение из и запись в файл. Сравните время работы без буфера и с буфером размером 1, 2, 4, 8, ..., 1024 байта.
с) кроме буферизации добавьте чтение в массив (read(byte[]) или read(byte[], int, int)) + запись массивом (write(byte[] или write(byte[], int, int))).
P.S. Для заметного ускорения проводите тестирование на большом (более 1Мб).