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

Эффективная Java! Разумно сочетайте дженерики и Варарги

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

Сегодня мы смотрим на пересечение вараргов и дженерики . Обе эти функции были представлены в Java 5, поэтому они имеют долгую историю; однако, как мы увидим в обзоре этой главы, они не очень хорошо работают вместе. Причина этого в том, что varargs – это дырявая абстракция. Если вы не взаимодействовали с аргументом varargs до того, как он разрешит клиенту вашего кода передавать переменное количество аргументов в вашу функцию. Он выполняет это, сворачивая их в массив с другой стороны. К сожалению, этот массив, который, по-видимому, должен быть просто деталью реализации, раскрыт и является тем, что приводит к далеко не идеальному взаимодействию между универсальными и varargs. Так что давайте углубимся в детали.

Мы снова обнаруживаем, что говорим о непроверяемых типах, которые, в качестве обновления, являются типами, которые имеют меньше информации о типах во время выполнения, чем во время компиляции. Массивы надежны, в то время как универсальные – нет. Если мы создадим функцию, которая принимает непроверяемый параметр varargs, нам будет выдано предупреждение о Возможном загрязнении кучи . Загрязнение кучи здесь имеется в виду наличие параметризованного типа, который ссылается на объект, не относящийся к этому типу. Это звучит, вероятно, более запутанно, чем есть на самом деле, поэтому давайте рассмотрим пример:

void badIdea(List... stringLists) {
  List integerList = List.of(13);
  Object[] objects = stringLists;
  objects[0] = integerList;
  String myString = stringList[0].get(0);
} 

Честно говоря, с вышеуказанной функцией много чего не так; однако это краткий, поучительный пример того, что нам нужно. В то время как приведенный выше код компилируется, во время выполнения наша последняя строка создает исключение ClassCastException без того, чтобы мы когда-либо писали приведение. Причина этого в том, что компилятор добавляет для нас невидимое приведение, которое из-за того, что Объект не является подтипом String , приводит к нашему ClassCastException во время выполнения. Это показывает, что мы потеряли нашу безопасность типов, которую мы ищем, когда используем дженерики. Применяя это в целом, это означает, что мы никогда не должны хранить значение в универсальном параметре varargs.

Учитывая эту небезопасность, зачем разработчикам языка вообще разрешать параметризованный аргумент varargs? Это особенно интересно, если учесть, что, как обсуждалось ранее, параметризованные массивы запрещены, почему возникает несоответствие? Они позволили этой несогласованности сохраниться, поскольку параметризованные аргументы vararg оказались чрезвычайно полезными на практике. Сам основной язык использует эту возможность в ряде мест, таких как Массивы.asList(T...a); и Коллекции.addAll(Коллекция супер T> c, T... элементы); . Их также можно безопасно использовать, если соблюдать определенные правила.

Учитывая, что основной язык использует эту возможность, и вы, скорее всего, использовали функции, указанные ранее, вы можете задать вопрос: “Почему я не видел этих предупреждений, о которых вы упоминали? “Это справедливый вопрос. До Java 7 у вызывающего или автора методов, которые использовали общие переменные, не было возможности избежать описанных выше предупреждений, кроме аннотирования вызывающего кода с помощью @SuppressWarnings ("непроверенный"). . Это привело к снижению читаемости и потенциальной усталости от предупреждений, что может привести к слепоте к реальным проблемам. Вот почему в Java 7 была введена аннотация @SafeVarargs .

Аннотация @SafeVarargs представляет собой обещание автора функции о том, что функция безопасна для использования с параметризованным аргументом varargs. Хотя язык не может обеспечить безопасность метода, он позволяет автору подавлять предупреждение для потребителя функции, приводящее к более чистому коду во всех учетных записях.

Я несколько раз упоминал, что существуют правила, которым необходимо следовать, чтобы гарантировать, что метод может безопасно использовать параметризованный аргумент varargs, так каковы же они?

  1. Как мы уже видели в первом примере, функция не должна хранить никаких значений в аргументе varargs.
  2. Функция не должна допускать, чтобы ссылка на массив varargs выходила из функции, так как это может привести к тому, что небезопасный код получит доступ к массиву.

