В 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)
orionll
13.11.2021 18:49+1А ещё есть пример
Comparator.comparing(String::toLowerCase)
, который почему-то не компилируется в Javaya_ne_znau
13.11.2021 22:15Comparator.comparing
требует либоToIntFunction
(что происходит приString::length
), либоFunction
с возвратом чего-то сравнимого:<T, U extends Comparable<U>>comparing(Function<? super T, ? extends U> keyExtractor)
Как понятно из
String::toLowerCase
, он возвращаетString
, который, кажется, не являетсяComparable
Так что здесь никакой магии нет, просто так и не должно быть.
orionll
13.11.2021 22:41+3String является Comparable
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(что-угодно) без присваивания или передачи куда-либо довольно трудно. Даже наверное невозможно.orionll
14.11.2021 12:31+1Перегрузка тут никак не мешает, потому что к Function<T, U> применима только String.toLowerCase(), но не String.toLowerCase(Locale).
А кто сказал, что я никуда не присваиваю? Если я хочу написать Comparator<String> cmp = Comparator.comparing(String::toLowerCase).reversed(), то работать не будет аналогично.
akhmelev
14.11.2021 15:56Я думаю, что именно без присваивания компилироваться не будет. C .reversed() тоже не будет.
Без .reversed(), но с контекстом - будет. С .reversed(), но в следующем операторе тоже будет (т.к. тип уже выведен).
На первый взгляд это конечно контринтуитивно, но Тагир как обычно все здорово же объяснил. Не могу отделаться от ощущения, что вы прикалываетесь. Буду, конечно, рад ошибиться.
pwflamy
14.11.2021 18:19Comparator.comparing(String::toLowerCase) - не компилируется "Reference to 'toLowerCase' is ambiguous, both 'toLowerCase(Locale)' and 'toLowerCase()' match";
Comparator<String> cmp = Comparator.comparing(String::toLowerCase) - компилируется;
Comparator<String> cmp = Comparator.comparing(String::toLowerCase).reversed() - не компилируется, причина как в 1 пункте
akhmelev
14.11.2021 18:40Да, спасибо. И так тоже должно скомпилироваться (хоть это и банально):
Comparator<String> comparing = Comparator.comparing(String::toLowerCase);
comparing = comparing.reversed();
tagir_valeev Автор
14.11.2021 03:33+2Специально же попросил не трогать перегруженные методы, чтобы не открывать портал в ад.
akhmelev
14.11.2021 10:01Тьфутынуты. Я уж было подумал человек реально не понимает, что происходит, а вы тут рофлите в междусобойчиках. Нехорошо :D
alxt
16.11.2021 09:49Мда, про перегруженные методы ещё Мейер писал, что это несовместимо с ООП.
Но создатели java не читатели были :(
tagir_valeev Автор
16.11.2021 09:53+2Ваш комментарий очень фанатично звучит. Как будто есть единственное тру-ООП, а всё остальное - ересь. На самом-то деле нет никакого правильного ООП, есть удобные и неудобные инструменты в программировании. И не надо думать, что вот деды знали как правильно, а мы все знания растеряли. Прогресс не стоит на месте, мир программирования меняется. Раньше вон визиторы были вполне себе тру-ООП, а теперь этот паттерн практически изжил себя.
alxt
16.11.2021 09:58Конечно нет никакого тру-ООП.
Просто перегруженные методы это, по сути своей, аналог синтаксического сахара. Позволяет вместо 5 методов с разными именами написать 5 перегруженных. Собственно ничего более это не даёт.
Зато создаёт проблемы- как в выводе типов, так и с null'ами.
А если сделать два метода с параметрами-интерфейсами, а потом сделать единый интерфейс-наследник - то тут сообщения об ошибках вместе с руганью разработчика могут вызвать Ктулху (я сам натыкался на это).
tagir_valeev Автор
16.11.2021 12:06+2Да, это синтаксический сахар! Вы так говорите будто это что-то плохое. Половина Джавы - синтаксический сахар. Кому не нужен сахар, тот на Лиспе пишет :-) Это весьма полезная возможность, особенно в отсутствии дефолт-параметров. А для конструирования объектов так вообще необходимая, так как у конструкторов нет имени.
alxt
16.11.2021 13:19Вы так говорите будто это что-то плохое.
Всё имеет свою цену. Сахар тоже. Когда он приводит к запутанности вывода типов (и это не только компилятор/анализатор путается, люди тоже) - это дорогая цена.
Это весьма полезная возможность, особенно в отсутствии дефолт-параметров
Вот, кстати, лучше б дефолт-параметры были.
А для конструирования объектов так вообще необходимая, так как у конструкторов нет имени.
А кто их запрещал иметь? Ну кроме зацикленности авторов на С++
tsypanov
18.11.2021 00:52+1Когда он приводит к запутанности вывода типов (и это не только компилятор/анализатор путается, люди тоже) - это дорогая цена.
Имхо, современная среда разработки + компилятор сильно облегчают жизнь. Вы хоть раз сталкивались с реальными багами, которые вызваны выводом типов?
Вот, кстати, лучше б дефолт-параметры были.
Мне кажется, вы тут себе же противоречите, ведь параметры по умолчанию по сути делают любой метод с параметром перегруженным, ведь вы можете вызвать его с аргументом или без (тогда подставится параметр по умолчанию). И представьте, насколько это усложнит чтение кода: допустим, что вы видите обращение к некоему методу впервые, и в этот метод не передаётся аргумент. Теперь как понять, объявлен ли этот метод без аргументов, или с аргументом (без его явного указания берётся значение по умолчанию).
BugM
18.11.2021 01:58Теперь как понять, объявлен ли этот метод без аргументов, или с аргументом (без его явного указания берётся значение по умолчанию).
Среда разработки тут поможет.
А вот с перегрузкой будут большие проблемы.
tsypanov
18.11.2021 11:29+1Среда разработки тут поможет.
А что делать в процессе вычитки кода на том же Гитхабе?
А вот с перегрузкой будут большие проблемы.
Аргументы по умолчанию - это по сути и есть перегрузка, т.к. для разработчика на деле существуют две сигнатуры.
BugM
18.11.2021 12:24Дописать Гитхаб. Встроенный веб VsCode это движение в эту сторону.
Если бы всего две. N^2 сигнатур там. И это проблема почти полностью запрещающая обычную перегрузку.
tsypanov
18.11.2021 15:24Дописать Гитхаб.
Язык должен быть самодостаточным. Если для понимания написанного нужно пользоваться дополнительными инструментами, то это стрёмный язык, ИМХО.
BugM
18.11.2021 20:43Любимые всеми var, auto и подобное уже часто сложно читаемы без помогающей среды разработки.
Про более сложные случаи многоуровневых лямбд я и не говорю.
Возможность удобно читать код без помогающей среды разработки уже утрачена и все довольны. Можно продолжать.
MediaNik5
19.11.2021 05:08+2Если вы используете var и делаете ваш код нечитаемым -- не делайте так. var нужно использовать там, где тип очевиден.
Maccimo
20.11.2021 13:47+1Запрет
var
на уровне линтера в pre-commit hook значительно улучшает читаемость.
MediaNik5
20.11.2021 13:56+1HashMap<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 хуже явного указания типа? Это особенно удобно при очевидном длинном типе.
Maccimo
20.11.2021 14:25+1- Даже видя место декларации переменной вы в общем случае не можете предсказать тип переменной без помощи IDE или похода к месту декларации метода из правой части выражения.
- Фактический тип будет совпадать с типом правой части присвоения, что не всегда нужно.
- Поведение может преподнести сюрпризы: Заменить Object на var: что может пойти не так?
Думаю, достаточно.
BugM
20.11.2021 15:34+1Зря вы так. Есть куча очевидных мест где var лучше. Типы бывают очень длинные и при этом очевидные. Зачем читать больше? Линтер глупый и такие места не обнаружит.
Maccimo
20.11.2021 16:00Длину идентификаторов можно ограничить тем же линтером и корпоративными style guide.
Зачем читать больше?
Из двух альтернатив «читать больше» и «читать меньше, но затем выводить типы в уме» я выбираю первую, как менее затратную. Тем паче, что по складам слова только в начальной школе читают.
BugM
20.11.2021 16:10Длину идентификаторов можно ограничить тем же линтером и корпоративными style guide.
Ой. Вы точно на джаве код пишите? Так такие монстрики иногда бывают...
Из двух альтернатив «читать больше» и «читать меньше, но затем выводить типы в уме» я выбираю первую, как менее затратную.
Не надо в уме. Пользуйтесь IDE. Особенно в любом неочевидном случае.
Запретить можно все. Но зачем? В плюсах я могу понять запрет части конструкций. Но в джаве это выглядит очень странно.
Maccimo
21.11.2021 13:58+1Не надо в уме. Пользуйтесь IDE. Особенно в любом неочевидном случае.
Вот пришёл нам pull request в GitLab-e, чем тут IDE поможет?
BugM
21.11.2021 14:16git checkout ...
Есть pr достаточно большой чтобы было непонятно смотреть его в IDE в любом случае удобнее.
Maccimo
21.11.2021 15:36Использование
var
способствует увеличению числа случаев, когда сделатьcheckout
действительно удобнее. О чём и речь.Если, к примеру, в одной из последущих версий в язык вкорячат string interpolation, то картина станет ещё хуже. Зато ХХВП можно быстрее делать, да.
BugM
21.11.2021 16:24var увеличивает, лямбды увеличивают, стримы увеличивают, пропуск дженерик типов увеличивает, да даже рекорды увеличивают.
И что теперь писать на синтаксисе jdk6? Язык развивается, игнорировать развитие и заставлять всех писать как 20 лет назад странно. И нанимать вам с таким подходом сложно будет.
tsypanov
22.11.2021 11:53Var не нужно запрещать, как выше написал @MediaNik5
var нужно использовать там, где тип очевиден
kacetal
19.11.2021 01:14Дак параметры по умолчанию это и есть перегрузка методов, ну по крайней мере самая очевидная реализация именно через неё. К примеру груви у которого есть параметры по умолчанию, посмотрите во что они превращается после компиляции в байт код.
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); }
И сразу ой. С параметрами по умолчанию так не сделать.
Авторы любого языка всегда выбирают между дефолтными параметрами и перегрузкой. Перегрузка в общем полезнее. Я с ними согласен.
alxt
20.11.2021 07:49А в чём проблема назвать их по-разному? У них разная суть - объём цилиндра и объём куба.
То, что второй использует целые числа - просто бред.
BugM
20.11.2021 13:49Потому что я пошел на википедию в статью о перегрузке функций и взял пример оттуда https://en.wikipedia.org/wiki/Function_overloading
Назвать по разному это и есть запрет перегрузки. Можно, но это будет фундаментальное свойство языка. Которое изменить потом уже нельзя. Никогда. Вы же не хотите очередной Питон3?
Не факт что с таким свойством язык станет лучше.
Maccimo
20.11.2021 13:53Авторы любого языка всегда выбирают между дефолтными параметрами и перегрузкой.
Технически, авторам ЯП не обязательно выбирать что-то одно. В Delphi были и параметры по умолчанию и перегрузка.
BugM
20.11.2021 15:38Технически разрешить можно. Но это порождает кучу неочевидных мест где нельзя. Так даже еще хуже.
alxt
20.11.2021 07:47Вы хоть раз сталкивались с реальными багами, которые вызваны выводом типов?
Да. Много раз. А совместно с extension-методами в kotlin это создаёт ещё больше проблем.
Мне кажется, вы тут себе же противоречите, ведь параметры по умолчанию по сути делают любой метод с параметром перегруженным
Нет.
Потому что метод остаётся один. И в наследнике он будет ровно такой же.
Смесь множественного (по факту дефолтных методов) наследования и перегрузки - это плохо.
tsypanov
20.11.2021 15:26Да. Много раз.
Не троллинга ради, но можно простенький пример?
Потому что метод остаётся один.
В объявлении метод действительно остаётся один, сигнатура написана единожды, здесь вопросов нет. Но как только доходит до исполнения, методов фактически два: с аргументом и без оного (подставляется значение по умолчанию). И если код написан не в ИДЕ, то такие методы сильно усложняют понимание написанного, ведь если аргумент явно не передаётся в метод, сигнатуру которого мы прямо сейчас не видим, то невозможно понять, есть ли там значение по умолчанию, или метод действительно без аргументов.
Смесь множественного (по факту дефолтных методов) наследования и перегрузки - это плохо
С точки зрения идеологической чистоты ООП - да. В жизни же рядового разраба методы по умолчанию сильно помогают.
alxt
22.11.2021 06:30Не троллинга ради, но можно простенький пример?
Я ж писал - когда есть два метода с типами-интерфейсами и класс, который наследует оба интерфейса.
И если код написан не в ИДЕ, то такие методы сильно усложняют понимание написанного
Ну не сильно - потому что, опять же, всегда есть ровно один метод и его проще найти.
В жизни же рядового разраба методы по умолчанию сильно помогают.
Вы про перегузку? Можно пример, когда они помогают (т.е. никак нельзя назвать два метода разными именами)?
tsypanov
22.11.2021 11:52Вы про перегузку?
Я про случай, когда у интерфейса есть много наследников, но всем им нужен одинаковый для всех метод, например, вычисляющий сложные проценты. Если у интерфейса нет методов по умолчанию, то остаётся либо написание абстрактного класса и наследование от него, что снижает гибкость, либо выносить функционал во внешней класс. Также см. https://stackoverflow.com/a/33721297/12473843
alxt
22.11.2021 11:54Ничего плохого про методы по-умолчанию не скажу.
Я ругаю только перегрузку методов.
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++).
VGoudkov
16.11.2021 16:09Были бы именованные параметры - не было бы большинства с перегрузкой. И шаблон Builder бы не потребовался. И ещё дофига всего. Но нет, Java - она не такая. Хотя на момент её (Java) пелёнок это уже лет пятнадцать как было много где. А ещё я иногда хочу перегружать по возвращаемому результату. Однако хрен там. Там же, где и наследование enum. Зла не хватает. Язык, на котором пишется куча бизнес-логики... и который не очень-то для этого предназначался при разработке, судя по всему
Maccimo
18.11.2021 02:52Язык, на котором пишется куча бизнес-логики должен быть осмотрителен в выборе фич и не тянуть себе в грамматику весь синтаксический мусор подряд, который успело понапридумывать человечество.
Были бы именованные параметры
Все, кому это важно, пишут на питоне.
И шаблон Builder бы не потребовался.
Как бы вам помогло наличие именованных параметров в деле отказа от
StringBuilder
? :DТам же, где и наследование enum.
В каком языке такое есть и для чего нужно?
Зла не хватает.
Не пора ли благородному дону в отпуск?
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 - не ждать так сказать милостей от природы
BugM
Не хватает заголовка как на N+1.
Сложность - 9/10