Метод имеет такое же стирание, что и другой метод в типе

381

Почему не разрешено использовать следующие два метода в одном классе?

class Test{
   void add(Set<Integer> ii){}
   void add(Set<String> ss){}
}

Я получаю compilation error

Метод add (Set) имеет то же самое стирание, что и Add (Set), что и другой метод в типе Test.

в то время как я могу обойти это, мне было интересно, почему javac не нравится это.

Я вижу, что во многих случаях логика этих двух методов будет очень похожа и может быть заменена одним

public void add(Set<?> set){}

метод, но это не всегда так.

Это очень раздражает, если вы хотите иметь два, constructorsкоторые принимают эти аргументы, потому что тогда вы не можете просто изменить имя одного из constructors.

Омри Ядан
источник
1
Что делать, если у вас закончились структуры данных и вам все еще нужно больше версий?
Омри Ядан
1
Вы можете создавать собственные классы, которые наследуются от базовых версий.
Виллма
1
OP, вы нашли какое-то решение проблемы конструктора? Мне нужно принять два вида, Listи я не знаю, как справиться с этим.
Томаш Зато - Восстановить Монику
9
Когда я работаю с Java, я очень скучаю по C # ...
Svish
1
@ TomášZato, я решил это, добавив фиктивные параметры в конструктор: Boolean noopSignatureOverload.
Nthalk

Ответы:

357

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

Вот иллюстрация того, почему это было запрещено, взято из JLS. Предположим, что до того, как дженерики были представлены в Java, я написал такой код:

class CollectionConverter {
  List toList(Collection c) {...}
}

Вы расширяете мой класс, вот так:

class Overrider extends CollectionConverter{
  List toList(Collection c) {...}
}

После введения дженериков я решил обновить свою библиотеку.

class CollectionConverter {
  <T> List<T> toList(Collection<T> c) {...}
}

Вы не готовы делать какие-либо обновления, поэтому вы оставляете свой Overriderкласс в покое. Чтобы правильно переопределить toList()метод, разработчики языка решили, что необработанный тип «эквивалентен переопределению» для любого обобщенного типа. Это означает, что хотя подпись вашего метода формально больше не равна подписи моего суперкласса, ваш метод все равно переопределяет.

Сейчас проходит время, и вы решаете, что готовы обновить свой класс. Но вы немного облажались, и вместо редактирования существующего необработанного toList()метода вы добавили новый метод, подобный этому:

class Overrider extends CollectionConverter {
  @Override
  List toList(Collection c) {...}
  @Override
  <T> List<T> toList(Collection<T> c) {...}
}

Из-за переопределенной эквивалентности необработанных типов оба метода имеют допустимую форму для переопределения toList(Collection<T>)метода. Но, конечно, компилятор должен разрешить один метод. Чтобы устранить эту неоднозначность, классам не разрешается иметь несколько методов, эквивалентных переопределению, то есть несколько методов с одинаковыми типами параметров после стирания.

Ключ заключается в том, что это правило языка, разработанное для обеспечения совместимости со старым кодом с использованием необработанных типов. Это не ограничение, требуемое стиранием параметров типа; поскольку разрешение метода происходит во время компиляции, было бы достаточно добавить универсальные типы к идентификатору метода.

Эриксон
источник
3
Отличный ответ и пример! Однако я не уверен, полностью ли я понимаю ваше последнее предложение («Поскольку разрешение метода происходит во время компиляции, перед удалением, преобразование типов не требуется для этой работы».). Не могли бы вы уточнить немного?
Джонас Эйхер
2
Имеет смысл. Я просто потратил некоторое время на размышления о типизации типов в шаблонных методах, но да: компилятор проверяет, что правильный метод выбран перед удалением типа. Прекрасный. Если бы он не был испорчен устаревшими проблемами совместимости кода.
Джонас Эйкер
1
@daveloyall Нет, я не знаю такой вариант для javac.
Эриксон
13
Я не первый раз сталкиваюсь с ошибкой Java, которая вообще не является ошибкой и может быть скомпилирована, если только авторы Java используют предупреждения, как все остальные. Только они думают, что знают все лучше.
Томаш Зато - Восстановить Монику
1
@ TomášZato Нет, я не знаю каких-либо чистых обходных путей для этого. Если вы хотите что-то грязное, вы можете передать List<?>и некоторые, enumкоторые вы определяете, чтобы указать тип. Специфичная для типа логика может быть в методе перечисления. В качестве альтернативы вы можете создать два разных класса, а не один класс с двумя разными конструкторами. Общая логика будет в суперклассе или вспомогательном объекте, которому делегируются оба типа.
erickson
118

В дженериках Java используется стирание типов. Бит в угловых скобках ( <Integer>и <String>) удаляется, поэтому вы получите два метода с одинаковой сигнатурой ( add(Set)вы видите в ошибке). Это не разрешено, потому что среда выполнения не знает, какой использовать для каждого случая.

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

