Thread’ом java не испортишь: часть v

Методы wait и notify

Последнее обновление: 27.04.2018

Иногда при взаимодействии потоков встает вопрос о извещении одних потоков о действиях других. Например, действия одного потока зависят от результата действий другого потока,
и надо как-то известить один поток, что второй поток произвел некую работу. И для подобных ситуаций у класса Object определено ряд методов:

  • wait(): освобождает монитор и переводит вызывающий поток в состояние ожидания до тех пор, пока другой поток не вызовет метод

  • notify(): продолжает работу потока, у которого ранее был вызван метод

  • notifyAll(): возобновляет работу всех потоков, у которых ранее был вызван метод

Все эти методы вызываются только из синхронизированного контекста — синхронизированного блока или метода.

Рассмотрим, как мы можем использовать эти методы. Возьмем стандартную задачу из прошлой темы — «Производитель-Потребитель» («Producer-Consumer»):
пока производитель не произвел продукт, потребитель не может его купить. Пусть производитель должен произвести 5 товаров, соответственно потребитель
должен их все купить. Но при этом одновременно на складе может находиться не более 3 товаров.
Для решения этой задачи задействуем методы и :

public class Program {
 
    public static void main(String[] args) {
         
        Store store=new Store();
        Producer producer = new Producer(store);
        Consumer consumer = new Consumer(store);
        new Thread(producer).start();
        new Thread(consumer).start();
    }
}
// Класс Магазин, хранящий произведенные товары
class Store{
   private int product=0;
   public synchronized void get() {
      while (product<1) {
         try {
            wait();
         }
         catch (InterruptedException e) {
         }
      }
      product--;
      System.out.println("Покупатель купил 1 товар");
      System.out.println("Товаров на складе: " + product);
      notify();
   }
   public synchronized void put() {
       while (product>=3) {
         try {
            wait();
         }
         catch (InterruptedException e) { 
         } 
      }
      product++;
      System.out.println("Производитель добавил 1 товар");
      System.out.println("Товаров на складе: " + product);
      notify();
   }
}
// класс Производитель
class Producer implements Runnable{
 
    Store store;
    Producer(Store store){
       this.store=store; 
    }
    public void run(){
        for (int i = 1; i < 6; i++) {
            store.put();
        }
    }
}
// Класс Потребитель
class Consumer implements Runnable{
     
     Store store;
    Consumer(Store store){
       this.store=store; 
    }
    public void run(){
        for (int i = 1; i < 6; i++) {
            store.get();
        }
    }
}

Итак, здесь определен класс магазина, потребителя и покупателя. Производитель в методе добавляет в объект Store с помощью его метода
6 товаров. Потребитель в методе в цикле обращается к методу объекта Store для получения
этих товаров. Оба метода Store — и являются синхронизированными.

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

while (product<1)

Если товар отсутсвует, вызывается метод . Этот метод освобождает монитор объекта Store и блокирует выполнение метода get, пока для этого же монитора не будет вызван
метод .

Когда в методе добавляется товар и вызывается , то метод получает монитор и выходит из
конструкции , так как товар добавлен. Затем имитируется получение покупателем товара. Для этого
выводится сообщение, и уменьшается значение product: . И в конце вызов метода дает сигнал методу продолжить работу.

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

И теперь программа покажет нам другие результаты:

Производитель добавил 1 товар
Товаров на складе: 1
Производитель добавил 1 товар
Товаров на складе: 2
Производитель добавил 1 товар
Товаров на складе: 3
Покупатель купил 1 товар
Товаров на складе: 2
Покупатель купил 1 товар
Товаров на складе: 1
Покупатель купил 1 товар
Товаров на складе: 0
Производитель добавил 1 товар
Товаров на складе: 1
Производитель добавил 1 товар
Товаров на складе: 2
Покупатель купил 1 товар
Товаров на складе: 1
Покупатель купил 1 товар
Товаров на складе: 0

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

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

НазадВперед

Класс Writer

Класс — это полный аналог класса , и снова только с одним отличием: он работает с символами, , вместо байт.

Это абстрактный класс: объекты класса создать нельзя. Его основная цель — быть единым классом-родителем для сотен классов-наследников и задать для них общие методы работы с символьными потоками.

Методы класса (и всех его классов-наследников):

