Java: общий метод перегрузки неоднозначности
Рассмотрим следующий код:
public class Converter {
public <K> MyContainer<K> pack(K key, String[] values) {
return new MyContainer<>(key);
}
public MyContainer<IntWrapper> pack(int key, String[] values) {
return new MyContainer<>(new IntWrapper(key));
}
public static final class MyContainer<T> {
public MyContainer(T object) { }
}
public static final class IntWrapper {
public IntWrapper(int i) { }
}
public static void main(String[] args) {
Converter converter = new Converter();
MyContainer<IntWrapper> test = converter.pack(1, new String[]{"Test", "Test2"});
}
}
Вышеприведенный код компилируется без проблем. Однако, если вы меняете String[]
на String...
в обеих подписях pack
и new String[]{"Test", "Test2"}
до "Test", "Test2"
, компилятор жалуется на то, что вызов converter.pack
был неоднозначным.
Теперь я могу понять, почему его можно считать двусмысленным (поскольку int
может быть автобоксирован в Integer
, что соответствует условиям или их отсутствию K
). Однако я не могу понять, почему двусмысленность не существует, если вы используете String[]
вместо String...
.
Может кто-нибудь объяснить это странное поведение?
Ответы
Ответ 1
Ваш 1 st случай довольно прост. Метод ниже:
public MyContainer<IntWrapper> pack(int key, Object[] values)
- точное соответствие для аргументов - (1, String[])
. Из JLS Раздел 15.12.2:
Первая фаза (§15.12.2.2) выполняет разрешение перегрузки без разрешения преобразования бокса или распаковки
Теперь во время передачи этих параметров во второй метод не задействован бокс. Поскольку Object[]
является супер-типом String[]
. И передать аргумент String[]
для параметра Object[]
был действительным вызовом еще до Java 5.
Компилятор, похоже, играет трюк во втором случае:
В вашем втором случае, поскольку вы использовали var-args, разрешение перегрузки метода будет выполняться с использованием как var-args, так и бокса или unboxing, согласно третьей фазе, описанной в этом разделе JLS:
Третья фаза (§15.12.2.4) позволяет комбинировать перегрузку с методами переменной arity, боксом и распаковкой.
Обратите внимание, что вторая фаза здесь не применима из-за использования var-args:
Вторая фаза (§15.12.2.3) выполняет разрешение перегрузки при разрешении бокса и распаковки, но все же исключает использование вызова метода переменной arity.
Теперь, что происходит здесь, компилятор не выводит правильный аргумент типа * (Фактически, он выводит его правильно, поскольку параметр типа используется как формальный параметр, см. обновление в конце этого ответ). Итак, для вызова метода:
MyContainer<IntWrapper> test = converter.pack(1, "Test", "Test2");
компилятор должен был вывести тип K
в родовом методе IntWrapper
из LHS. Но кажется, что он выводит K
как тип Integer
, из-за которого оба метода теперь одинаково применимы для этого вызова метода, поскольку оба требуют var-args
или boxing
.
Однако, если результат этого метода не привязан к какой-либо ссылке, я могу понять, что компилятор не может вывести правильный тип, как в этом случае, где вполне допустимо дать ошибку двусмысленности:
converter.pack(1, "Test", "Test2");
Возможно, я предполагаю, что для поддержания согласованности он также неоднозначен для первого случая. Но, опять же, я не совсем уверен, поскольку я не нашел достоверного источника из JLS или другой официальной ссылки, в которой говорится об этой проблеме. Я продолжу поиск, и если я его найду, обновит ответ.
Обобщите компилятор с помощью явной информации о типе:
Если вы изменяете вызов метода, чтобы предоставить явную информацию о типе:
MyContainer<IntWrapper> test = converter.<IntWrapper>pack(1, "Test", "Test2");
Теперь тип K
будет выведен как IntWrapper
, но поскольку 1
не конвертируется в IntWrapper
, этот метод будет отброшен, и будет вызван второй метод, и он будет работать отлично.
Честно говоря, я действительно не знаю, что здесь происходит. Я ожидаю, что компилятор также выведет параметр типа из контекста вызова метода в первом случае, поскольку он работает для следующей проблемы:
public static <T> HashSet<T> create(int size) {
return new HashSet<T>(size);
}
// Type inferred as `Integer`, from LHS.
HashSet<Integer> hi = create(10);
Но в этом случае это не делается. Так что это может быть ошибка.
* Или, может быть, я не совсем понимаю, как компилятор описывает аргументы типа, когда тип не передается в качестве аргумента. Поэтому, чтобы узнать больше об этом, я попытался пройти через JLS §15.12.2.7 и JLS §15.12.2.8, в котором говорится о том, как компилятор вводит аргумент типа, но это полностью перевернуто на самой вершине моей головы.
Итак, пока вы должны жить с ним и использовать альтернативу (предоставляя явный аргумент типа).
Оказывается, компилятор не играл никакой трюки:
Как объяснено в комментарии от @zhong.j.yu., компилятор применяет только раздел 15.12.2.8 для вывода типа, когда он не может сделать вывод в соответствии с разделом 15.12.2.7. Но здесь он может вывести тип как Integer
из передаваемого аргумента, так как явно параметр типа является параметром формата в методе.
Итак, да, компилятор правильно вводит тип как Integer
, и, следовательно, двусмысленность действительна. И теперь я думаю, что этот ответ завершен.
Ответ 2
Здесь вы идете, разница между двумя нижеуказанными способами:
Способ 1:
public MyContainer<IntWrapper> pack(int key, Object[] values) {
return new MyContainer<>(new IntWrapper(""));
}
Способ 2:
public MyContainer<IntWrapper> pack(int key, Object ... values) {
return new MyContainer<>(new IntWrapper(""));
}
Метод 2 не хуже
public MyContainer<IntWrapper> pack(Object ... values) {
return new MyContainer<>(new IntWrapper(""));
}
Вот почему вы получаете двусмысленность.
ИЗМЕНИТЬ
Да, я хочу сказать, что они одинаковы для компиляции. Вся цель использования переменных аргументов заключается в том, чтобы позволить пользователю определять метод, когда он/она не уверен в
количество аргументов данного типа.
Итак, если вы используете объект как переменные аргументы, вы просто говорите компилятору, что я не уверен, сколько объектов я отправлю, а с другой стороны, вы говорите: "Я передаю целое и неизвестное число объекты". Для компилятора целым является также и объект.
Если вы хотите проверить правильность, попробуйте передать целое число в качестве первого аргумента, а затем передать переменный аргумент String. Вы увидите разницу.
Например,
public class Converter {
public static void a(int x, String... y) {
}
public static void a(String... y) {
}
public static void main(String[] args) {
a(1, "2", "3");
}
}
Кроме того, не используйте взаимозаменяемые массивы и переменные args, они вообще имеют разные цели.
Когда вы используете varargs, метод не ожидает массив, а разные параметры одного и того же типа, к которым можно получить доступ индексированным образом.
Ответ 3
В этом случае
(1) m(K, String[])
(2) m(int, String[])
m(1, new String[]{..});
m (1) удовлетворяет 15.12.2.3. Фаза 2: Определение методов сопоставления Arity, применимых с помощью метода Invocation Conversion
m (2) удовлетворяет 15.12.2.2. Фаза 1: Определение методов соответствия Arity, применимых подтипами
Компилятор останавливается на этапе 1; он нашел m (2) как единственный применимый метод на этой фазе, поэтому выбирается m (2).
В случае var arg
(3) m(K, String...)
(4) m(int, String...)
m(1, str1, str2);
Оба m (3) и m (4) удовлетворяют 15.12.2.4. Этап 3: Определение применимых переменных переменных Arity. И не более конкретный, чем другой, поэтому двусмысленность.
Мы можем сгруппировать применимые методы в 4 группы:
- применяемый подтипированием
- применимый при преобразовании вызова метода
- vararg, применимый подтипированием
- vararg, применимый при преобразовании вызова метода
Спектр объединяет группы 3 и 4 и обрабатывает их как в фазе 3. Поэтому несогласованность.
Почему они это сделали? Майе они просто устали от этого.
Другая критика заключалась бы в том, что не должно быть всех этих этапов, потому что программисты так не думают. Мы должны просто найти все применимые методы без разбора, а затем выбрать наиболее конкретные (с некоторым механизмом, чтобы избежать бокса/распаковки)
Ответ 4
Прежде всего, это только некоторые первые подсказки... можно редактировать для большего.
Компилятор всегда ищет и выбирает наиболее доступный метод. Хотя немного неудобно читать, все это указано в JLS 15.12.2.5. Таким образом, вызывая
converter.pack(1, "Test", "Test2" )
для компилятора не определено, должен ли 1
растворяться до K
или int
. Другими словами, K может применяться к любому типу, поэтому он находится на том же уровне, что и int/Integer.
Разница заключается в числе и типе аргументов. Рассмотрим, что new String[]{"Test", "Test2"}
- массив, а "Test", "Test2"
- два аргумента типа String!
converter.pack(1);//неоднозначная ошибка компилятора
converter.pack(1, null);//вызывает метод 2, предупреждение компилятора
converter.pack(1, new String [] {});//вызывает метод 2, предупреждение компилятора
converter.pack(1, новый Object());//неоднозначный, ошибка компилятора
converter.pack(1, новый объект [] {});//вызывает метод 2, предупреждение