Лень - это добродетель. Иногда хочется, чтобы это повторялось.

В программировании лень может быть очень хорошей вещью. Ленивая инициализация позволяет нам избежать создания дорогостоящих ресурсов до тех пор, пока они не понадобятся. Ленивая итерация позволяет нам избежать создания временных структур данных и потенциально уменьшить общий объем работы, необходимой для выполнения вычислений. В Java до Java 8 существовали две встроенные парадигмы для ленивых итераций. Это Iterable и Iterator. Iterable можно использовать снова и снова, так как он может продолжать давать вам совершенно новый Iterator. Iterator можно использовать только один раз, так как невозможно сбросить его, когда вы перешли к последнему элементу с помощью next ().

В Java 8 потоки были добавлены с помощью ленивых методов (например, map, filter и т. Д.). Stream похож на Iterator в том смысле, что его можно использовать только один раз с операцией терминала, такой как forEach или collect. Это означает, что вы должны быть осторожны, чтобы не исчерпать Stream, а затем попытаться использовать его снова.

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

java.lang.IllegalStateException: stream has already been operated upon or closed

Я собираюсь использовать тест getAgeStatisticsOfPets в Упражнении 4 из Eclipse Collections Pet Kata, чтобы проиллюстрировать, как вы можете работать с Stream, не получая IllegalStateException. Я также покажу вам несколько других альтернатив, которые ленивы использовать Eclipse Collections.

Во-первых, вот код, который я хотел бы написать для теста в упражнении 4 Pet Kata. Я использую IntStream (полученный через mapToInt), чтобы избежать упаковки int как Integer. Этот код компилируется, но при выполнении завершается с ошибкой.

@Test
public void getAgeStatisticsOfPets()
{
    IntStream petAges = this.people
            .stream()
            .flatMap(person -> person.getPets().stream())
            .mapToInt(Pet::getAge);
    Set<Integer> uniqueAges =
        petAges.boxed().collect(Collectors.toSet());
    IntSummaryStatistics stats = petAges.summaryStatistics();
    Assert.assertEquals(Sets.mutable.with(1, 2, 3, 4), uniqueAges);
    Assert.assertEquals(stats.getMin(), petAges.min().getAsInt());
    Assert.assertEquals(stats.getMax(), petAges.max().getAsInt());
    Assert.assertEquals(stats.getSum(), petAges.sum());
    Assert.assertEquals(stats.getAverage(),
                        petAges.average().getAsDouble(), 0.0);
    Assert.assertEquals(stats.getCount(), petAges.count());
    Assert.assertTrue(petAges.allMatch(i -> i > 0));
    Assert.assertFalse(petAges.anyMatch(i -> i == 0));
    Assert.assertTrue(petAges.noneMatch(i -> i < 0));
}

Код будет выполняться до тех пор, пока эта строка не попытается выполнить.

IntSummaryStatistics stats = petAges.summaryStatistics();

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

Один из вариантов, которым я должен заставить код работать, - это предварительно рассчитать домашних животных как сплющенных List, а затем воссоздать IntStream для тех возрастов, когда они мне нужны.

@Test
public void getAgeStatisticsOfPets()
{
    List<Pet> petAges = this.people
            .stream()
            .flatMap(person -> person.getPets().stream())
            .collect(Collectors.toList());
    Set<Integer> uniqueAges =
            petAges.stream()
                    .mapToInt(Pet::getAge)
                    .boxed()
                    .collect(Collectors.toSet());
    IntSummaryStatistics stats =
            petAges.stream()
                    .mapToInt(Pet::getAge)
                    .summaryStatistics();
    Assert.assertEquals(Sets.mutable.with(1, 2, 3, 4), uniqueAges);
    Assert.assertEquals(stats.getMin(), petAges.stream()
            .mapToInt(Pet::getAge).min().getAsInt());
    Assert.assertEquals(stats.getMax(), petAges.stream()
            .mapToInt(Pet::getAge).max().getAsInt());
    Assert.assertEquals(stats.getSum(), petAges.stream()
            .mapToInt(Pet::getAge).sum());
    Assert.assertEquals(stats.getAverage(), petAges.stream()
                    .mapToInt(Pet::getAge).average().getAsDouble(),
            0.0);
    Assert.assertEquals(stats.getCount(), petAges.size());
    Assert.assertTrue(
            petAges.stream()
                    .mapToInt(Pet::getAge)
                    .allMatch(i -> i > 0));
    Assert.assertFalse(
            petAges.stream()
                    .mapToInt(Pet::getAge)
                    .anyMatch(i -> i == 0));
    Assert.assertTrue(
            petAges.stream()
                    .mapToInt(Pet::getAge)
                    .noneMatch(i -> i < 0));
}

Это работает, но мне пришлось написать много повторяющегося кода. Мне приходится вызывать этот код снова и снова, чтобы воссоздать 25 возрастов питомцев.

