Метод має таке ж стирання, як і інший тип


381

Чому не можна законно мати наступні два методи в одному класі?

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

Я розумію compilation error

Метод add (Set) має те ж додавання стирання (Set), як і інший метод типу Test.

хоча я можу обійти це, мені було цікаво, чому javac цього не любить.

Я бачу, що у багатьох випадках логіка цих двох методів була б дуже схожа і могла б бути замінена одиничною

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

метод, але це не завжди так.

Це додатково дратує, якщо ви хочете мати два, constructorsякі приймають ці аргументи, тому що тоді ви не можете просто змінити ім'я одного з constructors.


1
що робити, якщо у вас не вистачає структур даних, і вам ще потрібні додаткові версії?
Омрі Ядан

1
Ви можете зробити власні класи, які успадковуються від базових версій.
willlma

1
ОП, ви придумали якесь рішення проблеми конструктора? Мені потрібно прийняти два види, 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.
erickson

13
Не вперше я стикаюся з помилкою Java, яка зовсім не є помилкою, і її можна було б скласти, якби тільки автори Java використовували попередження, як це роблять усі. Тільки вони думають, що знають все краще.
Томаш Зато - Відновіть Моніку

1
@ TomášZato Ні, я не знаю жодних чітких способів вирішення цього питання. Якщо ви хочете що - то брудна, ви могли б пройти , List<?>а деякі , enumякі ви визначили , щоб вказати тип. Логіка, характерна для типу, насправді може бути у методі на перелік. Крім того, ви можете створити два різні класи, а не один клас з двома різними конструкторами. Загальна логіка полягатиме в надкласі або хелперному об'єкті, якому делегуються обидва типи.
erickson

117

Java-дженерики використовує стирання типу. Біт у кутових дужках ( <Integer>і <String>) видаляється, тож ви отримаєте два способи, що мають однакову підпис ( add(Set)ви бачите помилку). Це не дозволено, оскільки час виконання не знатиме, який використовувати для кожного випадку.

Якщо Java коли-небудь отримає вдосконалені дженерики, то ви могли б це зробити, але це, мабуть, зараз малоймовірно.


24
Мені дуже шкода, але ваша відповідь (та інші відповіді) не пояснюють, чому тут помилка. Розв’язання перевантажень проводиться під час компіляції, і компілятор, безумовно, має інформацію про тип, необхідний для вирішення способу зв’язання за адресою або будь-яким способом, на який посилається байт-код, який, на мою думку, не є підписом. Я навіть думаю, що деякі компілятори дозволять це компілювати.
Стілгар

5
@Stilgar Що зупинити виклик або перевірку методу за допомогою рефлексії? Список методів, повернутих Class.getMethods (), матиме два однакові методи, які не мали б сенсу.
Адріан Муат

5
Інформація про відображення може / повинна містити метадані, необхідні для роботи з загальною інформацією. Якщо ні, то як компілятор Java знає про загальні методи, коли ви імпортуєте вже складену бібліотеку?
Stilgar

4
Тоді метод getMethod потребує виправлення. Наприклад, введіть перевантаження, яка вказує на загальне перевантаження та зробить оригінальний метод поверненням лише негенеріальної версії, не повертаючи жодного методу, анотованого як загальний. Звичайно, це повинно було бути зроблено у версії 1.5. Якщо вони будуть робити це зараз, вони порушать зворотну сумісність методу. Я стою за своє твердження, що стирання типу не диктує цю поведінку. Саме реалізація не отримала достатньо роботи, ймовірно, через обмежені ресурси.
Stilgar

1
Це не точна відповідь, але це швидко підсумовує питання корисною вигадкою: підписи методу занадто схожі, компілятор може не сказати різниці, і тоді ви отримаєте "невирішені проблеми компіляції".
Worc

46

Це відбувається тому, що Java Generics реалізовані за допомогою типу Erasure .

Ваші методи будуть переведені під час компіляції на щось на зразок:

Дозвіл методу відбувається під час компіляції та не враховує параметрів типу. ( див. відповідь Еріксона )

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

Обидва методи мають однаковий підпис без параметрів типу, отже, помилка.


21

Проблема полягає в тому, що Set<Integer>і Set<String>насправді трактуються як SetСВМ. Вибір типу для набору (String або Integer у вашому випадку) - це лише синтаксичний цукор, який використовується компілятором. JVM не може розрізняти Set<String>та Set<Integer>.


7
Це правда, що час виконання JVM не має інформації для їх розрізнення Set, але оскільки роздільна здатність методу відбувається в час компіляції, коли є необхідна інформація, це не має значення. Проблема полягає в тому, що дозволення цих перевантажень суперечить допуску для сировинних типів, тому вони були незаконними в синтаксисі Java.
erickson

@erickson Навіть коли компілятор знає, який метод викликати, він не може, як у байт-коді, вони обидва виглядають абсолютно однаково. Вам потрібно буде змінити спосіб уточнення виклику методу, оскільки (Ljava/util/Collection;)Ljava/util/List;він не працює. Ви можете використовувати (Ljava/util/Collection<String>;)Ljava/util/List<String>;, але це несумісна зміна, і ви зіткнетеся з нерозв'язними проблемами в місцях, де все, що у вас є, є стираним типом. Напевно, вам доведеться повністю скинути стирання, але це досить складно.
maaartinus

@maaartinus Так, я погоджуюся, що вам доведеться змінити специфікатор методу. Я намагаюся зіткнутися з деякими нерозв'язними проблемами, які змусили їх відмовитися від цієї спроби.
erickson

7

Визначте єдиний метод без типу void add(Set ii){}

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


3

Можливо, компілятор переводить Set (Integer) у Set (Object) у байт-код Java. У цьому випадку Set (Integer) буде використовуватися лише на фазі компіляції для перевірки синтаксису.


5
Технічно це лише сирий тип Set. Джерела не існує в байтовому коді, вони синтаксичний цукор для лиття і забезпечують безпеку типу компіляції в часі.
Анджей Дойль

1

Я наткнувся на це, коли спробував написати щось на кшталт: Continuable<T> callAsync(Callable<T> code) {....} і Continuable<Continuable<T>> callAsync(Callable<Continuable<T>> veryAsyncCode) {...} вони стали для компілятора 2 визначеннями Continuable<> callAsync(Callable<> veryAsyncCode) {...}

Стирання типу буквально означає стирання інформації аргументів типу з дженерики. Це ДУЖЕ дратує, але це обмеження, яке буде з Java протягом певного часу. Для випадку конструкторів не дуже багато можна зробити, наприклад, 2 нові підкласи, спеціалізовані з різними параметрами в конструкторі. Або використовувати замість цього способи ініціалізації ... (віртуальні конструктори?) З різними іменами ...

подібні операційні методи перейменування допоможуть, як

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

Або з деякими більш описовими іменами, самодокументування таких випадків, як-небудь addNamesта addIndexesтаких.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.