Список <T>f () полезная подпись?Есть ли проблемы с этим / с его использованием? - PullRequest
0 голосов
/ 02 января 2019

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

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

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

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

Ответы [ 4 ]

0 голосов
/ 02 января 2019

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

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

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

В совокупности это означает, что 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 образуют фактическую алгебраическую структуру (amonoid) и такие методы, как 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]
  }

}
0 голосов
/ 02 января 2019

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

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

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

0 голосов
/ 02 января 2019

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

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 ().
0 голосов
/ 02 января 2019

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

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

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


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

...