Так все-таки, Java или Scala? Интернет полнится спорами о том, неужто Scala действительно лучше, чем Java, или «кто вам дал право так утверждать»? Урс Петер и Сандер ван ден Берг озаботились тем, чтобы провести подробное сравнение, из которого следует, что Java 8 – это все равно что улучшенный Scala. Парадокс?
Урс Петер – старший консультант в компании Xebia. Он работает в ИТ уже более 10 лет и занимал ряд различных позиций: разработчик, архитектор, тимлид и мастер Scrum. В ходе карьеры он работал с самыми разными языками, техниками разработки ПО и инструментами для платформы JVM. Урс Петер – один из первых сертифицированных тренеров по Scala в Европе и действующий председатель Голландского общества энтузиастов по изучению Scala (DUSE).
Сандер Ван дер Берг работает в сфере ИТ с 1999 года. Он был разработчиком в нескольких компаниях, в основном занимался решениями MDA/MDD. В качестве старшего консультанта в 2010 году перешел на работу в Xebia, где занялся внедрением гибкой архитектуры и продвижением Scala. Сандер интересуется языковым проектированием и занят в нескольких сообществах функциональных программистов. Сандер любит красивые решения сложных проблем, и потому предпочитает писать именно на Scala. Кроме того, Сандеру нравятся языки Clojure, Haskell и F#.
Введение
Выход JDK 8 (новый комплект для разработки на Java) запланирован на 2013 год, и в Oracle уже вполне четко представляют себе, что в него войдет. Саймон Риттер, выступавший на конференции QCon в Лондоне в этом году, обрисовал новые возможности, которые будут представлены в пакете. В частности, нас ожидают: модульная организация (проект Jigsaw), конвергенция JRockit/Hotspot, аннотирование типов и проект Lambda.
С точки зрения языка наиболее важным изменением является, пожалуй, проект Lambda. Он обеспечивает поддержку лямбда-выражений, виртуальных методов расширения, а также оптимизирует поддержку многоядерных платформ – в виде параллельных коллекций.
Большинство этих возможностей уже доступны во многих других языках, работающих с виртуальной машиной Java – в частности, в Scala. Кроме того, многие подходы, взятые на вооружение в Java 8, удивительно похожи на подходы Scala. И значит, упражняясь со Scala, уже можно составить впечатление о том, каким будет язык Java 8.
В этой статье мы изучим новые функции Java 8, ориентируясь как на предполагаемый новый синтаксис Java, так и Scala. Мы рассмотрим лямбда-выражения, функции высшего порядка, параллельные коллекции и виртуальные методы расширения, также называемые «типажами» (traits). Кроме того, мы расскажем о новых парадигмах, которые будут интегрированы в Java 8 – в частности, о функциональном программировании.
Читатели смогут убедиться, что новые концепции, внедряемые в Java и уже доступные в Scala, – это не лишнее украшательство, они способны привести к настоящей смене парадигм. Благодаря им мы приобретем массу великолепных возможностей – не исключено, что это приведет к большим переменам в самом искусстве программирования.
Лямбда-выражения/Функции
Ну наконец-то в Java 8 появятся лямбда-выражения! Правда, в рамках проекта «Lambda» эти выражения доступны уже с 2009 года (тогда они еще назывались «Java-замыканиями»). Прежде, чем перейти к обсуждению примеров кода, стоит объяснить, почему лямбда-выражения – такое полезное дополнение к инструментарию Java.
Зачем нужны лямбда-выражения
Как правило, лямбда-выражения используются при разработке графических пользовательских интерфейсов (GUI). Вообще, программирование GUI строится вокруг соединения поведений с событиями. Например, если пользователь нажимает кнопку (событие), то программа должна выполнить определенное поведение. Поведение может заключаться, например, в сохранении какой-то информации в базе данных. В Swing, скажем, это делается при помощи ActionListener:
Этот пример показывает, как использовать класс ButtonHandler в качестве замены обратного вызова. Класс ButtonHandler присутствует в коде только для того, чтобы содержать единственный метод: actionPerformed. Данный метод определен в интерфейсе ActionListener. Этот код можно упростить, воспользовавшись внутренними анонимными классами:
Итак, код стал немного чище. Рассмотрев его внимательнее, мы видим, что по-прежнему создаем экземпляр класса только для того, чтобы вызвать единственный метод. Вот для решения именно таких проблем как раз и применяются лямбда-выражения.
Лямбда-выражения в качестве функций
Лямбда-выражение – это функциональный литерал. Он определяет функцию с входными параметрами и тело функции. Синтаксис лямбда-выражений в Java 8 пока еще обсуждается, но, вероятно, будет выглядеть примерно так:
А вот и конкретный пример:
Это лямбда-выражение вычисляет разницу в длине двух строк. Этот синтаксис можно расширить, в частности, не определяя типы аргументов (такую ситуацию мы рассмотрим ниже). Кроме того, можно создавать многострочные определения, используя { and }, чтобы группировать выражения.
Идеальный пример использования такого выражения – метод Collections.sort(). Он позволяет нам отсортировать коллекцию строк (Strings) по их длине:
Итак, вместо того чтобы заполнять метод sort реализацией Comparator, как это делалось бы в современном языке Java, для достижения аналогичного результата нам достаточно просто передать приведенное выше лямбда-выражение.
Лямбда-выражения в качестве замыканий
У лямбда-выражений есть ряд интересных свойств. Одно из них состоит в том, что они являются замыканиями. Замыкание обеспечивает для функции доступ к переменным, находящимся вне ее непосредственной лексической области видимости (лексического контекста).
В примере показано, что лямбда-выражение имеет доступ к строке outer, определенной вне области видимости этого выражения. При реализации внутристрочных сценариев лямбда-выражения могут быть очень удобны.
Выведение типа – и для лямбда-выражений
Выведение типа (type inference) – возможность, появившаяся в Java 7, – также применима и к лямбда-выражениям. В сущности, выведение типа заключается в том, что программист может обойтись без определения типа везде, где компилятор сам может вывести тип – то есть определить его дедуктивным методом.
Если применить выведение типа с лямбда-выражением, предназначенным для сортировки, то получится такой код:
Как видите, типы параметров s1 и s2 опущены. Поскольку компилятору известно, что в списке содержится коллекция строк (Strings), ему также известно, что лямбда-выражение, используемое в качестве компаратора (сравнительного механизма), должно иметь два параметра типа String. Следовательно, отпадает необходимость в явном определении типов, хотя, конечно, это не возбраняется.
Основная польза от выведения типов – в сокращении шаблонного (boilerplate) кода. Если компилятор способен вывести тип за нас – зачем нам самим это делать?
Да здравствуют лямбда-выражения, прощайте, анонимные внутренние классы
Рассмотрим, как лямбда-выражения и выведение типов позволяют упростить пример с обратным вызовом, который мы затронули в начале статьи:
Вместо того чтобы определять класс для содержания нашего метода обратного вызова, мы просто передаем лямбда-выражение методу addActionListener. Таким образом, мы не просто сокращаем шаблонный код и улучшаем его читаемость, но и можем прямо выразить единственную инструкцию, которая нас интересует: код обработки события.
Пока мы не слишком увлеклись примерами использования лямбда-выражений, давайте посмотрим, как аналогичные выражения реализованы в Scala.
Лямбда-выражения в языке Scala
Функция – это основной первоэлемент функционального программирования. В Scala комбинируются объектно-ориентированные черты, известные всем, например, по языку Java, и функциональное программирование. В Scala базовым первоэлементом является как раз лямбда-выражение, называемое «функцией» или «функциональным литералом». В Scala функции являются «сущностями первого класса» (first-class citizens). Их можно присваивать финальным и нефинальным переменным (val или var, соответственно). В качестве аргумента функцию можно передавать другой функции, кроме того, их можно комбинировать для создания новых функций.
В Scala функциональный литерал записывается следующим образом:
Например, предыдущее лямбда-выражение на Java, вычисляющее разницу длины двух строк, в Scala будет записываться так:
В Scala функциональные литералы также являются замыканиями. Они могут получать доступ к переменным, определенным вне их лексической области видимости.
Результат этого примера равен 20. Как видите, мы присвоили функциональный литерал переменной myFuncLiteral.
Синтаксическое и семантическое сходство лямбда-выражений из Java 8 и функций из Scala само по себе примечательно. Семантически они практически идентичны, а синтаксическая разница заключается только в представлении символа-стрелки (Java8: -> , Scala: => ), а еще проявляется в сокращенной нотации, которая здесь не рассматривается.
Функции высшего порядка как многоразовые элементы конструкций
Важнейшее достоинство функциональных литералов заключается в том, что их можно передавать точно так же, как любые другие литералы, вроде строки (String) или произвольного объекта (Object). Такая ситуация открывает широчайшие возможности и позволяет работать с исключительно компактными, многоразовыми программными конструкциями.
Наша первая функция высшего порядка
Когда мы передаем функциональный литерал методу, мы, в сущности, имеем дело с такой ситуацией: один метод принимает другой. Такие методы называются «функциями высшего порядка» (higher-order functions). Метод addActionListener из предыдущего примера кода Swing – именно такой. Мы также можем определять собственные функции высшего порядка, которые нам очень пригодятся. Рассмотрим простой пример:
Здесь у нас есть метод measure, который измеряет время, необходимое для выполнения обратного вызова к функциональному литералу, называемому func. Сигнатура func такова, что он не принимает никаких параметров и возвращает результат обобщенного типа Т. Как видите, функции в Scala совсем не обязательно должны иметь параметры, хотя и могут их иметь – и зачастую будут их иметь.
Теперь можно передать любой функциональный литерал (или метод) методу измерения:
По сути, мы только что выполнили разделение обязанностей – отделили измерение длины вызова к методу от самих вычислений. Мы создали две многоразовые слабо связанные программные конструкции (измерительную часть и часть обратного вызова) – эту ситуацию можно сравнить с работой функции-перехватчика (interceptor).
Переиспользование при помощи функций высшего порядка
Рассмотрим другой пример, в котором два многоразовых конструкта связаны несколько теснее.
Метод doWithContact считывает контактную информацию из файла – например, vCard. Далее он предлагает эту информацию синтаксическому анализатору (парсеру), который преобразует ее в домен объекта-контакта. После этого такой объект передается handle, обратному вызову функционального литерала. Этот вызов выполняет с доменом объекта-контакта ту операцию, которая предписывается в функции. Метод doWithContact, как и функциональный литерал, возвращает тип Unit, аналогичный использованию void в Java.
Теперь можно определить различные обратные вызовы, которые могут быть переданы методу doWithContact:
Кроме того, передача обратного вызова может произойти внутристрочно:
В чем польза функций высшего порядка
Видимо, аналогичный код на Java 8 будет выглядеть как-то так:
Как видите, при помощи функций мы реализовали четкое разделение обязанностей: создание объекта домена происходит независимо от его обработки. Таким образом, к программе легко можно подключать новые способы обработки контактных доменных объектов, и при этом их не потребуется сочленять с логикой создания доменного объекта.
Итак, используя функции высшего порядка, мы избавляем код от лишних повторений – таким образом, программист извлекает максимальную пользу из многократного использования кода практически на микроуровне.
Коллекции и функции высшего порядка
Функции высшего порядка обеспечивают эффективный способ работы с коллекциями. Поскольку практически любая программа имеет дело с коллекциями, их обработка может иметь огромное значение.
Фильтрация коллекций: до и после
Давайте рассмотрим типичный пример использования коллекций. Допустим, мы применяем определенное вычисление к каждому элементу коллекции. Например, у нас есть список фотографий (объектов Photo), и мы хотим выделить все фотографии определенного размера.
Здесь у нас слишком много шаблонного кода. В частности, в нем выполняется создание результирующей коллекции и добавление новых элементов в список. В качестве альтернативы можно использовать класс Function, в который выносится все поведение функции:
Получится примерно такой код. Данный пример написан с использованием Guava.
Шаблон немного сократился, но код по-прежнему остается довольно беспорядочным и пространным. Чтобы в полной мере ощутить потенциал и красоту лямбда-выражений, можно перевести этот код на Java 8 или Scala.
Scala: Java 8:
Обе эти реализации красивы и очень лаконичны. Обратите внимание – в обоих вариантах применяется выведение типов: параметр p типа photo явно не определяется. Как мы уже говорили, при работе со Scala вы быстро убедитесь, что выведение типов – стандартная черта этого языка.
Цепочки функций в Scala
Итак, мы сократили код уже минимум на шесть строк и даже улучшили его читаемость. Но самое интересное начинается после сцепления нескольких функций высшего порядка. Чтобы это проиллюстрировать, давайте создадим класс photo и добавим ему дополнительные свойства на языке Scala:
Даже если вы не слишком хорошо знаете Scala, вы можете догадаться, что здесь произошло. Мы объявили класс Photo с тремя переменными экземпляра: name, sizeKb и rates. В переменной rates будут содержаться оценки, которые пользователи выставляют фотографии, – от 1 до 10. Теперь можно создать экземпляр класса Photo – это делается так:
Имея список фотографий, достаточно просто определять различные вопросы, при этом мы будем сцеплять друг с другом по нескольку функций высшего порядка. Предположим, нам требуется извлечь имена всех файлов-изображений, размер которых превышает 10 Мб. Первый вопрос: как преобразовать список названий фотографий в список имен файлов? Для этого мы воспользуемся одной из наиболее мощных функций высшего порядка, которая называется map:
Метод map преобразует каждый элемент коллекции в элемент того типа, который определен в функции, переданной этому методу. В этом примере у нас есть функция, которая получает объект Photo, а возвращает строку (String). Эта строка представляет собой имя файла изображения.
Применяя функцию map, мы можем решить поставленную задачу, сцепив методы map и filter, где map идет вторым.
Не стоит опасаться исключений NullPointerExceptions, поскольку каждый из методов (filter, map и т.д.) всегда возвращает коллекцию. А коллекция, даже если она пуста, не может быть null. Так, наша коллекция с фотографиями с самого начала была пустой, и в результате вычислений у нас, опять же, получится пустая коллекция.
Сцепление функций также называется «композицией функций». При применении композиции функций можно задействовать API Collections, чтобы найти в них блоки для решения нашей проблемы.
Рассмотрим более сложный пример:
Задача: «Вернуть имена всех фотографий, чей средний рейтинг выше 6, отсортировав их по общей сумме присвоенных рейтингов».
Для решения этой задачи воспользуемся методом sortBy, который ожидает функцию, принимающую в качестве ввода элемент типа «коллекция» (в данном случае – Photo). Далее этот метод возвращает объект типа Ordered (в данном случае – Int). Поскольку список (List) не имеет метода для нахождения среднего значения, определим функциональный литерал avg. Этот литерал вычислит среднее значение заданного списка целых чисел (Ints) в анонимной функции, которая будет передана методу filter.
Цепочки функций в Java 8
Пока не совсем ясно, какие функции высшего порядка будут предлагаться в классах коллекций языка Java 8. Скорее всего, будут поддерживаться filter и map. Следовательно, первая из приведенных выше цепочек, вероятно, будет выглядеть так:
Опять же стоит отметить, что в синтаксическом плане разница со Scala практически отсутствует.
Потенциал функций высшего порядка, используемых с коллекциями, очень велик. Они не только очень лаконичны и удобочитаемы, но и позволяют значительно сокращать шаблонный код со всеми вытекающими из этого выгодами – меньше тестов, меньше ошибок. Но и это еще далеко не все…
Параллельные коллекции
До сих пор мы еще не касались одного из самых важных преимуществ, связанных с использованием функций высшего порядка. Такие функции не только улучшают читаемость кода и делают его более кратким, но и добавляют очень важный уровень абстракции. Обратите внимание: во всех предыдущих примерах не было ни одного цикла. Кроме того, для фильтрации коллекций их не приходилось перебирать, ассоциировать (соотносить) или сортировать элементы. Итерация скрыта от пользователя коллекции, говоря иначе, – абстрагирована.
Дополнительный уровень абстракции необходим для максимально эффективного использования многоядерных платформ, поскольку базовая реализация цикла может самостоятельно «определять», как именно перебирать коллекцию. Следовательно, перебор может происходить не только последовательно, но и параллельно. Эффективное использование параллельной обработки при работе с многоядерными платформами – это уже не просто приятная, но обязательная мелочь.
Теоретически все мы можем писать собственный параллельный код. На практике – это не лучший вариант. Во-первых, написание отказоустойчивого параллельного кода с разделяемыми состояниями и получением промежуточных результатов, которые затем потребуется интегрировать, – крайне сложная задача. Во-вторых, нам не хотелось бы в итоге получить ряд совсем несхожих реализаций. Даже притом, что в Java 7 есть Fork/Join, проблему декомпозиции и пересборки данных приходится решать на стороне клиента, а это не тот уровень абстракции, который нам нужен. И, в-третьих, зачем изобретать велосипед, если решение описанных проблем уже найдено в рамках функционального программирования?
Параллельные коллекции в Scala
Рассмотрим простой пример на Scala, где применяется параллельная обработка:
Сначала определяем метод heavyComputation, который – как понятно из названия – занимается интенсивными вычислениями (heavy computation). На четырехъядерном ноутбуке приведенное вычисление выполняется примерно за 4 секунды. После этого мы инстанцируем коллекцию типа range (диапазон от 0 до 10) и инициируем метод par. Метод par возвращает параллельную реализацию, которая предлагает такие же интерфейсы, как и последовательный аналог. Метод par есть в большинстве типов коллекций языка Scala – и это все, что нужно о нем знать.
Далее зададимся вопросом, какой выгоды в плане производительности можно добиться на четырехъядерном компьютере. Чтобы найти ответ, давайте повторно используем измерительный метод, встречавшийся нам выше:
В данном случае довольно странным покажется тот факт, что параллельное исполнение всего в 2,5 раза быстрее последовательного, хотя мы и задействовали все четыре ядра. Причина в том, что при обеспечении параллелизма возникают дополнительные издержки: часть вычислительной мощности тратится на порождение новых потоков и интеграцию промежуточных результатов. Следовательно, не стоит использовать параллельные коллекции по умолчанию, их лучше применять только в ходе интенсивных вычислений.
Параллельные коллекции в Java 8
Интерфейс, который предположительно будет использоваться в Java 8 для работы с параллельными коллекциями, практически такой же, как и в Scala:
Парадигма точно такая же, как и в Scala. Единственное отличие заключается в том, что метод для создания параллельной коллекции называется parallel(), а не par.
А теперь – все сразу
Подведем итог: как может выглядеть работа с функциями высшего порядка/лямбда-выражениями в комбинации с параллельными коллекциями? Для этого мы рассмотрим довольно объемный пример кода на языке Scala. Здесь скомбинированы многие идеи, которые мы обсуждали выше.
Для работы с этим примером кода был выбран сайт, на котором предлагается коллекция пейзажных обоев. Мы напишем программу, которая будет извлекать url всех этих настольных изображений и параллельно загружать картинки. Кроме основных библиотек Scala, мы воспользуемся еще двумя – Dispatch для обеспечения http-коммуникации и FileUtils, бибилотеку Apache, которая упрощает решение некоторых задач. Здесь придется столкнуться с некоторыми феноменами языка Scala, которые не освещены в данной статье, но их назначение и так должно быть понятно.
Объяснение кода
Метод scrapeWallpapers обрабатывает поток управления, в котором URL изображений выбираются из html-документа, после чего загружается каждое из изображений.
При помощи fetchWallpaperImgURLsOfPage все URL обоев считываются из html, отображаемого на экране.
Объект Http – это класс из HTTP-библиотеки dispatch, которая предоставляет предметно-ориентированный язык (DSL) для работы с библиотекой Apache httpclient. Метод as_tagsouped преобразует html in xml, а xml уже является в Scala встроенным типом данных.
Далее мы получаем из html в виде xml соответствующие href тех изображений, которые хотим загрузить:
Поскольку XML нативен в Scala, мы можем использовать xpath-подобное выражение \ для выбора интересующих нас узлов. После получения всех href нам понадобится отфильтровать URL всех интересующих нас изображений и превратить href-ы в URL-объекты. Для этого мы сцепим несколько функций высшего порядка из API Collection языка Scala, как делали это выше с map и filter. В результате имеем список URL изображений.
Далее требуется выполнить параллельную загрузку всех изображений. Для достижения параллелизма мы преобразуем список изображений в параллельную коллекцию. Затем метод foreach запускает несколько потоков для одновременного перебора элементов коллекции. Каждый из этих потоков будет, в конце концов, вызывать метод copyToDir.
Метод copyToDir работает при помощи библиотеки FileUtils, относящейся к Apache Common. Статический метод copyURLToFile класса FileUtil импортируется также статически – следовательно, его можно вызывать непосредственно. Для ясности общей картины мы также выведем на экран имя потока, выполняющего задачу. При параллельном исполнении мы увидим, что одновременно интенсивно работает сразу несколько потоков.
Этот метод также показывает, что Scala в полной мере обеспечивает взаимодействие с имеющимися библиотеками Java.
Итак, функциональные черты Scala и связанные с ними очевидные выгоды – в частности, применение функций высшего порядка к коллекциям и «бесплатный» параллелизм – позволяют параллельно выполнять синтаксический анализ, ввод-вывод и преобразование данных. Для этого достаточно всего лишь нескольких строк кода.
Виртуальные методы расширения/типажи
Виртуальные методы расширения в Java напоминают типажи (traits) из Scala. Что же такое «типажи»? Типаж в Scala сразу предоставляет интерфейс и может также включать реализацию – такая структура предлагает массу возможностей.
Подобно Java 8, Scala не поддерживает множественное наследование. Как в Java, так и в Scala подкласс может дополнять только один суперкласс. Но в случае с типажами наследование строится иначе: в классе могут «смешиваться» несколько типажей. Интересно отметить, что класс приобретает как тип и все методы, так и состояние такого типажа (типажей). Поэтому типажи также именуются «примесями», поскольку они добавляют в класс новые состояния и поведения.
Однако встает вопрос: если типажи обеспечивают какую-то форму множественного наследования, не подвержены ли они пресловутой «проблеме ромба»? Ответ: нет, не подвержены. Scala определяет четкий набор правил старшинства (precedence rules), определяющих, какой код когда выполняется в иерархии множественного наследования. Эти правила не зависят от количества примесей. Работая с такими правилами, мы приобретаем все плюсы множественного наследования, но никаких связанных с ним проблем.
Если у нас есть типаж
В следующем примере показан код, понятный всем, кто работал в Java:
При всей его важности с точки зрения дизайна, в повседневной практике журналирование происходит как бы незаметно. Каждый класс, снова и снова, объявляет регистратор (логгер). А нам все время приходится проверять, активирован ли уровень журналирования, – это делается, например, с помощью isDebugEnabled(). Налицо нарушение правила DRY (Don’t Repeat Yourself) – «Не повторяйся». К тому же в Java нет возможности убедиться, объявил ли программист проверку на уровне лога и использует ли он подходящий логгер, ассоциированный с классом. Java-разработчики настолько привыкли к подобной практике работы без журналирования, что она уже считается паттерном.
Типажи предлагают отличную альтернативу такому паттерну. Если поместить функционал журналирования в типаж, то впоследствии мы сможем смешать этот типаж с любым классом, в какой только захотим. Таким образом, класс получает доступ к сквозной функциональности журналирования – и при этом ничем не ограничиваются возможности наследования от другого класса.
Журналирование в качестве типажа – решение проблемы журналирования
В Scala мы можем реализовать типаж Loggable следующим образом:
Типаж в Scala определяется при помощи ключевого слова trait. В теле типажа может содержаться всё то же, что и в абстрактном классе, – например, поля или методы. Еще один интересный момент – использование self =>. Применяется он, поскольку регистратор должен журналировать класс, с которым смешивается типаж Loggable, а не сам типаж. Синтаксис self => – такая конструкция называется в Scala “self-type” – позволяет типажу получить ссылку на класс, с которым этот типаж смешивается.
Обратите внимание на использование функции без параметров msg: => T, которая служит параметром ввода для метода отладки. Основная причина использования проверки isDebugEnabled() такая: нам необходимо убедиться, что занесенная в журнал строка String будет рассчитываться лишь в случае, когда активирован отладочный уровень. То есть, если метод debug будет принимать в качестве параметра ввода только строку String, то журнальное сообщение так или иначе будет создаваться независимо от того, активирован ли отладочный уровень, – а это нежелательно. Но, передавая вместо строки String функцию без параметров msg: => T, мы получаем именно то, что нужно. А именно: функция msg, возвращающая строку String для записи в регистрационный журнал, будет активирована лишь при условии, что проверка isDebugEnabled выполнена успешно. Если выполнить isDebugEnabled не удается, то функция msg так и не вызывается, и строка String не рассчитывается, поскольку в этом нет необходимости.
Если мы хотим использовать типаж Loggable в классе Photo, то для смешивания типажа с классом применяется ключевое слово extends:
Ключевое слово extends наводит на мысль, что класс Photo наследует от Loggable и, соответственно, не может дополнять никакой другой класс. Но это не так. Просто по правилам синтаксиса Scala необходимо, чтобы смешивание с классом или дополнение класса начиналось со слова extends. Если мы хотим смешать с классом несколько типажей, то с каждым типажом после первого используется ключевое слово with. Ниже будут приведены примеры работы с этим словом.
Чтобы продемонстрировать, что описанный метод работает, применим метод save() к классу Photo:
Добавление к классам дополнительных поведений
В предыдущей части мы говорили о том, что с классом может смешиваться несколько типажей. Итак, кроме функциональности журналирования, мы можем добавить классу Photo и другое поведение. Допустим, нам нужна возможность упорядочить фотографии Photos в зависимости от размера файла. Приятно отметить, что в языке Scala предлагается ряд готовых типажей. Среди них – типаж Ordered[T]. Несмотря на то, что типаж Ordered напоминает интерфейс Java Comparable, большое и очень важное отличие между ними заключается в том, что в случае со Scala мы располагаем готовой реализацией:
В показанном выше примере с классом смешиваются два типажа. Кроме определенного выше Loggable, мы также смешиваем с классом типаж Ordered[Photo]. Типаж Ordered[T] требует реализации метода compare(type:T), и это очень напоминает работу с интерфейсом Java Comparable.
Кроме compare, в типаже Ordered предлагается еще ряд разных методов. Эти методы позволяют сравнивать объекты разнообразными способами, и все они используют реализацию метода compare.
В отличие от Java, символьные имена, такие, как > и и т.д. мы обязаны типажу Ordered, который реализует методы с этими символами.
Классы, реализующие типаж Ordered, можно сортировать в любой коллекции Scala. Имея коллекцию, заполненную объектами Ordered, можно применять к этой коллекции ключевое слово sorted, сортирующее объекты по порядку, определенному в методе compare.
Достоинства типажей
Из приведенных примеров становится ясно, что при помощи типажа можно изолировать обобщенную функциональность в виде модуля. При необходимости такую изолированную функциональность можно подключить к любому классу. Чтобы добавить к классу Photo функцию журналирования, мы смешали с ним типаж Loggable, а для функции упорядочивания – типаж Ordered. Эти типажи можно вновь использовать и в других классах.
В этом заключается мощный механизм для создания модульного и «сухого» (DRY) кода. В ходе работы с ним мы обходимся лишь встроенными возможностями языка, не прибегая к дополнительным технологиям – например, аспектно-ориентированному программированию.
Зачем использовать виртуальные методы расширения?
В спецификации Java 8 содержится черновое описание виртуальных методов расширения. Виртуальные методы расширения добавляют стандартные реализации для новых и/или имеющихся методов имеющихся интерфейсов. А зачем?
При работе со многими уже существующими интерфейсами было бы очень полезно реализовать поддержку лямбда-выражений в форме функций высшего порядка. Давайте для примера рассмотрим интерфейс java.util.Collection. Было бы очень неплохо, если бы он включал метод forEach(lambdaExpr). Но если бы такой метод предоставлялся без стандартной реализации, то все классы, использующие его, должны были бы такую реализацию обеспечивать сами. Совершенно очевидно, что в таком случае наступил бы настоящий хаос совместимости.
Именно поэтому команда разработчиков JDK решила прибегнуть к виртуальным методам расширения. С ними можно, например, добавить метод forEach к интерфейсу java.util.Collection вместе со стандартной реализацией. Следовательно, все использующие его классы будут автоматически наследовать и этот метод, и его реализацию. В таком случае API будет эволюционировать как бы сам по себе, а ведь именно для этого и придуманы виртуальные методы расширения. Но если реализующему классу недостаточно стандартной реализации, то ее просто можно переопределить.
Сравнение виртуальных методов расширения и типажей
Итак, основной причиной для использования виртуальных методов расширения является эволюция API. К тому же работа с ними порождает приятный положительный эффект: обеспечивается своеобразное множественное наследование, проявляющееся только на уровне поведений. А в Scala типажи допускают множественное наследование не только поведений, но и состояния, и даже позволяют ссылаться на реализующий класс (в примере с типажом Loggable мы применили для этого поле self).
С практической точки зрения типажи предлагают более широкий спектр возможностей, чем виртуальные методы расширения, но используют их по разным причинам. В Scala типажи были задуманы как модульные строительные блоки, которые обеспечивают беспроблемное множественное наследование. В свою очередь, виртуальные методы расширения должны в первую очередь обеспечивать эволюцию API, и уже во вторую – множественное наследование поведений.
Типажи Loggable и Ordered в Java 8
Чтобы оценить, чего можно достичь при помощи виртуальных методов расширения, давайте попробуем реализовать типажи Ordered и Loggable в Java 8.
Типаж Ordered можно полностью реализовать при помощи виртуальных методов расширения, так как в нем не используется никаких состояний. Как уже упоминалось выше, аналогом Ordered в Java является java.lang.Comparable. Реализации будет иметь следующий вид:
К уже имеющемуся интерфейсу Comparable мы добавили новые методы сравнения («больше», «больше или равно», «меньше», «меньше или равно»), идентичные тем, что встречаются в типаже Ordered (>, >=, default, переадресует все вызовы к имеющемуся абстрактному методу сравнения. В результате существующие интерфейсы обогащаются новыми методами, а классы, реализующие Comparable, избавляются от необходимости реализовывать также и эти методы.
Если бы класс Photo реализовывал Comparable, мы могли бы производить операции сравнения с новыми добавляемыми методами.
Типаж Loggable реализуется при помощи виртуальных методов расширения не полностью, но почти полностью:
В этом примере мы добавили к интерфейсу Loggable методы журналирования, в частности, debug, info и т.д. По умолчанию эти методы д
12 онлайн-курсов по языку Java для новичков и профессионалов (август, 2023)
Java по-прежнему входит в список самых популярных языков программирования. Вместе с Digitaldefynd мы составили список курсов по Java, которые подойдут как новичкам, так и людям с опытом программирования, чтобы освоить этот востребованный язык.
Где изучать Scala тем, кто уже что-то знает. Собрали множество курсов и платформ (июнь, 2023)
Язык программирования Scala — один из самых популярных коммерческих языков, который используют Twitter, LinkedIn, WhatsApp. Scala-разработчики, возможно, не так востребованы как их коллеги, пишущие на Python или Java, но хороший специалист будет цениться высоко, а знание языка станет безусловным плюсом в резюме. В помощь тем, кто хочет пополнить ряды адептов Scala, Digitaldefynd составил (а мы дополнили) подборку онлайн-курсов и тренингов разных уровней сложности.
10 курсов по SQL для лучшего понимания работы с большими данными (май, 2023)
Собрали 10 платных и бесплатных онлайн-курсов для изучения SQL. Программы рассчитаны на слушателей, которые только начинают или продолжают знакомство с языком.
10 способов научиться программировать самостоятельно
Хотите научиться кодить и освоить алгоритмы? Собрали десять советов с чего начать изучение программирования для тех, кто только начинает своё путешествие в мир программирования и снабдили все это полезными ссылками на курсы для начинающих программистов.
Хотите сообщить важную новость? Пишите в Telegram-бот
Главные события и полезные ссылки в нашем Telegram-канале
Обсуждение
Комментируйте без ограничений
Релоцировались? Теперь вы можете комментировать без верификации аккаунта.
Релоцировались? Теперь вы можете комментировать без верификации аккаунта.