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

Способ обучения без FP - это когда вы учитесь, применяя предположения. Иногда предположение проявляется в виде совета / решения старшего инженера или в виде ответа, получившего наибольшее количество голосов на StackOverflow. Хотя они полезны, когда вам нужно исправить производственную ошибку, преднамеренное обучение - это то, что отделяет исправителя ошибок от того, кто настраивает дом таким образом, чтобы ошибка не существовала.

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

Нахождение максимального элемента в коллекции

Это довольно тривиальная задача в Java, большинство из нас уже сталкивалось с ней раньше. Если это Comparable, например Integer, все, что вам нужно сделать, это вызвать Collections.max, и вы получите максимальный элемент.

Integer maxInt = Collections.max(integerList);

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

Компилятор будет жаловаться, что Line не является Comparable, поскольку Collections.max принимает Comparable, он полагается на метод compareTo Comparable для нахождения максимума. Мы можем решить эту проблему, сделав Line реализует Comparable и переопределив метод compareTo для сравнения с использованием длины.

Теперь, если мы снова запустим код, он вернет самую длинную строку в списке.

Загадочная история Collections.max

Вы когда-нибудь смотрели пристально в Java SDK или исходный код любой сторонней библиотеки, чтобы увидеть, как все это реализовано? Вы должны это сделать по нескольким причинам:

  1. Знание реализации поможет вам оправдать ее использование или отсутствие такового.
  2. Узнайте о передовых методах работы и о том, как разрабатываются промышленные стандартные библиотеки.
  3. См. Практическое применение идиом и шаблонов из учебников.
  4. Откройте для себя новую концепцию.

Любопытство делает вас лучшим инженером. В общем, любопытство помогает вам лучше во всем, чем вы занимаетесь.

Для Collections API вам не нужно далеко ходить, прежде чем наткнетесь на что-то интересное. Я выбрал Collections.max, потому что он имеет излишне сложный API, идеально подходящий для излишне долгого обсуждения. Кроме того, он включает Comparable, который является учебным примером Recursive Type Bound вместе с Enum.

Посмотри на этого зверя.

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)

Если вы не опытный разработчик API, то WTF - правильный ответ.

Но что более важно, так это реакция после первоначального WTF. Вы заинтригованы или закрываете вкладку? Большинство из нас отвергнет это как то, что нас не касается. Хотя в большинстве случаев это так, мы могли бы продолжать это делать и быть посредственными, или мы могли бы собрать себя вместе и заняться тяжелым делом.

Рекурсивная привязка типов

Объявление класса class Line implements Comparable<Line> является интересным. Реализация Comparable позволяет сортировать / искать строку с помощью служебных методов Collections, но почему не просто class Line implements Comparable? Что с ограничением самой Линии?

Это называется рекурсивным ограничением типа - когда тип ограничен сам по себе.

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

Итак, class Line implements Comparable<Line> просто говорит, что Line - это класс, который можно сравнить с самим собой.

Если бы мы написали class Line implements Comparable<String>, мы бы сказали, что Line можно сравнить со String. Теоретически это сработает, но особого смысла в этом нет. Вот почему, когда класс реализует Comparable, он обычно ограничен сам по себе. Примером таких классов являются упакованные примитивы, String, Enum и File.

Реконструкция Collections.max

Вооружившись знаниями о рекурсивной привязке типов, давайте продолжим изучение Collections.max.

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

Беглый взгляд на реализацию Max в OpenJDK показывает, что реализация довольно тривиальна и проста. По сути, он перебирает коллекции и устанавливает текущий максимум, если compareTo возвращает положительное число. Остальное обсуждение будет опускать код реализации и объяснение исключительно через API, который является сигнатурой метода.

Минимально жизнеспособный продукт

Следуя Первому принципу, мы зададимся вопросом, какое минимальное требование составляет метод Макса? Вероятно, просто нужно взять Collection of Comparable и вернуть элемент max.

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

  1. Тип аргумента слишком строгий. Мы можем передать только Collection<Comparable<T>>, даже не Collection<Line>, хотя Line является подтипом Comparable<T>.
  2. Фактически этот API не будет работать, потому что мы не можем сравнивать элементы в коллекции. Что касается Comparable<T>, все, что мы знаем, это то, что это тип, который можно сравнить с некоторым T. Если мы передадим коллекцию class StyledLine implements Comparable<Line>, тогда их можно будет сравнить только с Line, но не друг с другом!
  3. По той же причине эта функция может возвращать только Comparable<T> вместо T.

Чтобы исправить это, нам нужно заверить компилятор, что Макс будет получать только коллекцию типа, который можно сравнить с самим собой, а не просто типа, который можно сравнить с некоторым T.

