image


В Java 8 кардинально переработали процедуру вывода типов выражений. В спецификации появилась целая новая глава на эту тему. Это весьма сложная штука, изложенная скорее на языке алгебры, чем на языке нормальных людей. Не каждый программист готов в этом разобраться. Я, разработчик IDE, которому приходилось ковыряться в соответствующем коде, к своему стыду тоже довольно плохо разбираюсь в этой теме и понимаю процесс только по верхам. Причём сложно не только мне, но и авторам компилятора Java. После выхода Java 8 обнаружились десятки багов, когда поведение компилятора не соответствовало спецификации, либо текст спецификации был неоднозначен. В средах разработки для Java ситуация обстояла не лучше: там тоже были баги, причём другие, поэтому код мог отображаться ошибочным в вашей IDE, но успешно компилироваться. Или наоборот. С годами ситуация улучшилась, многие баги исправили, хотя всё ещё в спецификации остались тёмные углы.


Если вы просто пишете на Java, вам в целом необязательно знать в деталях, как это всё работает. В большинстве случаев либо результат вывода типов соответствует вашей интуиции, либо вывод типов не работает вообще, и надо ему помочь. Например, указав типы-аргументы в <угловых скобках> при вызове метода, либо указав явно типы параметров лямбды. Тем не менее есть некоторый начальный уровень этого тайного знания, и его несложно освоить. Овладев им, вы будете лучше понимать, почему компилятор не всегда может вывести то, что вы хотите. В частности, вы сможете ответить на вопрос, который мне часто задавали в той или иной форме: какие из следующих строчек не компилируются и почему?


Comparator<String> c1 = Comparator.comparing(String::length).reversed();
Comparator<String> c2 = Comparator.comparing(s -> s.length()).reversed();
Comparator<String> c3 = Collections.reverseOrder(Comparator.comparing(s -> s.length()));

Первое важное знание: в Java есть два типа выражений (JLS §15.2). Первый тип — «автономные выражения» (standalone expression), а второй — «поли-выражения» (poly expression). Тип автономных выражений вычисляется, глядя исключительно на само выражение. Если выражение автономное, совершенно неважно, в каком оно встретилось контексте, то есть что вокруг этого выражения. Для поли-выражений контекст важен и может влиять на их тип. Если поли-выражение вложено в другое поли-выражение, то фактически выбирается самое внешнее из них, и для него запускается процесс вывода типов. По всем вложенным поли-выражениям собираются ограничения (constraints). Иногда к ним добавляется целевой тип. Например, если поли-выражение — это инициализатор переменной, то тип этой переменной является целевым и тоже включается в ограничения. После этого выполняется редукция ограничений и определяются типы для всех поли-выражений сразу. Скажем, простой пример:


Comparator<String> c2 = Comparator.comparing(s -> s.length());

Здесь лямбда является поли-выражением. Вообще лямбды и ссылки на методы всегда являются поли-выражениями, потому что их нужно отобразить на какой-то функциональный интерфейс, а по содержимому лямбды вы никогда не поймёте, на какой. Вызов метода Comparator.comparing тоже является поли-выражением (ниже мы поймём, почему). У лямбды надо определить точный функциональный тип, а у Comparator.comparing — значения типовых параметров T и U. В процессе вывода устанавливается, что


  • T = String
  • U = Integer
  • Тип лямбды = Function<String, Integer>
  • Тип параметра s = String

Только некоторые выражения в Java могут быть поли-выражениями. Вот их полный список (на момент Java 17):


  • Выражения в скобках
  • Создание нового объекта (new)
  • Вызов метода
  • Условные выражения (?:)
  • switch-выражения (те что в Java 14 появились)
  • Ссылки на методы
  • Лямбды

Но «могут» не значит «должны». Могут быть, а могут и не быть. Проще всего со скобками: они наследуют «полистость» выражения, которое в скобках. В остальных случаях важен контекст.


