Java — Реализация функции использующей java.io


Содержание

Занятие 2

Методы

Понятия функции и метода

Функции используются в программировании, чтобы уменьшить его сложность:

  1. Вместо того, чтобы писать непрерывную последовательность команд, в которой вскоре перестаешь ориентироваться, программу разбивают на подпрограммы, каждая из которых решает небольшую законченную задачу, а потом большая программа составляется из этих подпрограмм (этот прием называется декомпозицией).
  2. Уменьшается общее количество кода, потому что, как правило, одна функция используется в программе несколько раз.
  3. Написанная однажды и всесторонне проверенная функция, может быть включена в библиотеку функций и использоваться в других программах (при этом не надо вспоминать, как была запрограммирована эта функция, достаточно знать, что она делает). Существует множество полезных библиотек функций, которыми могут пользоваться все программисты, а некоторые библиотеки поставляются «в комплекте» с языком программирования (например, все, кто программировал на Паскале, пользовались библиотечной функцией writeln() для вывода на экран, а в Java для этих целей доступен метод System.out.println() , входящий в одну из стандартных библиотек).

В языке Java вся программа состоит только из классов и функции могут описываться только внутри них. Именно поэтому все функции в языке Java являются методами.

Суть понятия метод рассматривается на следующем занятии. А пока мы можем использовать его как синоним знакомого (по другим языкам программирования) понятия функция.

Объявление метода

Для того, чтобы использовать в программе собственный метод, его необходимо объявить.

При объявлении метода необходимо указать тип значения, которое будет возвращено после выполнения метода в программу. Если значение возвращать не нужно, указывается ключевое слово void . Затем идет произвольный идентификатор — имя метода. После имени метода в круглых скобках указывается список параметров (может быть пустым), а затем — в фигурных скобках — команды, составляющие тело метода.

Например, метод, рисующий круг, должен получить радиус и координаты центра круга. (Можно, конечно, было бы разработать метод без параметров, который рисует круг единичного радиуса с центром в начале координат, но он был бы значительно менее полезен).

Описание каждого параметра аналогично объявлению переменной (тип, а затем идентификатор — имя параметра). Параметры перечисляются через запятую.

В теле метода, возвращающего значение, должна быть команда return , после которой через пробел указывается выражение соответствующего типа. Эта команда заканчивает работу метода и передает указанное выражение в качестве возвращаемого значения основной программе — в то место, откуда метод был вызван * .

В качестве примера создадим метод для вычисления суммы квадратов двух целых чисел. Как и в случае с программой, важно в первую очередь определить входные и выходные данные. Входные данные — это параметры метода. Выходные данные — это его возвращаемое значение.

Замечание: в объектно-ориентированной концепции акцент несколько иной. Методы могут получать данные для работы, обращаясь к атрибутам своего класса, а результат их работы может заключаться в изменении этих атрибутов. Однако в рамках данного раздела мы рассматриваем методы как классические функции — подпрограммы.

Входными данными для рассматриваемого метода будут, очевидно, два целых числа. Выходные данные (результат) — целое число, представляющее собой сумму их квадратов.

Простейший вариант метода будет выглядеть следующим образом:

long squearSum( int x, int y)

Перед возвращаемым типом указывается одно или несколько ключевых слов модификаторов (которые будут изучены позднее). Нам необходимо добавить к объявлению метода squearSum() модификатор static , поскольку мы собираемся обращаться к нему из метода main() , имеющего такой модификатор.

Описание метода squearSum() должно находиться внутри того единственного класса, из которого состоит наша простая программа, но не внутри метода main() , а на одном уровне с ним. То есть:

В результате в классе MyClass теперь два метода, один из которых, main() , выполняется при старте программы. Чтобы выполнился второй метод, его необходимо вызвать.

Вызов метода

Чтобы вызвать метод из другого метода того же класса, необходимо указать его имя, а затем в скобках список фактических параметров (если метод не требует параметров, скобки все равно ставятся).

Например, мы можем обратиться к описанному нами методу squearSum(), передав ему в качестве параметров два целых числа 10 и 20 следующим образом:

В консоль будет выведено число 501.

Обратите внимание, вызов метода используется как операция, которую можно комбинировать с другими операциями (в данном случае, суммой) в выражении.

Чтобы вызвать метод другого класса, необходимо иметь объект этого класса * . Имя метода указывается через точку после имени объекта.

Например, в классе String (строка) имеется метод length() , возвращающий длину строки. Обратиться к этому методу можно только через объект класса String (что вполне логично, метод вызывается для той строки, длину которой мы хотим узнать).

String S = «Привет» ; // Создание объекта класса String, подробнее см. ниже int x = S.length(); // Вызов метода length() для объекта S. В результате x = 6

Подробнее о вызове методов других классов будет рассказано позже.

В дальнейшем, рассказывая о каких-либо методах, мы будем называть их не только именем, но и для наглядности указывать в скобках параметры метода, например сharAt(int i) . В результате становится легко объяснить назначение метода: «Метод charAt(int i) возвращает символ строки с индексом i ». Тип возвращаемого значения мы будем указывать только при необходимости.

Упражнение 1

Напишите метод, toUSD( int rur, double course) , переводящий рубли в доллары по заданному курсу. Вызовите его дважды в методе main() c любыми параметрами, результат напечатейте в консоль.

Массивы

Определения

Создание массива

Создание массива происходит в два этапа.

1. Объявление массива. На этом этапе создается переменная типа ссылка на массив, с помощью которой мы сможем потом к массиву обращаться. Для этого указывается тип элементов массива, затем квадратные скобки (они показывают, что мы имеем дело с массивом, а не с обычным типом данных) и идентификатор — имя массива. Идентификаторов может быть несколько (как и в случае с переменными простых типов).

int [] a; // Создается ссылка на массив типа int double [] b, c; // Создаются две ссылки на массивы типа double

2. Создание массива. Создать массив — значит выделить в памяти место, достаточное для хранения всех его элементов. Для этого надо указать длину массива — количество элементов в нем. Кроме того, переменная-ссылка, объявленная на предыдущем этапе, теперь будет «указывать» не в пустоту (в Java эта «пустота» называется null ), а на конкретный массив, с элементами которого можно работать.

Массив создается операцией new , которая выделяет участок памяти и возвращает указатель на этот участок. После ключевого слова new должен быть указан тип данных массива и его длина в квадратных скобках:

a = new int [5]; // В памяти выделяется место под массив из пяти целочисленных элементов, переменная a будет указывать на этот массив b = new double [4]; // В памяти выделяется место под массив из четырех действительных элементов, на него указывает переменная b

При этом элементам массива присваиваются значения по умолчанию * . Можно сразу проинициализировать массив нужными значениями, если перечислить их через запятую в фигурных скобках (длина массива при этом не указывается):

c = new double []<2.3, 1.02, 8>; // В памяти выделяется место под массив из трех действительных элементов, на него указывает переменная с, элементы массива сразу получают нужные значения

Работа с массивом

После объявления массива с ним можно работать. Например, присваивать значения элементам массива и вообще обращаться с ними как с обычными переменными. Для обращения к конкретному элементу необходимо указать имя переменной, указывающей на массив, и его индекс в квадратных скобках. Нумерация элементов массива начинается с нуля.

a[0] = 5; // Первому элементу массива a, присваивается значение 5 a[3] = 17; // Четвертому элементу массива a, присваивается значение 17 a[1] = a[0] – a[3]; // Второму элементу массива a присваивается значение -12

Объявление, создание и инициализацию массива можно соместить. Например:

int [] a = new int [5];

int [] a = new int [] <5, -12, 0, 17, 0>; // длина массива не указывается

Подчеркнем еще раз, что переменные a , b и c являются не собственно массивами, а только ссылками на массивы. Это значит, что можно заставить ссылку показывать на другой массив (если он соответствующего типа). Это делается командой присваивания:

В результате выполнения такой команды переменные b и c будут ссылаться (указывать) на один и тот же массив. Т.е., например, b[0] и c[0] — это теперь один и тот же элемент. А тот массив, на который раньше указывала переменная b , больше недоступен (поскольку на него не указывает ни одна ссылка) и будет удален из памяти так называемым сборщиком мусора Java.

Можно присвоить ссылке «пустое значение» null и тогда она не будет ссылаться ни на какую область памяти:

Обратите внимание, что с массивом, на который показывала переменная b , ничего не случится, просто обратиться к нему теперь можно будет только через переменную c . А вот если бы мы присвоили null переменной a , ее целочисленный массив был бы уничтожен.

Длину массива можно определить, используя запись:

Например, a.length будет равняться 5.

Очень удобно перебирать все элементы массива в цикле типа for . При этом обычно используется следующая форма:

for ( int i = 0; i // здесь можно что-нибудь сделать с элементом a[i] >

Например, следующий код присваивает всем элементам массива b числа от 1 до 4 (поскольку в массиве b четыре элемента):

for ( int i = 0; i b[i] = i; >

За границы массива выходить нельзя. Т.е., если в массиве a пять элементов, то обращение к шестому или восьмому элементу приведет к ошибке (точнее, возбуждению исключения).

a[5] = 8; // Нельзя, в массиве a только 5 элементов: a[0], a[1], a[2], a[3], a[4]

Многомерные массивы

Элементами массивов в Java могут быть другие массивы. Например, можно объявить:

Затем выделяется область памяти для внешнего массива:

Размеры внутренних массивов при этом указывать не нужно, это делается на следующем шаге:

d[0] = new char[3]; d[1] = new char[3]; d[2] = new char[2];

Теперь можно обращаться к элементам многомерного массива, используя два индекса: сначала индекс внешнего, а потом внутреннего массива: d[1][2] , d[0][0] , d[0][1] и т.д.

Обратите внимание, что многомерные массивы в Java не обязательно должны быть прямоугольными (внутренние массивы могут иметь разную длину).

Упражнение 2

Напишите метод, увеличивающий элементы массива на 10%.

Подсказка: подумайте, какие у этого метода входные и выходные данные. Решите сами, должен ли метод изменять исходный массив, или в результате работы будет создаваться новый. Обоснуйте свое решение преподавателю. Напишите к методу комментарии.

Работа со строками

Создание строк

Строки тоже являются переменными ссылочного типа, а точнее — ссылками на объекты одного из нескольких строковых классов Java. Мы рассмотрим класс String .

Самый распространенный способ создать строку — это организовать ссылку типа String на строку-константу:

String s = «Это строка»

Можно просто сначала объявить переменную (которая получит значение null ), а потом заставить ее ссылаться на строку-константу, другую строку или воспользоваться командой new , чтобы явным образом выделить память для строки:

String s1, s2, s3; // Объявление трех переменных, которые пока не связаны ни с какой строкой s1 = «Да здравствует день танкиста»; // Переменная s1 теперь ссылается на область памяти, в которой хранится строка «Да здравствует день танкиста» s2 = s1; // Теперь обе переменные s1 и s2 ссылаются на одно и то же место памяти s3 = new String(); // s3 ссылается на место в памяти, где хранится пустая строка

Объединение (сцепление) строк

Сцепление строк производится командой +. Ей соответствует оператор +=, который часто бывает очень удобен:

String S = «Привет» ; String S1 = «мир» ; S += «, » + S1 + «!» ; // Теперь S ссылается на строку “Привет, мир!”

Длина строки

Определить длину строки можно методом length() :

int x = S.length(); // Переменная x получит значение 12

Обратите внимание, String является классом (подробнее классы рассматриваются на следующем занятии), а length() — его методом, и поэтому указывается через точку после имени переменной. Аналогично записываются и другие методы класса String .

Получение отдельных символов строки

Метод charAt(int i) возвращает символ строки с индексом i . Индекс первого символа строки — 0 (т.е. символы строки индексируются (нумеруются) аналогично элементам массива. Например:

char ch = S.charAt(2); // Переменная ch будет иметь значение ‘и’

Метод toCharArray() преобразует строку в массив символов:

char [] ch = S.toCharArray(); // ch будет иметь представлять собой массив

Метод getChars(int begin, int end, char dst[], int i) берет символы строки, имеющие индексы от begin до end-1 включительно, и помещает их в массив dst , начиная с индекса i , и выдает получившийся массив в качестве результата. Например:

char [] ch = < 'М' , 'а' , 'с' , 'с' , 'и' , 'в' , '.' >; ch = S.getChars(1, 4, ch, 3); // Теперь ch =

Замена отдельного символа

Метод replace(int old, int new) возвращает новую строку, в которой все вхождения символа old заменены на символ new .

String SS = S.replace( ‘и’ , ‘ы’ ); // SS = «Прывет, мыр!»

Получение подстроки

Метод substring(int begin, int end) возвращает фрагмент исходной строки от символа с индексом begin до символа с индексом end-1 включительно. Если не указывать end , будет возвращен фрагмент исходной строки, начиная с символа с индексом begin и до конца:

String S2 = S.substring(4,7); // S2 = «ет,» S2 = S.substring(4); // S2 = «ет, мир!»

Разбиение строки на подстроки

Метод split(String regExp) разбивает строку на фрагменты, используя в качестве разделителей символы, входящие в параметр regExp , и возвращает ссылку на массив, составленный из этих фрагментов. Сами разделители ни в одну подстроку не входят.

String parts[] = S.split( » » ); // Разбили строку S на отдельные слова, используя пробел в качестве разделителя, в результате получили массив parts, где parts[0] = «Привет,», а parts[1] = «мир!» String parts[] = S.split( » и» ); // Разбили строку S на отдельные слова, используя в качестве разделителя пробел и букву и, в результате parts[0] = «Пр», parts[1] = «вет,», parts[2] = «м», parts[3] = «р!»

Сравнение строк

Если сравнивать строки, используя логическую операцию == , то ее результатом будет true только в том случае, если строковые переменные указывают (ссылаются) на один и тот же объект в памяти.

Если же необходимо проверить две строки на совпадение, следует использовать стандартный метод equals(Object obj) . Он возвращает true , если две строки являются полностью идентичными вплоть до регистра букв, и false в противном случае. Его следует использовать следующим образом:

S1.equals(S2); // Вернет true, если строки S1 и S2 идентичны S2.equals(S1); // Абсолютно то же самое boolean b = S.equals( «Привет, мир!» ); // b = true

Метод equalsIgnoreCase(Object obj) работает аналогично, но строки, записанные в разных регистрах, считает совпадающими.

Поиск подстроки

Метод indexOf(int ch) возвращает индекс первого вхождения символа ch в исходную строку. Если задействовать этот метод в форме indexOf(int ch, int i) , то есть указать два параметра при вызове, то поиск вхождения начнется с символа с индексом i . Если такого символа в строке нет, результатом будет -1.

int pos = S.indexOf( ‘в’ ); // pos = 3 pos = «Вася» .indexOf( ‘с’ ); // pos = 2 pos = «Корова» .indexOf( ‘о’ , 2); // pos = 3 pos = «Корова» .indexOf( ‘К’ , 2); // pos = -1, поиск ведется с учетом регистра

Последнее вхождение символа можно найти с помощью метода lastIndexOf(int ch) или lastIndexOf(int ch, int i) , который работает аналогично, но просматривает строку с конца.

У всех перечисленных методов есть одноименные варианты, которые принимают в качестве параметра строку вместо символа и проверяют, содержится ли эта строка в исходной строке.

pos = «Корова» .indexOf( «ор» ); // pos = 1 pos = «Барабанщик барабанил в барабан» .indexOf( «барабан» , 5); // pos = 11 pos = «Корова» .indexOf( «Вася» ); // pos = -1

Изменение регистра символов в строке

Метод toLowerCase() возвращает новую строку, в которой все буквы сделаны строчными. Метод toUpperCase() возвращает новую строку, в которой все буквы сделаны прописными.

S = S.toUpperCase(); // S = «ПРИВЕТ, МИР!»

Упражнение 3

Напишите метод, заменяющий в строке все вхождения слова «бяка» на «вырезано цензурой».

Подсказка: есть очень простой способ.

Заголовок метода main()

Теперь мы можем, наконец, понять большую часть описания метода main() (за исключением ключевых слов public и static .

Заголовок public static void main(String[] args) означает, что метод main() не возвращает значения (и действительно, мы ни разу не использовали в его теле команду return ), а в качестве единственного параметра принимает массив строк args .

В качестве параметра args методу main() передаются так называемые аргументы командной строки. Дело в том, что каждую программу можно запустить не просто щелкнув мышкой по ее значку. Можно ввести имя исполняемого файла программы в командной строке (нажмите комбинацию Windows + R , чтобы увидеть командную строку Windows, если вы работаете в этой операционной системе), а после имени через пробел указать один или несколько дополнительных параметров (аргументов командной строки).

Таким образом, в методе main() можно обратиться к элементам массива args и увидеть, какие дополнительные аргументы пользователь указал при запуске программу. Можно научить программу на некоторые из этих аргументов реагировать.

При этом необязательно запускать программу из командной строки. В диалоговом окне Run Run. есть вкладка (x) = Arguments , перейдя на которую, можно перечислить интересующие вас аргументы командной строки и в зависимости от них протестировать реакцию программу. Если, конечно, это необходимо: большинство программ никак не реагирует на аргументы командной строки.

Дополнительная литература

1. Вязовик Н.А. Программирование на Java. (глава 9)

2. Хабибуллин И.Ш. Самоучитель Java 2. (главы 1, 5)

Цукерберг рекомендует:  Интернет-маркетолог. Free. Интернет-маркетолог. Free
Задание

Создайте новый проект (назовите его MainTask). В вашей рабочей области должно быть два проекта. В одном вы будете пробовать примеры и выполнять упражнения во время изучения новой темы. В проекте MainTask вы будете работать над вашим основным заданием.

Сформулируйте требования к методу, с помощью которого можно было бы решить вашу задачу. Какие входные и выходные данные должен иметь этот метод? Используйте конструкции языка (примитивные типы, массивы, строки), изученные на первых двух занятиях. Напишите заголовок метода и покажите его преподавателю.

Приступайте к программированию этого метода параллельно с дальнейшим изучением пособия. Вы должны отчитаться по достигнутому результату на пятом занятии.

Для примера сформулируем требования к методу, с помощью которого решается задача определения суммы выигрыша на тотализаторе (см. задание №3):

Написать метод для подсчета суммы выигрыша на тотализаторе. В качестве параметров метод принимает массив строк (имена лошадей), упорядоченный по результатам забега и перечень ставок, заданный с помощью трех массивов равной длины. Первый массив — имя игрока, второй — кличка лошади, на которую он поставил и третий — сумма, которую игрок поставил на эту лошадь (то есть, игрок gamers[i] поставил на лошадь horses[i] сумму sums[i]). Метод должен возвращать массив строк, где каждая строка содержит имя игрока-победителя и выигранную им сумму.

19.1. Java — Потоки ввода/вывода и работа с файлами и каталогами

Пакет java.io содержит почти каждый класс, который может потребоваться Вам для совершения ввода и вывода в Java. Все данные потоки представлены потоком ввода и адресом вывода. Поток в пакете java.io осуществляет поддержку различных данных, таких как примитивы, объекты, локализованные символы и т.д.

Содержание

Потоки

Потоки в Java определяются в качестве последовательности данных. Существует два типа потоков:

  • InPutStream – поток ввода используется для считывания данных с источника.
  • OutPutStream – поток вывода используется для записи данных по месту назначения.

Java предоставляет сильную, но гибкую поддержку в отношении ввода/вывода, связанных с файлами и сетями, однако в данном руководстве рассмотрены лишь базовые функции, связанные с потоками и вводом/выводом. Рассмотрим далее по порядку наиболее распространенные примеры.

Байтовый поток

Потоки байтов в Java используются для осуществления ввода и вывода 8-битных байтов. Не смотря на множество классов, связанных с потоками байтов, наиболее распространено использование следующих классов: FileInputStream и FileOutputStream. Ниже рассмотрен пример, иллюстрирующий использование данных двух классов для копирования из одного файла в другой.

Примечание по примеру: чтобы скопировать файл, необходимо в папке проекта создать файл file.txt с любым или пустым содержимым.

Пример

Теперь рассмотрим файл file.txt со следующим содержимым:

В качестве следующего шага необходимо скомпилировать java-программу и выполнить ее, что позволит создать файл copied_file.txt с тем же содержимым, что имеется в file.txt. Таким образом, разместим обозначенный код в файле FileCopy.java и выполним следующее действие:

Символьные потоки

Потоки байтов в Java позволяют произвести ввод и вывод 8-битных байтов, в то время как потоки символов используются для ввода и вывода 16-битного юникода. Не смотря на множество классов, связанных с потоками символов, наиболее распространено использование следующих классов: FileReader и FileWriter. Не смотря на тот факт, что внутренний FileReader использует FileInputStream, и FileWriter использует FileOutputStream, основное различие состоит в том, что FileReader производит считывание двух байтов в конкретный момент времени, в то время как FileWriter производит запись двух байтов за то же время.

Мы можем переформулировать представленный выше пример, в котором два данных класса используются для копирования файла ввода (с символами юникода) в файл вывода.

Примечание по примеру: чтобы скопировать файл, необходимо в папке проекта создать файл file.txt с любым или пустым содержимым.

Пример

Теперь рассмотрим файл file.txt со следующим содержимым:

В качестве следующего шага необходимо скомпилировать программу и выполнить ее, что позволит создать файл copied_file.txt с тем же содержимым, что имеется в file.txt. Таким образом, разместим обозначенный код в файле FileCopy.java и выполним следующее действие:

Стандартные потоки

Все языки программирования обеспечивают поддержку стандартного ввода/вывода, где программа пользователя может произвести ввод посредством клавиатуры и осуществить вывод на экран компьютера. Если вы знакомы с языками программирования C либо C++, вам должны быть известны три стандартных устройства STDIN, STDOUT и STDERR. Аналогичным образом, Java предоставляет следующие три стандартных потока:

  • Стандартный ввод – используется для перевода данных в программу пользователя, клавиатура обычно используется в качестве стандартного потока ввода, представленного в виде System.in.
  • Стандартный вывод – производится для вывода данных, полученных в программе пользователя, и обычно экран компьютера используется в качестве стандартного потока вывода, представленного в виде System.out.
  • Стандартная ошибка – используется для вывода данных об ошибке, полученной в программе пользователя, чаще всего экран компьютера служит в качестве стандартного потока сообщений об ошибках, представленного в виде System.err.

Ниже представлена простая программа, которая создает InputStreamReader для чтения стандартного потока ввода, до введения пользователем «q»:

Пример

Разместим представленный выше код в файле ReadConsole.java и попробуем скомпилировать и выполнить его согласно тому, как это представлено в следующей программе. Данная программа продолжает чтение и вывод одного и того же символа до нажатия ‘q’:

Чтение и запись файла

Как было указано выше, поток представляет собой последовательность данных. InputStream используется для считывания данных с источника, OutputStream служит для записи данных по месту назначения.

Ниже представлена иерархия классов для управления потоками Ввода и Вывода.

В данном уроке нам предстоит рассмотреть два важных потока: FileInputStream и FileOutputStream.

Поток FileInputStream – чтение из файла

Поток FileInputStream – это поток, который используется в Java для чтения данных из файла. Объекты могут быть созданы при использовании ключевого слова new, доступны несколько типов конструкторов.

Представленный конструктор использует имя файла в качестве потока с целью создания объекта входного потока для считывания файла:

Представленный ниже конструктор использует объектный файл с целью создания объекта входного потока для чтения файла. Сперва мы создаем объектный файл при использовании метода File() следующим образом:

Теперь, получив объект InputStream, следует ознакомиться со следующим перечнем вспомогательных методов, которые могут быть использованы для считывания потока либо выполнения иных операций в потоке.

Метод и описание
1 public void close() throws IOException<>
Данный метод в Java закрывает выходной файловый поток. Освобождает какие-либо системные ресурсы, связанные с файлом. Выдает IOException.
2 protected void finalize()throws IOException <>
Данный метод выполняет очистку соединения с файлом. Позволяет удостовериться в вызове закрытого метода данного выходного файлового потока при отсутствии каких-либо ссылок на данный поток. Выдает IOException.
3 public int read(int r)throws IOException<>
Данный метод осуществляет в Java считывание заданных байтов данных из InputStream. Возврат данных типа int. Возврат следующего байта данных, в конце файла будет произведен возврат к -1.
4 public int read(byte[] r) throws IOException<>
Данный метод производит считывание байтов r.length из входного потока в массив. Возврат общего числа считанных байтов. В конце файла будет произведен возврат к -1.
5 public int available() throws IOException<>
Выдает число байтов, которые могут быть считаны из входного файлового потока. Возврат данных типа int.

Существуют также другие доступные входные потоки, более детальные сведения о которых представлены по следующим ссылкам:

Поток FileOutputStream – создание и запись файла

Поток FileOutputStream – это поток, который используется в Java для создания файла и последующей записи в него. Поток создаст файл в случае его отсутствия перед его открытием для вывода.

Далее представлены два конструктора, которые могут быть задействованы при создании объекта FileOutputStream.

Представленный конструктор использует имя файла в качестве строки с целью создания объекта входного потока для записи файла в Java:

Представленный ниже конструктор использует объектный файл с целью создания объекта выходного потока для записи файла. Сперва мы создаем объектный файл при использовании метода File() следующим образом:

Теперь, получив объект OutputStream, следует ознакомиться со следующим перечнем вспомогательных методов, которые могут быть использованы для записи потока либо выполнения иных операций в потоке.

Метод и описание
1 public void close() throws IOException<>
Данный метод в Java закрывает выходной файловый поток. Освобождает какие-либо системные ресурсы, связанные с файлом. Выдает IOException.
2 protected void finalize()throws IOException <>
Данный метод выполняет очистку соединения с файлом. Позволяет удостовериться в вызове закрытого метода данного выходного файлового потока при отсутствии каких-либо ссылок на данный поток. Выдает IOException.
3 public void write(int w)throws IOException<>
Данный метод осуществляет запись заданного байта в выходной поток.
4 public void write(byte[] w)
Запись байтов w.length из указанного массива байтов в OutputStream.

Существуют также другие доступные выходные потоки, более детальные сведения о которых представлены по следующим ссылкам:

Пример

В следующем примере представлены InputStream и OutputStream – потоки для чтения, создания и записи файла:

Представленный выше java-код создаст файл file.txt и пропишет заданные символы в формате char. То же самое будет выводиться на экран стандартного вывода.

Существует ряд других классов, которые нам предстоит рассмотреть с целью ознакомления с основами навигации в файловой системе и вводу/выводу.

Каталоги в Java

В Java каталог представлен Файлом, который может содержать список других файлов и каталогов. Используя объект File, вы можете создать каталог, прокрутить список файлов, представленных в каталоге. Для получения более детальных сведений, ознакомьтесь с перечнем всех методов, которые могут быть вызваны из объекта File, будучи связанными с каталогами.

Создание каталогов

Существуют два служебных метода File, которые могут быть использованы для создания каталогов:

  • Метод mkdir() позволяет создать папку в Java, возвращая значение true при успехе операции, и false в случае сбоя. Сбой свидетельствует о том, что путь указанный в объекте File уже существует, либо что каталог не может быть создан в связи с тем, что полный путь еще не существует.
  • Метод mkdirs() создает каталог и все вышестоящие каталоги.

В следующем примере представлено создание папки «/java/proglang/newdir»:

Пример

Скомпилируйте и выполните следующий код для создания каталога «/java/proglang/newdir».

Примечание ? Java автоматически формирует разделители пути в UNIX и Windows с учетом соглашений. При использовании косой черты (/) при работе с Java в системе Windows, производится корректное разрешение пути.

Список файлов в папке

Метод list(), представленный объектом File, может быть использован для предоставления перечня всех файлов и каталогов, имеющихся в заданной папке, в следующем виде:

Пример

Вследствие этого будет получен следующий результат, основанный на каталогах и файлах, доступных в вашем каталоге /NetBeans 8.2/Projects/ReadDirectory/ReadDirectory/:

Потоки ввода-вывода. Работа с файлами

Потоки ввода-вывода

Отличительной чертой многих языков программирования является работа с файлами и потоками. В Java основной функционал работы с потоками сосредоточен в классах из пакета java.io .

Ключевым понятием здесь является понятие потока . Хотя понятие «поток» в программировании довольно перегружено и может обозначать множество различных концепций. В данном случае применительно к работе с файлами и вводом-выводом мы будем говорить о потоке (stream), как об абстракции, которая используется для чтения или записи информации (файлов, сокетов, текста консоли и т.д.).

Поток связан с реальным физическим устройством с помощью системы ввода-вывода Java. У нас может быть определен поток, который связан с файлом и через который мы можем вести чтение или запись файла. Это также может быть поток, связанный с сетевым сокетом, с помощью которого можно получить или отправить данные в сети. Все эти задачи: чтение и запись различных файлов, обмен информацией по сети, ввод-ввывод в консоли мы будем решать в Java с помощью потоков.

Объект, из которого можно считать данные, называется потоком ввода , а объект, в который можно записывать данные, — потоком вывода . Например, если надо считать содержание файла, то применяется поток ввода, а если надо записать в файл — то поток вывода.

В основе всех классов, управляющих потоками байтов, находятся два абстрактных класса: InputStream (представляющий потоки ввода) и OutputStream (представляющий потоки вывода)

Но поскольку работать с байтами не очень удобно, то для работы с потоками символов были добавлены абстрактные классы Reader (для чтения потоков символов) и Writer (для записи потоков символов).

Все остальные классы, работающие с потоками, являются наследниками этих абстрактных классов. Основные классы потоков:

Потоки байтов

Класс InputStream

Класс InputStream является базовым для всех классов, управляющих байтовыми потоками ввода. Рассмотрим его основные методы:

int available() : возвращает количество байтов, доступных для чтения в потоке

void close() : закрывает поток

int read() : возвращает целочисленное представление следующего байта в потоке. Когда в потоке не останется доступных для чтения байтов, данный метод возвратит число -1

int read(byte[] buffer) : считывает байты из потока в массив buffer. После чтения возвращает число считанных байтов. Если ни одного байта не было считано, то возвращается число -1

int read(byte[] buffer, int offset, int length) : считывает некоторое количество байтов, равное length, из потока в массив buffer. При этом считанные байты помещаются в массиве, начиная со смещения offset, то есть с элемента buffer[offset] . Метод возвращает число успешно прочитанных байтов.

long skip(long number) : пропускает в потоке при чтении некоторое количество байт, которое равно number

Класс OutputStream

Класс OutputStream является базовым классом для всех классов, которые работают с бинарными потоками записи. Свою функциональность он реализует через следующие методы:

void close() : закрывает поток

void flush() : очищает буфер вывода, записывая все его содержимое

void write(int b) : записывает в выходной поток один байт, который представлен целочисленным параметром b

void write(byte[] buffer) : записывает в выходной поток массив байтов buffer.

void write(byte[] buffer, int offset, int length) : записывает в выходной поток некоторое число байтов, равное length , из массива buffer , начиная со смещения offset , то есть с элемента buffer[offset] .

Абстрактные классы Reader и Writer

Абстрактный класс Reader предоставляет функционал для чтения текстовой информации. Рассмотрим его основные методы:

absract void close() : закрывает поток ввода

int read() : возвращает целочисленное представление следующего символа в потоке. Если таких символов нет, и достигнут конец файла, то возвращается число -1

int read(char[] buffer) : считывает в массив buffer из потока символы, количество которых равно длине массива buffer. Возвращает количество успешно считанных символов. При достижении конца файла возвращает -1

int read(CharBuffer buffer) : считывает в объект CharBuffer из потока символы. Возвращает количество успешно считанных символов. При достижении конца файла возвращает -1

absract int read(char[] buffer, int offset, int count) : считывает в массив buffer, начиная со смещения offset, из потока символы, количество которых равно count

long skip(long count) : пропускает количество символов, равное count. Возвращает число успешно пропущенных символов

Класс Writer определяет функционал для всех символьных потоков вывода. Его основные методы:

Writer append(char c) : добавляет в конец выходного потока символ c. Возвращает объект Writer

Writer append(CharSequence chars) : добавляет в конец выходного потока набор символов chars. Возвращает объект Writer

abstract void close() : закрывает поток

abstract void flush() : очищает буферы потока

void write(int c) : записывает в поток один символ, который имеет целочисленное представление

void write(char[] buffer) : записывает в поток массив символов

absract void write(char[] buffer, int off, int len) : записывает в поток только несколько символов из массива buffer. Причем количество символов равно len, а отбор символов из массива начинается с индекса off

void write(String str) : записывает в поток строку

void write(String str, int off, int len) : записывает в поток из строки некоторое количество символов, которое равно len, причем отбор символов из строки начинается с индекса off

Функционал, описанный классами Reader и Writer, наследуется непосредственно классами символьных потоков, в частности классами FileReader и FileWriter соответственно, предназначенными для работы с текстовыми файлами.

Теперь рассмотрим конкретные классы потоков.

Программируйте во имя добра

среда, 8 августа 2020 г.

Java IO — часть 1

Классы системы ввода-вывода находятся в пакете java.io, и первый взгляд на них ужасает — около 90 классов, некоторые из которых понять не представляется возможным — такое оттолкнет даже самые пытливые умы. Но не следует пугаться, на самом деле Java IO базируется на довольно простых концепциях, о которых я сейчас вам расскажу.

Если мы хотим создать файл:
created — показывает, удалось ли создать файл. Если такой файл уже существует — то метод createNewFile() вернет false.

Теперь создадим папку:
Как и в случае с файлом, created показывает, удалось ли создать папку.
Нужно отметить, что если папка, в которой вы хотите создать свою папку, не существует, то метод mkdir() вернет false и папка не будет создана.
Для того, чтобы создать свою папку и все папки выше, если их нет, нужно использовать метод mkdirs():

Java — Реализация функции использующей java.io

Обобщенное понятие источника ввода относится к различным способам получения информации: к чтению дискового файла, символов с клавиатуры, либо получению данных из сети. Аналогично, под обобщенным понятием вывода также могут пониматься дисковые файлы, сетевое соединение и т.п. Эти абстракции дают удобную возможность для работы с вводом-выводом (I/O), не требуя при этом, чтобы каждая часть вашего кода понимала разницу между, скажем, клавиатурой и сетью. В Java эта абстракция называется потоком (stream) и реализована в нескольких классах пакета java.io. Ввод инкапсулирован в классе InputStream, вывод — в OutputStream. В Java есть несколько специализаций этих абстрактных классов, учитывающих различия при работе с дисковыми файлами, сетевыми соединениями и даже с буферами в памяти.

File — единственный объект в java.io , который работает непосредственно с дисковыми файлами. Хотя на использование файлов в апплетах наложены жесткие ограничения, файлы по прежнему остаются основными ресурсами для постоянного хранения и совместного использования информации. Каталог в Java трактуется как обычный файл, но с дополнительным свойством — списком имен файлов, который можно просмотреть с помощью метода list.

Java правильно обрабатывает разделители имен каталогов в пути, используемые в UNIX и DOS. Если вы используете стиль UNIX — символы ‘/’, то при работе в Windows Java автоматически преобразует их в ‘\’. Не забудьте, если вы привыкли к разделителям, принятым в DOS, то есть, к ‘\’, то для того, чтобы включить их в строку пути, необходимо их удвоить, аналогично тому, как это сделано в строке “\\java\\COPYRIGHT”.

Для определения стандартных свойств объекта в классе File есть много разных методов. Однако, класс File несимметричен. Есть много методов, позволяющих узнать свойства объекта, но соответствующие функции для изменения этих свойств отсутствуют. В очередном примере используются различные методы, позволяющие получить характеристики файла:

static void p(String s) <

public static void main(String args[]) <

File f1 = new File(«/java/COPYRIGHT»);


p(«File Name:» + f1 .getName());

p(«Abs Path:» + f1.getAbsolutePath());

p(f1.exists() ? «exists» : «does not exist»);

p(f1.canWrite() ? «is writeable» : «is not writeable»);

p(f1.canRead() ? «is readable» : «is not readable»);

Цукерберг рекомендует:  26 сервисов для сокращения URL

p(«is » + (f1.isDirectory() ? » » : «not») + » a directory»);

p(f1.isFile() ? «is normal file» : «might be a named pipe»);

p(f1.isAbsolute() ? «is absolute» : «is not absolute»);

p(«File last modified:» + f1. lastModified());

p(«File size:» + f1.length() + » Bytes»);

При запуске этой программы вы получите что-то наподобие вроде:

File Name:COPYRIGHT (имя файла)

Abs Path:/Java/COPYRIGHT (путь от корневого каталога)

Parent:/java (родительский каталог)

exists (файл существует)

is writeable (разрешена запись)

is readable (разрешено чтение)

is not a directory (не каталог)

is normal file (обычный файл)

File last modified:812465204000 (последняя модификация файла)

File size:695 Bytes (размер файла)

Существует также несколько сервисных методов, использование которых ограничено обычными файлами (их нельзя применять к каталогам). Метод renameTo(File dest) переименовывает файл (нельзя переместить файл в другой каталог ). Метод delete уничтожает дисковый файл. Этот метод может удалять только обычные файлы, каталог, даже пустой, с его помощью удалить не удастся.

Каталоги — это объекты класса File, в которых содержится список других файлов и каталогов. Если File ссылается на каталог, его метод isDirectory возвращает значение true. В этом случае вы можете вызвать метод list и извлечь содержащиеся в объекте имена файлов и каталогов. В очередном примере показано, как с помощью метода list можно просмотреть содержимое каталога.

public static void main(String args[]) <

String dirname = «/jav a»; // имя каталога

File f1 = new File(dirname);

System.out.println(«Directory of ‘ + dirname);

String s[] = f1.list();

System.out.println(s[i] + » is a file»);

System.out.println(dirname + » is not a directory»);

В процессе работы эта программа вывела содержимое каталога /java моего персонального компьютера в следующем виде:

Directory of /java

bin is a directory

COPYRIGHT is a file

README is a file

Зачастую у вас будет возникать потребность ограничить количество имен файлов, возвращаемых методом list, чтобы получить от него только имена, соответствующие определенному шаблону. Для этого в пакет java.io включен интерфейс FilenameFilter. Объекту, чтобы реализовать этот интерфейс, требуется определить только один метод — accept(), который будет вызываться один раз с каждым новым именем файла. Метод accept должен возвращать true для тех имен, которые надо включать в список, и false для имен, которые следует исключить.

У класса File есть еще два сервисных метода, ориентированных на работу с каталогами. Метод mkdir создает подкаталог . Для создания каталога, путь к которому еще не создан, надо использовать метод mkdirs — он создаст не только указанный каталог, но и все отсутствующие родительские каталоги.

Inpu tStream — абстрактный класс, задающий используемую в Java модель входных потоков. Все методы этого класса при возникновении ошибки возбуждают исключение IOException. Ниже приведен краткий обзор методов класса InputStream.

read() возвращает представление очередного доступного символа во входном потоке в виде целого.

read(byte b[]) пытается прочесть максимум b.length байтов из входного потока в массив b. Возвращает количество байтов, в действительности прочитанных из потока.

read(byte b[], int off, int len) пытается прочесть максимум len байтов, расположив их в массиве b, начиная с элемента off. Возвращает количество реально прочитанных байтов.

skip(long n) пытается пропустить во входном потоке n байтов. Возвращает количество пропущенных байтов.

available () возвращает количество байтов, доступных для чтения в настоящий момент.

close() закрывает источник ввода. Последующие попытки чтения из этого потока приводят к возбуждению IOException.

mark(int readlimit) ставит метку в текущей позиции входного потока, которую можно будет использовать до тех пор, пока из потока не будет прочитано readlimit байтов.

reset() возвращает указатель потока на установленную ранее метку.

markSupported() возвращает true, если данный поток поддерживает операции mark/reset.

Как и InputStream, OutputStream — абстрактный класс. Он задает модель выходных потоков Java. Все методы этого класса имеют тип void и возбуждают исключение IOException в случае ошибки. Ниже приведен список методов этого класса:

write(int b) записывает один байт в выходной поток. Обратите внимание — аргумент этого метода имеет тип int, что позволяет вызывать write, передавая ему выражение, при этом не нужно выполнять приведение его типа к byte.

write(byte b[]) записывает в выходной поток весь указанный массив байтов.

write(byte b[], int off, int len) записывает в поток часть массива — len байтов, начиная с элемента b[off].

flush() очищает любые выходные буферы, завершая операцию вывода.

close() закрывает выходной поток. Последующие попытки записи в этот поток будут возбуждать IOException.

Класс FileInputStream используется для ввода данных из файлов. В приведенном ниже примере создается два объекта этого класса, использующие один и тот же дисковый файл.

InputStream f0 = new FileInputStream(«/autoexec.bat»);

File f = new File(«/autoexec.bat»):

InputStream f1 = new FileInputStream(f);

Когда создается объект класса FileInputStream, он одновременно с этим открывается для чтения. FileInputStream замещает шесть методов абстрактного класса InputStream. Попытки применить к объекту этого класса методы mark и reset приводят к возбуждению исключения IOException. В приведенном ниже примере показано, как можно читать одиночные байты, массив байтов и поддиапазон массива байтов. В этом примере также показано, как методом available можно узнать, сколько еще осталось непрочитанных байтов, и как с помощью метода skip можно пропустить те байты, которые вы не хотите читать.

public static void main(String args[]) throws Exception <

InputStream f1 = new FileInputStream(«/wwwroot/default.htm»);

System.out.println(«Total Available Bytes: » + size);

System.out.println(«First 1/4 of the file: read()»);

У класса FileOutputStream — два таких же конструктора, что и у FileInputStream. Однако, создавать объекты этого класса можно независимо от того, существует файл или нет. При создании нового объекта класс FileOutputStream перед тем, как открыть файл для вывода, сначала создает его.

В очередном нашем примере символы, введенные с клавиатуры, считываются из потока System.in — по одному символу за вызов, до тех пор, пока не заполнится 12-байтовый буфер. После этого создаются три файла. В первый из них, file1.txt, записываются символы из буфера, но не все, а через один — нулевой, второй и так далее. Во второй, file2.txt, записывается весь ввод, попавший в буфер. И наконец в третий файл записывается половина буфера, расположенная в середине, а первая и последняя четверти буфера не выводятся.

public static byte getlnput()[] throws Exception <

byte buffer[] = new byte[12];

В настоящее время не существует способа открыть FileOutputStream для дозаписи в конец файла. Если вы открываете файл с помощью конструктора FileOutputStream, прежнее содержимое этого файла теряется. Это — явный недостаток реализации Java.

ByteArrayInputStream — это реализация входного потока, в котором в качестве источника используется массив типа byte. У этого класса два конструктора, каждый из которых в качестве первого параметра требует байтовый массив. В приведенном ниже примере создаются два объекта этого типа. Эти объекты инициализируются символами латинского алфавита.

String tmp = «abcdefghijklmnopqrstuvwxyz»;

byte b[] = new byte [tmp.length()];

tmp. getBytes(0, tmp.length(), b, 0);

ByteArrayInputStream input1 = new ByteArrayInputStream(b);

ByteArrayInputStream input2 = new ByteArreyInputStream(b,0,3);

У класса ByteArrayOutputStream — два конструктора. Первая форма конструктора создает буфер размером 32 байта. При использовании второй формы создается буфер с размером, заданным параметром конструктора (в приведенном ниже примере — 1024 байта):

OutputStream out0 = new ByteArrayOutputStream();

OutputStream out1 = new ByteArrayOutputStream(1024);

В очередном примере объект ByteArrayOutputStream заполняется символами, введенными с клавиатуры, после чего с ним выполняются различные манипуляции.

public static void main(String args[]) throws Exception <

ByteArrayOutputStream f0 = new ByteArrayOutputStream(12);

System.out.println(«Enter 10 characters and a return»);

while (f0.size() != 10) <

System.out.println(«Buffer as a string»);

System.out.println («Into array»);

byte b[] = f0.toByteArray();

Заглянув в созданный в этом примере файл test.txt, мы увидим там именно то, что ожидали:

StringBufferInputStream идентичен классу ByteArrayInputStream с тем исключением, что внутренним буфером объекта этого класса является экземпляр String, а не байтовый массив. Кроме того, в Java нет соответствующего ему класса StringBufferedOutputStream. У этого класса есть единственный конструктор:

StringBufferInputStream( String s)

При работе системы вывода в среде с параллельными процессами при отсутствии синхронизации могут возникать неожиданные результаты. Причиной этого являются попытки различных подпроцессов одновременно обратиться к одному и тому же потоку. Все конструкторы и методы, имеющиеся в этом классе, идентичны тем, которые есть в классах InputStream и OutputSt r eam, единственное отличие классов фильтруемых потоков в том, что их методы синхронизованы.

Буферизованные потоки являются расширением классов фильтруемых потоков, в них к потокам ввода-вывода присоединяется буфер в памяти. Этот буфер выполняет две основные функции:

Он дает возможность исполняющей среде java проделывать за один раз операции ввода-вывода с более чем одним байтом, тем самым повышая производительность среды.

Поскольку у потока есть буфер, становятся возможными такие операции, как пропуск данных в потоке, установка меток и очистка буфера.

Буферизация ввода-вывода — общепринятый способ оптимизации таких операций. Класс BufferedlnputStream в Java дает возможность “окружить” любой объект InputStream буферизованным потоком, и, тем самым, получить выигрыш в производительности. У этого класса два конструктора. Первый из них

создает буферизованный поток, используя для него буфер длиной 32 байта. Во втором

BufferedInputStream(InputStream in, int size)

размер буфера для создаваемого потока задается вторым параметром конструктора. В общем случае оптимальный размер буфера зависит от операционной системы, количества доступной оперативной памяти и конфигурации компьютера.

Вывод в объект BufferedOutputStream идентичен выводу в любой OutputStream с той разницей, что новый подкласс содержит дополнительный метод flush, применяемый для принудительной очистки буфера и физического вывода на внешнее устройство хранящейся в нем информации. Первая форма конструктора этого класса:

создает поток с буфером размером 32 байта. Вторая форма:

BufferedOutputStream(OutputStream out, int size)

позволяет задавать требуемый размер буфера.

Одно из необычных применений буферизации — реализация операции pushback (вернуть назад). Pushback применяется к InputStream для того, чтобы после прочтения символа вернуть его обратно во входной поток. Однако возможности класса PushbacklnputStream весьма ограничены — любая попытка вернуть в поток более одного символа приведет к немедленному возбуждению исключения IOException. У этого класса — единственный конструктор

Помимо уже хорошо нам знакомых методов класса InputStream, PushbacklnputStream содержит метод unread(int ch), который возвращает заданный аргументом символ ch во входной поток.

Класс SequencelnputStream поддерживает новую возможность слияния нескольких входных потоков в один. В конструкторе класса SequenceInputStream в качестве параметра используется либо два объекта InputStream, либо перечисление, содержащее коллекцию объектов InputStream:

SequenceInputStream(Enumeration e) SequenceInputStream(InputStream s0, InputStream s1)

В процессе работы класс выполняет поступающие запросы, считывая информацию из первого входного потока до тех пор, пока он не закончится, после чего переходит ко второму и т.д.

Класс PrintStream предоставляет все те утилиты форматирования, которые мы использовали в примерах для вывода через файловые дескрипторы пакета System с самого начала книги. Вы уже привыкли писать “System.out.println”, не сильно задумываясь при этом о тех классах, которые занимаются форматированием выводимой информации. У класса PrintStream два конструктора: PrintStream(OutputStream out) и PrintStream(OutputStream out, boolean autoflush) . Параметр autoflush второго из них указывает, должна ли исполняющая среда Java автоматически выполнять операцию очистки буфера над выходным потоком.

В Java-объектах PrintStream есть методы print и println, “умеющие” работать с любыми типами данных, включая Object. Если в качестве аргумента этих методов используется не один из примитивных типов, то они вызывают метод toString класса Object, после чего выводят полученный результат.

В настоящее время в Java отсутствуют средства для форматирования выводимых данных простых типов, например, типов int и float. В C++ предусмотрены функции для форматирования чисел с плавающей точкой, позволяющие, например, задать вид вывода, при котором в напечатанном числе будет четыре цифры до десятичной точки и три — после.

По течению грести легче

Потоки в Java предоставляют программисту ясную абстракцию для выполнения сложных и зачастую громоздких операций ввода-вывода данных. Java-программы, опирающиеся на абстракции высокого уровня — классы InputStream и OutputStream, будут и в будущем функционировать правильно — даже тогда, когда будут изобретены новые улучшенные реализации классов ввода-вывода. Как вы увидите в следующей главе, такая модель прекрасно работает и при переходе от набора потоков, ориентированных на файловую систему, к работе с сетевыми потоками и сокетами.

Интерфейсы в Java. Полное руководство

Интерфейсы являются основной частью языка программирования Java. Кроме JDK они используются еще и в шаблонах проектирования, а также множестве различных инструментах и фреймворках. Также интерфейсы обеспечивают абстракцию в Java.

Например, нам нужно создать рисунок, который состоит из нескольких фигур. Для этого мы можем создать интерфейс Shape и определить методы для работы с объектами Shape. Для простоты, давайте определим только два метода — draw() — чтобы нарисовать фигуру и метод getArea() , который будет возвращать площадь фигуры.

Пример Java интерфейса

На основании вышеуказанных требований, наш интерфейс Shape будет выглядеть следующим образом:

Основная информация об интерфейсах в Java

  • interface — ключевое слово для создания интерфейса в Java.
  • Создать экземпляр интерфейса в Java нельзя.
  • Интерфейс обеспечивает абсолютную абстракцию. В прошлых постах мы узнали об абстрактных классах в Java, которые также обеспечивают абстракцию, но абстрактные классы могут иметь реализаций метода, а интерфейс не может.
  • Интерфейсы не могут иметь конструкторов, потому что мы не можем создать экземпляр интерфейса. Также интерфейсы не могут иметь методы с реализацией.
  • По умолчанию любой атрибут интерфейса является public, static и final, так что нам не нужно определять модификаторы доступа к атрибутам. Если же вы попробуете это сделать, то компилятор жаловаться не будет.
  • По умолчанию методы интерфейса неявно abstract и public. Это очень умное решение, потому что в интерфейсе метод не имеет реализации — этим занимаются подклассы, реализующие этот интерфейс.
  • Интерфейс в Java не может быть подклассом у другого класса, но он может реализовать другой интерфейс. public interface Shape extends Cloneable<> — пример интерфейса, который наследует другой интерфейс. На самом деле Java обеспечивает множественное наследование интерфейсов — это означает, что интерфейс может наследовать несколько интерфейсов.
  • Ключевое слово implements используется классами для реализации интерфейса.
  • Класс, реализующий интерфейс, должен обеспечить реализацию всех его метода, если только это не абстрактный класс. Например, мы можем реализовать наш интерфейс в абстрактном классе. Вот пример:
  • Программисты должны писать программы в терминах интерфейсов, а не реализаций, потому что в будущем переписать реализацию интерфейса можно будет и в новом классе.

Пример реализации интерфейса на Java

Теперь давайте посмотрим на реализацию интерфейса Shape:

Обратите внимание, что класс Circle реализует все методы, определенные в интерфейсе, а также собственный метод getRadius() . Реализация интерфейса может иметь несколько типов конструкторов. Давайте посмотрим другую реализацию интерфейса Shape.

А теперь напишем программу для тестирования реализаций нашего интерфейса.

Результат работы программы будет такой:

Преимущества интерфейсов в Java

  • Интерфейс обеспечивает так называемый контракт для всех классов, которые реализуют интерфейс.
  • Интерфейсы хороши для создания начальной точки и создания иерархии в проекте.

Недостатки интерфейсов в Java

Хотя интерфейсы дают программисту множество преимуществ, они также имеют и недостатки.

Java — Реализация функции использующей java.io

Начиная разговор о потоках ввода-вывода, в первую очередь я бы хотел, чтобы вы увидели главную задачу — программа должна иметь возможность передать данные кому-то еще. Еще раз — современная программа не существует в вакууме, в сегодняшних условиях подавляющее большинство программ требует интеграции с другими. Интеграция всегда подразумевает передачу данных. Т.е. одна программа как-то передает данные в другую.
Нам, как программистам,. нужен некий механизм ПЕРЕДАЧИ данных.

Дальше уже идет специализация — каким образом это можно сделать. На сегодня основных вариантов три:

  1. Файловая система — одна программа записывает в файл и другая программа читает данные из файла
  2. Сетевое взаимодействие — две программы используют сеть для передачи данных друг другу
  3. Передача из одной области памяти в другую. Данный вариант достаточно часто используется в рамках одной программы, но это не обязательно

Конечно же есть внешние устройства, которые подключаются, например, по USB, COM-порту или как-то еще. И для них тоже требуется передача данных. Хотя нередко для таких устройств операционная система может предложить некое подобие работы с файловой системой.

Сосредоточимся на передаче данных. Что это с технической точки зрения ? Да очень просто — надо “переслать” некое количество байт. Т.е одна программа (отправитель), используя какой-то механизм, “отправляет” байты, а другая (потребитель), используя тот же механизм, “потребляет” эти байты. Причем логично, что байты идут друг за другом от отправителя к потребителю … в виде некоего ПОТОКА байтов.

Это конечно же исключительно мои догадки, но мне кажется, что разработчики Java думали как-то так, когда создавали библиотеку для передачи данных. Основой для них стало понятие ПОТОКА данных.

Причем, что на мой взгляд, важно отметить — ПОТОК данных предполагает, что среда переноса данных может быть разная. Это файловая система, по сути контроллер жесткого диска. Или это сетевая карта, в которую (или из которой) записываются (читаются) байты. Во всех случаях, по сути, вы должны записать/считать байт по определенному адресу. Дальше в дело вступает электроника — вы записали байт по адресу, контроллер той же сетевой карты отправит это байт в сеть, где контроллер сетевой карты на другом компьютере поместит этот байт по определенному адресу, который будет доступен для чтения.

Таким образом можно рассматривать механизм потоков, как инструмент. С помощью которого вы как-бы подключаетесь к источнику данных (в случае чтения) или приемнику данных (в случае записи) и после подключения вам просто надо либо читать оттуда, либо писать.туда.
Мне почему-то всегды было удобно видеть это как некоторую “трубу”, с одной стороны которой мой код, а с другой стороны все, что может принимать (или порождать) набор байтов. Я просто “заливаю” в эту трубу байты. А как они там на другом конце трубы попадают в приемник — не мое дело. Я просто использую “трубы” под каждый вид приемника — для файла, для сети.

Еще крайне интересным моментов является возможность “соединять” разные потоки. Чуть позже мы увидим практическое воплощение этой идеи.
Если создать образное представление, то вы можете использовать трубу, которая например окрашивает воду, которая проходит через нее, в другой цвет. Или фильтрует. Или насыщает какими-то добавками. И на конец этой трубы вы “прикрепляете” трубу в другими возможностями. Вода (или байты) проходя по этим трубам, трансформируется.
Если смогли представить, то будет здОрово, если не получается — ничего страшного. Надеюсь, что дальнейшие примеры позволят вам это увидеть.

Типы потоков

По направлению движения данных потоки можно разделить на две группы:

  1. Поток ввода (Input) — данные поступают из потока в нашу программу. Мы их читаем из этого потока
  2. Поток вывода (Output) — данные поступают в поток из нашей программы. Мы их пишем в этот поток

Вторым критерием разделения может служить ТИП передаваемых данных. Да-да, байты не всегда являются удобным вариантом передачи — есть еще текст. Символы. Я надеюсь, вы помните, что символ в Java занимает ДВА байта. Так вот передача двух байтов как одного целого числа имеет сложность — какой байт должен идти первым ? Есть такая неприятная ситуация — в разных операционных системах этот вопрос решается по-разному.- вы можете поискать информацию в Интернете на тему big endian little endian и узнать, как непросто бывает подружить Windows и Linux или просто Linux на разных компьютерах. В данной статье я не ставлю такой задачи — просто констатирую факт: передача символов длиной в два байта требует дополнительных усилий. Поэтому и появилось такое разделение:

Цукерберг рекомендует:  Садиться и писать код

В итоге мы получаем 4 типа потоков. Для каждого из этих типов Java предлагает отдельный базовый абстрактный класс. Почему абстрактный ? Потому, что у нас есть специализация — файлы, сеть, память. И расширяя базовый класс специальный класс решает свои специальные задачи. Но базовые функции для всех одинаковые. Что удобно — все специальные потоки по своей сути одно и тоже. Это дает гибкость и универсальность. Вот эти классы:

  1. InputStream — поток для чтения байтов (поток ввода)
  2. Reader — поток для чтения символов (поток ввода)
  3. OutputStream — поток для записи байтов (поток вывода)
  4. Writer — поток для записи символов (поток вывода)

Основной функцией для потоков ввода является метод read в нескольких модификациях, которые мы рассмотрим позже. Разница для InputStream и Reader состоит только в том, что первый читает байты (byte), а второй — символы (char).
Вполне логично вытекает название основного метода для классов OutputStream и Writer — метод write. Тоже в нескольких модификациях.

Основные действия с потоком

Схема работы с потоком в упрощенном виде выглядит так:

  1. Создается экземпляр потока
  2. Поток открывается (для чтения или записи)
  3. Производится чтение из потока/запись в поток
  4. Поток закрывается

Первые два пункта часто совмещены в рамках одного действия. По сути потоки можно представить как трубу, в которую “заливаются” байты или символы. Причем что еще интереснее, эти трубы можно “склеивать” друг с другом. Т.е один поток может передавать данные в другой, предварительно как-то их модифицируя.
Этот прием мы еще увидим, а пока давайте решим простую задачу — запишем строку в файл. В текстовый файл. Т.е. Нам потребуется поток для символов — Writer. Потом мы прочитаем этот файл — и для этого используем Reader.
Чуть выше я говорил, что Reader и Writer — абстрактные классы. Для работы с файлами нам потребуются уже конкретные и это будут FileReader и FileWriter.
Первым шагом мы запишем текст в файл. Порядок работы с потоком я в принципе описал, поэтому давайте конкретизируем наши действия. Создание и открытие файлового потока на запись делает в момент создания экземпляра объекта FileWriter, у которого конструктор принимает в качестве параметра строку с именем файла. Далее в цикле мы пишем в поток символы из строки и потом закрываем наш поток. Обратите внимание на конструкцию блока try. Мы уже встречали такое в разделе JDBC — групповые операции.
Повторю его идею — в момент начала блока try .. catch вы можете открыть ресурс (важно, чтобы он реализовывал интерфейс AutoCloseable). В таком случае по окончании блока ресурс будет автоматически закрыт и вам не надо вызывать метод close. Блок try .. catch мы должны использовать, т.к. Операции по открытию и записи в файл могут порождать исключения FileNotFoundException и IOException. Исключение FileNotFoundException является подклассом IOException и в принципе нам нет необходимости обрабатывать его отдельно. Так что мы сократили все до IOException.
Также советую внимательно прочитать комментарии в коде — они объясняют многие моменты.
Перейдем от слов к делу — смотрим код нашего примера

Как работает виртуальная машина Java — взгляд изнутри

Рассказывает Роман Иванов

Каждому Java-разработчику будет очень полезно понимать, что из себя представляет JVM, как в неё попадает код и как он исполняется. Статья больше подойдёт новичкам, но найти в ней что-то новое смогут и более опытные программисты. В статье вкратце описано, как устроен class-файл и как виртуальная машина обрабатывает и исполняет байт-код.

Основной задачей разработчиков Java было создание переносимых приложений. JVM играет центральную роль в переносимости — она обеспечивает должный уровень абстракции между скомпилированной программой и базовой аппаратной платформой и операционной системой. Несмотря на этот дополнительный «слой», скорость работы приложений необычайно высока, потому что байт-код, который выполняет JVM, и она сама отлично оптимизированы.

Рассмотрим схему работы JVM более подробно.

Структура class-файла

Напишем простейшее приложение и скомпилируем его. Компилятор заботливо создаст файл с расширением class и поместит туда всю информацию о нашем мини-приложении для JVM. Что мы увидим внутри? Файл поделён на десять секций, последовательность которых строго задана и определяет всю структуру class-файла.

Granatum, удалённо, средняя по рынку от 60 000 до 150 000 ₽

Файл начинается со стартового (магического) числа: 0xCAFEBABE. Данное число присутствует в каждом классе и является обязательным флагом для JVM: с его помощью система понимает, что перед ней class-файл.

Следующие четыре байта class-файла содержат старший и младший номера версий Java. Они идентифицируют версию формата конкретного class-файла и позволяют JVM проверять, возможна ли его поддержка и загрузка. Каждая JVM имеет ограничение версии, которую она может загрузить — более поздние версии будут игнорироваться. Как видно на примере файла выше, у нас major версия 0x34 , что соответствует Java SE 8. Для Java SE 11 будет значение 0x37 .

С девятого байта идёт пул констант, в котором содержатся все константы нашего класса. Так как в каждом классе их может быть различное количество, то перед массивом находится переменная, указывающая на его длину, то есть пул констант представляет из себя массив переменной длины. Каждая константа занимает один элемент в массиве. Во всём class-файле константы указываются целочисленным индексом, который обозначает их положение в массиве. Начальная константа имеет индекс 1, вторая константа — 2 и т. д.

Каждый элемент пула констант начинается с однобайтового тега, определяющего его тип. Это позволяет JVM понять, как правильно обработать идущую далее константу. Всего зарезервировано 14 типов констант:

Тип константы Значение тега
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

Например, если тег указывает, что константа является строкой, JVM получает значение тега 1 и обрабатывает следующее за тегом число как длину массива байт, которые необходимо считать, чтобы получить нужную нам строку полностью.

Прочитав блок с константами, JVM переходит к следующим двум байтам — флагам доступа, которые определяют, описывает этот файл класс или интерфейс, общедоступный или абстрактный, является ли класс финальным.

Имена класса и его родительского класса хранятся в массиве констант, на которые указывают последующие 4 байта в файле.

Немного иначе обстоят дела с интерфейсами. Так как класс может наследоваться от множества интерфейсов одновременно, то хранить необходимо массив ссылок на пул констант. То есть за определением класса и его родительского класса идёт число, характеризующее размер массива интерфейсов, и сам массив. Все возможные значения кодов представлены ниже.

Имя флага Код Определение
ACC_PUBLIC 0x0001 Объявлен публичным
ACC_FINAL 0x0010 Объявлен финальным
ACC_SUPER 0x0020 Специальный флаг, введённый в версии Java 1.1 для совместимости при вызове методов родителя
ACC_INTERFACE 0x0200 Объявлен интерфейсом
ACC_ABSTRACT 0x0400 Объявлен абстрактным
ACC_SYNTHETIC 0x1000 Зарезервированное определение
ACC_ANNOTATION 0x2000 Объявлен аннотацией
ACC_ENUM 0x4000 Объявлен перечислением

Подобную структуру имеет и следующий блок — Fields.

Этот блок начинается с двухбайтового параметра количества полей в этом классе или интерфейсе. За ним идёт массив структур переменной длины. Каждая структура содержит информацию об одном поле: имя поля, тип, значение, если это, например, финальная переменная. В списке отображаются только те поля, которые были объявлены классом или интерфейсом, определённым в файле. Поля от родительских классов и имплементированных интерфейсов здесь не присутствуют, они задаются в своих class-файлах.

Далее мы переходим к самому важному месту в любом классе — его методам, именно в них сосредоточена вся логика любой программы, весь исполняемый байт-код.

Ситуация абсолютно аналогична описанным выше полям. В массиве переменной длины содержатся структуры, в которые входит полное описание сигнатуры метода: модификаторы доступа, имя метода и его атрибуты, которые также представляют из себя структуру, так как их может быть множество и каждый из них может принадлежать разным типам.

В последнем блоке идёт дополнительная мета-информация, например имя файла, который был скомпилирован. Она может присутствовать, а может и нет. В случае каких-то проблем JVM просто игнорирует этот блок.

Мы рассмотрели структуру файлов и готовы перейти к следующей части — загрузке class-файла в JVM и последующему выполнению байт-кода из этого класса. В качестве закрепления полученных знаний по структуре class-файла можете воспользоваться встроенным декомпилятором Java и посмотреть результат его выполнения с ключами -c -verbose (javap -c -verbose TestJava.class) .

Загрузка классов

Теперь, разобравшись с общей структурой файла, посмотрим, как JVM его обрабатывает.

Чтобы попасть в JVM, класс должен быть загружен. Для этого существуют специальные классы-загрузчики:

  1. Bootstrap — базовый загрузчик, загружает платформенные классы. Этот загрузчик является родителем всех остальных классов и частью платформы.
  2. Extension ClassLoader — загрузчик расширений, потомок Bootstrap-загрузчика. Загружает классы расширений, которые по умолчанию находятся в каталоге jre/lib/ext .
  3. AppClassLoader — системный загрузчик классов из classpath, который является непосредственным потомком Extension ClassLoader. Он загружает классы из каталогов и jar-файлов, указанных переменной среды CLASSPATH , системным свойством java.class.path или параметром командной строки -classpath .
  4. Собственный загрузчик — у приложения могут быть свои собственные загрузчики.

Главный класс приложения всегда загружается системным загрузчиком, остальные же классы могут быть загружены различными пользовательскими загрузчиками. Стоит упомянуть, что имя загрузчика создаёт уникальное пространство имён, то есть в программе может существовать несколько классов с одним и тем же полным именем, если они обрабатывались разными загрузчиками.
Поэтому каждый загрузчик делегирует свои полномочия родителю, то есть перед поиском класса для загрузки он попытается узнать, не был ли загружен нужный класс раньше.

После загрузки класса начинается этап линковки, который делится на три части.

  1. Верификация байт-кода. Это статический анализ кода, выполняется один раз для класса. Система проверяет, нет ли ошибок в байт-коде. Например, проверяет корректность инструкций, переполнение стека и совместимость типов переменных.
  2. Выделение памяти под статические поля и их инициализация.
  3. Разрешение символьных ссылок — JVM подставляет ссылки на другие классы, методы и поля. В большинстве случаев это происходит лениво, то есть при первом обращении к классу.

Класс инициализируется, и JVM может начать выполнение байт-кода методов.

JVM получает один поток байтовых кодов для каждого метода в классе. Байт-код метода выполняется, когда этот метод вызывается в ходе работы программы. Поток байт-кода метода — это последовательность инструкций для виртуальной машины Java. Каждая инструкция состоит из однобайтового кода операции, за которым может следовать несколько операндов. Код операции указывает действие, которое нужно предпринять. Всего на данный момент в Java более 200 операций. Все коды операций занимают только 1 байт, так как они были разработаны компактными, поэтому их максимальное число не может превысить 256 штук.

В основе работы JVM находится стек — основные инструкции работают с ним.

Рассмотрим пример умножения двух чисел. Ниже представлен байт-код метода:

На Java это будет выглядеть так:

По листингу выше можно заметить, что коды операций сами по себе указывают тип и значение. Например, код операции iconst_1 указывает JVM на целочисленное значение, равное единице. Такие байт-коды определены для самых часто используемых констант. Эти инструкции занимают 1 байт и введены специально для повышения эффективности выполнения байт-кода и уменьшения размера его потока. Подобные короткие константы также присутствуют и для других типов данных.

Всего JVM поддерживает семь примитивных типов данных: byte, short, int, long, float, double и char.

Если бы мы хотели положить в переменную а другое значение, например 11112, то нам пришлось бы использовать инструкцию sipush :

Данные операции выполняются в так называемом фрейме стека метода. У каждого метода есть некоторая своя часть в общем стеке. Таким образом в нашем главном потоке исполнения программы создаются множество подстеков на каждый вызов метода. Более наглядно это представлено на картинке ниже:

В каждом стек-фрейме хранится массив локальных переменных, который позволяет сохранять и доставать локальные переменные, как мы сделали в примере выше, поместив значения 1 и 5 в переменные 1 и 2. Стоить отметить, что здесь компилятор также оптимизировал байт-код, используя однобайтовые инструкции. Если бы переменных в нашем методе было много, использовался бы код операции сохранения значения вместе с указанием позиции переменной в массиве.

Чтобы достучаться до пула констант класса и получить нужное значение, используются инструкции lcd и lcd_w . При этом lcd может ссылаться только на константы с индексами от 1 до 255, поскольку размер её операнда составляет всего 1 байт. Lcd_w имеет 2-байтовый индекс, поэтому может ссылаться на более широкий диапазон индексов.

Вызовы методов

Java предоставляет два основных вида методов: методы экземпляра и методы класса. Методы экземпляра используют динамическое (позднее) связывание, тогда как методы класса используют статическое (раннее) связывание.

Виртуальная машина Java вызывает метод класса, выбирая его на основании типа ссылки на объект, который всегда известен во время компиляции. С другой стороны, когда виртуальная машина вызывает метод экземпляра, она выбирает метод для вызова на основе фактического класса объекта, который может быть известен только во время выполнения. Поэтому для вызова методов используются разные инструкции: invokevirtual и invokestatic . Данные функции ссылаются на запись в пуле констант в виде полного пути к необходимой функции. Виртуальная машина снимает нужное количество переменных со стека и передает в метод.

Возвращаемое методом значение кладётся на стек. Типы возвращаемых значений методов указаны ниже:

Операция Описание
ireturn Помещает на стек значение типа int
lreturn Помещает на стек значение long,
freturn Помещает на стек значение float
dreturn Помещает на стек значение double
areturn Помещает на стек значение object
return Не изменяет стек

Циклы

Осталось рассмотреть последнюю часто используемую конструкцию языка — циклы. Посмотрим, во что превратится код, представленный ниже:

Каждый раз на стеке оказывается два числа, которые сравниваются, и если i > 9999 , происходит выход из цикла. При этом для создания цикла используется конструкция на основе goto , которая запрещена в самом языке Java, хотя ключевое слово и зарезервировано.

Заключение

Изначально байт-код интерпретируется в большинстве JVM, но как только система замечает, что некоторый код используется очень часто, она подключает встроенный компилятор, который компилирует байт-коды в машинный код, тем самым значительно ускоряя работу приложения.

Таким образом, мы поверхностно рассмотрели жизненный цикл байткода в JVM: class-файлы, их загрузку и выполнение байт-кода и базовые инструкции.

Байтовые потоки

1. Класс InputStream

Класс InputStream – это абстрактный класс. Все байтовые потоки чтения наследуются от класса InputStream.

Методы класса InputStream:

Существует 3 основных read()-метода:

  • int read()— возвращает целочисленное представление следующего доступного байта в потоке. При достижении конца файла возвращается значение -1.
  • int read(byte[] buffer) — пытается прочесть максимум buffer.length байт из входного потока в массив buffer . Возвращает количество байт, в действительности прочитанных из потока. По достижении конца файла возвращает значение -1.
  • int read(byte[] buffer, int offset, int length) — пытается прочесть максимум length байт, расположив их в массиве buffer , начиная с элемента offset . Возвращает количество реально прочитанных байт. По достижении конца файла возвращает -1.

Методы read() будут блокированы, пока доступные данные не будут прочитаны.

  • int available()— возвращает количество байтов ввода, доступные в данный момент для чтения.
  • close()— закрывает источник ввода. Следующие попытки чтения передадут исключение IOException .
  • long skip(long byteCount) — пропускает byteCount байт ввода, возвращая количество проигнорированных байтов.

Все методы выбрасывают исключение IOException , если происходит ошибка ввода-вывода.

2. Класс OutputStream

Все байтовые потоки записи наследуются от абстрактного класса OutputStream.

Методы класса OutputStream:

Существуют 3 основных write()-метода:

  • void write(int data) — записывает один байт в выходной поток. Аргумент этого метода имеет тип int , что позволяет вызывать write, передавая ему выражение, при этом не нужно выполнять приведение его типа к byte .
  • void write(byte[] buffer) — записывает в выходной поток весь указанный массив байт.
  • void write(byte[] buffer, int offset, int length)— записывает в поток часть массива — length байт, начиная с элемента buffer[offset] .
  • flush() — очищает любые выходные буферы, завершая операцию вывода.
  • close() — закрывает выходной поток. Последующие попытки записи в этот поток будут возбуждать IOException .

Методы выбрасывают исключение IOException , если происходит ошибка ввода-вывода.

3. Класс FileInputStream

Поток FileInputStream используется в Java для чтения данных из файла.

Конструкторы класса FileInputStream:

Представленный конструктор использует имя файла в качестве потока с целью создания объекта входного потока для считывания файла.

Этот конструктор использует объектный файл с целью создания объекта входного потока для чтения файла.

4. Класс FileOutputStream

В Javа FileOutputStream используется для создания файла и последующей записи в него. Поток создаст файл в случае его отсутствия перед его открытием для вывода.

Конструкторы класса FileOutputStream:

Представленный конструктор использует имя файла в качестве строки с целью создания объекта входного потока для записи файла в Java.

Этот конструктор использует объектный файл с целью создания объекта выходного потока для записи файла.

Реализация Java IO для unix/linux «tail -f»

Мне интересно, какие методы и/или библиотеки использовать для реализации функциональности команды linux «tail -f». Я по существу ищу возможность добавления/замены для java.io.FileReader . Код клиента может выглядеть примерно так:

Пропущенный фрагмент является разумной реализацией TailFileReader . Он должен иметь возможность читать части файла, которые существуют до открытия файла, а также строки, которые добавляются.

Возможность продолжить чтение файла и подождать до тех пор, пока у файла не появится больше обновлений для вас, это не так сложно выполнить в коде. Вот несколько псевдокодов:

Я бы предположил, что вы захотите включить этот тип функций в свой собственный поток, чтобы вы могли спать и не влияли на другие области вашего приложения. Вы хотите показать keepReading в сеттере, чтобы ваш основной класс/другие части приложения могли безопасно закрыть поток без каких-либо других головных болей, просто позвонив stopReading() или что-то подобное.

Взгляните на реализацию Apache Commons класса Tailer. Кажется, что он также обрабатывает вращение журнала.

Отметьте JLogTailer, который выполняет эту логику.

Основной смысл в коде:

Я построил короткую реализацию «tail -f» в Scala некоторое время назад: tailf. Он также заботится о вращении файла, и вы можете определить свою собственную логику, что делать, когда он достигнет EOF или обнаружил, что файл был переименован.

Понравилась статья? Поделиться с друзьями:
Все языки программирования для начинающих