petAges.stream().mapToInt(Pet::getAge)

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

@Test
public void getAgeStatisticsOfPets()
{
    List<Pet> pets = this.people
            .stream()
            .flatMap(person -> person.getPets().stream())
            .collect(Collectors.toList());
    Supplier<IntStream> petAges =
            () -> pets.stream().mapToInt(Pet::getAge);
    Set<Integer> uniqueAges =
            petAges.get().boxed().collect(Collectors.toSet());
    IntSummaryStatistics stats =
            petAges.get().summaryStatistics();
    Assert.assertEquals(Sets.mutable.with(1, 2, 3, 4), uniqueAges);
    Assert.assertEquals(stats.getMin(), 
            petAges.get().min().getAsInt());
    Assert.assertEquals(stats.getMax(), 
            petAges.get().max().getAsInt());
    Assert.assertEquals(stats.getSum(), 
            petAges.get().sum());
    Assert.assertEquals(stats.getAverage(), 
            petAges.get().average().getAsDouble(),
            0.0);
    Assert.assertEquals(stats.getCount(), 
            petAges.get().count());
    Assert.assertTrue(petAges.get().allMatch(i -> i > 0));
    Assert.assertFalse(petAges.get().anyMatch(i -> i == 0));
    Assert.assertTrue(petAges.get().noneMatch(i -> i < 0));
}

Это уменьшает количество повторяющегося кода, который мне приходилось писать. Я могу пойти еще дальше и сделать так, чтобы flatCollect не собирались в List, если Supplier сделает больше работы.

@Test
public void getAgeStatisticsOfPets()
{
    Supplier<IntStream> petAges =
            () -> this.people
                    .stream()
                    .flatMap(person -> person.getPets().stream())
                    .mapToInt(Pet::getAge);
    Set<Integer> uniqueAges =
            petAges.get().boxed().collect(Collectors.toSet());
    IntSummaryStatistics stats =
            petAges.get().summaryStatistics();
    Assert.assertEquals(Sets.mutable.with(1, 2, 3, 4), uniqueAges);
    Assert.assertEquals(stats.getMin(),
            petAges.get().min().getAsInt());
    Assert.assertEquals(stats.getMax(),
            petAges.get().max().getAsInt());
    Assert.assertEquals(stats.getSum(),
            petAges.get().sum());
    Assert.assertEquals(stats.getAverage(),
            petAges.get().average().getAsDouble(),
            0.0);
    Assert.assertEquals(stats.getCount(),
            petAges.get().count());
    Assert.assertTrue(petAges.get().allMatch(i -> i > 0));
    Assert.assertFalse(petAges.get().anyMatch(i -> i == 0));
    Assert.assertTrue(petAges.get().noneMatch(i -> i < 0));
}

Это почти похоже на создание ленивого Iterable, где каждый раз, когда нам нужно что-то сделать, мы создаем Iterator для выполнения дополнительной функции. В Коллекциях Eclipse есть тип LazyIterable, который может быть создан из любого RichIterable. LazyIterable можно безопасно использовать сколько угодно раз. Пересчет функций снова и снова может оказаться дорогостоящим, но это позволит вам сделать это и не будет исчерпано после первого использования.

Ниже показано, как можно решить эту проблему, используя LazyIntIterable с коллекциями Eclipse.

@Test
public void getAgeStatisticsOfPets()
{
    LazyIntIterable petAges = this.people
            .asLazy()
            .flatCollect(Person::getPets)
            .collectInt(Pet::getAge);
    IntSet uniqueAges = petAges.toSet();
    IntSummaryStatistics stats = petAges.summaryStatistics();
    Assert.assertEquals(
            IntSets.mutable.with(1, 2, 3, 4),
            uniqueAges);
    Assert.assertEquals(stats.getMin(), petAges.min());
    Assert.assertEquals(stats.getMax(), petAges.max());
    Assert.assertEquals(stats.getSum(), petAges.sum());
    Assert.assertEquals(stats.getAverage(), petAges.average(), 0.0);
    Assert.assertEquals(stats.getCount(), petAges.size());
    Assert.assertTrue(petAges.allSatisfy(i -> i > 0));
    Assert.assertFalse(petAges.anySatisfy(i -> i == 0));
    Assert.assertTrue(petAges.noneSatisfy(i -> i < 0));
}

Когда у меня есть LazyIntIterable, мне не нужно упаковывать уникальный возраст в Set из Integer. Вместо этого я могу сохранить их в IntSet, как указано выше, просто позвонив toSet() на LazyIntIterable.

Поскольку LazyIntIterable является ленивым, он не рассчитывает и не сохраняет возраст питомца. Он должен выполнять flatCollect() и collectInt() каждый раз, когда вы вызываете метод терминала, например toSet, summaryStatistics, min, max, sum, average, size, _54 _ / _ 55 _ / _ 56_. Если я хочу, чтобы код был более эффективным, я могу предварительно рассчитать возраст питомца и сохранить его в IntList или IntBag. Я буду использовать здесь IntBag, так как возраст повторяется, но порядок не имеет значения.