Методы Описание
Записывает один символ (не ) в поток.
Записывает массив символов в поток
Записывает часть массива символов в поток
Записывает строку в поток
Записывает часть строки в поток
Записывает в поток все данные, которые хранятся в буфере
Закрывает поток

Методы очень похожи на методы класса , только работают с символами вместо байт.

Краткое описание методов:

Метод

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

Метод

Записывает в поток вывода переданный массив символов.

Метод

Записывает в поток вывода часть переданного массива символов. Переменная задает номер первого элемента массива, — длина записываемого фрагмента.

Метод

Записывает в поток вывода переданную строку.

Метод

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

Метод

Метод используется, чтобы принудительно записать в целевой поток данные, которые могут кэшироваться в текущем потоке. Актуально при использовании буферизации и/или нескольких объектах потоков, организованных в цепочку.

Метод

Записывает в целевой объект все незаписанные данные. Метод можно не вызывать, если вы используете -with-resources.

Пример программы, которая копирует текстовый файл:

Код Примечание
для чтения из файла для записи в файл
Буфер, в который будем считывать данные
Пока данные есть в потоке
Читаем данные в буфер
Записываем данные из буфера во второй поток

Класс

Есть еще один интересный класс-наследник от класса — это . В нем находится изменяемая строка — объект . И каждый раз, когда вы что-то «пишете» в объект , текст просто добавляется во внутренний буфер.

Пример:

Код Примечание
Создается целевой символьный поток
Строка пишется в буфер внутри
Строка пишется в буфер внутри
Преобразовываем содержимое объекта к строке

В данном случае класс — это, по сути, обертка над классом , однако класс — это наследник класса-потока , и он может использоваться в цепочках из объектов-потоков. Довольно полезное свойство на практике.

Проблемы, которые создает многопоточность

Deadlock

  1. Поток-1 перестанет работать с Объектом-1 и переключится на Объект-2, как только Поток-2 перестанет работать с Объектом 2 и переключится на Объект-1.
  2. Поток-2 перестанет работать с Объектом-2 и переключится на Объект-1, как только Поток-1 перестанет работать с Объектом 1 и переключится на Объект-2.

Потоки никогда не поменяются местами и будут ждать друг друга вечно. deadlock

Поток-0 достает яйца из холодильника.
Поток-1 включает плиту.
Поток-2 достает сковородку и ставит на плиту.
Поток-3 зажигает огонь на плите.
Поток-4 выливает на сковороду масла.
Поток-5 разбивает яйца и выливает их на сковороду.
Поток-6 выбрасывает скорлупу в мусорное ведро.
Поток-7 снимает готовую яичницу с огня.
Поток-8 выкладывает яичницу в тарелку.
Поток-9 моет посуду.Выполнен поток Thread-0
Выполнен поток Thread-2
Выполнен поток Thread-1
Выполнен поток Thread-4
Выполнен поток Thread-9
Выполнен поток Thread-5
Выполнен поток Thread-8
Выполнен поток Thread-7
Выполнен поток Thread-3
Выполнен поток Thread-6

Запуск задач с помощью java.util.concurrent.ExecutorService

Облегчив с помощью интерфейса Callable создание задач для параллельного выполнения, пакет java.util.concurrent также берет на себя работу по запуску и остановке потоков. Вместо объекта Thread предлагается использовать объект типа ExecutorService, с помощью которого пользователь может просто поместить задачу в очередь на выполнение и ждать получения результата. Можно сказать, что ExecutorService – это значительно усовершенствованная реализация шаблона WorkerThread.

ExecutorService – это интерфейс, поэтому для выполнения задач используются его конкретные потомки, адаптированные под требования разрабатываемого приложения. Однако программисту нет необходимости создавать собственную реализацию ExecutorService, так как в пакете java.util.concurrent уже присутствуют различные варианты реализации ExecutorService. Доступ к ним можно получить через статические методы служебного класса Executors, метод которого newFixedThreadPool возвращает объект типа ExecutorService со встроенной поддержкой шаблона ThreadPool. Также в классе Executors есть и другие методы для создания объектов ExecutorService с различными свойствами.

