Рубрики
Без рубрики

Эффективная Java! Используйте ограниченные подстановочные знаки для повышения гибкости API

Погружение в главу 31 Эффективной Java. Помеченный как java, эффективный, универсальный, архитектура.

Сегодня мы рассмотрим немного сложную тему, но при правильном выполнении она действительно может сделать ваш код намного более гибким в использовании. Ранее мы говорили о том, насколько параметризованные типы инвариантны . Это означает, что для двух типов TypeA и TypeB , Collection не могут быть ни подтипом , ни супертипом Collection . Чтобы сделать это более конкретным, List не является подтипом List. Это связано с тем, что любой объект может попасть в Список<Объект> но не любой объект может попасть в List . Хотя это может следовать логике, иногда мы хотим/нуждаемся в большей гибкости, и именно этому посвящена глава, которую мы сегодня рассматриваем.

Давайте рассмотрим предыдущий пример, который мы рассмотрели.

public class Stack {
  public Stack();
  public void push(E e);
  public E pop();
}

Выше приведен открытый в настоящее время API. Допустим, мы хотим добавить новый метод для одновременного добавления множества элементов в стек. Мы можем рассмотреть возможность написания его как такового:

public void pushAll(Iterable newItems) {
  for (E e : newItems) {
    push(e);
  }
}

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

Stack numberStack = new Stack<>();
Iterable integers = ...;
numberStack.pushAll(integers);

В то время как вышесказанное кажется, что это должно сработать ( Целое число является подтипом Числа ) на самом деле он не будет компилироваться из-за инвариантов. Вот тут-то и пригодятся наши ограниченные типы подстановочных знаков , чтобы спасти положение. Если мы просто изменим сигнатуру функции на следующую, приведенный выше код будет работать.

public void pushAll(Iterable newItems) {
  for (E e : newItems) {
    push(e);
  }
}

Итак, что означает приведенная выше подпись, ? расширяет E , значит? Это в основном означает, что он может принимать любой подтип E (а также E сам по себе, что может показаться немного странным с ключевым словом extends ). Это действительно точно соответствует тому, к чему мы стремились.

Теперь давайте рассмотрим аналогичный, но другой случай. Давайте создадим двоюродного брата функции PushAll и создадим функция pop All . Как это будет работать, мы дадим функции коллекцию, и она поместит все содержимое стека в нее. Лично я не большой поклонник использования параметров в качестве выходных данных функции, но это хорошо иллюстрирует этот момент. Таким образом, наша первоначальная реализация может быть чем-то вроде:

public void popAll(Collection destination) {
  while(!isEmpty()) {
    destination.add(pop());
  }
}

Еще раз, это будет компилироваться без проблем и может быть использовано, но не так удобно, как могло бы быть. Также у нас снова возникают проблемы с очень похожим примером, с которым мы столкнулись с функцией PushAll .

Collection numberCollection = ...;
Stack integerStack = ...;
integerStack.popAll(numberCollection);

Такое ощущение, что все должно работать просто отлично но это не так, потому что снова нас кусают инварианты. Разница с этим заключается в том, что вместо поиска подтипа E мы хотим передать коллекцию, которая имеет тип E или супертип E. Java предоставляет способ сделать это следующим образом:

public void popAll(Collection destination) {
  while(!isEmpty()) {
    destination.add(pop());
  }
}

С этой новой подписью клиентский код с компиляцией и выполнением работает нормально, а также код стека.

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

В приведенных выше двух примерах мы использовали extends в одном и супер в другом. Вы можете спросить, когда я должен использовать какой из них. Это действительно важный вопрос. Мнемоника, предложенная в книге, звучит так: PECS - producer-extends, consumer-super |/. Честно говоря, эта мнемоника на самом деле не работает для меня, так как терминология producer/consumer в данном случае немного странная для меня. То, как я думаю об этом, - это GEPS – get-extends, put-super . Например, если я получаю от него значения, я должен использовать extends и если я вкладываю в него значения, я должен использовать super . Используйте любую мнемонику, которая вам подходит, или придумайте свою собственную! Итак, давайте применим это к нашим примерам выше. В вытолкнуть все мы получали значения из предоставленного параметра, чтобы поместить их куда-нибудь еще, поэтому мы использовали extends . В pop All мы вводили значения в предоставленный параметр, поэтому super . Это может потребовать некоторой практики, чтобы научиться думать об этом. Давайте рассмотрим некоторые примеры из предыдущих примеров в нашем общем разделе книги.

Во-первых, функция Chooser , первоначально написанная как public Chooser(Collection choices); . Это включает в себя выбор вариантов, из которых можно выбрать. Перестаньте читать и думать о том, какое ключевое слово мы должны использовать? Вы сказали extends ? Отлично! Поскольку мы будем получать значения из коллекции choices (или, другими словами, если это производитель), мы используем extends ключевое слово.