GaryF
источник
24
Извините, но ваш ответ (и другие ответы) не объясняют, почему здесь есть ошибка. Разрешение перегрузки выполняется во время компиляции, и у компилятора, несомненно, есть информация о типе, необходимая для того, чтобы решить, какой метод ссылаться по адресу или по какому-либо методу, на который ссылается метод, в байт-коде, который, я считаю, не является сигнатурой. Я даже думаю, что некоторые компиляторы позволят это компилировать.
Стилгар
5
@ Стилгар, что мешает вызвать метод или проверить его методом рефлексии? Список методов, возвращаемых Class.getMethods (), будет иметь два идентичных метода, которые не имеют смысла.
Адриан Муат
5
Информация об отражении может / должна содержать метаданные, необходимые для работы с генериками. Если нет, то как Java-компилятор узнает об общих методах при импорте уже скомпилированной библиотеки?
Стилгар
4
Затем метод getMethod нуждается в исправлении. Например, введите перегрузку, которая задает универсальную перегрузку, и заставьте оригинальный метод возвращать только неуниверсальную версию, не возвращая ни одного метода, аннотированного как универсальный. Конечно, это должно было быть сделано в версии 1.5. Если они сделают это сейчас, они нарушат обратную совместимость метода. Я придерживаюсь своего утверждения, что стирание типов не диктует такое поведение. Это реализация, которая не получила достаточно работы, вероятно, из-за ограниченных ресурсов.
Стилгар
1
Это не точный ответ, но он быстро суммирует проблему в полезной фикции: сигнатуры методов слишком похожи, компилятор может не заметить разницу, и тогда вы получите «нерешенные проблемы компиляции».
Воск
46

Это потому, что Java Generics реализованы с помощью типа Erasure .

Ваши методы будут переведены во время компиляции в нечто вроде:

Разрешение метода происходит во время компиляции и не учитывает параметры типа. ( см. ответ Эриксона )

void add(Set ii);
void add(Set ss);

Оба метода имеют одинаковую сигнатуру без параметров типа, отсюда и ошибка.

Бруно Конде
источник
21

Проблема в том, что Set<Integer>и Set<String>на самом деле рассматриваются как Setот JVM. Выбор типа для Set (String или Integer в вашем случае) является только синтаксическим сахаром, используемым компилятором. JVM не может различить Set<String>и Set<Integer>.

kgiannakakis
источник
7
Это правда, что среда выполнения JVM не имеет информации, чтобы различать каждого из них Set, но поскольку разрешение метода происходит во время компиляции, когда доступна необходимая информация, это не имеет значения. Проблема состоит в том, что разрешение этих перегрузок будет конфликтовать с разрешением для необработанных типов, поэтому они были объявлены недопустимыми в синтаксисе Java.
Эриксон
@erickson Даже когда компилятор знает, какой метод вызывать, он не может, как в байт-коде, они оба выглядят совершенно одинаково. Вам нужно изменить способ указания вызова метода, так как (Ljava/util/Collection;)Ljava/util/List;он не работает. Вы можете использовать (Ljava/util/Collection<String>;)Ljava/util/List<String>;, но это несовместимое изменение, и вы столкнетесь с неразрешимыми проблемами в местах, где все, что у вас есть, это стертый тип. Возможно, вам придется полностью отказаться от стирания, но это довольно сложно.
Maaartinus
@maaartinus Да, я согласен, что вам придется изменить спецификатор метода. Я пытаюсь решить некоторые из неразрешимых проблем, которые заставили их отказаться от этой попытки.
Эриксон
7

Определите один метод без типа, как void add(Set ii){}

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

Идрис Ашраф
источник
3

Вполне возможно, что компилятор преобразует Set (Integer) в Set (Object) в байт-коде Java. В этом случае Set (Integer) будет использоваться только на этапе компиляции для проверки синтаксиса.

rossoft
источник
5
Технически это просто сырой тип Set. Обобщения не существуют в байт-коде, они являются синтаксическим сахаром для приведения и обеспечивают безопасность типов во время компиляции.
Анджей Дойл
1

Я столкнулся с этим, когда попытался написать что-то вроде: Continuable<T> callAsync(Callable<T> code) {....} и Continuable<Continuable<T>> callAsync(Callable<Continuable<T>> veryAsyncCode) {...} они стали для компилятора двумя определениями Continuable<> callAsync(Callable<> veryAsyncCode) {...}

Стирание типа буквально означает стирание информации аргументов типа из обобщений. Это ОЧЕНЬ раздражает, но это ограничение, которое будет с Java некоторое время. Для конструкторов мало что можно сделать, например, 2 новых подкласса, специализирующихся с разными параметрами в конструкторе. Или используйте вместо этого методы инициализации ... (виртуальные конструкторы?) С разными именами ...

для подобных методов работы переименование поможет, как

class Test{
   void addIntegers(Set<Integer> ii){}
   void addStrings(Set<String> ss){}
}

Или с некоторыми более описательными именами, самодокументирующими для случаев oyu, как addNamesи тому addIndexesподобное.

Котэ Исаев
источник