Многопоточность в java: суть, «плюсы» и частые ловушки

Как выбирать производителя твердотельных накопителей

Дополнительные материалы

Чтение

  • Блог Алексея Шипилёва — знаю, что очевидно, но просто грех не упомянуть
  • Блог Черемина Руслана — последнее время не пишет активно, нужно искать в блоге его старые записи, поверьте это стоит того — там кладезь
  • Хабр Глеба Смирнова — есть отличные статьи про многопоточность и модель памяти
  • Блог Романа Елизарова — заброшен, но археологические раскопки провести нужно. В целом Роман очень много сделал для просветления народа в области теории многопоточного программирования, ищите его в медиа.

Подкасты

  • SDCast #62: в гостях Александр Титов и Амир Аюпов, инженеры из Intel и Алексей Маркин, программист из МЦСТ
  • SDCast #63: в гостях Алексей Маркин, программист из МЦСТ
  • Разбор Полетов: #107 Истории альпинистов
  • Разбор Полетов: #154 Кишочки — Атака на Новый Год

Видео

  • Computer Science Center — Лекция 11. Модели памяти и проблемы видимости
  • Теория и практика многопоточного программирования

Ограничения классического подхода

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

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

Первым, что бросается в глаза, оказывается слияние низкоуровневого кода, отвечающего за многопоточное исполнение, и высокоуровневого кода, отвечающего за основную функциональность приложения (так называемый «спагетти-код»). В листинге 1 показано, что бизнес—код и поточный код вообще находятся в одном классе, но даже в более удачном варианте из листинга 2 для выполнения задачи все равно требуется создать объект Thread и запустить его. Подобное перемешивание снижает качество архитектуры приложения и может затруднить его последующее сопровождение.

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

  • поток занимает относительно много места в куче, так что после его завершения необходимо проследить, чтобы память, занимаемая им, была освобождена (например, присвоить ссылке на поток значение null);
  • для выполнения новой задачи потребуется запустить новый поток, что приведет к увеличению «накладных расходов» на виртуальную машину, так как запуск потока – это одна из самых требовательных к ресурсам операций.

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

Важно сказать, что «среднестатистический» программист будет отнюдь не первым, кто сталкивается с подобными проблемами в многозадачных приложениях. Все эти проблемы были давно проанализированы Java-сообществом и нашли решение в признанных шаблонах проектирования (например, ThreadPool (пул потоков) и WorkerThread (рабочий поток))

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

Запуск задач с помощью 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 находятся невыполненные задачи, то потоки будут остановлены только, когда завершится последняя задача

1 Потоки данных

Любая программа редко существует сама по себе. Обычно она как-то взаимодействует с «внешним миром». Это может быть считывание данных с клавиатуры, отправка сообщений, загрузка страниц из интернета или, наоборот, загрузка файлов на удалённый сервер.

Все эти вещи мы можем назвать одним словом — процесс обмена данными между программой и внешним миром. Хотя это уже не одно слово.

Сам процесс обмена данными можно разделить на два типа: получение данных и отправка данных. Например, вы считываете данные с клавиатуры с помощью объекта — это получение данных. И выводите данные на экран с помощью команды — это отправка данных.

Для описания процесса обмена данными в программировании используется термин поток. Откуда вообще взялось такое название?

В реальной жизни им может быть поток воды или поток людей (людской поток). В программировании же под потоком подразумевают поток данных.

Потоки — это универсальный инструмент. Они позволяют программе получать данные откуда угодно (входящие потоки) и отправляют данные куда угодно (исходящие потоки). Делятся на два вида:

  • Входящий поток (Input): используется для получения данных
  • Исходящий поток (Output): используется для отправки данных

Чтобы потоки можно было «потрогать руками», разработчики Java написали два класса: и .

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

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

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

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

Поэтому такие потоки еще называют байтовыми потоками.

Особенность потоков в том, что данные из них можно читать (писать) только последовательно. Вы не можете прочитать данные из середины потока, не прочитав все данные перед ними.

Именно так работает чтение с клавиатуры через класс : вы читаете данные с клавиатуры последовательно: строка за строкой. Прочитали строку, прочитали следующую строку, прочитали следующую строку и т.д. Поэтому метод чтения строки и называется (дословно — «следующая срока»).

Запись данных в поток тоже происходит последовательно. Хороший пример — вывод на экран. Вы выводите строку, за ней еще одну и еще одну. Это последовательный вывод. Вы не можете вывести 1-ю строку, затем 10-ю, а затем вторую. Все данные записываются в поток вывода только последовательно.

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

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