Далее public static Set union(Set s1, Set s2); который принимает два набора и предоставляет набор, который является объединением обоих наборов вместе. Какие ключевые слова мы бы использовали здесь для наших параметров? Ты снова сказал extends ? Идеально! Опять же, мы извлекаем значения из предоставленных наборов, и поэтому мы хотим использовать расширяться таким образом, новая функция будет выглядеть как |/public static Set union(Set расширяет E> s1, Устанавливает (E> s2); Но подождите, что нам делать с возвращаемым типом, у него по-прежнему нет подстановочных знаков, это нормально? Действительно, так оно и есть, на самом деле, так предпочтительнее. Возвращаемые типы не должны использовать подстановочные знаки, поскольку это вынуждает использовать подстановочные знаки для получателя этих значений. Как правило, если пользователю вашей функции нужно подумать о подстановочных знаках, которые вы используете, вы, скорее всего, делаете это неправильно. С учетом вышеуказанных изменений теперь мы можем писать код, подобный:

Set integerSet = Set.of(1,3,5);
Set doubleSet = Set.of(2.0,4.0,6.0);
Set numberSet = union(integerSet, doubleSet);

Приведенный выше код великолепен и довольно чист в том смысле, что компилятор способен определить нужный нам тип. Несмотря на то, что в Java 8 и за ее пределами были предприняты большие шаги по улучшению вывода типов Java, мы все еще сталкиваемся со случаями, когда компилятор все еще не может вывести типы для нас. В этих случаях мы получим довольно запутанные сообщения об ошибках, но, к счастью, есть способ помочь компилятору с помощью аргумента явного типа . Давайте посмотрим, как должен был бы выглядеть наш приведенный выше вызов union до Java 8.

Set numberSet = Union.union(integerSet, doubleSet);

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

Хорошо, давайте вернемся от этого обхода к нашей тренировочной игре в Super или Extends . Давайте рассмотрим max функция из предыдущей главы. общедоступный статический расширяет сопоставимый> расширяет сопоставимый> T max(Список список) . Это тренировка даже для того, чтобы печатать эту вещь, но давайте подумаем о том, как бы мы написали это, когда ищем большей гибкости. Рассмотрите каждый параметр типа в отдельности. В итоге мы получаем public static extends Comparable extends Comparable супер T>> T max(список расширяет список T>); расширяет список T>); Это должно быть связано с одной из самых сложных сигнатур типов, которые я когда-либо писал. Давайте рассмотрим эти две части. С помощью нашей части Comparable мы придаем значение чему-то там, поэтому в этой области мы захотим использовать super в то время как в списке параметр, из которого мы извлекаем элементы/| Список и, таким образом, мы хотим использовать расширяться . Эмпирическое правило заключается в том, что Сопоставимые всегда являются потребителями и, следовательно, всегда будут использовать super . Однако эта дополнительная сложность не лишена своих преимуществ. Теперь мы можем найти максимальное количество типов, таких как Запланированное будущее , которое не реализует Comparable но вместо этого реализует Сопоставимый<Отложенный> супертип Запланированного будущего .

Есть еще одна последняя вещь, которую следует учитывать. Когда использовать подстановочные знаки, а когда использовать параметры типа. Рассмотрим следующие две сигнатуры функций, которые реализуют функцию swap :

public static  void swap(List list, int from, int to);
public static void swap(List list, int from, int to);

Итак, какому из них следует отдать предпочтение? Эффективный Java говорит, что правило, которому следует следовать, заключается в том, что если параметр типа отображается в сигнатуре метода только один раз, мы должны использовать параметр с подстановочным знаком (второй вариант). Это также похоже на более простую подпись типа. Однако, прежде чем мы перейдем к этому решению, давайте рассмотрим, как будет работать реализация.

public static void swap(List list, int from, int to) {
  list.set(from, list.set(to, list.get(from));
}

Это на самом деле не будет компилироваться, потому что единственное, что разрешено помещать в коллекцию подстановочных знаков, – это null . Однако есть способ обойти это с помощью вспомогательной функции, которая может фиксировать тип. Так что, если мы изменим его на следующий, он будет работать.

public static void swap(List list, int from, int to) {
  swapHelper(list, from, to);
}

private static  void swapHelper(List list, int from, int to){
  list.set(from, list.set(to, list.get(from));
}

Поскольку функция swapHelper может фиксировать тип E и проверять, что операция безопасна, это сработает. Однако взгляните на это еще раз, разве мы только что не написали первый вариант? Мы действительно это сделали, поэтому в этом случае я бы определенно проголосовал за использование параметра типа, а не подстановочного знака.

Это была довольно тяжелая глава. Вероятно, потребуется больше практики и знакомства с этими параметрами подстановочных знаков, чтобы действительно разобраться в них, я знаю, что это помогло мне и я продолжаю время от времени путаться в них. Поскольку вы используете их в дикой природе, вы, вероятно, обнаружите, что используете опцию extends намного чаще, чем опцию super . Это просто природа того факта, что мы обычно передаем параметры для чтения из большего количества, чем мы передаем их для ввода значений. Поскольку мы используем эти возможности ограниченных параметров подстановочных знаков в нашем коде, мы действительно можем сделать наш код намного более гибким, и я думаю, что это стоит затраченных усилий.

Оригинал: “https://dev.to/kylec32/effective-java-use-bounded-wildcards-to-increase-api-flexibility-m84”