@Test
public void getAgeStatisticsOfPets()
{
    IntBag petAges = this.people
            .asLazy()
            .flatCollect(Person::getPets)
            .collectInt(Pet::getAge)
            .toBag();
    IntSet uniqueAges = petAges.toSet();
    IntSummaryStatistics stats = petAges.summaryStatistics();
    Assert.assertEquals(
            IntSets.mutable.with(1, 2, 3, 4),
            uniqueAges);
    Assert.assertEquals(stats.getMin(), petAges.min());
    Assert.assertEquals(stats.getMax(), petAges.max());
    Assert.assertEquals(stats.getSum(), petAges.sum());
    Assert.assertEquals(stats.getAverage(), petAges.average(), 0.0);
    Assert.assertEquals(stats.getCount(), petAges.size());
    Assert.assertTrue(petAges.allSatisfy(i -> i > 0));
    Assert.assertFalse(petAges.anySatisfy(i -> i == 0));
    Assert.assertTrue(petAges.noneSatisfy(i -> i < 0));
}

Все, что мне нужно было изменить в коде, чтобы заставить эту работу, - это вызвать метод toBag() после вызова collectInt() и изменить тип petAges с LazyIntIterable на IntBag. Никакой другой код менять не нужно. Это потому, что наши примитивные коллекции и примитивные ленивые итерации в Eclipse Collections обладают хорошей симметрией. Обратите внимание, что ни в решении LazyIntIterable, ни в IntBag нет упаковки объектов int в Integer.

Я легко могу изменить тип с IntBag на IntList, просто изменив вызов метода toBag() на toList().

@Test
public void getAgeStatisticsOfPets()
{
    IntList petAges = this.people.asLazy()
            .flatCollect(Person::getPets)
            .collectInt(Pet::getAge)
            .toList();
    IntSet uniqueAges = petAges.toSet();
    IntSummaryStatistics stats = petAges.summaryStatistics();
    Assert.assertEquals(
            IntSets.mutable.with(1, 2, 3, 4),
            uniqueAges);
    Assert.assertEquals(stats.getMin(), petAges.min());
    Assert.assertEquals(stats.getMax(), petAges.max());
    Assert.assertEquals(stats.getSum(), petAges.sum());
    Assert.assertEquals(stats.getAverage(), petAges.average(), 0.0);
    Assert.assertEquals(stats.getCount(), petAges.size());
    Assert.assertTrue(petAges.allSatisfy(i -> i > 0));
    Assert.assertFalse(petAges.anySatisfy(i -> i == 0));
    Assert.assertTrue(petAges.noneSatisfy(i -> i < 0));
}

Опять же, больше ничего менять не нужно.

Когда вы звоните min, max и average на IntStream, вы получите OptionalInt или OptionalDouble. Это хорошо, если у вас есть потенциал получить пустой результат. OptionalInt и OptionalDouble позволят вам обрабатывать случаи, когда результат пуст. В коллекциях Eclipse для этих трех методов есть другой вариант, помогающий в случае, когда Iterable или Collection пусто.

Assert.assertEquals(stats.getMin(), petAges.minIfEmpty(0));
Assert.assertEquals(stats.getMax(), petAges.maxIfEmpty(0));
Assert.assertEquals(stats.getSum(), petAges.sum());
Assert.assertEquals(stats.getAverage(), petAges.averageIfEmpty(0.0), 0.0);

Методы minIfEmpty, maxIfEmpty и averageIfEmpty позволяют указать значение по умолчанию для использования в случае пустого результата. В будущем мы также можем добавить minOptional, maxOptional и averageOptional, если они понадобятся.

Если вы используете потоки и хотите, чтобы их можно было использовать повторно, подумайте об их использовании вместе с Supplier. Это уменьшит количество повторяющегося кода, который вам придется писать. Если вам нужна неиссякаемая лень из коробки, подумайте об использовании Eclipse Collections, так как вы получите множество дополнительных опций, которые вы можете использовать в дополнение к Streams.

Я надеюсь, что этот блог был полезным и информативным и показал некоторые варианты эффективного использования Streams и Eclipse Collections LazyIterables для решения тех же проблем. Я также надеюсь, что вы попробуете ката Eclipse Collections самостоятельно. Я часто обучаю ката, используя как Streams, так и Eclipse Collections, чтобы разработчики могли изучить оба API и понять, какие опции они им доступны.

Я руководитель проекта и ответственный за проект OSS Коллекции Eclipse в Eclipse Foundation. Eclipse Collections открыта для пожертвований. Если вам нравится библиотека, вы можете сообщить нам об этом, отметив ее на GitHub.