Наибольший интерес в ExecutorService представляет метод submit, через который задача ставится в очередь на выполнение. На вход этот метод принимает объект типа Callable или Runnable, а возвращает некий параметризованный объект типа Future. Этот объект можно использовать для доступа к результату выполнения задачи, который будет возвращен из метода call соответствующего Callable-объекта. При этом через объект Future можно проверить, закончено ли уже выполнение задачи – с помощью метода isDone и через метод get получить доступ к результату или исключительной ситуации, если в процессе выполнения задачи произошла ошибка.

Таким образом, при запуске задач с помощью классов из пакета java.util.concurrent не требуется прибегать к низкоуровневой поточной функциональности класса Thread, достаточно создать объект типа ExecutorService с нужными свойствами и передать ему на исполнение задачу типа Callable. Впоследствии можно легко просмотреть результат выполнения этой задачи с помощью объекта Future, как показано в листинге 4.

Листинг 4. Запуск задачи с помощью классов пакета java.util.concurrent
1 public class ExecutorServiceSample {
2     public static void main(String[] args) {
3         //создать ExecutorService на базе пула из пяти потоков
4         ExecutorService es1 = Executors.newFixedThreadPool(5);
5         //поместить задачу в очередь на выполнение
6         Future<String> f1 = es1.submit(new CallableSample());        
7         while(!f1.isDone()) {
8             //подождать пока задача не выполнится
9         }
10        try {
11            //получить результат выполнения задачи
12            System.out.println("task has been completed : " + f1.get());
13        } catch (InterruptedException ie) {           
14            ie.printStackTrace(System.err);
15        } catch (ExecutionException ee) {
16            ee.printStackTrace(System.err);
17        }
18        es1.shutdown();
19    }
20}

Стоит обратить внимание на строку 18, где происходит остановка объекта ExecutorService с помощью метода shutdown. Дело в том, что потоки в объекте ExecutorService не останавливаются сами, как обычно, поэтому их необходимо явно остановить с помощью этого метода, при этом если в ExecutorService находятся невыполненные задачи, то потоки будут остановлены только, когда завершится последняя задача

Класс PrintStream

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

Самый интересный и многофункциональный из всех промежуточных потоков вывода — . У него несколько десятков методов и аж целых 12 конструкторов.

Класс унаследован от класса , а тот унаследован от . Поэтому класс имеет все методы классов-родителей и плюс свои. Вот самые интересные из них:

Методы Описание
Преобразует переданной объект в строку и выводит в целевой поток.
Преобразует переданный объект в строку и выводит в целевой поток. Добавляет в конце символ переноса строки
Выводит в целевой поток символ переноса строки
Конструирует и выводит строку на основе строки шаблона и переданных аргументов, по аналогии с методом

А где же несколько десятков методов, спросите вы?

Все дело в том, что у него много вариантов метода и с разными аргументами. Их вполне можно свести к этой таблице.

Мы даже не будем разбирать эти методы, т.к. вы их и так уже хорошо знаете. Догадываетесь, к чему я клоню?

Помните команду ? А ведь ее можно записать в две строки:

Код Вывод на экран

Наша любимая команда — это вызов метода у статической переменной класса . А тип у этой переменной — .

Уже много уровней вы почти в каждой задаче вызываете методы класса и даже не догадываетесь об этом!

Практическое использование

Есть в Java один интересный класс — , который представляет из себя динамически увеличивающийся массив байт, унаследованный от .

Объект и объект можно выстроить в такую цепочку:

Код Описание
Создали в памяти буфер для записи
Обернули буфер в объект
Записывает данные как в консоль
Преобразовываем массив в строку!
Вывод на экран:

Настройка размера пула

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

Если вы помните, есть два основных преимущества в организации поточной обработки сообщений в приложениях: возможность продолжения процесса во время ожидания медленных операций, таких, как I/O (ввод — вывод), и использование возможностей нескольких процессоров. В приложениях с ограничением по скорости вычислений, функционирующих на N-процессорной машине, добавление дополнительных потоков может улучшить пропускную способность, по мере того как количество потоков подходит к N, но добавление дополнительных потоков свыше N не оправдано. Действительно, слишком много потоков разрушают качество функционирования из-за дополнительных издержек переключения процессов

Оптимальный размер пула потоков зависит от количества доступных процессоров и природы задач в рабочей очереди. На N-процессорной системе для рабочей очереди, которая будет выполнять исключительно задачи с ограничением по скорости вычислений, вы достигните максимального использования CPU с пулом потоков, в котором содержится N или N+1 поток.

