Подтвердить что ты не робот

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....

Может кто-нибудь объяснить это странное поведение?

4b9b3361

Ответ 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, предупреждение