Анбоксинг в современной Java

    Сейчас новые версии Java выходят раз в полгода. В них время от времени появляются новые возможности: var в Java 10, switch-выражения в Java 14, рекорды и паттерны в Java 16. Про это всё, конечно, написано множество статей, блог-постов, сделано множество докладов на конференциях. Оказалось, однако, что мы все пропустили один очень крутой апгрейд языка, который произошёл в Java 14 - апгрейд обычного цикла for по набору целых чисел. Дело в том, что этот апгрейд случился не в языке, а в виртуальной машине, но заметно повлиял на то как мы можем программировать на Java.

    Вспомним старый добрый цикл for:

    for (int i = 0; i < 10; i++) {
      System.out.println(i);
    }

    У такого синтаксиса уйма недостатков. Во-первых, переменная цикла упоминается три раза. Очень легко перепутать и упомянуть не ту переменную в одном или двух местах. Во-вторых, такая переменная не является effectively final. Её не передашь в явном виде в лямбды или анонимные классы. Но ещё важнее: нельзя застраховаться от случайного изменения переменной внутри цикла. Читать код тоже трудно. Если тело цикла большое, не так-то легко сказать, изменяется ли она ещё и внутри цикла, а значит непонятно, просто мы обходим числа по порядку или делаем что-то более сложное. Есть ещё потенциальные ошибки, если необходимо сменить направление цикла или включить границу. Да и выглядит это старомодно.

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

    for (x in 0 until 10) {
      println(x)
    }

    Тут всё в шоколаде: переменная упоминается один раз, она неизменяемая внутри тела цикла, есть варианты для включения или исключения верхней границы, а также для задания шага. Красиво.

    Можно ли приблизиться к этому варианту в Java? Да, с помощью цикла for-each, который появился в Java 5. Достаточно написать такой библиотечный метод в утилитном классе:

    /**
     * Возвращает диапазон целых чисел
     * @param fromInclusive начальное значение (включительно)
     * @param toExclusive конечное значение (не включается)
     * @return Iterable, содержащий числа от fromInclusive до toExclusive.
     */
    public static Iterable<Integer> range(int fromInclusive, 
                                          int toExclusive) {
      return () -> new Iterator<Integer>() {
        int cursor = fromInclusive;
        public boolean hasNext() { return cursor < toExclusive; }
        public Integer next() { return cursor++; }
      };
    }

    Никакого rocket science, исключительно тривиальный код, даже комментировать нечего. После этого вы легко можете писать красивые циклы и в Java:

    for (int i : range(0, 10)) { // импортируем наш метод статически
      System.out.println(i);
    }

    Красиво. Можно явно объявить переменную final, чтобы запретить случайные изменения. Несложно сделать такие же методы с шагом или с включённой верхней границей и пользоваться ими. Почему же никто так до сих пор не делает? Потому что в данном случае из-за боксинга фатально страдает производительность. Давайте для примера посчитаем сумму кубов чисел в простеньком JMH-бенчмарке:

    @Param({"1000"})
    private int size;
    
    @Benchmark
    public int plainFor() {
      int result = 0;
      for (int i = 0; i < size; i++) {
        result += i * i * i;
      }
      return result;
    }
    
    @Benchmark
    public int rangeFor() {
      int result = 0;
      for (int i : range(0, size)) {
        result += i * i * i;
      }
      return result;
    }

    Тело цикла весьма быстрое и не выделяет никакой памяти, но при этом делает какую-то полезную работу, что не позволит JIT-компилятору выкинуть цикл совсем. Также я на всякий случай параметризовал верхнюю границу, чтобы JIT не заложился на конкретное значение количества итераций. Запустим на Java 8 и увидим безрадостную картину:

    Benchmark            (size)  Mode  Cnt     Score     Error  Units 
    BoxedRange.plainFor    1000  avgt   30   622.679 ±   7.286  ns/op 
    BoxedRange.rangeFor    1000  avgt   30  3591.052 ± 792.159  ns/op

    Использование метода range снизило производительность практически в шесть раз: тест выполняется 3,5 мкс вместо 0,6 мкс. Если посчитать аллокации с помощью -prof gc, мы обнаружим, что версия rangeFor выделяет 13952 байта, тогда как версия plainFor ожидаемо не выделяет памяти вообще. Легко понять, откуда взялось это число, если вспомнить, что целые числа до 127 кэшируются. Новые объекты Integer выделяются на итерациях 128-999, то есть создаётся 872 объекта по 16 байт. Заметьте, кстати, что ни объект Iterable, ни объект Iterator не создаются: здесь наш код прекрасно обрабатывается оптимизацией скаляризации (scalar replacement). Однако боксинг всё портит.

    Понятно, что такие накладные расходы на обычный цикл for часто неприемлемы, поэтому программировать в таком стиле на Java никто всерьёз рекомендовать не будет. Однако давайте попробуем более новые версии Java:

    Вот тут нас ждёт приятный сюрприз: начиная с Java 14 производительность варианта с range сравнялась с простой версией! JIT-компилятор стал достаточно умным, чтобы сгенерировать настолько же хороший ассемблер, как и в простой версии.

    На самом деле работа над оптимизацией по уничтожению ненужного боксинга велась много лет. Её плоды можно было пощупать ещё с версии Java 8 с помощью опций JVM -XX:+UnlockExperimentalVMOptions -XX:+AggressiveUnboxing. Мы можем попробовать запустить наш тест с этой опцией, и окажется, что с ней уже в восьмёрке производительность была существенно лучше:

    В Java 8-11 мы имели производительность на уровне 0,9 мкс, в 12 стало в районе 0,8, а начиная с 13 сравнялось с обычным циклом. И вот к Java 14 эта оптимизация стала достаточно стабильной, чтобы её включить по умолчанию. Вы можете пытаться сделать это и в более ранних версиях, но я бы не рекомендовал этого на серьёзном продакшне. Смотрите, например, какие страшные баги приходилось исправлять в связи с этой опцией.

    В чём была сложность реализации автоматического удаления боксинга? Одна из основных проблем - как раз тот самый кэш объектов Integer до 127. При боксинге целых чисел выполняется нетривиальный метод valueOf (цитата по Java 16):

    public static Integer valueOf(int i) {
      if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
      return new Integer(i);
    }

    Как видно, этот метод берёт значения в диапазоне от IntegerCache.low до IntegerCache.high из кэша, который заполняется на ранней стадии инициализации виртуальной машины. Поэтому, если у нас происходит боксинг с последующим анбоксингом, нельзя просто положиться на механизм скаляризации: иногда мы должны возвращать закэшированные объекты. В режиме AggressiveUnboxing JIT-компилятор принудительно начинает игнорировать этот кэш, если может доказать, что ссылка на объект никуда не уплывает. В этом можно убедиться, написав какой-нибудь такой код:

    Field field = Class.forName("java.lang.Integer$IntegerCache").getDeclaredField("cache");
    field.setAccessible(true);
    Integer[] arr = (Integer[]) field.get(null);
    arr[130] = new Integer(1_000_000);
    for (int i = 0; i < 10000; i++) {
      int res = rangeFor();
      if (res != -1094471800) {
        System.out.println("oops! " + res + "; i = " + i);
        break;
      }
    }

    Мы грязным рефлекшном подменяем одно из чисел в кэше на другое, а затем в цикле сравниваем результат с тем, который должен получиться в результате подмены. В честной Java if не должен выполняться. Но с опцией AggressiveUnboxing мы получаем результат в духе

    oops! 392146832; i = 333

    То есть когда JIT-компилятор C2 добирается до метода rangeFor, он выкидывает обращение к испорченному кэшу, и начинает выдавать правильный ответ, который должен получаться, если бы мы не залезли в кэш грязными руками.

    Однако хоть кэш и игнорируется, до Java 12 включительно я вижу в ассемблерных листингах инструкции вида cmp r10d,7fh, то есть счётчик сравнивается с числом 127 (=0x7f). Похоже, условие полностью выкинуть удалось только в Java 13. Я могу спекулировать, что эти лишние проверки не только отъедают такты процессора, но и занимают дополнительные регистры, из-за чего страдает уровень развёртки цикла. Во всяком случае, до Java 12 цикл с rangeFor разворачивается по 8 итераций, а начиная с Java 13 лишние проверки исчезают и развёртка уже охватывает 16 итераций, сравниваясь с plainFor.

    Так или иначе, мы видим результат: агрессивное уничтожение боксинга стало поведением по умолчанию с Java 14, что позволяет гораздо чаще наплевать на боксинг и пользоваться удобными конструкциями. В связи с этим цикл вида for (int i : range(0, 10)) должен стать каноническим в новых версиях Java и должен заменить динозавра for (int i = 0; i < 10; i++), в том числе в учебниках по языку.

    Окончательное решение проблемы с боксингом должно прийти к нам после специализации дженериков в проекте Valhalla. Тогда можно будет возвращать Iterable<int>, и боксинг в данном случае не потребуется вообще. Вероятно, мы не увидим в стандартной библиотеке методов вроде range пока этого не случится. Однако боксинг уже не так страшен и с Iterable<Integer> можно комфортно жить.

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

    Вы будете использовать циклы вида for(int i: range(...)) в своих проектах?

    • 5,2%Уже программирую в таком стиле!23
    • 6,1%Уже перешёл на Java 14+ и обязательно попробую!27
    • 26,9%Когда перейду на Java 14+, обязательно попробую!119
    • 18,8%Это подозрительно! Кто его знает, сработает оптимизатор или нет! Лучше не надо!83
    • 22,8%Скорее наступит тепловая смерть вселенной, чем мой проект переведут на Java 14+.101
    • 20,1%Не пишу на Java и отлично себя чувствую. Жалко мне вас с вашими проблемами!89

    Комментарии 59

      +3

      А что нам по поводу range предлагает стандартная библиотека?

        +3
        IntStream.range(a, b).forEach(i -> ...);
        Интересно, как у этого варианта с бенчмарком. Боксинга здесь нет.
          +2

          У forEach другие проблемы.

            0
            IntStream.range(0, this.size).map(i -> i * i * i).sum() работает так же быстро, как и простой for
            +1
            Ничего, про это написано в последнем абзаце.
            +11
            То, что EliminateAutoBox оптимизация здесь сработала, скорее, повезло. Она чаще не работает, чем работает :) Даже на свежайшей JDK 17-ea простой пример отсюда (setPrimitive) по-прежнему аллоцирует лишний объект.

            Зато если вместо автобоксинга написать new Integer(i), то сразу всё хорошо оптимизируется. На этом фоне очень обидно, что конструкторы обёрток задеприкейтили, не предоставив сопоставимую по скорости альтернативу :(
              +9

              От new Integer(i) на обычном кроваво-энтерпрайзном проекте "Сонар" кондратий хватит

                0
                То, что EliminateAutoBox оптимизация здесь сработала, скорее, повезло

                Может повезло, а может и специально такие сценарии обрабатывали.


                не предоставив сопоставимую по скорости альтернативу :(

                Думаю, можно создать свою тривиальную обёртку, если где-то необходимо работать с боксами, а Integer.valueOf не дожимает производительность.

                  0

                  К сожалению, обертка не всегда подходит и иногда нужен именно тип Integer, поэтому приходится использовать new Integer(i).


                  Интересно, получится ли в итоге такой конструктор удалить в будущих версиях Java, не сломав существенную часть экосистемы

                    0
                    обертка не всегда подходит и иногда нужен именно тип Integer, поэтому приходится использовать new Integer(i)

                    А разве есть разница между типом возвращаемым из new Integer(i) и Integer.valueOf(i)?

                      0

                      А на Integer.valueOf(i) заметно хуже работает escape analysis в нетривиальных случаях, с комментария про это и началась эта ветка обсуждения :)

                        0

                        Я в курсе, сам писал об этом и сам удивился, в насколько простых случаях стирание аллокаций не работает, меня смутила вот это часть


                        обертка не всегда подходит

                        ведь и Integer.valueOf(i), и new Integer(i) возвращает один и тот же тип-обёртку

                          +1

                          Я говорил про свой собственный тип-обёртку, а не Integer. Читай внимательнее.

                            0

                            Я протупил )

                +8
                В опросе не хватает варианта для староверов, «Не люблю синтаксический сахар».
                  0

                  А я вот наоборот, люблю синтаксический сахар, но при этом конкретно в такой формулировке range пользы в своём проекте не вижу. Потому для меня там тоже нет подходящего ответа — я использую Java 14+, но не пишу в таком стиле, и пока не вижу смысла начинать писать в таком стиле.

                    0

                    Какие недостатки вы видите?

                      +2

                      Да не то чтобы тут были какие-то прямо общие недостатки.


                      Мне практически нигде и никогда за последние, скажем так, три года не было нужды писать


                         for (int i = J; i < K; i+=N) { ... }

                      при любых J, K и N. В отличие от, к примеру for (var el : c) {...}.


                      Из этого следует, что такая утилита мне просто не нужна ввиду отсутствия нужды в Range как Iterable<N>. Зато есть нужда в Range как представлении [N;K) и прочих подобных формах, включая случаи когда N и K — это даты, время или вообще произвольный Comparable.

                        +1
                        Мне практически нигде и никогда за последние, скажем так, три года не было нужды писать

                        Хм. Удивительно. А мне довольно часто приходится писать и читать такие циклы. Видимо, задачи сильно разные.

                          +2
                          задачи сильно разные.

                          Да, самый вероятный вариант. В моём случае все ограничения по количеству находятся за пределами домена программы, а программа должна либо обработать вообще всё, что ей придёт, либо обработать всё, что пришло до срабатывания предиката (takeWhile), либо отбросить все элементы, кроме первого.

                  –2

                  Интересно, появится ли в яве когда-нибудь возможность самостоятельно определять синтаксические конструкции для своего проекта, иными словами возможность написать что-то вроде


                  define (int x...int y) => MyUtilityClass.range(x, y);

                  чтобы компилятор самостоятельно мог привести


                  for (i : 0...10) {
                   //...
                  }

                  к


                  for (i : range(0, 10)) {
                   //...
                  }

                  ?

                    +14
                    Мое мнение: это худшее, что может произойти с java.

                    Джависты очень ревниво относятся к синтаксису языка и очень неохотно затаскивают даже элементарный сахар. А тут не просто сахар, а целый язык шаблонов предлагаете. Очень сомнительно, что сделают нечто подобное.
                      +5

                      Такого не будет, не переживайте.

                        +4
                        Джависты очень ревниво относятся к синтаксису языка и очень неохотно затаскивают даже элементарный сахар.

                        С одной стороны вроде да, а с другой стороны Ломбок большинство разработчиков затащило только в путь.

                          +1

                          Разработчики языка не равны пользователям языка. Ну и насчёт большинства ещё неизвестно.

                            +3

                            Расстояние между разработчиками Lombok и пользователями языка Java гораздо меньше, чем между разработчиками Kotlin или Scala и пользователями этих языков.


                            А что касается большинства, я конечно никогда не видел результатов опросов, но могу отослать к выступлениям таких авторитетов, как Барух Садогурский или Евгений Борисов. По ним создаётся ощущение, что Lombok повсюду уже давно и надолго.

                            +1
                            Ломбок используют, чтобы спрятать простыни геттеров/сеттеров и конструкторы т.е. всякий бойлерплейт. В синтаксис языка он не лезет, разве что val добавили.

                            По смыслу Ломбок мало отличается от butterknife или di типа dagger. Просто еще одна «библиотека», которая делает какую-то полезную магию.

                            В проектах, где есть особые требования к качеству кода, обычно ломбок, DI и т.п не используют.
                              +1
                              В проектах, где есть особые требования к качеству кода, обычно ломбок, DI и т.п не используют.

                              А как DI снижает качество кода?

                                0
                                Мнение про DI часто слышал в разных командах и особо сильно в это не вникал. Как я понял проблема не в самом DI, а в библиотеках типа dagger за их архитектуру и проблемы.
                                  +1

                                  В том-то и дело, что внедрение зависимостей и управление ими сильно улучшает качество кода, т.к. очищает его от инфраструктуры.

                          +3

                          Никто вам не запрещает написать собственный препроцессор для разворачивания таких конструкций и прочих макросов. Через Gradle или maven вопрос решается элементарно.

                            +2

                            Но это уже будет не Java, а что-то другое. Тогда лучше перейти на другой язык, где есть уже инструментальная поддержка.

                              +1
                              Но это уже будет не Java, а что-то другое.

                              Авторов и пользователей Lombok это не остановило, а скорее вдохновило ))


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

                              Я очень часто встречаюсь с рекомендациями не использовать Lombok, а перейти на Kotlin. Потому что Lombok для джавы чужеродный, а в Kotlin фичи уже есть.


                              И пока что мне ещё никто не ответил, как в Kotlin решаются проблемы с Entity или как сделать билдер, или как преобразовать неизменяемый объект в билдер, а потом из билдера получить новый объект. Или ещё 100500 юскейсов которые отрабатывает Lombok.


                              Но допустим Kotlin, или какой-то другой язык завтра выровняется по фичам с Lombok. Послезавтра какой-нибудь разработчик добавит в Lombok генератор equals и hashCode для Entity и Lombok опять убежит вперёд. А когда это всё появится в другом языке совершенно неизвестно. Да и вообще — появится ли? Если экстраполировать будущее исходя из текущего положения дел — то очевидно, что нет.

                                +1

                                Это всё долгий и отдельный разговор, но


                                И пока что мне ещё никто не ответил, как в Kotlin решаются проблемы с Entity или как сделать билдер, или как преобразовать неизменяемый объект в билдер, а потом из билдера получить новый объект. Или ещё 100500 юскейсов которые отрабатывает Lombok.

                                В Котлине билдеры не нужны вообще. Для этого есть copy.

                                  0
                                  В Котлине билдеры не нужны вообще. Для этого есть copy.

                                  Вот о чём-то таком я и говорил. copy есть, но для Entity ничего нет и в Lombok или чём-то подобном оно может появиться, а в Kotlin — врядли. Разговор, конечно, долгий и отдельный и самое главное очевидного и простого ответа найти, скорее всего, не получится.

                                  0
                                  И пока что мне ещё никто не ответил, как в Kotlin решаются проблемы с Entity


                                  Интересно. А можно проблему с Entity описать?
                                    0
                                    1. Entity должны быть изменяемыми
                                    2. Должен существовать конструктор по умолчанию
                                    3. equals должен быть заявзан только на @Id или только на естественный ключ
                                    4. Если equals завязан на id, который делается генератором, то он должен возвращать false, если @Id == null, а hashCode должен возвращать 31.

                                    Lombok помогает с пунктами 1 и 2, а также 3.

                                      0
                                      Наверное я упускаю какой то контекст. А апелляция к чему? К тому, что дата классы не могут быть использованы в качестве Entity? Вот прям только что использовал дата классы для того чтобы работать со Spring Data — ничто не помешало.
                                        0
                                        Наверное я упускаю какой то контекст. А апелляция к чему?

                                        К тому, что использовать дата классы в качестве Entity не проще, чем использовать в качестве Entity обычные классы. И тут врядли что-то изменится.


                                        А сделать библиотеку, которая с помощью генерации кода решает эту проблему можно сделать в любой момент и работать будет для любой версии джавы, поддерживающей annotation processing.


                                        Вот прям только что использовал дата классы для того чтобы работать со Spring Data — ничто не помешало.

                                        Только пришлось обозначить поля как изменяемые, вручную написать конструктор без параметров, вручную написать hashCode и equals, а так же toString, правильно?


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

                                          0
                                          > вручную написать hashCode и equals, а так же toString, правильно?

                                          Нет. Примерно вот так:

                                          @Table("MESSAGES")
                                          data class Message(
                                              val content: String,
                                              val contentType: ContentType,
                                              val sent: Instant,
                                              val username: String,
                                              val userAvatarImageLink: String,
                                              @Id var id: String? = null)


                                          или это не по фен-шую? Не все требования выполняются, да, наверное в каких-то ситуациях надо будет писать свой equals и hashCode.
                                            0
                                            или это не по фен-шую?

                                            Не по фен шую )). Конструктора без параметров нет. И ещё val это опечатка наверное, да? Или это вообще не Entity и к Jpa никак не относится?


                                            Не все требования выполняются, да, наверное в каких-то ситуациях надо будет писать свой equals и hashCode.

                                            Как будут использоваться Entity в будущем предугадать невозможно, поэтому если не сделать equals и hashCode сразу, то потом замучаешься искать причины непонятного поведения в самые неподходящие моменты.

                                              0
                                              > И ещё val это опечатка наверное, да? Или это вообще не Entity и к Jpa никак не относится?

                                              Используется со Spring Data. JPA — так сразу и надо было сказать :)
                                              Для JPA дата классы конечно же не очень хорошо подходят.
                                                +1
                                                Используется со Spring Data. JPA — так сразу и надо было сказать :)

                                                У меня слово Entity с большой буквы в связке с джавой ассоциируется только с Jpa. Профдеформация наверное )))

                                                  0
                                                  У меня слово Entity с большой буквы в связке с джавой ассоциируется только с Jpa. Профдеформация наверное )))

                                                  Обнимаю!

                                    +1
                                    Ломбок — альфа версия потенциально полезного сахара.
                                    Котлин — бета.
                                    Джава уже релиз.
                                  0

                                  Вот только у IDE после подобных упражнений начнутся проблемы. И придется после этого писать для нее плагин. И скорее всего, это будет плагин для IDEA, который тоже нужно будет поддерживать. Кому-тот придется расширять файлы синтаксиса для vim, а синтакстическим плагинам вима тоже голову будет переодически сносить. Так что если что-то подобное иметь, то из из коробки либо не иметь вовсе.

                                    0

                                    Любителей Lombok это не особо смущает.

                                +1

                                Я бы предпочел использовать специализированные стримы.


                                for (int i: IntStream.range(0, 10)) {
                                    System.out.println(i);
                                }

                                И еще я бы хотел опцию компилятора -XX:+TurnOffAutoBoxingUnboxingCompletely.

                                  –5

                                  Добавьте пожалуйста вариант в голосование "Пишу на Kotlin/Scala/Clojure/Other-JVM и мне всё-равно что там в ванильной Java"

                                    +6

                                    Это последний пункт.

                                    +3

                                    (*) Подожду Вальгаллу, чтобы уж наверняка!

                                      +5

                                      Я как человек с компиляторным бэкграундом и хорошо знающий, как работает JIT с его куриными мозгами, не стал бы в критическом коде писать новомодный синтаксис, а всё бы развернул руками. Ещё есть Android (а так же различные способы запустить Java-приложения на iOS) и там уж точно всё работает совсем не так. Так что если нужно писать кроссплатформенный код (например, библиотеку, которую можно использовать на бэкэнде и на Android), да ещё и критический по производительности, я бы точно воспользовался старым добрым синтаксисом. Такие мелочи не особо влияют на читаемость кода, а IDE позволяет быстро его писать. Вот что действительно плохо влияет на читаемость кода — это более глобальные вещи, вроде правильных абстракций, соблюдения принципов вроде SOLID и т.д., а не какие-то отдельно взятые циклы.

                                        +2

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

                                          0

                                          Кстати очень хороший поинт. Хотелось бы посмотреть как развертка будет работать для ARM, а так же как оно будет работать после компиляции в нативный код.

                                            +1

                                            Такие преобразования выполняются на платформенно-независимом IR задолго до кодогенерации, поэтому на ARM проблем быть не должно. Но если у кого-нибудь есть ARM под рукой, можете измерить. Что касается компиляции в нативный код — если это делать с помощью GraalVM, то она щёлкает боксинг как орехи уже давно. Там эскейп-анализ и скаляризация наголову выше, чем в C2. Они даже хвастались, что Objects.hash(i, j, k) будет работать с той же скоростью как и явный аналог типа ((31+i)31+j)31+k, то есть они и от массива-варарга, и он боксов в каждом элементе избавляются на раз.

                                          0
                                          Я лично не люблю доморощенный синтаксический сахар. Может быть намерения благие, но в результате придет на проект молодое поколение spring, либо будут спотыкаться каждый раз и думать, «что за хрень», либо даже не обратят внимания и будут писать по своему или того хуже выдумают еще вариант, как удобнее. Причем может быть 100 лет тому назад это была прогрессивная идея, а сейчас страшно даже подумать, что придется поменять в коде, что бы эту краказябру (не вашу конкретно, а в общем) поменять на эквивалент из стандартной библиотеки.

                                          Недавно видел класс, обертка вокруг jdbc из допотопных времен. Чуть ли не предтеча hibernate. Там жуть. 20 лет назад это наверное сокращало написание кода, а сейчас spring все те же проблемы решает аннотацией и парой строк. И не переделать ничего уже, так как ни бюджета ни желания, а софту еще лет 20 жизни предрекли. Переписывать никто не будет.
                                            0
                                            Ну не знаю, заменить простой и понятный for (int i = 0...) цикл на кастомный метод, по-моему так себе разумная идея. Как написали в комментариях выше — очень легко забыть об этом кастомном методе, да и понять его с первого взгляда непросто. Да я и сам через полгода не работы над проектом забыл бы о нем, а что говорить о джунах…
                                              0

                                              Вы как-то сильно недооцениваете память программистов. В любом большом проекте есть тысячи внутренних методов, и знакомый с проектом программист обычно помнит и понимает, какой из них что делает, когда читает код. Редко когда требуется заглянуть в документацию. Да и в стандартной библиотеке тысячи методов, и в используемых библиотеках тоже. Пара новых ничего не усложняют. Конечно, было бы удобнее, если бы они появились в OpenJDK, но и свой утилитный класс это не большая проблема. При написании кода можно, кстати, стандартные шаблоны IDE переопределить, чтобы использовать новые методы автоматически.

                                              0
                                              Ответ на опрос: использую range(from, to), когда мне нужно использовать i в лямбде внутри тела цикла. В остальных случаях пишу обычный for (int i = ...).

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

                                              Самое читаемое