Для задач, которые могут ждать осуществления I/O (ввода — вывода) — например, задачи, считывающей HTTP-запрос из сокета – вам может понадобиться увеличение размера пула свыше количества доступных процессоров, потому, что не все потоки будут работать все время. Используя профилирование, вы можете оценить отношение времени ожидания (WT) ко времени обработки (ST) для типичного запроса. Если назвать это соотношение WT/ST, для N-процессорной системе вам понадобится примерно N*(1+WT/ST) потоков для полной загруженности процессоров.

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

Недостатки JavaAPIStream

Параллельные потоки действительно могут замедлить вас

Java 8 обещает параллелизм как одну из самых ожидаемых новых функций. Метод. parallelStream () реализует это в коллекциях и потоках. Он разбивает их на подзадачи, которые затем запускаются в отдельных потоках для обработки, они могут идти в разные ядра, а затем объединяться, когда они закончат. Все это происходит под капотом с помощью фреймворка fork/join. Хорошо, звучит круто, это должно ускорить работу с большими наборами данных в многоядерных средах, не так ли?

Нет, это действительно может заставить ваш код работать медленнее, если он не используется правильно. Примерно на 15% медленнее на этом бенчмарке мы бежали, но могло быть и хуже. Допустим, мы уже запускаем несколько потоков и используем .parallelStream() в некоторых из них, добавляя все больше и больше потоков в пул. Это может легко превратиться в нечто большее, чем наши ядра могут справиться, и замедлить все из-за увеличения переключения контекста.

Для чего Jigsaw?

Цель головоломки заключается в том, чтобы сделать Java модульные и сломать JRE, чтобы интероперабельных компонентов. Мотивация, стоящая за этим, в первую очередь исходит из желания иметь лучшую, более быструю и сильную встроенную Java. Я стараюсь избегать упоминания «Интернета вещей», но там я это сказал. Уменьшение размеров банок, повышение производительности и повышение безопасности-вот еще некоторые из обещаний этого амбициозного проекта.

Так где же он? Jigsaw совсем недавно вступила в фазу 2, прошла исследовательскую фазу и теперь переключается на качественный дизайн и внедрение производства, говорит Марк Рейнхольд, главный архитектор Oracle по Java. Проект сначала планировалось завершить в Java 8 и было отложено до Java 9, ожидалось, что это будет одна из его флагманских новых функций.

Проблемы, которые все еще существуют

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

Функциональное программирование

Функциональное программирование было возможно с помощью Java и раньше, хотя это довольно неудобно. Java 8 улучшает это, в частности, с помощью лямбд. Это очень приветствуется, но не такой огромный сдвиг, который был изображен ранее. Определенно более элегантный, чем в Java 7, но некоторые изгибы назад все еще необходимы, чтобы быть действительно функциональным.

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

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

Методы по умолчанию отвлекают

Методы по умолчанию позволяют реализовать функцию по умолчанию в самом интерфейсе. Это, безусловно, одна из самых крутых новых функций Java 8, но она несколько мешает тому, как мы привыкли делать вещи. Так почему же все-таки это было введено? А что с ним не делать?

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

Класс InputStream

Класс интересен тем, что является классом-родителем для сотен классов-наследников. В нем самом нет никаких данных, однако у него есть методы, которые есть у всех его классов-наследников.

Объекты-потоки вообще редко хранят в себе данные. Поток — это инструмент чтения/записи данных, но не хранения. Хотя бывают и исключения.

Методы класса и всех его классов-наследников:

Методы Описание
Читает один байт из потока
Читает массив байт из потока
Читает все байты из потока
Пропускает байт в потоке (читает и выкидывает)
Проверяет, сколько байт еще осталось в потоке
Закрывает поток

Вкратце пройдемся по этим методам:

Метод

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

Метод

Это вторая модификация метода . Он позволяет считать из сразу массив байт. Массив для сохранения байт нужно передать в качестве параметра. Метод возвращает число — количество реально прочитанных байт.

Допустим у вас буфер на 10 килобайт, и вы читаете данные из файла с помощью класса . Если файл содержит всего 2 килобайта, все данные будут помещены в массив-буфер, а метод вернет число 2048 (2 килобайта).