Давайте рассмотрим другой пример, чтобы увидеть, как правило два может повредить нашему коду. Рассмотрим следующую вспомогательную функцию:

static  T[] toArray(T... items) {
  return items;
}

Эта функция хороша и просто возвращает предоставленный ей массив. Теперь давайте добавим функцию для ее вызова:

static  T[] pickTwo(T item1, T item2, T item3) {
  switch(ThreadLocalRandom.current().nextInt(3)) {
    case 0: return toArray(item1, item2);
    case 1: return toArray(item2, item3);
    case 2: return toArray(item1, item3);
  }
  // can't get here
  throw new AssertionError();
}

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

public static void main(String[] args) {
  String[] picked = pickTwo("Item1", "Item2", "Item3");
}

Вышесказанное опять же довольно просто, и при компиляции здесь даже нет предупреждения. Так что же происходит во время выполнения? Снова мы получаем ClassCastException из невидимой Строки приведения, которая находится перед выберите два ("Элемент1", "Элемент2", "Элемент3") . Давайте разберемся, почему это так. Поскольку единственным типом, который может содержать все возможные T значения, является Объект , компилятор выделяет Объект[] , который должен быть возвращен выбери Два. Затем в нашей функции main мы присваиваем возвращаемый массив String[] с невидимым приведением к String[] , добавляемым компилятором. Все это вместе взятое приводит к нашему ClassCastException. Это исключение довольно раздражает, так как мы находимся на уровне, удаленном от того места, где находится фактическая проблема, и на месте сбоя у нас даже нет предупреждения, указывающего нам направление нашей проблемы. Это показывает опасность передачи этих параметризованных параметров varargs другим функциям. Единственными двумя местами, где допустимо передавать универсальные массивы varargs, являются другие @SafeVarargs функции, а также функции, не являющиеся varargs, которые просто вычисляют что-то со значениями массива.

Давайте рассмотрим типичный пример функции, которая правильно использует @SafeVarargs

@SaveVarargs
static  List flatten(List... lists) {
  List result = new ArrayList<>();
  for (List list : lists) {
    result.addAll(list);
  }
  return result;
}

Этот метод безопасен, потому что он не устанавливает значение какой-либо записи в массиве, а также не передает массив varargs какому-либо ненадежному коду.

Эмпирическое правило состоит в том, что мы должны использовать @SafeVarargs для каждой функции, которую мы пишем, которая принимает общий или параметризованный тип. Следует отметить, что мы не можем аннотировать метод с помощью @SafeVarargs , который переопределяется, потому что невозможно гарантировать безопасность всех переопределений функции. В Java 8 аннотация была разрешена только для статических методов и методов конечного экземпляра. В Java 9 и за ее пределами это также законно для методов частных экземпляров.

Какие у нас есть альтернативы использованию общих методов vararg? Как предлагалось в предыдущей главе, мы можем заменить многие способы использования массива на Список s. Вот как выглядела бы описанная выше функция без varargs, но вместо этого с использованием Списка .

static  List flatten(List> lists) {
  List result = new ArrayList<>();
  for (List list : lists) {
    result.addAll(list);
  }
}

За исключением подписи, этот метод точно такой же, как и описанный выше вариант. Мы бы назвали это так сгладить (Список. из (друзей, римлян, соотечественников)) вместо сплющить (друзья, римляне, соотечественники) как и раньше. Это добавляет дополнительный вызов функции в List.of , которого у нас не было в версии varargs, но он обеспечивает нам безопасность типов, которую мы хотим, и устраняет необходимость аннотировать метод с помощью @SafeVarargs.

К сожалению, варарги и дженерики плохо сочетаются. При этом, если мы можем гарантировать безопасность операций, которые мы выполняем с массивом, лежащим в основе нашего параметра vararg, у нас есть возможность подавить предупреждения. У нас также есть возможность заменить наши методы использования vararg на List s, которые часто могут обеспечить нам безопасность типов, а также избежать сбоев метода varargs.

Оригинал: “https://dev.to/kylec32/effective-java-combine-generics-and-varargs-judiciously-4hf5”