Статьи

Конечные генераторы последовательностей в Java 8

… И введение методов по умолчанию.

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

  • Более старый способ Java 7 с классом, подобным итератору
  • Использование метода итерации Stream
  • Используя метод генерирования Stream

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

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

  • Мы не можем решить это или трудно
  • Есть случайный элемент
  • Мы хотим отделить генерирующую функцию от потока, потребляющего

Мы увидели простую конечную последовательность с нашим Hello World! пример в первой статье. В этом случае мы не генерировали последовательность с помощью функции, мы вместо этого обрабатывали предварительно инициализированный список. Мы также видели, что можем использовать итератор, когда обсуждали бесконечные последовательности, но обсуждали и другие способы. Мы увидим, что в этом случае мы фактически вынуждены в решение Итератор.

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

public class SixGame
{
public static class DieThrowIterator implements Iterator<Integer>
{
int dieThrow = 0;
Random rand = new Random(System.nanoTime());
@Override
public boolean hasNext()
{
return dieThrow != 6;
}
@Override
public Integer next()
{
dieThrow = Math.abs(rand.nextInt()) % 6 + 1;
return dieThrow;
}
}
public static void main(String args[])
{
DieThrowIterator dieThrowIterator = new DieThrowIterator();
while (dieThrowIterator.hasNext())
{
System.out.println("You threw a " + dieThrowIterator
.next());
}
}
}

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

Давайте попробуем использовать поток. Мы не можем использовать функцию итерации Stream, потому что она не допускает условие остановки. Также IntSupplier с generate не является опцией, если мы не хотим использовать limit (1) и создавать поток для каждого броска броска, когда мы могли бы также не использовать поток вообще.

Если мы посмотрим изнутри на реализацию функции генерирования IntStream, то увидим, что она создает InfiniteSupplyingSpliterator. Это проблема для нас — функция tryAdvance всегда возвращает истинное значение, которое мы никогда не сможем остановить.

Мы могли бы реализовать наш собственный Spliterator, где наш tryAdvance проверяет состояние остановки. Мы не можем просто расширить InfiniteSupplyingSpliterator и переопределить метод tryAdvance, поскольку это внутренний класс класса доступа по умолчанию. Таким образом, единственный способ — это копирование, а не наследование. Я очень нервничаю по поводу копирования больших частей кода, которые могут измениться в будущих версиях; это говорит мне, что это не то, что было задумано. Мы должны искать другие пути в первую очередь.

Давайте посмотрим, как List выполняет потоковую передачу. Потоковая передача списка происходит от наследования Iterable. Чтобы создать Iterable, нам нужно только реализовать его функцию iterator () — для примера давайте сделаем это как внутренний класс:

public static class DieThrowIterable implements Iterable<Integer>
{
@Override
public Iterator<Integer> iterator()
{
return new DieThrowIterator();
}
}

Мы можем тогда течь:

public static void main(String args[])
{
Stream<Integer> stream = StreamSupport.stream(
new DieThrowIterable().spliterator(), false);
stream.map(i -> "You threw a " + i).forEach(System.out::println);
}

Подождите немного … Iterable — это интерфейс, но он имеет метод spliterator (). Как это может быть?

Если мы посмотрим на интерфейс, то действительно существует метод spliterator (), создающий для нас новый сплитератор. Если мы тоже посмотрим внимательно, мы увидим ключевое слово по умолчанию. Это было добавлено в Java 8 (по умолчанию это уже зарезервированное слово для операторов switch, поэтому оно не сломает старый код). Когда мы пишем интерфейсы, мы можем предоставить уже реализованные методы по умолчанию. Теперь некоторые люди могут иметь сомнения по этому поводу, поскольку он превращает интерфейс в фактически абстрактный класс. Есть несколько веских причин и преимуществ, которые это дает нам:

  • Это избавляет от необходимости создавать абстрактные классы для реализации интерфейсов для предоставления методов по умолчанию. Таким образом, меньше написано, что также мешает потребителю API реализовать интерфейс, когда мы ожидали, что вместо него будет использоваться абстрактный класс.
  • Это помогает с множественными проблемами наследования (наследование от двух или более классов, которые поддерживает C ++, но не Java). В Java проблемы с множественным наследованием были исключены благодаря разрешению только одного наследования от классов, но мы можем реализовать столько интерфейсов, сколько необходимо. Проблема в том, что мы должны были реализовать большие части этих интерфейсов в каждом классе, который их использовал — настоящая боль.
  • Наши поставщики API теперь также могут добавлять новые методы в интерфейсы, не нарушая старый код. Это одна из причин, по которой мы видим здесь метод по умолчанию. Новый интерфейс сделает JDK еще больше и усложнит работу.
  • Лямбда-выражения связываются с интерфейсами с помощью одного метода, оставшегося для реализации. Если бы было больше, мы не смогли бы сделать это связывание. Мы рассмотрим это в следующей статье.

Примечание. Если мы реализуем более одного интерфейса с идентичным методом по умолчанию и не переопределяем его, реализуя в классе или родительском классе, это ошибка. Будет ли когда-нибудь возможно использовать черты типа Scala-mix-in-in — интересный вопрос.

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

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