Лень - это добродетель. Иногда хочется, чтобы это повторялось.
В программировании лень может быть очень хорошей вещью. Ленивая инициализация позволяет нам избежать создания дорогостоящих ресурсов до тех пор, пока они не понадобятся. Ленивая итерация позволяет нам избежать создания временных структур данных и потенциально уменьшить общий объем работы, необходимой для выполнения вычислений. В 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.