Сегодня мы обсудим, как наиболее эффективно внедрить дженерики в ваш код.
Не Используйте Необработанные Типы
Это утверждение кажется очевидным. Необработанные типы ломают всю идею дженериков. Его использование не позволяет компилятору обнаруживать ошибки типа. Но это не единственная проблема. Предположим, у нас есть такой класс.
class Container{ private final T value; ... public List getNumbers() { return numbersList; } }
Предположим, что нас не волнует сам универсальный тип. Все, что нам нужно сделать, это пересечь список номеров .
public void traverseNumbersList(Container container) {
for (int num : container.getNumbers()) {
System.out.println(num);
}
}
Удивительно, но этот код не компилируется.
error: incompatible types: Object cannot be converted to int
for (int num : container.getNumbers()) {
^
Дело в том, что использование необработанного типа стирает не только информацию об общем типе класса, но даже о предопределенных типах. Итак, Список<Целое число> становится просто списком.
Что мы можем с этим поделать? Ответ прост. Если вас не волнует универсальный тип, используйте оператор подстановки .
public void traverseNumbersList(Container> container) {
for (int num : container.getNumbers()) {
System.out.println(num);
}
}
Этот фрагмент кода работает отлично.
Предпочитаю Входные Данные На основе Подстановочных Знаков
Основное различие между массивами и дженериками заключается в том, что массивы являются ковариантными , в то время как дженерики таковыми не являются. Это означает, что Number[] является супертипом для Integer[] . И Object[] является супертипом для любого массива (кроме примитивных). Это кажется логичным, но это может привести к ошибкам во время выполнения.
Number[] nums = new Long[3]; nums[0] = 1L; nums[1] = 2L; nums[2] = 3L; Object[] objs = nums; objs[2] = "ArrayStoreException happens here";
Этот код компилируется, но выдает неожиданное исключение. Для решения этой проблемы были созданы дженерики.
Listnums = new ArrayList (); List longs = new ArrayList (); nums = longs; // compilation error
Список<Длинный> не может быть присвоен Списку<Номер> . Хотя это помогает нам избежать ArrayStoreException , он также устанавливает границы, которые могут сделать API не гибким и слишком строгим.
interface Employee {
Money getSalary();
}
interface EmployeeService {
Money calculateAvgSalary(Collection employees);
}
Все выглядит хорошо, не так ли? Мы даже предусмотрительно ввели Collection в качестве входного параметра. Это позволяет нам передавать Список , Установить , Очередь и т.д. Но не забывайте, что Employee – это просто интерфейс. Что, если бы мы работали с коллекциями конкретных реализаций? Например, Список<Менеджер> или Установить<Бухгалтер> ? Мы не могли пройти мимо них напрямую. Таким образом, для этого потребуется каждый раз переносить элементы в коллекцию типа Employee .
Или мы можем использовать оператор подстановки.
interface EmployeeService {
Money calculateAvgSalary(Collection extends Employee> employees);
}
List managers = ...;
Set accountants = ...;
Collection engineers = ...;
// All these examples compile successfully
employeeService.calculateAvgSalary(managers);
employeeService.calculateAvgSalary(accountants);
employeeService.calculateAvgSalary(engineers);
Как вы можете видеть, правильное универсальное использование значительно облегчает жизнь программиста. Давайте посмотрим на другой пример.
Предположим, нам нужно объявить API для службы сортировки. Вот первая наивная попытка.
interface SortingService {
void sort(List list, Comparator comparator);
}
Теперь у нас есть проблема другого рода. Мы должны быть уверены это Компаратор был создан именно для типа T . Но это не всегда так. Мы могли бы создать универсальный для Сотрудника , который в данном случае не работал бы ни для Бухгалтера , ни для Менеджера .
Давайте сделаем API немного лучше.
interface SortingService {
void sort(List list, Comparator super T> comparator);
}
// universal comparator
Comparator comparator = ...;
List managers = ...;
List accountants = ...;
List engineers = ...;
// All these examples compile successfullly
sortingService.sort(managers, comparator);
sortingService.sort(accountants, comparator);
sortingService.sort(engineers, comparator);
Вы знаете, ограничения немного сбивают с толку. Все эти ? распространяется На и ? супер T кажется слишком сложным. К счастью, существует простое правило, которое может помочь определить правильное использование — PECS (производитель-расширяет, потребитель-супер). Это означает, что производитель должен быть типа ? расширяет T в то время как потребитель ? супер Т один.
Давайте рассмотрим конкретные примеры. Способ Денежный сервис.Рассчитать заработную плату , который мы описали ранее, принимает производителя. Потому что коллекция создает элементы, которые используются для дальнейших вычислений.
Другой пример взят прямо из стандартной библиотеки JDK. Я говорю о методе Collection.addAll .
interface Collection{ boolean addAll(Collection extends E> c); ... }
Определение верхней границы generic позволяет нам объединять Collection > и Collection или любые другие классы, которые используют один и тот же интерфейс.
А как насчет потребителей? Компаратор , который мы использовали в Служба сортировки является прекрасным примером. Этот интерфейс имеет один метод, который принимает универсальный тип и возвращает конкретный. Типичный пример потребителя. Другими являются Предикат , Потребитель , Сопоставимый и многие другие из пакета java.util . В основном все эти интерфейсы следует использовать с ? супер Т связанный.
Существует также уникальный продукт, который одновременно является производителем и потребителем. Это java.util. Функция . Он преобразует входное значение из одного типа в другой. Итак, обычно используется функция Function супер T, ? расширяет R> . Это может показаться пугающим, но это действительно помогает создавать надежное программное обеспечение. Вы можете узнать, что все функции сопоставления в Stream interface следуют этому правилу.
interface Streamextends BaseStream > { Stream map(Function super T, ? extends R> mapper); Stream flatMap(Function super T, ? extends Stream extends R>> mapper); IntStream flatMapToInt(Function super T, ? extends IntStream> mapper); ... }
Можно заметить, что Служба сортировки.сортировка принимает Список вместо из списка расширяет T> . Почему это так? Это продюсер после всего. Ну, дело в том, что верхняя и нижняя границы имеют смысл в по сравнению с предопределенным типом. Поскольку Sorting Service.sort метод параметризует сам себя, нет смысла ограничивать Список с дополнительными границами. С другой стороны, если бы Служба сортировки имела универсальный тип, ? расширяет T будет иметь свое значение.
interface SortingService{ void sort(List extends T> list, Comparator super T> comparator); }
Не Возвращайте Ограниченные Контейнеры
Верхние Границы
Некоторые разработчики, которые обнаружили силу ограниченных универсальных типов, могут посчитать, что это серебряная пуля. Это может привести к появлению фрагментов кода, подобных следующему.
interface EmployeeRepository {
List extends Employee> findEmployeesByNameLike(String nameLike);
}
Что здесь не так? Во-первых, List расширяет сотрудника> не может быть назначен Список<Сотрудник> без приведения. расширяет сотрудника>
Например, значения какого типа мы можем поместить в коллекцию, возвращаемую EmployeeRepository.findEmployeesByNameLike(Строка) ? Вы можете предположить, что это что-то вроде / Бухгалтер , Менеджер ,
List extends Employee> employees = employeeRepository.findEmployeesByNameLike(nameLike); employees.add(new Accountant()); // compile error employees.add(new SoftwareEngineer()); // compile error employees.add(new Manager()); // compile error employees.add(null); // passes successfully 👍
Этот фрагмент кода выглядит нелогичным, но на самом деле все работает просто отлично. Давайте разберем это дело. Прежде всего, нам нужно определить, какие коллекции могут быть назначены List расширяет сотрудника> .
List extends Employee> accountants = new ArrayList(); List extends Employee> managers = new ArrayList (); List extends Employee> engineers = new ArrayList (); // ...any other type that extends from Employee
Итак, в основном список любого типа, который наследуется от Employee , может быть назначен List расширяет сотрудника> . Это усложняет добавление новых элементов. Компилятор не может знать точный тип списка. Вот почему он запрещает добавлять какие-либо элементы, чтобы устранить потенциальное загрязнение кучи. Но null – это особый случай. Это значение не имеет своего собственного типа. Он может быть присвоен чему угодно (кроме примитивов). Именно по этой причине null является единственным допустимым значением для добавления.
Как насчет извлечения элементов из списка?
List extends Employee> employees = ...;
// passes successfully 👍
for (Employee e : employees) {
System.out.println(e);
}
Employee – это супертип для любого потенциального элемента, который может содержаться в списке. Здесь нет никаких оговорок.
Нижние Границы
Какой элемент мы можем добавить в List супер Сотрудник> ? Логика подсказывает нам, что это либо Объект или Сотрудник . И это снова вводит нас в заблуждение.
List super Employee> employees = ...;
employees.add(new Accountant()); // passes successfully 👍
employees.add(new Manager()); // passes successfully 👍
employees.add(new SoftwareEngineer()); // passes successfully 👍
employees.add(
new Employee(){/*implementation*/} // passes successfully 👍
);
employees.add(new Object()); // compile error
Опять же, чтобы разобраться в этом случае, давайте выясним, какие коллекции могут быть назначены List супер сотрудник> .
List super Employee> employees = new ArrayList(); List super Employee> objects = new ArrayList
Компилятор знает, что список может состоять либо из типов Object , либо из типов Employee . Вот почему Бухгалтер , Менеджер , Инженер-программист и Сотрудник могут быть безопасно добавлены. Все они реализуют интерфейс Employee и наследуются от класса Object . В то же время Object не может быть добавлен, поскольку он не реализует Employee .
Напротив, чтение из List супер сотрудник> это не так просто.
List super Employee> employees = ...;
// compile error
for (Employee e : employees) {
System.out.println(e);
}
Компилятор не может быть уверен, что возвращаемый элемент имеет тип Employee . Возможно, это Объект . Вот почему код не компилируется.
Вывод о Верхних и Нижних Границах
Мы можем возобновить, что верхняя граница делает коллекцию доступной только для чтения , в то время как нижняя граница делает ее доступной только для записи . Означает ли это, что мы можем использовать их в качестве возвращаемых типов, чтобы ограничить доступ клиента к манипулированию данными? Я бы не рекомендовал этого делать.
Коллекции с верхней границей не являются полностью доступными только для чтения, потому что вы все равно можете добавить к ним null . Коллекции с нижними границами не являются полностью доступными только для записи, потому что вы все еще можете считывать значения как Объект . Я считаю, что гораздо лучше использовать специальные контейнеры, которые должны предоставлять необходимый доступ к экземпляру. Вы можете либо применить стандартные утилиты JDK, такие как Collections.unmodifiableList , либо использовать библиотеки, которые выполнят эту работу (например, Vavr ).
Коллекции с верхней и нижней границами действуют намного лучше в качестве входных параметров. Вы не должны смешивать их с возвращаемыми типами.
Рекурсивные дженерики
Мы уже упоминали рекурсивные дженерики в этой статье. Это интерфейс Stream . Давайте взглянем еще раз.
interface Streamextends BaseStream > { Stream map(Function super T, ? extends R> mapper); Stream flatMap(Function super T, ? extends Stream extends R>> mapper); IntStream flatMapToInt(Function super T, ? extends IntStream> mapper); ... }
Как вы можете видеть, Stream простирается от BaseStream , который параметризован самим Stream . В чем причина этого? Давайте погрузимся в BaseStream , чтобы выяснить это.
public interface BaseStream> extends AutoCloseable { S sequential(); S parallel(); S unordered(); S onClose(Runnable closeHandler); Iterator iterator(); Spliterator spliterator(); boolean isParallel(); void close(); }
Базовый поток является типичным примером fluent API но вместо того, чтобы возвращать сам тип, методы возвращают S расширяет базовый поток S> . S> . Давайте представим, что BaseStream
public interface BaseStreamextends AutoCloseable { BaseStream sequential(); BaseStream parallel(); BaseStream unordered(); BaseStream onClose(Runnable closeHandler); Iterator iterator(); Spliterator spliterator(); boolean isParallel(); void close(); }
Как это повлияет на весь Stream API?
Listemployees = ...; employees.stream() .map(Employee::getSalary) .parallel() .reduce(0L, (acc, next) -> acc + next); // compile error ⛔: cannot find symbol
Метод reduce принадлежит Stream но не к интерфейсу BaseStream . Следовательно, параллельный метод возвращает Базовый поток . Итак, reduce не может быть найден. Это становится более понятным в приведенной ниже схеме.
Рекурсивные дженерики пригодятся в этой ситуации.
Такой подход позволяет нам разделять интерфейсы, что приводит к лучшей ремонтопригодности и удобочитаемости.
Вывод
Я надеюсь, что вы узнали что-то новое о Java generics. Если у вас есть какие-либо вопросы или предложения, пожалуйста, оставьте свои комментарии ниже. Спасибо за чтение!
Ресурсы
Оригинал: “https://dev.to/kirekov/java-generics-advanced-cases-3iah”