Метод

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

Метод

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

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

Метод

Метод возвращает количество байт, которое еще осталось в потоке

Метод

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

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

Код Примечание
для чтения из файла для записи в файл
Буфер, в который будем считывать данные
Пока данные есть в потоке
Считываем данные в буфер
Записываем данные из буфера во второй поток

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

Еще один интересный момент — это переменная . Когда из файла будет читаться последний блок данных, легко может оказаться, что его длина меньше 64Кб. Поэтому в output нужно тоже записать не весь буфер, а только его часть: первые байт. Именно это и делается в методе .

Внешняя итерация

При работе с Java Collections мы используем внешнюю итерацию.

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

Следующий код вычисляет сумму квадратов всех нечетных чисел в списке.

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

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

Java

//nookery.ru
import java.util.Arrays;
import java.util.List;

public class Test {
public static void main(String[] args) {
List<Integer> num = Arrays.asList(1, 2, 3, 4, 5);
int result= 0;
for (int n : num) {
if (n % 2 == 1) {
int square = n * n;
result = result + square;
}
}
System.out.println(result);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

//nookery.ru

importjava.util.Arrays;

importjava.util.List;

publicclassTest{

publicstaticvoidmain(Stringargs){

List<Integer>num=Arrays.asList(1,2,3,4,5);

intresult=;

for(intnnum){

if(n%2==1){

intsquare=n*n;

result=result+square;

}

}

System.out.println(result);

}

}

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

Выбор между интерфейсом java.lang.Runnable и классом java.lang.Thread

Как было показано ранее, при необходимости обеспечить параллельное выполнение нескольких задач у программиста есть возможность выбрать, как именно реализовать эти задачи: с помощью класса Thread или интерфейса Runnable. У каждого подхода есть свои преимущества и недостатки.

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

Использование интерфейса Runnable по умолчанию лишено этого недостатка, но если реализовать задачу таким способом, то придется потратить дополнительные усилия на ее запуск. Как было показано в листинге 2, для запуска Runnable-задачи все равно потребуется объект Thread, также в этом случае исчезнет возможность прямого управления потоком из задачи. Хотя последнее ограничение можно обойти с помощью статических методов класса Thread (например, метод currentThread() возвращает ссылку на текущий поток).

Поэтому сделать однозначный вывод о превосходстве какого-либо подхода довольно сложно, и чаще всего в приложениях одновременно используются оба варианта, но для решения задач различной направленности. Считается, что наследование класса Thread следует применять только тогда, когда действительно необходимо создать «новый вид потока, который должен дополнить функциональность класса java.lang.Thread», и подобное решение применяется при разработке системного ПО, например, серверов приложений или инфраструктур. Использование интерфейса Runnable показано в случаях, когда просто «необходимо одновременно выполнить несколько задач» и не требуется вносить изменений в сам механизм многопоточности, поэтому в бизнес-ориентированных приложениях в основном используется вариант с интерфейсом Runnable.

Нововведения в Java 8: Функциональное программирование

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

Перед изучением функционального программирования в Java, рекомендуем хорошо разобраться в трех вещах:

  1. ООП, наследование и интерфейсы (1-2 уровни квеста Java Core).
  2. Дефолтная реализация методов в интерфейсе.
  3. Внутренние и анонимные классы.

Хорошая новость заключается в том, что без знания всего этого можно пользоваться многими возможностями функционального программирования в Java. Плохая новость — понять, как именно все устроено и как все работает, без тех же внутренних анонимных классов уже сложно.

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

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

Создание потока данных

Последнее обновление: 02.05.2018

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

  • : возвращается поток данных из коллекции

  • : возвращается параллельный поток данных из коллекции

Так, рассмотрим пример с ArrayList:

import java.util.stream.Stream;
import java.util.*;
public class Program {

    public static void main(String[] args) {
		
		ArrayList<String> cities = new ArrayList<String>();
        Collections.addAll(cities, "Париж", "Лондон", "Мадрид");
		cities.stream() // получаем поток
			.filter(s->s.length()==6) // применяем фильтрацию по длине строки
			.forEach(s->System.out.println(s)); // выводим отфильтрованные строки на консоль
	}
}

Здесь с помощью вызова получаем поток, который использует данные из списка cities. С помощью каждой промежуточной операции,
которая применяется к потоку, мы также можем получить поток с учетом модификаций. Например, мы можем изменить предыдущий пример следующим образом:

ArrayList<String> cities = new ArrayList<String>();
Collections.addAll(cities, "Париж", "Лондон", "Мадрид");

Stream<String> citiesStream = cities.stream(); // получаем поток
citiesStream = citiesStream.filter(s->s.length()==6); // применяем фильтрацию по длине строки
citiesStream.forEach(s->System.out.println(s)); // выводим отфильтрованные строки на консоль

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

citiesStream.forEach(s->System.out.println(s)); // терминальная операция употребляет поток
long number = citiesStream.count(); // здесь ошибка, так как поток уже употреблен
System.out.println(number);
citiesStream = citiesStream.filter(s->s.length()>5); // тоже нельзя, так как поток уже употреблен

Фактически жизненный цикл потока проходит следующие три стадии:

  1. Создание потока

  2. Применение к потоку ряда промежуточных операций

  3. Применение к потоку терминальной операции и получение результата

Кроме вышерассмотренных методов мы можем использовать еще ряд способов для создания потока данных. Один из таких способов представляет метод
Arrays.stream(T[] array), который создает поток данных из массива:

Stream<String> citiesStream = Arrays.stream(new String[]{"Париж", "Лондон", "Мадрид"}) ;
citiesStream.forEach(s->System.out.println(s)); // выводим все элементы массива

Для создания потоков IntStream, DoubleStream, LongStream можно использовать соответствующие перегруженные версии этого метода:

IntStream intStream = Arrays.stream(new int[]{1,2,4,5,7});
intStream.forEach(i->System.out.println(i));

LongStream longStream = Arrays.stream(new long[]{100,250,400,5843787,237});
longStream.forEach(l->System.out.println(l));

DoubleStream doubleStream = Arrays.stream(new double[] {3.4, 6.7, 9.5, 8.2345, 121});
doubleStream.forEach(d->System.out.println(d));

И еще один способ создания потока представляет статический метод of(T..values) класса Stream:

Stream<String> citiesStream =Stream.of("Париж", "Лондон", "Мадрид");
citiesStream.forEach(s->System.out.println(s));

// можно передать массив
String[] cities = {"Париж", "Лондон", "Мадрид"};
Stream<String> citiesStream2 =Stream.of(cities);
       
IntStream intStream = IntStream.of(1,2,4,5,7);
intStream.forEach(i->System.out.println(i));

LongStream longStream = LongStream.of(100,250,400,5843787,237);
longStream.forEach(l->System.out.println(l));

DoubleStream doubleStream = DoubleStream.of(3.4, 6.7, 9.5, 8.2345, 121);
doubleStream.forEach(d->System.out.println(d));

НазадВперед

Что такое Stream?

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

С чисто технической точки зрения поток-это типизированный интерфейс-поток T. Это означает, что поток может быть определен для любого вида объекта, потока чисел, потока символов, потока людей или даже потока города.

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

Поток не содержит никаких данных

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

Это очень важно иметь в виду и понимать

В потоке нет данных, однако есть данные, хранящиеся в коллекции

Collection — это структура, в которой хранятся ее данные. Поток существует только для того, чтобы обрабатывать данные и извлекать их из данного источника или перемещать в пункт назначения. Источником может быть коллекция, хотя это также может быть массив или ресурс ввода-вывода. Поток будет соединяться с источником, потреблять данные и каким-то образом обрабатывать элементы в нем.

Поток не должен изменять источник

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

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

Источник может быть неограниченным

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

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

Теперь мы можем знать точный размер этого текстового файла, но если мы не откроем его и не пройдем вручную по содержимому, мы никогда не узнаем, сколько в нем строк. Это то, что означает unbounded — мы не всегда можем заранее знать количество элементов, которые поток будет обрабатывать из источника.

Таковы три определения потока. Таким образом, из этих трех определений мы можем видеть, что stream на самом деле не имеет ничего общего с collection. Collection содержит свои данные.Collection может изменять данные, которые она содержит. И конечно, коллекция содержит известное и конечное количество данных.

Вывод

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

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

Оцените статью
Рейтинг автора
5
Материал подготовил
Илья Коршунов
Наш эксперт
Написано статей
134
Добавить комментарий