Контексты определяются в пятой главе спецификации. Нам будут интересны только три из них:


  • Контекст присваивания (assignment context) — это контекст, при котором автоматически выполняется преобразование присваивания. Включает в себя инициализацию переменной (кроме переменной с неявным типом var), оператор присваивания, а также возврат значения из метода или лямбды (как с использованием return, так и без).
  • Контекст вызова (invocation context) — аргумент вызова метода или конструктора.
  • Контекст приведения (cast context) — аргумент оператора приведения типа.

Для определения контекста можно подниматься через скобки, условные операторы и switch-выражения. Поли-выражения могут быть только в контексте присваивания и контексте вызова. Для лямбд и ссылок на методы дополнительно разрешён контекст приведения. В любых других контекстах использование лямбд и ссылок на методы недопустимо вообще. Это правило, кстати, приводит к интересным последствиям:


Runnable r1 = () -> {}; // можно
Runnable r2 = true ? () -> {} : () -> {}; // можно
Object r3 = (Runnable)() -> {}; // можно
Object r4 = (Runnable)(true ? () -> {} : () -> {}); // нельзя!
Object r5 = true ? (Runnable)() -> {} : (Runnable)() -> {}; // можно

Условный оператор во второй строке является поли-выражением, потому что он в контексте присваивания. Поэтому он может посмотреть наружу и увидеть, что результат должен быть типа Runnable, а значить использовать эту информацию для вывода типов веток и в итоге присвоить обеим лямбдам тип Runnable. Однако четвёртая строчка в таком виде не работает, несмотря на большое сходство. Здесь условный оператор true ? () -> {} : () -> {} находится в контексте приведения, что по спецификации делает его автономным выражением. Поэтому мы не можем выглянуть за его пределы и увидеть тип Runnable, а значит мы не знаем, какой тип назначить лямбдам — возникает ошибка компиляции. В этом случае придётся переносить приведение типов в каждую ветку условного оператора (или не писать такой код вообще).


Не только контекст, но и вид выражения может влиять на полистость. Например, выражение new может быть поли-выражением (в соответствующем контексте), только если используется оператор «ромб» (new X<>(), JLS §15.9). В противном случае тип результата всё равно однозначен и нет смысла усложнять компиляцию. Аналогичная мысль применяется к выражениям вызова метода, только это приводит к более сложным условиям (JLS §15.12):


  • Мы вызываем generic-метод
  • Этот generic-метод упоминает хотя бы один из своих типовых параметров в возвращаемом типе
  • Типы-аргументы не заданы явно при вызове в <угловых скобках>

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


Интересная история с условным оператором (JLS §15.25). Сначала в зависимости от типов выражений в ветках выясняется разновидность оператора: это может быть булев условный оператор, числовой условный оператор или ссылочный условный оператор. Только ссылочный условный оператор может быть поли-выражением, а булев и числовой всегда автономные. С этим связано много странностей. Вот например:


static Double get() {
  return null;
}

public static void main(String[] args) {
  Double x = true ? get() : 1.0;
  System.out.println(x);
}

Здесь типы веток условного оператора — конкретно Double и конкретно double. Это означает, что условный оператор числовой (numeric conditional expression, JLS §15.25.2), то есть автономный. Соответственно, мы не смотрим наружу, нас не волнует, что мы присваиваем результат в объектный Double. Мы определяем тип только по самому оператору, и этот тип — примитивный double. Соответственно, для балансировки типов добавляется unboxing левой ветки, а потом для присваивания добавляется снова boxing:


Double x = Double.valueOf(true ? get().doubleValue() : 1.0);

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


Ситуация в корне меняется, если мы объявим метод get() по-другому:


static <T> T get() { ... }

Теперь в одной из веток не числовой тип Double, а неизвестный ссылочный тип T. Весь условный оператор становится ссылочным (reference conditional expression, JLS §15.25.3), соответственно становится поли-выражением и может посмотреть наружу, на целевой тип Double и использовать именно его как целевой тип веток. В итоге обе ветки успешно приводятся к типу Double, для чего добавляется boxing в правой ветке:


Double x = true ? get() : Double.valueOf(1.0);

