Используйте Stream API проще (или не используйте вообще)

    С появлением Java 8 Stream API позволило программистам писать существенно короче то, что раньше занимало много строк кода. Однако оказалось, что многие даже с использованием Stream API пишут длиннее, чем надо. Причём это не только делает код длиннее и усложняет его понимание, но иногда приводит к существенному провалу производительности. Не всегда понятно, почему люди так пишут. Возможно, они прочитали только небольшой кусок документации, а про другие возможности не слышали. Или вообще документацию не читали, просто видели где-то пример и решили сделать похоже. Иногда это напоминает анекдот про «задача сведена к предыдущей».


    В этой статье я собрал те примеры, с которыми столкнулся в практике. Надеюсь, после такого ликбеза код программистов станет чуточку красивее и быстрее. Большинство этих штук хорошая IDE поможет вам исправить, но IDE всё-таки не всесильна и голову не заменяет.


    1. Стрим из коллекции без промежуточных операций обычно не нужен


    Если у вас промежуточных операций нет, часто можно и нужно обойтись без стрима.


    1.1. collection.stream().forEach()


    Хотите что-то сделать для всех элементов коллекции? Замечательно. Но зачем вам стрим? Напишите просто collection.forEach(). В большинстве случаев это одно и то же, но короче и производит меньше мусора. Некоторые боятся, что различие в функциональности есть, но не могут толком объяснить, какое оно. Говорят, мол, forEach не гарантирует порядок. Как раз в стриме по спецификации не гарантирует (по факту он есть), а без стрима для упорядоченных коллекций гарантирует. Если порядок вам не нужен, вам не станет хуже, если он появится. Единственное отличие из стандартной библиотеки, которое мне известно — это синхронизированные коллекции, созданные через Collections.synchronizedXyz(). В этом случае collection.forEach() синхронизирует всю операцию, тогда как collection.stream().forEach() не синхронизирует ничего. Скорее всего, если вы уж используете синхронизированные коллекции, вам всё-таки синхронизация нужна, поэтому станет только лучше.


    1.2. collection.stream().collect(Collectors.toList())


    Собираетесь преобразовать произвольную коллекцию в список? Замечательно. Начиная с Java 1.2 у вас есть отличная возможность для этого: new ArrayList<>(collection) (ну хорошо, до Java 5 дженериков не было). Это не только короче, но и быстрее и опять же создаст меньше мусора в куче. Может быть значительно меньше, так как в большинстве случаев у вас выделится один массив нужного размера, тогда как стрим будет добавлять элементы по одному, растягивая по мере необходимости. Аналогично вместо stream().collect(toSet()) создаём new HashSet<>(), а вместо stream().collect(toCollection(TreeSet::new))new TreeSet<>().


    1.3. collection.stream().toArray(String[]::new)


    Новый способ преобразования в массив ничем не лучше старого доброго collection.toArray(new String[0]). Опять же: так как на пути меньше абстракций, преобразование может оказаться более эффективно. Во всяком случае объект стрима вам не нужен.


    1.4. collection.stream().max(Comparator.naturalOrder()).get()


    Есть замечательный метод Collections.max, который почему-то незаслуженно многими забыт. Вызов Collections.max(collection) сделает то же самое и опять же с меньшим количеством мусора. Если у вас свой компаратор, используйте Collections.max(collection, comparator). Метод Collections.max() подойдёт хуже, если вы хотите специально обработать пустую коллекцию, тогда стрим более оправдан. Цепочка collection.stream().max(comparator).orElse(null) смотрится лучше, чем collection.isEmpty() ? null : Collections.max(collection, comparator).


    1.5. collection.stream().count()


    Это совсем ни в какие ворота не лезет: есть ведь collection.size()! Если в Java 9 count() отработает быстро, то в Java 8 этот вызов всегда пересчитывает все элементы, даже если размер очевиден. Не делайте так.


    2. Поиск элемента


    2.1. stream.filter(condition).findFirst().isPresent()


    Такой код вижу на удивление часто. Суть его: проверить, выполняется ли условие для какого-то элемента стрима. Именно для этого есть специальный метод: stream.anyMatch(condition). Зачем вам Optional?


    2.2. !stream.anyMatch(condition)


    Тут некоторые поспорят, но я считаю, что использовать специальный метод stream.noneMatch(condition) более выразительно. А вот если и в условии отрицание: !stream.anyMatch(x -> !condition(x)), то тут однозначно лучше написать stream.allMatch(x -> condition(x)). Тот, кто будет читать код, скажет вам спасибо.


    2.3. stream.map(condition).anyMatch(b -> b)


    И такой странный код иногда пишут, чтобы запутать коллег. Если увидите такое, знайте, что это просто stream.anyMatch(condition). Здесь же вариации на тему вроде stream.map(condition).noneMatch(Boolean::booleanValue) или stream.map(condition).allMatch(Boolean.TRUE::equals).


    3. Создание стрима


    3.1. Collections.emptyList().stream()


    Нужен пустой стрим? Бывает, ничего страшного. И для этого есть специальный метод Stream.empty(). Производительность одинаковая, но короче и понятнее. Метод emptySet здесь не отличается от emptyList.


    3.2. Collections.singleton(x).stream()


    И тут можно упростить жизнь: если вам потребовался стрим из одного элемента, пишите просто Stream.of(x). Опять же без разницы singleton или singletonList: когда в стриме один элемент, никого не волнует, упорядочен стрим или нет.


    3.3. Arrays.asList(array).stream()


    Развитие этой же темы. Люди почему-то так делают, хотя Arrays.stream(array) или Stream.of(array) отработает не хуже. Если вы указываете элементы явно (Arrays.asList(x, y, z).stream()), то Stream.of(x, y, z) тоже сработает. Аналогично с EnumSet.of(x, y, z).stream(). Вам же стрим нужен, а не коллекция, так и создавайте сразу стрим.


    3.4. Collections.nCopies(N, "ignored").stream().map(ignored -> new MyObject())


    Нужен стрим из N одинаковых объектов? Тогда nCopies() — ваш выбор. А вот если нужно сгенерировать стрим из N объектов, созданных одним и тем же способом, то тут красивее и оптимальнее воспользоваться Stream.generate(() -> new MyObject()).limit(N).


    3.5. IntStream.range(from, to).mapToObj(idx -> array[idx])


    Нужен стрим из куска массива? Есть специальный метод Arrays.stream(array, from, to). Опять же короче и меньше мусора, плюс так как массив больше не захвачен лямбдой, он не обязан быть effectively-final. Понятно, если from — это 0, а to — это array.length, тогда вам просто нужен Arrays.stream(array), причём тут код станет приятнее, даже если в mapToObj что-то более сложное. Например, IntStream.range(0, strings.length).mapToObj(idx -> strings[idx].trim()) легко превращается в Arrays.stream(strings).map(String::trim).


    Более хитрая вариация на тему — IntStream.range(0, Math.min(array.length, max)).mapToObj(idx -> array[idx]). Немножко подумав, понимаешь, что это Arrays.stream(array).limit(max).


    4. Ненужные и сложные коллекторы


    Иногда люди изучают коллекторы и всё пытаются делать через них. Однако не всегда они нужны.


    4.1. stream.collect(Collectors.counting())


    Многие коллекторы нужны только как вторичные в сложных каскадных операциях вроде groupingBy. Коллектор counting() как раз из них. Пишите stream.count() и не мучайтесь. Опять же если в Java 9 count() может иногда выполниться за константное время, то коллектор всегда будет пересчитывать элементы. А в Java 8 коллектор counting() ещё и боксит зазря (я это исправил в Java 9). Из этой же оперы коллекторы maxBy(), minBy() (есть методы max() и min()), reducing() (используйте reduce()), mapping() (просто добавьте шаг map(), а затем воспользуйтесь вторичным коллектором напрямую). В Java 9 добавились filtering() и flatMapping(), которые также дублируют соответствующие промежуточные операции.


    4.2. groupingBy(classifier, collectingAndThen(maxBy(comparator), Optional::get))


    Частая задача: хочется сгруппировать элементы по классификатору, выбрав в каждой группе максимум. В SQL это выглядит просто SELECT classifier, MAX(...) FROM ... GROUP BY classifier. Видимо, пытаясь перенести опыт SQL, люди пытаются использовать тот же самый groupingBy и в Stream API. Казалось бы должно сработать groupingBy(classifier, maxBy(comparator)), но нет. Коллектор maxBy возвращает Optional. Но мы-то знаем, что вложенный Optional всегда не пуст, так как в каждой группе по крайней мере один элемент есть. Поэтому приходится добавлять некрасивые шаги вроде collectingAndThen, и всё начинает выглядеть совсем чудовищно.


    Однако отступив на шаг назад, можно понять, что groupingBy тут не нужен. Есть другой замечательный коллектор — toMap, и это как раз то что надо. Мы просто хотим собрать элементы в Map, где ключом будет классификатор, а значением сам элемент. В случае же дубликата выберем больший из них. Для этого, кстати, есть BinaryOperator.maxBy(comparator), который можно статически импортировать вместо одноимённого коллектора. В результате имеем: toMap(classifier, identity(), maxBy(comparator)).


    Если вы порываетесь использовать groupingBy, а вторичным коллектором у вас maxBy, minBy или reducing (возможно, с промежуточным mapping), посмотрите в сторону коллектора toMap — может полегчать.


    5. Не считайте то, что не нужно считать


    5.1. listOfLists.stream().flatMap(List::stream).count()


    Это перекликается с пунктом 1.5. Мы хотим посчитать суммарное число элементов во вложенных коллекциях. Казалось бы всё логично: растянем эти коллекции в один стрим с помощью flatMap и пересчитаем. Однако в большинстве случаев размеры вложенных списков уже посчитаны, хранятся у них в поле и легко доступны с помощью метода size(). Небольшая модификация существенно увеличит скорость операции: listOfLists.stream().mapToInt(List::size).sum(). Если боитесь, что int переполнится, mapToLong тоже сработает.


    5.2. if(stream.filter(condition).count() > 0)


    Опять же забавный способ записать stream.anyMatch(condition). Но в отличие от довольно безобидного 2.1 вы тут теряете короткое замыкание: будут перебраны все элементы, даже если условие сработало на самом первом. Аналогично если вы проверяете filter(condition).count() == 0, лучше воспользоваться noneMatch(condition).


    5.3. if(stream.count() > 2)


    Этот случай более хитрый. Вам теперь важно знать, больше двух элементов в стриме или нет. Если вас волнует производительность, возможно, стоит вставить stream.limit(3).count(). Вам ведь не важно, сколько их, если их больше двух.


    6. Разное


    6.1. stream.sorted(comparator).findFirst()


    Что хотел сказать автор? Отсортируй стрим и возьми первый элемент. Это же всё равно что взять минимальный элемент: stream.min(comparator). Иногда видишь даже stream.sorted(comparator.reversed()).findFirst(), что аналогично stream.max(comparator). Реализация Stream API не соптимизирует тут (хотя могла бы), а сделает всё как вы сказали: соберёт стрим в промежуточный массив, отсортирует его весь и выдаст вам первый элемент. Вы существенно потеряете в памяти и скорости на такой операции. Ну и, конечно, замена существенно понятнее.


    6.2. stream.map(x -> {counter.addAndGet(x);return x;})


    Некоторые люди пытаются выполнить какой-нибудь побочный эффект в стриме. Вообще это в принципе уже звоночек, что может стрим вам вовсе и не нужен. Но так или иначе для этих целей есть специальный метод peek. Пишем stream.peek(counter::addAndGet).




    На этом у меня всё. Если вы сталкивались со странными и неэффективными способами использования Stream API, напишите о них в комментариях.

    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 46
    • +1
      Отличная статья. А что есть из новшеств стримов в java 9?
      • +2

        Там не очень много. Почти всё перечислено, например, здесь

      • +2
        После scala стримы на джаве кажутся жутко неудобными (или неочевидными). Так же пришлось какие-то привычные вещи портировать в джаву. Было бы здорово избавиться от ненужного явного преобразования в стрим и добавить синтаксический сахар для «большей наглядности кода». Тогда джава заблистает опять новыми красками (я не говорю что сейчас плохо, но хочется лучшего)
        • +14

          У джавы своя философия. В частности, в джаве ценится ясность кода. Наличие методов типа map прямо у коллекций не даёт явного понимания, метод ленивый или нет, будет ли промежуточная коллекция, если сделать два раза map или нет. А со стримами всё понятно. Вы можете сказать, мол, давайте скажем, что у коллекции все методы неленивы. Но если сама коллекция — это вьюшка над другой коллекцией? Скала тут как раз засорила все абстрактные интерфейсы кучей всего. Пришёл вам Traversable, вы вызываете у него map. Что вы получите, новую независимую копию или вьюшку над старой? Зависит от того, что вам на самом деле там передали, TraversableView или тупо List. По факту протекающая абстракция. Кому-то нравится такой подход, абстрагировать всё и вся, те пишут на Скале. Мне нравится, когда я лучше понимаю, что в моём коде происходит, поэтому я пишу на Джаве. Это замечательно, что есть разные языки с разной философией.

          • 0

            Guava Collections неплохи, и с лямбдами получается достаточно компактный код.

          • 0

            Примеры кажутся нереальными) неужели действительно все они из реальных проектов?)

            • +7

              Мне многие из них тоже казались нереальными, пока я их не увидел в реальном коде :-)

              • 0
                Сначала прочитал «пока я их не увидел в *своем* реальном коде». Потом подумал и понял, что часто так оно и бывает :)
            • –4
              бесполезная штука в реальной жизни, потому как разобраться откуда эксепшен и что в данных не так практически не реально. проще сразу писать так, что бы при проблеме получить вменяемый эксепшен
              • +3
                Не далее как вчера наткнулся вот на такое: List sortedParams = params.stream().sorted(...).collect(Collectors.toList()); Мало того, что Collections.sort(..) по-прежнему работает, в Java8 же есть ещё default-метод sort(...) на List-е!

                В примере 2.3 наверное имелось в виду b -> true, а не b->b?
                • +1

                  Нет-нет, именно b->b.


                  Ваш пример интересен. sort() меняет текущий список. По сути дела замена — это два выражения, List<T> sortedParams = new ArrayList(params);sortedParams.sort(...);. Это заметно оптимальнее, но можно спорить, красивее ли. Теперь, к примеру, sortedParams не заинлайнишь, к примеру.

                  • 0
                    params.stream().sorted(...).collect(Collectors.toList())

                    А если нужно сохранить исходный params в неприкосновенности? Коллектор toList ведь порождает новую коллекцию, не трогая ту, из которой приготовлен стрим.
                    • 0
                      Ниже уже разобрались. Конкретно в том месте не нужно было, если бы и нужно было, я бы всё равно предпочёл написать List sortedParams = new ArrayList(params); sortedParams.sort(...);, хотя тут уже действительно, возможно, дело вкуса.
                  • +2
                    Чорт, перечитал пример 2.3 внимательнее! Я просто видел такое: stream.filter(condition).anyMatch(x -> true) И ещё очень часто встречается first (find/match), там где достаточно any.

                    Насчёт примера с sorted: если бы копия списка там была нужна! Но копия списка там была не нужна.
                    • 0

                      С filter().anyMatch() тоже хорошо.

                    • 0
                      Спасибо! как раз в нужное время
                      • +7

                        Я ждал этого! IDEA-178614

                        • 0
                          Когда не посмотрел, кто автор статьи. Но замыкание зачетное, да :)
                        • 0

                          А для Optional и CompletionStage такие статьи ожидаются?

                          • 0

                            Про Optional может быть. С CompletionStage не наберётся материала, его используют сильно реже.

                          • 0

                            Меня удручает, что оба Stream и Optional имеют метод #map(map(Function<? super T, ? extends U> mapper), который выглядит одинаково, но имеет несколько разный смысл:


                            • в Optional mapper вызовется только для ненулевого элемента
                            • в Stream mapper вызовется для любых элементов стрима (нужно делать filter(), если хотим исключить нулевые)

                            Сейчас уже привык и веду себя осторожнее, но вижу иногда, как новички наступают на те же грабли.

                            • 0

                              "Нулевой элемент" в Optional обозначает отсутствие элемента. Как в принципе можно вызвать mapper для отсутствующего элемента?


                              Или с другой стороны: Optional делали чтобы избавиться от null. Как можно использовать Optional и при этом ожидать где-то null?

                              • 0

                                У Optional действительно есть особенность, нарушающая монадический закон: map(map(opt, F), G) ≠ map(opt, G○F) или в Java-терминах opt.map(F).map(G) != opt.map(G.compose(F)). Если F возвращает null для какого-либо значения, то в композиции G выполнится с аргументом null, а в Optional G не выполнится вообще. Пуристы в этом месте начинают плеваться. Ну и, например, соответствующий рефакторинг для Optional, который предлагает IDEA, по факту меняет семантику кода (а для Stream не меняет).

                            • +2
                              На английском не планируется статьи? Хотел коллегам показать.
                            • +2

                              А еще есть rx, там вообще ад. :D

                              • +1
                                Где такие примеры откапываешь?
                                • +1

                                  Не поверишь, но около половины нашлось в коде IDEA Ultimate / Android Studio :-)

                                  • 0
                                    Сначала пиши код, потом читай спеку — наш подход 8)
                                • 0
                                  Видел несколько раз следующий код с ненужным промежуточным collect. Часто получается в процессе рефакторинга.

                                  List<T> temp = collection.stream().filter(...).map(...).collect(Collectors.toList());
                                  ... some unrelated to "temp" stuff...
                                  List<T> result = temp.stream().filter(...).map(...).collect(Collectors.toList());        
                                  


                                  Вместо

                                  List<T> result = collection.stream().filter(...).map(...).filter(...).map(...).collect(Collectors.toList());
                                  

                                  • 0

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


                                    List<CompletableFuture<T>> futures = callables.stream()
                                       .map(CompletableFuture::supplyAsync).collect(toList());
                                    List<T> results = futures.stream()
                                       .map(CompletableFuture::join).collect(toList());

                                    Здесь если склеить в один стрим, то все фоновые задачи будут выполняться последовательно (причём этого можно не заметить, скорее всего тесты не упадут).

                                    • +1
                                      Такой код лучше как-нибудь отрефакторить.
                                  • +1
                                    Встречал вот такое:
                                    List<String> list = Arrays.asList("A", "B", "C");
                                    List<String> copy = list.stream().map(String::new).collect(Collectors.toList());
                                    
                                    • +1
                                      6.1. stream.sorted(comparator).findFirst()

                                      Тут на самом деле есть некоторая ментальная ловушка.
                                      Когда мы говорим о методе min или max, то невольно в голове ссылаемся на числовое представление.
                                      А когда объекты в стриме мы не представляем как последовательность чисел, то некоторые и не думают, что можно использование данные операции.
                                      То есть если бы методы назывались last(Comparator) и first(Comparator), то скорее всего эти методы были бы использованы.
                                      То есть разработчик здесь говорит упорядочи каким-то способом набор объектов и возьми самый первый(Он не думает про объект в терминах минимальный/максимальный).

                                      • +1
                                        То есть если бы методы назывались last(Comparator) и first(Comparator), то скорее всего эти методы были бы использованы.

                                        До чтения javadoc/просмотра реализации, ориентируясь только по названиям, я бы решил, что это методы, возвращающие первое и последнее по порядку следования вхождения элементов в stream, на которые сделал стойку компаратор. А вот с min / max всё однозначно.


                                        То есть разработчик здесь… не думает про объект в терминах минимальный/максимальный.

                                        Это неправильный разработчик и он даёт неправильный мёд.

                                      • +2
                                        Была необходимость определить что либо хоть один элемент в стриме удовлетворяет предикату, либо стрим пуст. Пришлось использовать peek
                                                final AtomicBoolean streamIsEmpty = new AtomicBoolean(true);
                                                final boolean anyMatch = someStream(...)
                                                        .map(...)
                                                        .peek(obj -> streamIsEmpty.set(false))
                                                        .anyMatch(condition);
                                                return anyMatch || streamIsEmpty.get();
                                        
                                        Можно придумать вариант лучше?
                                        • +1
                                          Вот только 30 минут назад в очередной раз столкнулся с этой проблемой. К счастью, у меня доступна исходная коллекция, и я ее могу проверить на пустоту. Как элегантно решить проблему в общем случае я не знаю.
                                          • 0

                                            Лучше, наверное, в таких случаях свой Collector писать.

                                            • 0

                                              Однако он не будет короткозамкнутым, потому что в стандартном Stream API не бывает короткозамкнутых коллекторов (у меня в StreamEx они есть)

                                            • 0

                                              Собственно да. С помощью стандартного Stream API вроде красиво не решить. Моя библиотека StreamEx позволяет легко сделать такой коллектор:


                                              static <T> Collector<T, ?, Boolean> anyMatchOrEmpty(Predicate<T> predicate) {
                                                  return MoreCollectors.pairing(
                                                        MoreCollectors.filtering(predicate, MoreCollectors.first()), 
                                                        MoreCollectors.first(),
                                                        (filtered, nonFiltered) -> filtered.isPresent() || !nonFiltered.isPresent());
                                              }

                                              С обычным стримом он не будет короткозамкнут, но если источник обернуть в StreamEx (или если источник сразу возвращает StreamEx), то будет:


                                              return StreamEx.of(someStream(...)).collect(anyMatchOrEmpty(condition));
                                            • +1

                                              Насчёт IDE не заменяет голову, кстати, пример. IDEA предупредит, если вы напишете Arrays.asList(array).stream(). Но если у вас сложный внешний стрим и в нём встретится .map(Arrays::asList).flatMap(List::stream) вместо простого .flatMap(Arrays::stream), то тут уже не предупредит. Или про filter().findFirst().isPresent() скажет, но если вы промежуточный Optional присвоите в переменную и используете её только для isPresent, то уже увы. Может когда-нибудь и эти случаи покроем, но не стоит надеяться, что IDE вам подскажет всегда.

                                              • +1
                                                1.1. collection.stream().forEach()

                                                Напишите просто collection.forEach().

                                                Если для метода не передается Consumer из вне, то лучше использовать старый-добрый форыч.
                                                for (T it : collection) {
                                                ...
                                                }


                                                Более того, обычно метод forEach() используют в связке с замыканием, чтобы туда сложить значения. Что еще более печально выглядит.

                                                ps: Я до сих пор не понимаю, почему разработчики Java решили добавить Stream API непосредственно в интерфейс коллекция?
                                                Ведь практически всегда можно: Stream.of(… ) и погнали. Зачем было уродовать интерфейс коллекций? Притом с подходом Stream.of – коллекции бы не зависели от Стримов, а так получается довольно жесткая связанность между Стримом и Коллекцией :(
                                                • 0

                                                  forEach изящнее выглядит со ссылкой на метод. Кроме того может быть несколько быстрее, потому что не создаётся итератор.


                                                  Я до сих пор не понимаю, почему разработчики Java решили добавить Stream API непосредственно в интерфейс коллекция?

                                                  Ну вот любят люди всё такое fluent-fluent. Все вон трещат, чтобы им прямо в коллекцию добавили map или filter. Написать свой утилитный класс и вызывать статические методы религия не позволяет. Stream.from(veryLongCallProducingACollection).many().stream().operations() тоже смотрится не очень.


                                                  Вообще с точки зрения разработчиков Java, стримы более базовая вещь, чем коллекции. От коллекций по факту зависит только класс Collectors, который утилитный. Остальные стримы от коллекций не зависят.

                                                  • +1
                                                    forEach изящнее выглядит со ссылкой на метод. Кроме того может быть несколько быстрее, потому что не создаётся итератор.
                                                    Изящнее это только с ссылкой на метод, согласен. Но если пишут лямбду или того хуже – лямбду-многострочник, это смотрится хуже, особенно с захватом переменной.

                                                    А по производительности скорее всего будет одинаково (в приделах погрешности).

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

                                                    list.stream().(transforming).collect(toList()).stream(). ...


                                                    хотя, я наверно еще не сталкивался с реально сложной работой с данными, где действительно пригодился бы .stream() как интерфейсный метод.
                                                • +1
                                                  Очень часто неопытные программиста при работе с dao/repository делают repo.findAll().stream().sorted(..).findFirst(). То есть выгребают с БД всё, сортируют, и берут первый элемент… Что есть очень плохо, так как: во первых это можно сделать запросом к БД, а во вторых это может привести к ООМ. Советую всем проверять такие места :)

                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.