Таким образом, вместо того, чтобы брать коллекцию Comparable<T>, Макс должен брать коллекцию T extends Comparable<T>. Это еще один прекрасный пример рекурсивной привязки типов, он захватывает любой тип, который можно сравнить с самим собой.

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

Напомним,

  1. Comparable<T> относится к типу, который можно сравнить с некоторыми T.
  2. T extends Comparable<T> относится к типу, который можно сравнить с самим собой.

На этом этапе мы подошли к MVP метода Max. Тем не менее, он все еще довольно далек от первоначального мутанта, который мы видели ранее. Пришло время исследовать некоторые нюансы и трагедию, которые сделали Collections.max то, чем оно является сегодня.

Нюанс - Тип, который можно сравнить со своим супертипом

Предположим, у нас есть новый класс под названием StyledLine. Он наследует все от Line и добавляет поле LineStyle.

Поскольку StyledLine является подклассом Line, он также является Comparable, однако он не отменяет compareTo, поскольку при сравнении StyledLine не нужно учитывать LineStyle. Например, пунктирная линия StyledLine по сравнению с Line или пунктирная StyledLine считается равной, если они имеют одинаковую длину.

Итак, мы знаем, что StyledLine можно сравнить с самим собой или его супертипом Line.

Имея дело с этим типом класса, наша версия метода Max MVP не работает, потому что он имеет дело только с типом, который можно сравнить с самим собой. Если мы попытаемся передать Max List of StyledLine, мы получим ошибку компиляции.

Для работы с такими классами, как StyledLine, T extends Comparable<T> необходимо изменить на T extends Comparable<? super T>.

Принцип подстановки Лискова в действии

<T extends Comparable<? super T>> контравариантен. В этом контексте тип, который можно сравнивать сам с собой (Line), является подтипом типа, который можно сравнивать с самим собой или его супертипом (StyledLine).

Вернитесь и прочтите о дисперсии, если вы еще не сделали этого.

Делая это, мы применяем LSP, в частности концепцию контравариантности в аргументе метода. Другими словами, аргумент метода можно заменить его супертипом. Метод Max принимает более широкие возможности, чем должен, он не только поддерживает Line, но и выходит за рамки поддержки StyledLine.

Парадоксально, да? LSP - это довольно глубокая философия с простыми формулировками. В следующий раз, когда кто-нибудь покажет вам эту дурацкую картинку с уткой, расскажите им о <T extends Comparable<? super T>>.

Трагедия - Совместимость с кодом Pre-Generics

При стирании типа ограниченный универсальный тип будет стерт до его крайнего левого конкретного типа, поэтому <T extends Comparable<? super T>> будет стерт до просто Comparable. Для метода Max, который мы получили до сих пор, сгенерированный код после стирания типа выглядит следующим образом:

public static Comparable max(Collection<Comparable> coll) { ... }

Однако Java до версии 5 не поддерживает дженерики, все приводится между типом и объектом. На тот момент Collection была написана для работы с Object, это выглядит примерно так:

public static Object max(Collection coll) { ... }

Как обеспечить обратную совместимость?

Поэтому они исправляют ™, используя тот факт, что,

  1. Параметр универсального типа стирается до самого левого типа, а
  2. Множественная привязка, например <T extends A & B>, T будет удалено до A.

Если добавить Object в качестве первой границы, T будет удален в Object вместо Comparable, таким образом, полностью совместим со старым кодом, обеспечивая при этом проверку безопасности типов во время компиляции для дженериков.

public static <T extends Object & Comparable<? super T>> T max(Collection<T> coll) { ... }

После компиляции он сгенерирует подпись, как если бы он был написан в 2000 году.

Ну наконец то

Помните PECS? В общем, если метод только читает из коллекции, рекомендуется связать параметр типа коллекции с extends. Поскольку Макс только читает из коллекции, Collection<T> следует изменить на Collection<? extends T>, чтобы Макс мог также работать с любым подтипом T.

Хотя в этом конкретном методе это не имеет особого значения, потому что граница T очень четко определена в <T extends Object & Comparable<? super T>>. Эти, казалось бы, ненужные дополнения сделаны ради единообразия API.

Вот мы и подошли к оригинальной подписи Collection.max, какая поездка!

Резюме

  1. Comparable<T> - тип, который можно сравнить с некоторыми Т.
  2. T extends Comparable<T> - тип, который можно сравнить сам с собой.
  3. T extends Comparable<? super T> - тип, который можно сравнивать сам с собой, или его супертип.
  4. T extends Object & Comparable<? super T> - То же, что и в предыдущем пункте, но тип удален на Object для обратной совместимости.
  5. Collection<? extends T> - коллекция только для чтения.

Чтобы собрать это вместе,

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)

Collections.max принимает доступную только для чтения коллекцию типа, который можно сравнить с самим собой, или с его супертипом. Также сохраняется обратная совместимость.

Следуй за мной @ https://twitter.com/darrenbkl