Теперь программа успешно печатает null и завершается. Такие нестыковки обусловлены историческими причинами и необходимостью совместимости. В первых версиях Java никаких поли-выражений не было, все были автономными, поэтому надо было выкручиваться, и выкручивались не всегда идеально. К счастью, это не распространяются на более новые switch-выражения. Для них нет дополнительных условий на полистость кроме контекста, поэтому такой код вполне ожидаемо печатает null вместо падения с исключением:


static Double get() {
    return null;
}

public static void main(String[] args) {
    Double x = switch (0) {
        case 0 -> get();
        default -> 1.0;
    };
    System.out.println(x);
}

Вернёмся к нашему примеру с компараторами. Я раскрою карты: второй вариант не компилируется.


Comparator<String> c1 = Comparator.comparing(String::length).reversed(); // можно
Comparator<String> c2 = Comparator.comparing(s -> s.length()).reversed(); // нельзя
Comparator<String> c3 = Collections.reverseOrder(Comparator.comparing(s -> s.length())); // можно

Вот главное, что следует запомнить:


Квалификатор никогда не находится в контексте присваивания или контексте вызова, поэтому не может быть поли-выражением.

В первых двух строчках у нас есть квалификаторы: Comparator.comparing(String::length) и Comparator.comparing(s -> s.length()). При определении типа квалификатора мы не можем смотреть на то что происходит вокруг, нам остаётся пользоваться только самим содержимым квалификатора.


Comparator.comparing возвращает Comparator<T>, принимая функцию Function<? super T, ? extends U>, и нам необходимо определить значения T и U. В случае со ссылкой на метод у нас есть дополнительная информация: ссылка однозначно указывает на метод length() в классе String. соответственно, выводу типов хватает этого, чтобы понять, что T = String и U = Integer. Однако в случае с лямбдой у нас нет никаких указаний на то что s — это строка. Соответственно, у нас нет ограничений на T, а значит в соответствии с правилами редукции выбирается максимально общий тип: T = Object. Далее запускается анализ тела лямбды и мы обнаруживаем, что у класса Object нет метода length(), из-за чего компиляция останавливается. Вот такое, кстати, бы сработало, потому что hashCode() в объекте есть:


Comparator<Object> cmp = Comparator.comparing(s -> s.hashCode()).reversed();

Понятно и почему работает строчка c3. Так как Comparator.comparing здесь в контексте вызова, мы можем подняться наверх и добраться до контекста присваивания, а значит, использовать целевой тип Comparator<String>. Тут вывод сложнее, потому что есть ещё переменная типа в методе reverseOrder. Тем не менее компилятор справляется и успешно всё выводит.


Как починить c2, если всё-таки хочется использовать квалификатор? Мы уже знаем достаточно, чтобы понять, что вот это не сработает:


Comparator<String> c2 = 
  ((Comparator<String>)Comparator.comparing(s -> s.length())).reversed();

Приведение типа не чинит результат вызова метода, так как контекст приведения не действует на вызовы. Зато приведение типа срабатывает с лямбдой, хотя и выглядит ужасно:


Comparator<String> c2 = 
  Comparator.comparing((Function<String, Integer>) s -> s.length()).reversed();

Вариант проще: сделать вызов метода автономным. Для этого надо добавить типы-аргументы. В итоге тип вызова comparing устанавливается однозначно, и из него уже выводится тип лямбды:


Comparator<String> c2 = 
  Comparator.<String, Integer>comparing(s -> s.length()).reversed();

Ещё проще в данном случае — явно указать тип параметра лямбды. Тут у нас вызов comparing по-прежнему является поли-выражением, но появляется ограничение на тип s, и его хватает, чтобы вывести всё остальное правильно:


Comparator<String> c2 = 
  Comparator.comparing((String s) -> s.length()).reversed();

