Шпаргалка Java программиста 7.1 Типовые задачи: Оптимальный путь преобразования InputStream в строку
SavePearlHarbor
Ещё одна копия хабора
Главное меню
Навигация по записям
Шпаргалка Java программиста 7.1 Типовые задачи: Оптимальный путь преобразования InputStream в строку
У меня есть хобби: я собираю различные решения типовых задач в Java, которые нахожу в инете, и пытаюсь выбрать наиболее оптимальное по размеру/производительности/элегантности. В первую очередь по производительности. Давайте рассмотрим такую типовую задач, которые часто встречаются в программировании на Java как “преобразование InputStream в строку” и разные варианты её решения.
Посмотрим какие ограничения есть у каждого (требования подключения определенной библиотеки/определенной версии, корректная работа с unicode и т.д.). Английскую версию этой статьи можно найти в моем ответе на stackoverflow. Тесты в моем проекте на github.
Если вы ещё не видели, советую посмотреть статью Список полезных ссылок для Java программиста, на мой взгляд наиболее полезная из всех моих статей на данный момент.
Преобразование InputStream в строку (String)
Очень часто встречающая задача, давайте рассмотрим какими способами можно это сделать (их будет 11):
Используя IOUtils.toString из библиотеки Apache Commons . Один из самых коротких однострочников.
Используя CharStreams из библиотеки guava . Тоже довольно короткий однострочник.
Используя Scanner (JDK). Решение короткое, хитрое, с помощью чистого JDK, но это скорее хак, который вынесет мозг тем кто о таком фокусе не знает.
Используя Stream Api с помощью Java 8 . Предупреждение: Оно заменяет разные переносы строки (такие как rn ) на n , иногда это может быть критично.
Используя parallel Stream Api ( Java 8 ). Предупреждение: Как и 4 решение, оно заменяет разные переносы строки (такие как rn ) на n .
Используя InputStreamReader и StringBuilder из обычного JDK
Используя StringWriter и IOUtils.copy из Apache Commons
Используя ByteArrayOutputStream и inputStream.read из JDK
Используя BufferedReader из JDK . Предупреждение: Это решение заменяет разные переносы строк (такие как nr ) на line.separator system property (например, в Windows на “rn”).
Используя BufferedInputStream и ByteArrayOutputStream из JDK
Используя inputStream.read() и StringBuilder ( JDK ). Предупреждение: Это решение не работает с Unicode, например с русским текстом
Итак о использовании:
Решения 4 , 5 и 9 преобразую разные переносы строки в одну.
Решения 11 не работает с Unicode текстом
Замеры производительности
Предупреждение: замеры производительности всегда сильно зависят от системы, условий замера и т.п. Я измерял на двух разных компьютерах, один Windows 8.1, Intel i7-4790 CPU 3.60GHz2, 16Gb, второй — Linux Mint 17.2, Celeron Dual-Core T3500 2.10Ghz2, 6Gb, однако не могу гарантировать что результаты являются абсолютной истиной, вы всегда можете повторить тесты (test1 и test2) на вашей системе.
Замеры производительности для небольших строк (длина = 175), тесты можно найти на github (режим = среднее время выполнения (AverageTime), система = Linux Mint 17.2, Celeron Dual-Core T3500 2.10Ghz*2, 6Gb, чем значение ниже тем лучше, 1,343 — наилучшее):
Замеры производительности для больших строк (длина = 50100), тесты можно найти на github (режим = среднее время выполнения (AverageTime), система = Linux Mint 17.2, Celeron Dual-Core T3500 2.10Ghz*2, 6Gb, чем значение ниже тем лучше, 200,715 — наилучшее):
График зависимости среднего времени от длины строки, система Windows 8.1, Intel i7-4790 CPU 3.60GHz 3.60GHz, 16Gb:
Таблица зависимости среднего времени от длины строки, система Windows 8.1, Intel i7-4790 CPU 3.60GHz 3.60GHz, 16Gb:
Выводы
Самым быстрым решением во всех случаях и всех системах оказался 8 тест: Используя ByteArrayOutputStream и inputStream.read из JDK
Коротким и весьма быстрым решением будет использование IOUtils.toString из Apache Commons
Stream Api из Java 8 показывает среднее время, а использование параллельных стримов имеет смысл только при довольно большой строки, иначе он работает очень долго (что в общем-то было ожидаемо)
P.S.
- Английскую версию этой статьи можно найти в моем ответе на stackoverflow. Тесты в моем проекте на github. Если эта статья вам понравилась и вы поставите плюс на stackoverflow/звезду на github’е буду вам благодарен.
- Буду очень благодарен за любые замечания, исправления, указания на ошибки или другие способы преобразования InputStream в строку
- Если вы ещё не видели, советую посмотреть статью Список полезных ссылок для Java программиста, на мой взгляд наиболее полезная из всех моих статей на данный момент.
Класс InputStream
Базовый класс InputStream представляет классы, которые получают данные из различных источников:
- массив байтов
- строка (String)
- файл
- канал (pipe): данные помещаются с одного конца и извлекаются с другого
- последовательность различных потоков, которые можно объединить в одном потоке
- другие источники (например, подключение к интернету)
Для работы с указанными источниками используются подклассы базового класса InputStream:
BufferedInputStream Буферизированный входной поток ByteArrayInputStream Позволяет использовать буфер в памяти (массив байтов) в качестве источника данных для входного потока. DataInputStream Входной поток, включающий методы для чтения стандартных типов данных Java FileInputStream Для чтения информации из файла FilterInputStream Абстрактный класс, предоставляющий интерфейс для классов-надстроек, которые добавляют к существующим потокам полезные свойства. InputStream Абстрактный класс, описывающий поток ввода ObjectInputStream Входной поток для объектов StringBufferInputStream Превращает строку (String) во входной поток данных InputStream PipedInputStream Реализует понятие входного канала. PushbackInputStream Входной поток, поддерживающий однобайтовый возврат во входной поток SequenceInputStream Сливает два или более потока InputStream в единый поток
- int available() – возвращает количество байтов ввода, доступные в данный момент для чтения
- close() – закрывает источник ввода. Следующие попытки чтения передадут исключение IOException
- void mark(int readlimit) – помещает метку в текущую точку входного потока, которая остаётся корректной до тех пор, пока не будет прочитано readlimint байт
- boolean markSupported() – возвращает true, если методы mark() и reset() поддерживаются потоком
- int read() – возвращает целочисленное представление следующего доступного байта в потоке. При достижении конца файла возвращается значение -1
- int read(byte[] buffer) – пытается читать байты в буфер, возвращая количество прочитанных байтов. По достижении конца файла возвращает значение -1
- int read(byte[] buffer, int byteOffset, int byteCount) – пытается читать до byteCount байт в buffer, начиная с смещения byteOffset. По достижении конца файла возвращает -1
- reset() – сбрасывает входной указатель в ранее установленную метку
- long skip(long byteCount) – пропускает byteCount байт ввода, возвращая количество проигнорированных байтов
Как преобразовать InputStream в строку
- Using IOUtils.toString (Apache Utils):
- Using CharStreams (guava)
- Using Scanner (JDK)
символ «А» является символом начала текста, таким образом вызов next() вернет сразу всю строку. - Using Stream Api (Java 8). Warning: This solution convert different linebreaks (like rn) to n.
- Using parallel Stream Api (Java 8). Warning: This solution convert different linebreaks (like rn) to n.
- Using InputStreamReader and StringBuilder (JDK)
- Using StringWriter and IOUtils.copy (Apache Commons)
- Using ByteArrayOutputStream and inputStream.read (JDK)
- Using BufferedReader (JDK). Warning: This solution convert different linebreaks (like nr) to line.separator system property (for example, in Windows to “rn”).
- Using BufferedInputStream and ByteArrayOutputStream (JDK)
- Using inputStream.read() and StringBuilder (JDK). Warning: This soulition has problem with Unicode, for example with Russian text (work correctly only with non-Unicode text)
Warning:
Solutions 4, 5 and 9 convert different linebreaks to one.
Soulution 11 can’t work correclty with Unicode text
BufferedInputStream
Буферизация ввода-вывода является удобным способом оптимизации производительности, позволяя заключить в оболочку любой поток класса InputStream.
У класса есть конструктор, где размер буфера устанавливается по умолчанию. Также можно использовать конструктор, где размер буфера устанавливается вручную. Рекомендуется использовать размеры буфера, кратные размеру страницы памяти, дисковому блоку и т.п. и может зависеть от принимающей операционной системы, объёма доступной памяти и конфигурации машины.
ByteArrayInputStream
Класс ByteArrayInputStream использует байтовый массив в качестве источника данных. У данного класса можно не вызывать метод close().
DataInputStream – Форматированное чтение из памяти
Для чтения байтовых данных (не строк) применяется класс DataInputStream. В этом случае необходимо использовать классы из группы InputStream.
Для преобразования строки в массив байтов, пригодный для помещения в поток ByteArrayInputStream, в классе String предусмотрен метод getBytes(). Полученный ByteArrayInputStream представляет собой поток InputStream, подходящий для передачи DataInputStream.
При побайтовом чтении символов из форматированного потока DataInputStream методом readByte() любое полученное значение будет считаться действительным, поэтому возвращаемое значение неприменимо для идентификации конца потока. Вместо этого можно использовать метод available(), который сообщает, сколько еще осталось символов.
Класс DataInputStream позволяет читать элементарные данные из потока через интерфейс DataInput, который определяет методы, преобразующие элементарные значения в форму последовательности байтов. Такие потоки облегчают сохранение в файле двоичных данных.
FileInputStream
Класс FileInputStream создаёт объект класса InputStream, который можно использовать для чтения байтов из файла.
- FileInputStream (File file) – указывается объекта типа File
- FileInputStream (FileDescriptor fd)
- FileInputStream (String path) – указывается полное имя файла
При создании объект открывается для чтения. Класс переопределяет методы класса InputStream, кроме методов mark() и reset().
Для чтения байтов входного потока из файла используется конструкция:
PushbackInputStream
Разновидность буферизации, обеспечивающая чтение байта с последующим его возвратом в поток. Класс PushbackInputStream представляет механизм “заглянуть” во входной поток и увидеть, что оттуда поступит в следующий раз, не извлекая информации.
У класса есть дополнительный метод unread().
SequenceInputStream
Класс SequenceInputStream позволяет соединять вместе несколько экземпляров класса InputStream. Конструктор принимает в качестве аргумента либо пару объектов класса InputStream, либо интерфейс Enumeration.
Во время работы класс выполняет запросы на чтение из первого объекта класса InputStream и до конца, а затем переключается на второй. При использовании интерфейса работа продолжится по всем объектам класса InputStream. По достижении конца каждого файла, связанный с ним поток закрывается. Закрытие потока, созданного объектом класса SequenceInputStream, приводит к закрытию всех открытых потоков.
Потоки ввода, InputStream
Существуют две параллельные иерархии классов ввода : InputStream и Reader. Класс Reader введен в последних версиях Java. В данной статье рассматривается вопрос использования потока байтового ввода InputStream, иерархия которого представлена на следующем рисунке.
Поток Stream– это абстрактное понятие источника или приёмника данных, которые способны обрабатывать информацию. Есть два типа потоков: байтовые и символьные. В некоторых ситуациях символьные потоки более эффективны, чем байтовые. Классы, производные от базовых InputStream или Reader, имеют методы read() для чтения отдельных байтов или массива байтов.
Входной поток InputStream
Базовый класс InputStream – это абстрактный класс, определяющий входной поток данных, и является родителем для классов, получающих данные из различных источников : массив байтов, строки (String), файлы, каналы pipe, у которых одна из сторон является входом, а вторая сторона играет роль выхода, и т.д. Методы класса InputStream при возникновении ошибки вызывают исключение IOException.
Методы класса InputStream :
Класс InputStream часто выступает в качестве параметров конструкторов или методов различных классов. Согласно правилам наследования это означает, что в качестве параметра может быть передан объект любого класса-наследника. Это позволяет комбинировать классы для достижения нужных нам целей.
ByteArrayInputStream
Класс ByteArrayInputStream использует байтовый массив в качестве источника данных. Он имеет следующие конструкторы :
В качестве параметров конструкторы ByteArrayInputStream используют массив байтов buf для чтения, смещение относительно начала массива offset и количество считываемых символов length.
Пример чтения массив байтов в консоль:
В отличие от других классов потоков для закрытия объекта ByteArrayInputStream не требует вызывать метод close.
FileInputStream
FileInputStream – основной класс из данной иерархии для работы с файлами. Имеет два основных конструктора.
Подробно FileInputStream описан на странице Файлы и директории
FilterInputStream
FilterInputStream – абстрактный класс, предоставляющий интерфейс для классов-надстроек, которые добавляют к существующим потокам полезные свойства. FilterInputStream является базовым классом для двух других классов. Его единственный конструктор требует передачи в качестве параметра объекта класса InputStream, т.е. фактически объекта некоторого неабстрактного класса, порожденного от InputStream.
Прямое использование FilterInputStream в программе нецелесообразно.
BufferedInputStream
BufferedInputStream служит для организации более эффективного “буферизованного” ввода данных. Буферизация ввода-вывода является удобным способом оптимизации производительности, позволяя заключить в оболочку любой поток класса InputStream.
В конструкторе класса BufferedInputStream необходимо передать InputStream. В данном случае таким объектом является экземпляр класса ByteArrayInputStream.
Как и все потоки ввода BufferedInputStream обладает методом read(), который считывает данные с помощью метода read из массива buffer.
Фактические все то же самое можно было сделать и с помощью одного ByteArrayInputStream, не прибегая к буферизированному потоку. Класс BufferedInputStream просто оптимизирует производительность при работе с потоком ByteArrayInputStream.
DataInputStream
Для чтения байтовых данных (не строк) применяется класс DataInputStream. В этом случае необходимо использовать классы из группы InputStream. Для преобразования строки в массив байтов, пригодный для помещения в поток ByteArrayInputStream, в классе String предусмотрен метод getBytes(). Полученный ByteArrayInputStream представляет собой поток InputStream, подходящий для передачи DataInputStream.
При побайтовом чтении символов из форматированного потока DataInputStream методом readByte() любое полученное значение будет считаться действительным, поэтому возвращаемое значение неприменимо для идентификации конца потока. Вместо этого можно использовать метод available(), который сообщает, сколько еще осталось символов.
Класс DataInputStream позволяет читать элементарные данные из потока через интерфейс DataInput, который определяет методы, преобразующие элементарные значения в форму последовательности байтов. Такие потоки облегчают сохранение в файле двоичных данных.