<T> Список <? расширяет ли T> f() полезную сигнатуру? Есть ли проблемы с этим/используя его?

<T> List<? extends T> f() <T> List<? extends T> f() полезную сигнатуру? Есть ли проблемы с этим/используя его?

Это был вопрос для интервью. Я знаю это:

  1. Хорошо компилируется
  2. Используйте это как List<? extends Number> lst = obj.<Number>f() List<? extends Number> lst = obj.<Number>f(), и тогда я могу вызвать lst только те методы List, которые не содержат T в своих сигнатурах (скажем, isEmpty(), size(), но не add (T), удалить (T)

Это полностью отвечает на вопрос?

Ответ 1

Эта сигнатура метода "полезна" в том смысле, что вы можете реализовать с ней нетривиальные, невырожденные методы (то есть возвращать null и выбрасывать ошибки - не единственные варианты). Как показывает следующий пример, такой метод может быть полезен для реализации некоторых алгебраических структур, таких как, например, моноиды.

Во-первых, обратите внимание, что List<? extends T> List<? extends T> - это тип со следующими свойствами:

  • Вы знаете, что все элементы этого списка соответствуют типу T, поэтому всякий раз, когда вы извлекаете элемент из этого списка, вы можете использовать его в позиции, где ожидается T Вы можете прочитать из этого списка.
  • Точный тип неизвестен, поэтому вы никогда не можете быть уверены, что экземпляр определенного подтипа T может быть добавлен в этот список. То есть вы фактически не можете добавлять новые элементы в такой список (если только вы не используете null/type приведение/использование несостоятельности системы типов Java, то есть).

В сочетании это означает, что List<? extends T> List<? extends T> вроде как защищенный от добавления список с защитой от добавления уровня типа.

На самом деле вы можете делать значимые вычисления с такими "защищенными от добавления" списками. Вот несколько примеров:

  • Вы можете создавать списки с защитой от добавления одним элементом:

    public static <T> List<? extends T> pure(T t) {
      List<T> result = new LinkedList<T>();
      result.add(t);
      return result;
    }
    
  • Вы можете создавать списки с защитой от добавления из обычных списков:

    public static <T> List<? extends T> toAppendProtected(List<T> original) {
      List<T> result = new LinkedList<T>();
      result.addAll(original);
      return result;
    }
    
  • Вы можете комбинировать списки с защитой от добавления:

    public static <T> List<? extends T> combineAppendProtected(
      List<? extends T> a,
      List<? extends T> b
    ) {
      List<T> result = new LinkedList<T>();
      result.addAll(a);
      result.addAll(b);
      return result;
    }
    
  • И, что наиболее важно для этого вопроса, вы можете реализовать метод, который возвращает пустой список с защитой от добавления данного типа:

    public static <T> List<? extends T> emptyAppendProtected() {
      return new LinkedList<T>();
    }
    

Вместе они combine и empty форму фактической алгебраической структуры (моноида), и такие методы, как pure гарантируют, что она не вырождена (т.е. Имеет больше элементов, чем просто пустой список). В самом деле, если у вас интерфейс, похожий на обычный Monoid класс типов:

  public static interface Monoid<X> {
    X empty();
    X combine(X a, X b);
  }

тогда вы могли бы использовать описанные выше методы для его реализации следующим образом:

  public static <T> Monoid<List<? extends T>> appendProtectedListsMonoid() {
    return new Monoid<List<? extends T>>() {
      public List<? extends T> empty() {
        return ReadOnlyLists.<T>emptyAppendProtected();
      }

      public List<? extends T> combine(
        List<? extends T> a,
        List<? extends T> b
      ) {
        return combineAppendProtected(a, b);
      }
    };
  }

Это показывает, что методы с сигнатурой, приведенной в вашем вопросе, могут быть использованы для реализации некоторых общих шаблонов проектирования/алгебраических структур (моноидов). Следует признать, что пример несколько надуманный, вы, вероятно, не захотите использовать его на практике, потому что вы не хотите слишком сильно удивлять пользователей своего API.


Полный скомпилированный пример:

import java.util.*;

class AppendProtectedLists {

  public static <T> List<? extends T> emptyAppendProtected() {
    return new LinkedList<T>();
  }

  public static <T> List<? extends T> combineAppendProtected(
    List<? extends T> a,
    List<? extends T> b
  ) {
    List<T> result = new LinkedList<T>();
    result.addAll(a);
    result.addAll(b);
    return result;
  }

  public static <T> List<? extends T> toAppendProtected(List<T> original) {
    List<T> result = new LinkedList<T>();
    result.addAll(original);
    return result;
  }

  public static <T> List<? extends T> pure(T t) {
    List<T> result = new LinkedList<T>();
    result.add(t);
    return result;
  }

  public static interface Monoid<X> {
    X empty();
    X combine(X a, X b);
  }

  public static <T> Monoid<List<? extends T>> appendProtectedListsMonoid() {
    return new Monoid<List<? extends T>>() {
      public List<? extends T> empty() {
        return AppendProtectedLists.<T>emptyAppendProtected();
      }

      public List<? extends T> combine(
        List<? extends T> a,
        List<? extends T> b
      ) {
        return combineAppendProtected(a, b);
      }
    };
  }

  public static void main(String[] args) {
    Monoid<List<? extends String>> monoid = appendProtectedListsMonoid();
    List<? extends String> e = monoid.empty();
    // e.add("hi"); // refuses to compile, which is good: write protection!
    List<? extends String> a = pure("a");
    List<? extends String> b = pure("b");
    List<? extends String> c = monoid.combine(e, monoid.combine(a, b));
    System.out.println(c); // output: [a, b]
  }

}

Ответ 2

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

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

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


На самом деле, другое значение, которое можно безопасно вернуть, - это список, в котором все элементы имеют нулевое значение. Но это также бесполезно, поскольку вы можете вызывать только те методы, которые добавляют или удаляют буквенное значение null из возвращаемого значения из-за ? extends ? extends в типе границы. Таким образом, все, что у вас есть, это то, что подсчитывает количество нулей, которые он содержит. Что тоже не полезно.

Ответ 3

Официальный учебник Generics предлагает не использовать подстановочные типы возврата.

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

Однако приведенные аргументы не совсем убедительны.

Ответ 4

Это не особенно полезно, по причинам, приведенным в других ответах. Тем не менее, считайте это "полезностью" в классе, подобном следующему (хотя это, вероятно, немного антипаттерн):

public class Repository {
    private List<Object> lst = new ArrayList<>();

    public <T> void add(T item) {
        lst.add(item);
    }

    @SuppressWarnings("unchecked")
    public <T> List<? extends T> f() {
        return (List<? extends T>) lst;
    }

    public static void main(String[] args) {
        Repository rep = new Repository();
        rep.add(BigInteger.ONE);
        rep.add(Double.valueOf(2.0));
        List<? extends Number> list = rep.f();
        System.out.println(list.get(0).doubleValue() + list.get(1).doubleValue());
    }
}

Обратите внимание на следующие особенности:

  1. Объявленный тип возврата f() означает, что вызывающая сторона может установить любой T, который им нравится, и метод вернет требуемый тип. Если бы f() не было объявлено таким образом, то вызывающая сторона должна была бы привести каждый вызов, чтобы get(N) требуемый тип.
  2. Поскольку он использует подстановочный знак, объявленный тип возврата делает возвращаемый список доступным только для чтения. Это часто может быть полезной функцией. Когда вы вызываете геттер, вы не хотите, чтобы он возвращал список, в который вы можете написать. Зачастую получатели используют Collections.unmodifiableList() что заставляет список быть доступным только для чтения во время выполнения, но с использованием универсального параметра подстановочного знака заставляет список быть доступным только для чтения во время компиляции!
  3. Недостатком является то, что он не особенно безопасен для типов. Вызывающий должен убедиться, что f() возвращает тип, где T является общим суперклассом всех ранее добавленных элементов.
  4. Как правило, было бы намного лучше сделать класс Repository родовым вместо метода f().