Можно ли было распространить вывод типов на квалификаторы, чтобы c2 работало без дополнительных подсказок компилятору? Возможно. Но, как я уже говорил, процедура вывода типов и так невообразимо сложная. В ней и так до сих пор есть тёмные места, а даже когда она правильно работает, она может работать ужасно долго. К примеру, возможно написать относительно несложный код, который создаст сотню ограничений и поставит на колени и IDE, и компилятор javac, потому что реализация вывода типов может быть полиномом довольно высокой степени от количества ограничений. Если мы в этот замес добавим квалификаторы, всё станет сложнее на порядок, ведь они будут интерферировать со всем остальным. Также возникнут проблемы из-за того, что мы можем вообще толком не знать, какой метод какого класса мы пытаемся вызвать. Например:


<T> T fn(UnaryOperator<T> op) {
  return op.apply((T) " hello "); // грязно, но имеем право!
}

String s = fn(t -> t).trim();

Если мы выводим тип квалификатора fn(t -> t) вместе с типом всего выражения, то мы даже не знаем, у какого класса вызывается метод trim(). Нам подходит любой метод trim() в любом классе, который не принимает аргументов и возвращает строку. Например, метод String.trim() подойдёт. Или ещё какой-нибудь. У этого уравнения может быть много решений. Придётся как-то отдельно обговаривать в спецификации такие случаи. Так или иначе, я не был бы счастлив заниматься поддержкой данной возможности в IDE.