Java-программисты учли этот факт и написали еще два класса: и . Класс — это аналог класса , только его метод читает не байты, а символы — . Класс соответствует классу , и так же, как и класс , работает с символами (), а не байтами.

Если сравнить эти четыре класса, мы получим такую картину:

Байты (byte) Символы (char)
Чтение данных
Запись данных

Практическое применение

Сами классы , , и в явном виде никто не использует: они не присоединены ни к каким внешним объектам, из которых можно читать данные (или в которые можно писать данные). Однако у этих четырех классов много классов-наследников, которые умеют очень многое.

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

Последнее обновление: 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));

НазадВперед

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

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

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

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

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

Пример создания потока. Наследуем класс Thread

Мы можем наследовать класс  для создания собственного класса Thread и переопределить метод . Тогда мы можем создать экземпляр этого класса и вызвать метод  для того, чтобы выполнить метод .

Вот простой пример того, как наследоваться от класса Thread:

Java

package ua.com.prologistic;

public class MyThread extends Thread {

public MyThread(String name) {
super(name);
}

@Override
public void run() {
System.out.println(«Стартуем наш поток » + Thread.currentThread().getName());
try {
Thread.sleep(1000);
// для примера будем выполнять обработку базы данных
doDBProcessing();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(«Заканчиваем наш поток » + Thread.currentThread().getName());
}
// метод псевдообработки базы данных
private void doDBProcessing() throws InterruptedException {
Thread.sleep(5000);
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

packageua.com.prologistic;

publicclassMyThreadextendsThread{

publicMyThread(Stringname){

super(name);

}

@Override

publicvoidrun(){

System.out.println(«Стартуем наш поток «+Thread.currentThread().getName());

try{

Thread.sleep(1000);

// для примера будем выполнять обработку базы данных

doDBProcessing();

}catch(InterruptedExceptione){

e.printStackTrace();

}

System.out.println(«Заканчиваем наш поток «+Thread.currentThread().getName());

}

// метод псевдообработки базы данных

privatevoiddoDBProcessing()throwsInterruptedException{

Thread.sleep(5000);

}

}

Вот тестовая программа, показывающая наш поток в работе:

Java

package ua.com.prologistic;

public class ThreadRunExample {

public static void main(String[] args){
Thread t1 = new Thread(new HeavyWorkRunnable(), «t1»);
Thread t2 = new Thread(new HeavyWorkRunnable(), «t2»);
System.out.println(«Стартуем runnable потоки»);
t1.start();
t2.start();
System.out.println(«Runnable потоки в работе»);
Thread t3 = new MyThread(«t3»);
Thread t4 = new MyThread(«t4»);
System.out.println(«Стартуем наши кастомные потоки»);
t3.start();
t4.start();
System.out.println(«Кастомные потоки в работе»);

}
}

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

packageua.com.prologistic;

publicclassThreadRunExample{

publicstaticvoidmain(Stringargs){

Thread t1=newThread(newHeavyWorkRunnable(),»t1″);

Thread t2=newThread(newHeavyWorkRunnable(),»t2″);

System.out.println(«Стартуем runnable потоки»);

t1.start();

t2.start();

System.out.println(«Runnable потоки в работе»);

Thread t3=newMyThread(«t3»);

Thread t4=newMyThread(«t4»);

System.out.println(«Стартуем наши кастомные потоки»);

t3.start();

t4.start();

System.out.println(«Кастомные потоки в работе»);

}

}

Проблемы, которые решает многопоточность в Java

  1. Одновременно выполнять несколько действий.

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

    Можно привести и более «программистский» пример. Представь, что у тебя есть программа с пользовательским интерфейсом. При нажатии кнопки «Продолжить» внутри программы должны произойти какие-то вычисления, а пользователь должен увидеть следующий экран интерфейса. Если эти действия осуществляются последовательно, после нажатия кнопки «Продолжить» программа просто зависнет. Пользователь будет видеть все тот же экран с кнопкой «Продолжить», пока все внутренние вычисления не будут выполнены, и программа не дойдет до части, где начнется отрисовка интерфейса.

    Что ж, подождем пару минут!

    А еще мы можем переделать нашу программу, или, как говорят программисты, «распараллелить». Пусть нужные вычисления выполняются в одном потоке, а отрисовка интерфейса — в другом. У большинства компьютеров хватит на это ресурсов. В таком случае программа не будет «тупить», и пользователь будет спокойно переходить между экранами интерфейса не заботясь о том, что происходит внутри. Одно другому не мешает 🙂

  2. Ускорить вычисления.

    Тут все намного проще. Если наш процессор имеет несколько ядер, а большинство процессоров сейчас многоядерные, список наших задач могут параллельно решать несколько ядер. Очевидно, что если нам нужно решить 1000 задач и каждая из них решается за секунду, одно ядро справится со списком за 1000 секунд, два ядра — за 500 секунд, три — за 333 с небольшим секунды и так далее.

java.lang.Thread

I’m Thread! My name is Thread-2
I’m Thread! My name is Thread-1
I’m Thread! My name is Thread-0
I’m Thread! My name is Thread-3
I’m Thread! My name is Thread-6
I’m Thread! My name is Thread-7
I’m Thread! My name is Thread-4
I’m Thread! My name is Thread-5
I’m Thread! My name is Thread-9
I’m Thread! My name is Thread-8I’m Thread! My name is Thread-0
I’m Thread! My name is Thread-4
I’m Thread! My name is Thread-3
I’m Thread! My name is Thread-2
I’m Thread! My name is Thread-1
I’m Thread! My name is Thread-5
I’m Thread! My name is Thread-6
I’m Thread! My name is Thread-8
I’m Thread! My name is Thread-9
I’m Thread! My name is Thread-7I’m Thread! My name is Thread-0
I’m Thread! My name is Thread-3
I’m Thread! My name is Thread-1
I’m Thread! My name is Thread-2
I’m Thread! My name is Thread-6
I’m Thread! My name is Thread-4
I’m Thread! My name is Thread-9
I’m Thread! My name is Thread-5
I’m Thread! My name is Thread-7
I’m Thread! My name is Thread-8

Заключение

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

Похожие темы

  • Оригинал статьи: Thread pools and work queues.
  • Даг Ли, Параллельное программирование в Java: принципы дизайна и модели, второе издание — умело написанная книга о проблемных вопросах, связанных с многопоточным программированием в Java-приложениях.
  • Изучите пакет Дага Ли , который содержит множество полезных классов для построения эффективных параллельных приложений.
  • Формируется пакет на основе Java Community Process JSR 166, для включения в версию 1.5 JDK (комплект разработчика для Java).
  • Книга Аллена Холуб Укрощение потоков Java — интересное знакомство с проблемами программирования Java-потоков.
  • Конечно, есть некоторые проблемы в Java Thread API; прочитайте, что сделал бы Аллен Холуб, если бы был королем (developerWorks, октябрь 2000 г.)
  • Алекс Ройтер предлагает некоторые рекомендации для написания классов с многопоточной поддержкой (developerWorks, февраль 2001 г.)
  • Другие ресурсы Java можно найти в разделе технологий Java developerWorks.

Заключение

В этой статье были описаны приемы для параллельного запуска задач в JSE-приложениях с помощью класса Thread и интерфейса Runnable или пакета java.util.concurrent. Несмотря на простоту и известность первого способа, у него есть несколько недостатков, которые становятся заметны по мере «взросления» проекта или программиста, поэтому при разработке новых приложений стоит сразу использовать возможности java.uti.concurrent.

Этот пакет доступен для использования, начиная с версии Java 5, и содержит в себе готовые реализации известных шаблонов проектирования WorkerThread и ThreadPool, а также классы, устраняющие другие недостатки классической модели многопоточного программирования. Поэтому дополнительное время, затраченное на изучение пакета java.util.concurrent, приведет к сокращению затрат на разработку следующих проектов и написанию более качественного кода.

2 Потоки ввода-вывода: цепочки потоков

Помните, когда-то вы изучали потоки ввода-вывода: , , , и т.п.?

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

Эти потоки можно было организовывать в цепочки обработки данных. Например, так:

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

И только кода мы вызовем метод , произойдет следующее:

  1. Объект вызовет метод у объекта
  2. Объект вызовет метод у объекта
  3. Объект начнет читать данные из файла

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

Коллекции и потоки

Начиная с Java 8, появилась возможность получить поток для чтения данных у коллекций (и не только у них). Но и это еще не самое интересное. На самом деле появилась возможность легко и просто конструировать сложные цепочки потоков данных, при этом тот код, который раньше требовал 5-10 строк, теперь можно было записать в 1-2 строки.

Пример — находим строку максимальной длины в списке строк:

Поиск строки максимальной длины
Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector