Локальный тип вывода и контравариантность в переменных общего типа - программирование
Подтвердить что ты не робот

Локальный тип вывода и контравариантность в переменных общего типа

Я пришел по следующему коду:

public static <T> Set<T> distinct(
        Collection<? extends T> list,
        Comparator<? super T> comparator) {

    Set<T> set = new TreeSet<>(comparator);
    set.addAll(list);
    return set;
}

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

Пусть дает локальный тип вывода возможность, я (наивно) думал... Поэтому я изменил приведенный выше код на:

public static <T> Set<T> distinct(
        Collection<? extends T> list,
        Comparator<? super T> comparator) {

    var set = new TreeSet<>(comparator);
    set.addAll(list);
    return set;
}

Это имело смысл для меня, потому что тип set можно вывести из типа comparator, или так я думал. Однако модифицированный код не компилируется и генерирует следующую ошибку:

java: incompatible types: java.util.TreeSet<capture#1 of ? super T> cannot be converted to java.util.Set<T>

Теперь я понимаю, почему возникает ошибка, и я признаю, что тип компаратора на самом деле является Comparator<? super T> Comparator<? super T>, поэтому тип, выводимый var - TreeSet<? super T> TreeSet<? super T>.

Однако мне интересно, почему var не может вывести общий тип TreeSet только как T вместо ? super T ? super T В конце концов, (java.util.Comparator) rel="nofollow noreferrer">согласно документам, TreeSet<E> имеет конструктор, который принимает аргумент типа Comparator<? super E> Comparator<? super E>. Поэтому для вызова этого конструктора следует создать TreeSet<E>, а не TreeSet<? super E> TreeSet<? super E>. (Это показывает первый фрагмент). Я ожидал, что var будет следовать этой же логике.

Примечание 1: Один из способов сделать компиляцию кода - изменить тип возвращаемого значения на Set<? super T> Set<? super T>. Однако это вряд ли можно использовать...

Примечание 2: Другой способ заключается в том, чтобы не использовать контравариантность в компараторе, но я не хочу этого, потому что я не смог бы использовать Comparator который сравнивает предков T

Примечание 3: Я знаю, что первый фрагмент работает, поэтому кажется очевидным, что я не должен использовать var и объявлять набор явно как Set<T>. Однако мой вопрос заключается не в том, должен ли я отказаться от второго фрагмента или как его исправить. Вместо этого я хотел бы знать, почему var не TreeSet<T> как тип set локальной переменной в моем втором фрагменте.


EDIT 1: В этом комментарии пользователь @nullpointer правильно указывает, что я должен сделать следующее тонкое изменение, чтобы сделать компиляцию второго фрагмента:

var set = new TreeSet<T>(comparator); // T brings in the magic!

Теперь общий параметр типа T является явным для TreeSet, поэтому var правильно вводит тип set локальной переменной как TreeSet<T>. Тем не менее, я хотел бы знать, почему я должен указать T явно.


EDIT 2: В этом другом комментарии пользователь @Holger ловко упоминает, что на языке запрещено:

var set = new TreeSet<? super T>(comparator);

Приведенный выше код не может скомпилироваться со следующей ошибкой:

java: unexpected type
  required: class or interface without bounds
  found:    ? super T

Итак, теперь вопрос становится более очевидным: если я не могу явно указать ограниченный общий тип ? super T ? super T в выражении создания экземпляра new TreeSet<? super T>(comparator) new TreeSet<? super T>(comparator), почему компилятор выводит TreeSet<? super T> TreeSet<? super T> как тип set локальной переменной?

4b9b3361

Ответ 1

По словам Брайана Гетца, отвечая на мой вопрос, он заявляет:

Локальный вывод типа переменной говорит: типы, которые мне нужны, вероятно, уже присутствуют с правой стороны, поэтому повторите их слева.

Что касается кода в вашем вопросе, единственным типом, доступным для вывода (с использованием предоставленного Comparator) является TreeSet<? super T> TreeSet<? super T>. У нас люди достаточно умны, чтобы видеть, что distinct возвраты set и ожидает Set<T>. Тем не менее, компилятор либо может не быть достаточно умным, чтобы его разрешить (я уверен, что это возможно), но скорее это факт, что var указывает на наиболее специфический тип, используя информацию, предоставленную в RHS, и архитекторам не хотелось сломать это.

Теперь, поскольку в своем комментарии указано nullpointer, вы можете явно определить, что ваш TreeSet имеет тип T а не тип предполагаемого захвата ? super T ? super T со следующим:

var set = new TreeSet<T>(comparator);

Я предполагаю, что явные общие типы переопределяют выводимые типы, переданные конструктору, что имеет смысл.

JLS §14.4.1: Локальные деклараторы и типы переменных, похоже, поддерживают мою заявку, заявив следующее:

enter image description here

Примечание: "восходящая проекция T", которая может просто выводить на этот тип (TreeSet вместо Set), но также может включать и общий тип.

Я считаю, что та же самая причина, почему list в list var list = List.<Number>of(1, 2, 3); это List<Number> а не List<Integer>, который работает так же хорошо, как и var.

Ответ 2

Использование локальной переменной во втором фрагменте требует, чтобы вы явно указали границу TreeSet как в:

public static <T> Set<T> distinct(Collection<? extends T> list, Comparator<? super T> comparator) {
    var set = new TreeSet<T>(comparator);
    set.addAll(list);
    return set;
}

причина в том, что inferred var в противном случае использует самую очевидную границу, используемую с компаратором, и получает вывод как TreeSet<? super T> TreeSet<? super T> и не выполняется с указанной ошибкой компиляции из-за несовместимости преобразования.

почему я должен указать T явно

Подумав наоборот, как указал Иаков, и сформулировал код как

  private static Set<Integer> distincts(Collection<? extends Integer> list, Comparator<Number> comparator) {
        var set = new TreeSet<>(comparator);
        // if you don't specify the bound, you get a compiler error on the return statement 
        // since the inferred type would be 'Number'
        set.addAll(list);
        return set;
    }

Чтобы просто ответить на вопрос, какой тип вы бы хотели получить здесь по умолчанию для TreeSet? Integer, Byte который из списка подклассов Number и how (на основе типа возврата, который может быть просто выведен позже)?

Изменить: - Я делаю во-вторых мысль, что данный конструктор (java.util.Comparator) rel="nofollow noreferrer"> TreeSet(Comparator<? super E> comparator) создает TreeSet<E>, поэтому вызов такого конструктора должен быть выведен как TreeSet<E> вместо TreeSet<? super E> TreeSet<? super E>. Кроме того, как заметил Брайан, не все может быть выведено и для любых таких конкретных типов, можно было бы попросить об этом (предполагая в списке рассылки jdk).