Комментарии (56)


  1. BugM
    13.11.2021 18:31
    +2

    Не хватает заголовка как на N+1.

    Сложность - 9/10


  1. orionll
    13.11.2021 18:49
    +1

    А ещё есть пример Comparator.comparing(String::toLowerCase), который почему-то не компилируется в Java


    1. ya_ne_znau
      13.11.2021 22:15

      Comparator.comparing требует либо ToIntFunction (что происходит при String::length), либо Function с возвратом чего-то сравнимого: <T, U extends Comparable<U>>comparing(Function<? super T, ? extends U> keyExtractor)

      Как понятно из String::toLowerCase, он возвращает String, который, кажется, не является Comparable

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


      1. orionll
        13.11.2021 22:41
        +3

        String является Comparable


        1. akhmelev
          14.11.2021 00:51

          То что вы хотите можно получить либо так
          Comparator.comparing(s->s.toString().toLowerCase()) //из Object
          либо так
          Comparator<String> comparator=Comparator.comparing(String::toLowerCase); //из String

          В лоб (без контекста, т.е. в смысле без самого объекта) из Comparator.comparing(String::toLowerCase) компилятору ничего вывести нельзя, т.к. самих этих toLowerCase - много, и непонятно на какой именно вы ссылаетесь. А если ::length или ::hashcode или ::toString - то тут такой неоднозначности не возникает, у этих методов нет перегрузки и компилятор понимает объект какого типа имелся ввиду, и какой из методов этого объекта нужен для извлечения признака (keyExtractor).

          Но вообще представить себе где может понадобиться такой код Comparator.comparing(что-угодно) без присваивания или передачи куда-либо довольно трудно. Даже наверное невозможно.


          1. orionll
            14.11.2021 12:31
            +1

            Перегрузка тут никак не мешает, потому что к Function<T, U> применима только String.toLowerCase(), но не String.toLowerCase(Locale).

            А кто сказал, что я никуда не присваиваю? Если я хочу написать Comparator<String> cmp = Comparator.comparing(String::toLowerCase).reversed(), то работать не будет аналогично.


            1. pwflamy
              14.11.2021 13:13

              А вы с какой jdk пробуете скомпилировать этот пример?


            1. akhmelev
              14.11.2021 15:56

              Я думаю, что именно без присваивания компилироваться не будет. C .reversed() тоже не будет.

              Без .reversed(), но с контекстом - будет. С .reversed(), но в следующем операторе тоже будет (т.к. тип уже выведен).

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


              1. pwflamy
                14.11.2021 18:19

                1. Comparator.comparing(String::toLowerCase) - не компилируется "Reference to 'toLowerCase' is ambiguous, both 'toLowerCase(Locale)' and 'toLowerCase()' match";

                2. Comparator<String> cmp = Comparator.comparing(String::toLowerCase) - компилируется;

                3. Comparator<String> cmp = Comparator.comparing(String::toLowerCase).reversed() - не компилируется, причина как в 1 пункте


                1. akhmelev
                  14.11.2021 18:40

                  Да, спасибо. И так тоже должно скомпилироваться (хоть это и банально):
                  Comparator<String> comparing = Comparator.comparing(String::toLowerCase);
                  comparing = comparing.reversed();


    1. tagir_valeev Автор
      14.11.2021 03:33
      +2

      Специально же попросил не трогать перегруженные методы, чтобы не открывать портал в ад.


      1. akhmelev
        14.11.2021 10:01

        Тьфутынуты. Я уж было подумал человек реально не понимает, что происходит, а вы тут рофлите в междусобойчиках. Нехорошо :D


  1. pwflamy
    16.11.2021 01:31

    Поясните, пожалуйста, что имеется в виду под "квалификатором" ?


  1. alxt
    16.11.2021 09:49

    Мда, про перегруженные методы ещё Мейер писал, что это несовместимо с ООП.

    Но создатели java не читатели были :(


    1. tagir_valeev Автор
      16.11.2021 09:53
      +2

      Ваш комментарий очень фанатично звучит. Как будто есть единственное тру-ООП, а всё остальное - ересь. На самом-то деле нет никакого правильного ООП, есть удобные и неудобные инструменты в программировании. И не надо думать, что вот деды знали как правильно, а мы все знания растеряли. Прогресс не стоит на месте, мир программирования меняется. Раньше вон визиторы были вполне себе тру-ООП, а теперь этот паттерн практически изжил себя.


      1. alxt
        16.11.2021 09:58

        Конечно нет никакого тру-ООП.

        Просто перегруженные методы это, по сути своей, аналог синтаксического сахара. Позволяет вместо 5 методов с разными именами написать 5 перегруженных. Собственно ничего более это не даёт.

        Зато создаёт проблемы- как в выводе типов, так и с null'ами.

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


        1. tagir_valeev Автор
          16.11.2021 12:06
          +2

          Да, это синтаксический сахар! Вы так говорите будто это что-то плохое. Половина Джавы - синтаксический сахар. Кому не нужен сахар, тот на Лиспе пишет :-) Это весьма полезная возможность, особенно в отсутствии дефолт-параметров. А для конструирования объектов так вообще необходимая, так как у конструкторов нет имени.


          1. alxt
            16.11.2021 13:19

            Вы так говорите будто это что-то плохое. 

            Всё имеет свою цену. Сахар тоже. Когда он приводит к запутанности вывода типов (и это не только компилятор/анализатор путается, люди тоже) - это дорогая цена.

            Это весьма полезная возможность, особенно в отсутствии дефолт-параметров

            Вот, кстати, лучше б дефолт-параметры были.

            А для конструирования объектов так вообще необходимая, так как у конструкторов нет имени.

            А кто их запрещал иметь? Ну кроме зацикленности авторов на С++


            1. tsypanov
              18.11.2021 00:52
              +1

              Когда он приводит к запутанности вывода типов (и это не только компилятор/анализатор путается, люди тоже) - это дорогая цена.

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

              Вот, кстати, лучше б дефолт-параметры были.

              Мне кажется, вы тут себе же противоречите, ведь параметры по умолчанию по сути делают любой метод с параметром перегруженным, ведь вы можете вызвать его с аргументом или без (тогда подставится параметр по умолчанию). И представьте, насколько это усложнит чтение кода: допустим, что вы видите обращение к некоему методу впервые, и в этот метод не передаётся аргумент. Теперь как понять, объявлен ли этот метод без аргументов, или с аргументом (без его явного указания берётся значение по умолчанию).


              1. BugM
                18.11.2021 01:58

                Теперь как понять, объявлен ли этот метод без аргументов, или с аргументом (без его явного указания берётся значение по умолчанию).

                Среда разработки тут поможет.

                А вот с перегрузкой будут большие проблемы.


                1. tsypanov
                  18.11.2021 11:29
                  +1

                  Среда разработки тут поможет.

                  А что делать в процессе вычитки кода на том же Гитхабе?

                  А вот с перегрузкой будут большие проблемы.

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


                  1. BugM
                    18.11.2021 12:24

                    Дописать Гитхаб. Встроенный веб VsCode это движение в эту сторону.

                    Если бы всего две. N^2 сигнатур там. И это проблема почти полностью запрещающая обычную перегрузку.


                    1. tsypanov
                      18.11.2021 15:24

                      Дописать Гитхаб.

                      Язык должен быть самодостаточным. Если для понимания написанного нужно пользоваться дополнительными инструментами, то это стрёмный язык, ИМХО.


                      1. BugM
                        18.11.2021 20:43

                        Любимые всеми var, auto и подобное уже часто сложно читаемы без помогающей среды разработки.

                        Про более сложные случаи многоуровневых лямбд я и не говорю.

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


                      1. MediaNik5
                        19.11.2021 05:08
                        +2

                        Если вы используете var и делаете ваш код нечитаемым -- не делайте так. var нужно использовать там, где тип очевиден.


                      1. Maccimo
                        20.11.2021 13:47
                        +1

                        Запрет var на уровне линтера в pre-commit hook значительно улучшает читаемость.


                      1. MediaNik5
                        20.11.2021 13:56
                        +1

                        HashMap<String, List<String>> temporaryMap = new HashMap<>();
                        var temporaryMap1 = new HashMap<String, List<String>>();
                        
                        MyObject object = new MyObject();
                        var object1 = new MyObject();
                        
                        MethodHandle methodHandle = parseMethodHandle(sourceOfMethodHandle);
                        var methodHandle1 = parseMethodHandle(sourceOfMethodHandle);

                        Ну и чем здесь var хуже явного указания типа? Это особенно удобно при очевидном длинном типе.


                      1. Maccimo
                        20.11.2021 14:25
                        +1

                        • Даже видя место декларации переменной вы в общем случае не можете предсказать тип переменной без помощи IDE или похода к месту декларации метода из правой части выражения.
                        • Фактический тип будет совпадать с типом правой части присвоения, что не всегда нужно.
                        • Поведение может преподнести сюрпризы: Заменить Object на var: что может пойти не так?

                        Думаю, достаточно.


                      1. BugM
                        20.11.2021 15:34
                        +1

                        Зря вы так. Есть куча очевидных мест где var лучше. Типы бывают очень длинные и при этом очевидные. Зачем читать больше? Линтер глупый и такие места не обнаружит.


                      1. Maccimo
                        20.11.2021 16:00

                        Длину идентификаторов можно ограничить тем же линтером и корпоративными style guide.


                        Зачем читать больше?

                        Из двух альтернатив «читать больше» и «читать меньше, но затем выводить типы в уме» я выбираю первую, как менее затратную. Тем паче, что по складам слова только в начальной школе читают.


                      1. BugM
                        20.11.2021 16:10

                        Длину идентификаторов можно ограничить тем же линтером и корпоративными style guide.

                        Ой. Вы точно на джаве код пишите? Так такие монстрики иногда бывают...

                        Из двух альтернатив «читать больше» и «читать меньше, но затем выводить типы в уме» я выбираю первую, как менее затратную.

                        Не надо в уме. Пользуйтесь IDE. Особенно в любом неочевидном случае.

                        Запретить можно все. Но зачем? В плюсах я могу понять запрет части конструкций. Но в джаве это выглядит очень странно.


                      1. Maccimo
                        21.11.2021 13:58
                        +1

                        Не надо в уме. Пользуйтесь IDE. Особенно в любом неочевидном случае.

                        Вот пришёл нам pull request в GitLab-e, чем тут IDE поможет?


                      1. BugM
                        21.11.2021 14:16

                        git checkout ...

                        Есть pr достаточно большой чтобы было непонятно смотреть его в IDE в любом случае удобнее.


                      1. Maccimo
                        21.11.2021 15:36

                        Использование var способствует увеличению числа случаев, когда сделать checkout действительно удобнее. О чём и речь.


                        Если, к примеру, в одной из последущих версий в язык вкорячат string interpolation, то картина станет ещё хуже. Зато ХХВП можно быстрее делать, да.


                      1. BugM
                        21.11.2021 16:24

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

                        И что теперь писать на синтаксисе jdk6? Язык развивается, игнорировать развитие и заставлять всех писать как 20 лет назад странно. И нанимать вам с таким подходом сложно будет.


                      1. tsypanov
                        22.11.2021 11:53

                        Var не нужно запрещать, как выше написал @MediaNik5

                        var нужно использовать там, где тип очевиден


                1. kacetal
                  19.11.2021 01:14

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


                  1. BugM
                    19.11.2021 01:24
                    +2

                    Возьмем самый что ни на есть учебный перегруженный метод.

                    Прямо с вики взял и косметически поправил:

                    double volume(int h, double r) {     return(3.14*r*r*h); }   // volume of a cuboid
                    long volume(int l, int b, int h) {     return(l*b*h); }

                    И сразу ой. С параметрами по умолчанию так не сделать.

                    Авторы любого языка всегда выбирают между дефолтными параметрами и перегрузкой. Перегрузка в общем полезнее. Я с ними согласен.


                    1. alxt
                      20.11.2021 07:49

                      А в чём проблема назвать их по-разному? У них разная суть - объём цилиндра и объём куба.

                      То, что второй использует целые числа - просто бред.


                      1. BugM
                        20.11.2021 13:49

                        Потому что я пошел на википедию в статью о перегрузке функций и взял пример оттуда https://en.wikipedia.org/wiki/Function_overloading

                        Назвать по разному это и есть запрет перегрузки. Можно, но это будет фундаментальное свойство языка. Которое изменить потом уже нельзя. Никогда. Вы же не хотите очередной Питон3?

                        Не факт что с таким свойством язык станет лучше.


                    1. Maccimo
                      20.11.2021 13:53

                      Авторы любого языка всегда выбирают между дефолтными параметрами и перегрузкой.

                      Технически, авторам ЯП не обязательно выбирать что-то одно. В Delphi были и параметры по умолчанию и перегрузка.


                      1. BugM
                        20.11.2021 15:38

                        Технически разрешить можно. Но это порождает кучу неочевидных мест где нельзя. Так даже еще хуже.


              1. alxt
                20.11.2021 07:47

                 Вы хоть раз сталкивались с реальными багами, которые вызваны выводом типов?

                Да. Много раз. А совместно с extension-методами в kotlin это создаёт ещё больше проблем.

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

                Нет.

                Потому что метод остаётся один. И в наследнике он будет ровно такой же.

                Смесь множественного (по факту дефолтных методов) наследования и перегрузки - это плохо.


                1. tsypanov
                  20.11.2021 15:26

                  Да. Много раз.

                  Не троллинга ради, но можно простенький пример?

                  Потому что метод остаётся один.

                  В объявлении метод действительно остаётся один, сигнатура написана единожды, здесь вопросов нет. Но как только доходит до исполнения, методов фактически два: с аргументом и без оного (подставляется значение по умолчанию). И если код написан не в ИДЕ, то такие методы сильно усложняют понимание написанного, ведь если аргумент явно не передаётся в метод, сигнатуру которого мы прямо сейчас не видим, то невозможно понять, есть ли там значение по умолчанию, или метод действительно без аргументов.

                  Смесь множественного (по факту дефолтных методов) наследования и перегрузки - это плохо

                  С точки зрения идеологической чистоты ООП - да. В жизни же рядового разраба методы по умолчанию сильно помогают.


                  1. alxt
                    22.11.2021 06:30

                    Не троллинга ради, но можно простенький пример?

                    Я ж писал - когда есть два метода с типами-интерфейсами и класс, который наследует оба интерфейса.

                    И если код написан не в ИДЕ, то такие методы сильно усложняют понимание написанного

                    Ну не сильно - потому что, опять же, всегда есть ровно один метод и его проще найти.

                    В жизни же рядового разраба методы по умолчанию сильно помогают.

                    Вы про перегузку? Можно пример, когда они помогают (т.е. никак нельзя назвать два метода разными именами)?


                    1. tsypanov
                      22.11.2021 11:52

                      Вы про перегузку?

                      Я про случай, когда у интерфейса есть много наследников, но всем им нужен одинаковый для всех метод, например, вычисляющий сложные проценты. Если у интерфейса нет методов по умолчанию, то остаётся либо написание абстрактного класса и наследование от него, что снижает гибкость, либо выносить функционал во внешней класс. Также см. https://stackoverflow.com/a/33721297/12473843


                      1. alxt
                        22.11.2021 11:54

                        Ничего плохого про методы по-умолчанию не скажу.

                        Я ругаю только перегрузку методов.


                      1. tsypanov
                        22.11.2021 12:35

                        А как без неё ООП сделать? Никак, ИМХО.


                      1. alxt
                        22.11.2021 12:37

                        Тут путаница в терминах. Я против перегузки: https://ru.wikipedia.org/wiki/%D0%9F%D0%B5%D1%80%D0%B5%D0%B3%D1%80%D1%83%D0%B7%D0%BA%D0%B0_%D0%BF%D1%80%D0%BE%D1%86%D0%B5%D0%B4%D1%83%D1%80_%D0%B8_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B9

                        Она не нужна для ОПП. Более того, это некоторая замена ООП для процедурных языков типа C (не C++).


        1. VGoudkov
          16.11.2021 16:09

          Были бы именованные параметры - не было бы большинства с перегрузкой. И шаблон Builder бы не потребовался. И ещё дофига всего. Но нет, Java - она не такая. Хотя на момент её (Java) пелёнок это уже лет пятнадцать как было много где. А ещё я иногда хочу перегружать по возвращаемому результату. Однако хрен там. Там же, где и наследование enum. Зла не хватает. Язык, на котором пишется куча бизнес-логики... и который не очень-то для этого предназначался при разработке, судя по всему


          1. Maccimo
            18.11.2021 02:52

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


            Были бы именованные параметры

            Все, кому это важно, пишут на питоне.


            И шаблон Builder бы не потребовался.

            Как бы вам помогло наличие именованных параметров в деле отказа от StringBuilder? :D


            Там же, где и наследование enum.

            В каком языке такое есть и для чего нужно?


            Зла не хватает.

            Не пора ли благородному дону в отпуск?


            1. VGoudkov
              18.11.2021 15:17

              Как бы вам помогло наличие именованных параметров в деле отказа от StringBuilder?

              Не этот. https://projectlombok.org/features/Builder + https://projectlombok.org/features/NonNull

              Там же, где и наследование enum.

              В каком языке такое есть и для чего нужно

              Про языки не знаю. Случай примерно следующий - класс X умеет обрабатыать что-то из множества A (т.е. множество - тип параметра функции), его наследник Y - умеет всё из A и ещё пару случаев. Хорошо бы расширить множество A в наследник B добавивив ещё пару вариантов и обрабатывать в Y уже его, чтобы получился статически проверяемый код, но нет. Я знаю, как такое собрать из static final членов того же класса + приватный конструктор, но это как раз из разряда извращений. И не поддерживается языковыми конструкциями

              Не пора ли благородному дону в отпуск?

              Пора. Уже года два как. Но некогда. И проблему говнокода из-за отсутствия фич это не решит - его проекте только прибавляется.

              PS: Видимо пора коммитить в lombok - не ждать так сказать милостей от природы


      1. Maccimo
        18.11.2021 02:24

        Что в этом сезоне модно использовать вместо посетителей?


        1. tsypanov
          18.11.2021 14:31

          Издатель-подписчик, например.


  1. tsypanov
    18.11.2021 14:47

    del


  1. therealalexz
    20.11.2021 22:31
    +2

    спасибо, хорошая статья