diff --git a/lessons/java-core/051/Spliterator.md b/lessons/java-core/051/Spliterator.md new file mode 100644 index 0000000..1b3e7a1 --- /dev/null +++ b/lessons/java-core/051/Spliterator.md @@ -0,0 +1,231 @@ +# Iterable, Iterator и Spliterator + +В рамках сегодняшнего урока поговорим о том, откуда берутся Stream’ы. + +Начнем с интересного факта: `Stream`, в отличии от `Optional`, не хранит обрабатываемые элементы внутри себя. Он лишь +обрабатывает данные, которые поставляются из другого источника информации - массива, коллекции, InputStream’а и пр. + +Для того, чтобы понять всю эволюционную цепочку, нырнем в практически забытого предка `Collection` - `Iterable` и +посмотрим, какие методы он нам предоставляет. Возвращаемый тип одного из них является той основой, на которой строится +Stream API. + +## Интерфейс Iterable + +Сам интерфейс, как понятно из названия, характеризует его наследников как “итерируемых”, т.е. обрабатываемых итерациями. + +Интерфейс `Iterable` предоставляет 3 метода: + +1. `void forEach()`. С ним мы уже знакомы, он представляет собой аналог цикла `foreach`: выполнит переданную параметром + лямбду для каждого из элементов `Iterable`; +2. `Iterator iterator()`. Возвращает объект типа `Iterator`, позволяющий обрабатывать элементы, содержащимися в + наследнике `Iterable`, в определенном (реализацией итератора) порядке. Подробнее мы рассмотрим интерфейс `Iterator` в + следующем пункте статьи; +3. `Spliterator spliterator()`. Метод, возвращающий объект типа `Spliterator`. Он же `Iterator` на стероидах, он же + гвоздь сегодняшней программы – основа `Stream`. Ему будет посвящен отдельный пункт ниже. + +Важно понимать, что любой класс, имплементирующий `Iterable` декларирует существование итератора и сплитератора у такого +класса. А любой сплитератор может быть обработан как `Stream`. В более широком смысле – любой объект, чей итератор мы +можем получить, можно обработать как `Stream`, ведь любой итератор может быть превращен в сплитератор (возможно, +топорный и не самый эффективный, но все же). + +Отсюда следует, что Stream’ом можно обработать не только коллекцию, а объект любого класса, имплементирующий `Iterable` +или `Iterator`. К таким, например, относятся `Scanner`, что, по сути, означает возможность обработать Stream’ом +любой `InputStream` и некоторые другие классы, связанные с IO (и NIO). + +Кроме того, некоторые классы не имплементируют `Iterable`, но имеют методы, возвращающие уже готовый `Stream`. К таким +относятся, например, `BufferedReader`, знакомый нам по теме I/O Stream. Или `Optional`, знакомый по последним урокам. +Коллекции (кроме `Map`) тоже имеют метод `stream()`, но у них он не так важен – это лишь обертка над сплитератором, +т.е. `Stream` на основе коллекций можно создать и без данного метода. + +## Iterator + +Однако вернемся к основам. К временам, когда о ФП и Stream API еще никто не думал, но обрабатывать элементы по порядку +уже хотелось, а `foreach` не всегда давал нужную функциональность. + +> До конца не уверен, но думаю, что `foreach` для циклов использует именно итератор. + +Кроме того, не всегда заранее известен размер обрабатываемого массива данных. Скажем, при реализации итератора +у `Scanner`, работающего на базе `System.in`, мы понятия не имеем, сколько вводов совершит пользователь. А значит не +знаем, сколько элементов будет обработано. + +Итератор выступил отличным решением – ведь он позволяет производить обход элементов, абстрагируясь от способа хранения +этих элементов. Используя итератор, мы можем обработать элементы списка, дерева, InputStream’а или другого объекта, +предоставляющего доступ к массиву данных одного типа. + +К слову, итератор является паттерном проектирования. Подробнее можно почитать +[здесь](https://refactoring.guru/ru/design-patterns/iterator) (сайт не доступен из РФ без VPN). + +Вернемся к Java. `Iterator` является интерфейсом, реализации которого можно найти в качестве вложенных классов +некоторых коллекций, в классе `Scanner` и ряде других. Рассмотрим, какие методы у него есть, и какие из них требуют +явного переопределения – ведь часть имеет дефолтную реализацию: + +- `boolean hasNext()`. Возвращает `true`, если следующий элемент существует; +- `E next()`. Возвращает объект типа, которым параметризован `Iterator`. Если объекта нет – бросает + `NoSuchElementException`. Не советую вызывать данный метод, предварительно не проверив наличие объекта с помощью + `hasNext()`; +- `void remove()`. Удаляет текущий элемент (полученный с помощью последнего вызова `next()`) из источника данных + (например, из коллекции). Может быть вызван не более одного раза после `next()`. Имеет реализацию по умолчанию, + которая бросает исключение `UnsupportedOperationException`. Таким образом, не каждый итератор поддерживает удаление + элементов; +- `void forEachRemaining()`. Исполняет лямбду, переданную параметром, для каждого элемента, не обработанного через + данный объект итератора ранее. По сути, равноценен `forEach()` у `Iterable`, если ни разу не был вызван `next()`. + Реализован по умолчанию. + +На данном этапе, предлагаю завершить знакомство с итератором. Он имеет ряд нюансов, касающихся некоторых имплементаций, +а также изменения источника данных, обрабатываемых итератором. Но это не существенно на данном этапе. Впрочем, в рамках +курса тоже несущественно. Полагаю, каждый из вас быстро поймет, что я имел ввиду, если столкнется на практике:) + +## Spliterator + +`Spliterator`, в свою очередь, является следующим шагом в эволюции итератора – он позволяет строить более гибкую работу +с учетом характеристик заданного итератора, а также умеет делиться – из одного сплитератора можно получить два, на чем, +например, строится возможность параллельной (многопоточной) обработки данных Stream’ами. Но об этом поговорим в свое +время. + +С особенностями `Spliterator` можно познакомиться в [статье](https://habr.com/ru/post/256905/). +Она посвящена реализации собственной имплементации Spliterator’а и требует определенного багажа знаний, но все равно +достаточно подробно рассказывает о возможностях и роли данного интерфейса в Stream API. Рекомендую обратиться к ней вне +зависимости от того, насколько вам будут понятны мои объяснения ниже. Полагаю, статья по ссылке будет интересна даже +тем, кто уже был знаком с данным интерфейсом ранее. + +Данный пункт будет разбит на две логические части: знакомство с методами `Spliterator` и знакомство с характеристиками, +которые позволяют обрабатывать каждый сплитератор наиболее оптимально. + +Начнем с характеристик. Найти их можно в исходниках `Spliterator`. Каждая из характеристик – константа типа `int`. +Характеристик всего всего 8: + +- _ORDERED_. Указывает на то, что элементы данного сплитератора должны обрабатываться в заданном порядке. + +> Такая характеристика актуальна для списков – порядок элементов определен их индексами. +> Или для `LinkedHashSet` – порядок соответствует порядку добавления элементов. +> Или для `BufferedReader` – там порядок элементов, полагаю, очевиден. +> А для `HashSet` данная характеристика не актуальна – коллекция не гарантирует какого-то определенного порядка +> элементов; + +- _DISTINCT_. Указывает на то, что все элементы сплитератора уникальны. Уникальность элементов определяется + по `equals()`. + +> Это актуально для сплитераторов на базе Set’ов. Такая же характеристика появляется у сплитератора, лежащего внутри +> `Stream` при вызове метода `distinct()`; + +- _SORTED_. Указывает на то, что элементы сплитератора отсортированы и будут обработаны в порядке, определенном + Comparator’ом. Пример класса, имеющего сплитератор с такой характеристикой – `TreeSet`; +- _SIZED_. Указывает на то, что `Spliterator` знает, сколько элементов будет обработано при полном обходе; + +> Характерно для сплитераторов большинства коллекций. Но помните, что сплитератор умеет делиться (об этом будет чуть +> позже). +> В ряде случаев, скажем, для сплитераторов на базе **списков**, это не критично. Что основной сплитератор, что его +> «дочерние» сплитераторы будут знать свой размер. +> А, например, в случае с **HashSet** нет гарантии разделения сплитератора на два равных, а значит, такие «дочерние» +> сплитераторы уже не будут знать, сколько элементов должны обработать, следовательно, не будут обладать подобной +> характеристикой. + +- _NONNULL_. Характеристика, гарантирующая, что элемент, обрабатываемый сплитератором не может быть равен `null`. + Характерно для ряда коллекций, в основном, из `java.util.concurrent`; +- _IMMUTABLE_. Указывает на то, что состав источника данных не можем быть изменен во время обработки сплитератором. Т.е. + данные не будут добавлены или удалены, элементы не будут заменены другими; + +> Из простых примеров, наверно, только сплитератор потокобезопасного списка в `java.util.concurrent` – +> `CopyOnWriteArrayList`. Вообще, примеры таких сплитераторов можно найти и в `Scanner`, и в `String`, но описание +> ситуаций, в которых они применяются, будет слишком многословным. + +- _CONCURRENT_. Характеристика, указывающая, что состав источника данных может быть безопасно изменен при обработке + сплитератором – реализация сплитератора для такого источника данной характеристикой декларирует умение обрабатывать + такие ситуации. + +> По сути, противоположен _IMMUTABLE_. Также исключает использование _SIZED_ для верхнеуровневых (не рожденных +> делением другого сплитератора) сплитераторов. В целом, логично, что сплитератор не может знать свой точный размер, +> если элементы могут добавиться или удалиться в любой момент. Такая характеристика актуальна для сплитераторов многих +> потокобезопасных коллекций в `java.util.concurrent`; + +- _SUBSIZED_. Указывает на то, что дочерние сплитераторы от данного будут _SIZED_. + +> На самом деле, немного интереснее – в зависимости от ситуации может гарантироваться также и характеристика _SUBSIZED_ +> у дочернего сплитератора. Вы можете разобраться самостоятельно, если интересно. На текущем этапе эта информация +> кажется мне избыточной; + +Теоретически, можно добавить сплитератору собственные характеристики. Но разработчики языка не рекомендуют это делать, +поскольку набор характеристик может быть изменен по мере развития Java. И тогда особенности хранения характеристик в +сплитераторе могут привести к некорректной обработке ваших кастомных характеристик. + +Перейдем к обзору методов интерфейса `Spliterator`. Именно они реализуют те возможности и особенности, которые +декларируют рассмотренные выше характеристики: + +- `boolean tryAdvance()`. Применит лямбду, переданную параметром, к следующему элементу сплитератора, если он + существует, и вернет `true`. Если элементов не осталось – вернет `false`; + +> Помните, что элементов может не быть именно сейчас – например, пользователь не произвел следующий ввод. В таком +> случае будет возвращен `false`. Но при этом элемент может добавиться впоследствии. + +- `void forEachRemaining()`. В целом, метод аналогичен таковому у `Iterator`; +- `Spliterator trySplit()`. Разделяет сплитератор на двое, если это возможно; + +> В идеале – пополам, если деление пополам невозможно – в зависимости от реализации. +> Проблемы могут быть как с определением равных половин (скажем, у `HashSet`), так и с тем, что не всегда определено +> число элементов в сплитераторе – например, если сплитератор создан на базе Scanner’а. +> В общем виде, поведение метода заключается в том, чтобы обработку части элементов делегировать новому сплитератору, +> а оставшуюся часть обработать текущим (у которого и был вызван `trySplit()`). + +- `long estimateSize()`. Возвращает точное или приблизительное число элементов, которые будут обработаны сплитератором, + если прямо сейчас вызвать у него `forEachRemaining()`. Если число элементов неизвестно или вычислять их количество + трудозатратно – вернет `Long.MAX_VALUE`; +- `long getExactSizeIfKnown()`. Возвращает результат `estimateSize()`, если сплитератор имеет характеристику _SIZED_. + В противном случае вернет -1; +- `int characteristics()`. Возвращает характеристики сплитератора в виде **битовой маски**. + +> Не буду углубляться в особенности реализации, скажу лишь, что такая реализация является причиной, по которой +> нежелательно добавление кастомных характеристик в сплитератор. Результат метода не очевиден новичку, но достаточен +> для обработки классам, использующим его. Например, имплементациям интерфейса `Stream`. + +- `boolean hasCharacteristics()`. Возвращает `true`, если переданная параметром характеристика (в виде int’овой + константы) актуальна для данного сплитератора. Иначе – `false`; +- `Comparator getComparator()`. Возвращает компаратор, по которому были отсортированы значения, если + сплитератор имеет характеристику _SORTED_. В противном случае бросит `IllegalStateException`. + +На этом мы завершаем рассмотрение методов интерфейса `Spliterator` и знакомство с ним. + +## В качестве заключения + +Сплитератор является достаточно низкоуровневым интерфейсом, обеспечивающим работу Stream API. Как при +обработке `Optional` (почти) каждая новая промежуточная операция возвращает новый объект `Optional` с новым объектом в +поле `value`, так и каждая новая промежуточная операция при обработке `Stream` возвращает новый объект `Stream` c новым +сплитератором внутри. + +Я сомневаюсь, что большинству из вас придется работать с интерфейсом `Spliterator` напрямую, не считая создание +экземпляров данного типа. Тем более, вряд ли вам придется писать собственную имплементацию для этого интерфейса. Однако +понимание, хотя бы на уровне данной статьи, особенностей его работы, поможет понять то, как работает Stream API и, что +важнее, позволит понять суть классификации методов Stream, что даст возможность использовать их эффективно. + +На самом деле, для понимания указанных особенностей, хватило бы и описания характеристик сплитераторов, без разбора их +методов. Поэтому нет ничего страшного, если они не отложатся в памяти. Но запас карман не тянет, по крайней мере, вы +всегда сможете вернуться к их описанию, если они вам понадобятся. + +> Нет повести печальнее на свете, чем Java-разработчик, использующий Stream наобум + +Не становитесь таким разработчиком:) + +#### С теорией на сегодня все! + +![img.png](../../../commonmedia/defaultFooter.jpg) + +Переходим к практике + +Урок теоретический. Однако желающим предлагаю реализовать `Stream` на основе `System.in`. Операции в самом Stream’е +можете описать на свой вкус. Главное, разберитесь, когда будет запускаться обработка элементов и в каком порядке она +будет происходить. + +Подсказки: + +1. Самая простая реализация возможна через `BufferedReader` – он содержит метод `stream()`; +2. Более интересная реализация возможна через `Scanner`. Рекомендую пойти этим путем, он позволит познакомиться с новыми + для вас классами, которые могут быть полезны в дальнейшем. Если не удалось самостоятельно – см. следующую подсказку; +3. Сплитератор на базе итератора можно создать с помощью методов класса `Spliterators`. `Stream` на базе сплитератора – + с помощью методов класса `StreamSupport`. + +> Если что-то непонятно или не получается – welcome в комменты к посту или в лс:) +> +> Канал: https://t.me/ViamSupervadetVadens +> +> Мой тг: https://t.me/ironicMotherfucker +> +> **Дорогу осилит идущий!** \ No newline at end of file