Одной из многих причин, почему мне нравится работать именно с функциональным программированием, является высокий уровень абстракции. Это связано с тем, что в конечном итоге мы имеем дело с более читаемым и лаконичным кодом, что, несомненно, способствует сближению с логикой предметной области.
В данной статье большее внимание уделяется на четыре вещи, представленные в Java 8, которые помогут вам овладеть новым уровнем абстракции.
Я уже говорил это ранее, и скажу снова. Попрощайтесь с циклами, и приветствуйте Stream API. Дни обсуждения Java о циклах элементов подходят к концу. Со Stream API в Java мы можем сказать что мы хотим получить, вместо того чтобы говорить как этого можно добиться.
Давайте рассмотрим следующий пример.
Мы имеем список статей, каждая из которых имеет свой список тегов. Теперь мы хотим получить первую статью, содержащую тег «Java».
Взглянем на стандартный подход.
Решим задачу, используя Stream API.
Довольно круто, правда?
Сначала мы используем filter, чтобы найти все статьи, которые содержат тег Java, далее с помощью findFirst получаем первое включение.
Возникает вопрос: почему мы должны фильтровать весь список, если нам необходимо только первое включение? Так как потоки… ленивы и filter возвращает поток, вычисления происходят до тех пор, пока не будет найдено первое включение.
Я уже ранее посвятил статью о замене циклов на stream API. Прочтите его, если вам нужно больше примеров.
Можно заметить, что в предыдущем примере мы можем вернуть Optional<Article>.
Optional — это контейнер объекта, который может содержать или не содержать ненулевое значение.
Этот объект имеет некоторые функции высшего порядка, избавляющие от добавления повторяющихся if null/notNull проверок, что позволяет нам сфокусироваться на том, что мы хотим сделать.
Теперь усовершенствуем метод getFirstJavaArticle. Если не найдётся Java-статья, мы будем рады получению последней статьи.
Давайте рассмотрим, как выглядит типичное решение.
А теперь решение с использованием Optional<T>.
Отлично выглядит, не правда ли?
Ни лишних переменных, ни if-конструкций, ни каких-либо упоминаний null. Мы просто используем Optional.orElseGet, чтобы сказать, что мы хотим получить, если не будет найдено значение.
Взглянем на другой пример использования Optional. Предположим, мы хотим получить название первой Java-статьи, если она будет найдена.
Опять же, используя типичное решение, нам пришлось бы добавлять null-проверку, но… знаете что? Optional здесь, чтобы спасти этот день.
Как вы можете видеть, Optional реализует функцию высшего порядка map, помогая нам применить функцию к результату, если он есть.
Для большей информации об Optional смотрите документацию.
Как видите, Java 8 поставляется с уже готовым набором функций высшего порядка, и вы можете с ними творить чудеса. Но зачем останавливаться? И почему бы не создать свои собственные функции высшего порядка?
Единственное, что нужно, чтобы сделать функцию высшего порядка, — это взять один из функциональных интерфейсов Java, или интерфейса SAM-типа в качестве аргумента и / или возвращать одно из них.
Чтобы проиллюстрировать это, рассмотрим следующий сценарий.
У нас есть принтер, который может печатать различные виды документов. Перед печатью принтер должен прогреться, а после печати перейти в спящий режим.
Теперь мы хотим получить возможность посылать команды на принтер без заботы о его процедурах включения и выключения. Это может быть решено путём создания функции высшего порядка.
Как видите, мы используем Consumer<Printer>, являющийся одним из функциональных интерфейсов, в качестве аргумента. Затем мы выполняем данную функцию в качестве шага между процедурами запуска и отключения.
Теперь мы можем с легкостью использовать наш принтер, не заботясь ни о чём другом, кроме того, что мы хотим распечатать.
Для более подробного примера прочтите мою статью о том, как создать TransactionHandler.
Написание функций — дело легкое и быстрое. Тем не менее, с лёгким написанием кода приходит желание дублирования.
Рассмотрим следующий пример.
Этот метод нам отлично послужил единожды, но он не является универсальным. Нам нужен метод, который будет в состоянии найти статьи, основанные на других тегах и требований в целом.
Весьма заманчивым является создавать новые потоки. Они настолько малы и их так просто можно сделать, как же это может навредить? Напротив, небольшие участки кода должны мотивировать принцип DRY дальше.
Давайте реорганизуем наш код. Для начала сделаем нашу функцию getFirstJavaArticle более универсальной — она будет принимать предикат в качестве аргумента, чтобы отфильтровать статьи в соответствии с тем, что нам нужно.
Попробуем теперь воспользоваться этой функцией, чтобы получить несколько различных статей.
И, тем не менее, мы всё ещё имеем дело с повторяющимся кодом. Вы можете видеть, что используется один и тот же предикат для различных значений. Давайте попробуем удалить эти дублирования посредством интерфейса Function. Теперь наш код выглядит следующим образом.
Отлично! Я бы сказал, что этот код соответствует принципу DRY, не так ли?
Я надеюсь, что эта статья оказалась для вас полезной и придала несколько идей относительно того, что бы вы могли сделать на более высоком уровне абстракции с использованием Java 8 и функциональных особенностей.
В данной статье большее внимание уделяется на четыре вещи, представленные в Java 8, которые помогут вам овладеть новым уровнем абстракции.
1. Больше никаких циклов
Я уже говорил это ранее, и скажу снова. Попрощайтесь с циклами, и приветствуйте Stream API. Дни обсуждения Java о циклах элементов подходят к концу. Со Stream API в Java мы можем сказать что мы хотим получить, вместо того чтобы говорить как этого можно добиться.
Давайте рассмотрим следующий пример.
Мы имеем список статей, каждая из которых имеет свой список тегов. Теперь мы хотим получить первую статью, содержащую тег «Java».
Взглянем на стандартный подход.
public Article getFirstJavaArticle() {
for (Article article: articles) {
if (article.getTags().contains("Java")) {
return article;
}
}
return null;
}
Решим задачу, используя Stream API.
public Optional<Article> getFirstJavaArticle() {
return articles.stream()
.filter(article -> article.getTags().contains("Java"))
.findFirst();
}
Довольно круто, правда?
Сначала мы используем filter, чтобы найти все статьи, которые содержат тег Java, далее с помощью findFirst получаем первое включение.
Возникает вопрос: почему мы должны фильтровать весь список, если нам необходимо только первое включение? Так как потоки… ленивы и filter возвращает поток, вычисления происходят до тех пор, пока не будет найдено первое включение.
Я уже ранее посвятил статью о замене циклов на stream API. Прочтите его, если вам нужно больше примеров.
2. Избавьтесь от null-проверок
Можно заметить, что в предыдущем примере мы можем вернуть Optional<Article>.
Optional — это контейнер объекта, который может содержать или не содержать ненулевое значение.
Этот объект имеет некоторые функции высшего порядка, избавляющие от добавления повторяющихся if null/notNull проверок, что позволяет нам сфокусироваться на том, что мы хотим сделать.
Теперь усовершенствуем метод getFirstJavaArticle. Если не найдётся Java-статья, мы будем рады получению последней статьи.
Давайте рассмотрим, как выглядит типичное решение.
Article article = getFirstJavaArticle();
if (article == null) {
article = fetchLatestArticle();
}
А теперь решение с использованием Optional<T>.
getFirstJavaArticle()
.orElseGet(this::fetchLatestArticle);
Отлично выглядит, не правда ли?
Ни лишних переменных, ни if-конструкций, ни каких-либо упоминаний null. Мы просто используем Optional.orElseGet, чтобы сказать, что мы хотим получить, если не будет найдено значение.
Взглянем на другой пример использования Optional. Предположим, мы хотим получить название первой Java-статьи, если она будет найдена.
Опять же, используя типичное решение, нам пришлось бы добавлять null-проверку, но… знаете что? Optional здесь, чтобы спасти этот день.
playGround.getFirstJavaArticle()
.map(Article::getTitle);
Как вы можете видеть, Optional реализует функцию высшего порядка map, помогая нам применить функцию к результату, если он есть.
Для большей информации об Optional смотрите документацию.
3. Создайте свои функции высшего порядка
Как видите, Java 8 поставляется с уже готовым набором функций высшего порядка, и вы можете с ними творить чудеса. Но зачем останавливаться? И почему бы не создать свои собственные функции высшего порядка?
Единственное, что нужно, чтобы сделать функцию высшего порядка, — это взять один из функциональных интерфейсов Java, или интерфейса SAM-типа в качестве аргумента и / или возвращать одно из них.
Чтобы проиллюстрировать это, рассмотрим следующий сценарий.
У нас есть принтер, который может печатать различные виды документов. Перед печатью принтер должен прогреться, а после печати перейти в спящий режим.
Теперь мы хотим получить возможность посылать команды на принтер без заботы о его процедурах включения и выключения. Это может быть решено путём создания функции высшего порядка.
public void print(Consumer<Printer> toPrint) {
printer.prepare();
toPrint.accept(printer);
printer.sleep();
}
Как видите, мы используем Consumer<Printer>, являющийся одним из функциональных интерфейсов, в качестве аргумента. Затем мы выполняем данную функцию в качестве шага между процедурами запуска и отключения.
Теперь мы можем с легкостью использовать наш принтер, не заботясь ни о чём другом, кроме того, что мы хотим распечатать.
// Распечатать одну статью
printHandler.print(p -> p.print(oneArticle));
// Распечатать несколько статей
printHandler.print(p -> allArticles.forEach(p::print));
Для более подробного примера прочтите мою статью о том, как создать TransactionHandler.
4. Опасайтесь дублирования. Принцип DRY
Написание функций — дело легкое и быстрое. Тем не менее, с лёгким написанием кода приходит желание дублирования.
Рассмотрим следующий пример.
public Optional<Article> getFirstJavaArticle() {
return articles.stream()
.filter(article -> article.getTags().contains("Java"))
.findFirst();
}
Этот метод нам отлично послужил единожды, но он не является универсальным. Нам нужен метод, который будет в состоянии найти статьи, основанные на других тегах и требований в целом.
Весьма заманчивым является создавать новые потоки. Они настолько малы и их так просто можно сделать, как же это может навредить? Напротив, небольшие участки кода должны мотивировать принцип DRY дальше.
Давайте реорганизуем наш код. Для начала сделаем нашу функцию getFirstJavaArticle более универсальной — она будет принимать предикат в качестве аргумента, чтобы отфильтровать статьи в соответствии с тем, что нам нужно.
public Optional<Article> getFirst(Predicate<Article> predicate) {
return articles.stream()
.filter(predicate)
.findFirst();
}
Попробуем теперь воспользоваться этой функцией, чтобы получить несколько различных статей.
getFirst(article -> article.getTags().contains("Java"));
getFirst(article -> article.getTags().contains("Scala"));
getFirst(article -> article.getTitle().contains("Clojure"));
И, тем не менее, мы всё ещё имеем дело с повторяющимся кодом. Вы можете видеть, что используется один и тот же предикат для различных значений. Давайте попробуем удалить эти дублирования посредством интерфейса Function. Теперь наш код выглядит следующим образом.
Function<String, Predicate<Article>> basedOnTag = tag -> article -> article.getTags().contains(tag);
getFirst(basedOnTag.apply("Java"));
getFirst(basedOnTag.apply("Scala"));
Отлично! Я бы сказал, что этот код соответствует принципу DRY, не так ли?
Я надеюсь, что эта статья оказалась для вас полезной и придала несколько идей относительно того, что бы вы могли сделать на более высоком уровне абстракции с использованием Java 8 и функциональных особенностей.
DbLogs
Сам еще не перешел на Java 8, но глядя на это все стало весьма интересно: а кто-нибудь делал замеры производительности Stream API vs. for/foreach. И есть ли какие-то общие рекомендации когда и что лучше использовать.
grossws
Совсем недавно было: habrahabr.ru/post/255813
lazant
ikirin
Итерация через Stream API задействует все ядра многоядерной системы, for/foreach нет.
grossws
Без вызова parallel() тоже?
ikirin
Без вызова похоже нет :( Простите про parallel() я забыл.
Semenych
Stream API имеет один недостаток — там для всех streams используется один и тот же пул так что если у вас в программе много много поточной обработки то могут быть очень странные побочные эффекты
senia
При желании можно использовать кастомный пул. Если код, использующий Stream API, выполняется в кастомном пуле, то Stream API из него не выйдет.
TimReset
А пример можно? Хотя бы гипотетический? Т.е. в чём будет заключаться возможные проблемы?
Semenych
Ну пример вполне не гипотетический. Если все все программисты в мире понимают что ForkJoin пул можно использовать только для легковесных коротких операций то ОК.
Но предположим я хочу использовать stream API (оно же так здорово заменяет циклы) и для каждого элемента списка скажем сделать запрос в БД или к какому-то третьестороннему API. В общем-то это не такой уж редкий случай.
Логично использовать parallel stream чтобы выполнить всю связку запросов быстрее.
Но если не знать о том что пул один на все-все приложение, то пока выполняются эти запросы все остальные parallel streams будут стоять и ждать.
Причем это будет достаточно «весело» в одном месте приложения невинные операции будут занимать иногда 10 мс, а иногда 10,000 мс.
Опять же если все программисты в вашем проекте и все разработчики библиотек используют parallel streams правильным образом — все хорошо, но достаточно попасться одному джуниору недочившему доки и все пойдет криво.
TimReset
Понятно. Спасибо за объяснение!
gurinderu
А в вашем решении есть какой-либо шанс сделать асинхронный API?
grossws
Да, после scala оно выглядит довольно жалко. Нет в мире совершенства.
senia
Надо отдать должное и Java 8: ParView в Scala так и нет, тут Stream API впереди.
grossws
Видимо, исходили из разных задач. В java 8 steam api заточен, в том числе, под параллельную обработку. В scala на это упора не было, исходно шли в сторону элементов функциональщины, поэтому map/flatMap/filter есть у многих стандартных типов.
А по поводу «жалко» я имею ввиду следующие неудобства:
— куча статических импортов, без которых с использованием stream api код перестаёт нормально выглядеть (конструкции слишком громоздкие, взгляд начинает цепляться за всякие Collectors.toList()),
— отсутствие синтаксического сахара для apply приводит к более громоздким конструкциям,
— проблемы с оборачиванием функций, которые могут выбрасывать checked exceptions
— отсутствие операций foldL/foldR,
Разная нотация для вызова метода и передачи «ссылки» на него — просто особенность, в java получается явное представление (типа method1().orElseGet(this::method2())), а в scala — более удобное, но менее явное (если бы интерфейс Option был бы такой же, то что-то вида method1.orElseGet(method2)).
Кроме того, пример из статьи getFirstJavaArticle().orElseGet(this::fetchLatestArticle), если по заветам автора fetchLatestArticle также возвращает Optional, как и getFirstJavaArticle не скомпилируется, т. к. this::fetchLatestArticle будет иметь тип
Supplier<Optional<Article>>
, а неSupplier<Article>
.lany
Это я в некоторой мере починил. Если есть дополнительные пожелания к либе, готов выслушать.
Можно пример на эту тему?
foldR, foldL, насколько я понимаю, антипараллельны. Концепция стримов как раз была в том, чтобы ограничиться операциями, которые нормально параллелятся. По этой же причине zip убрали. Можно, кстати, пример кода, когда с foldR/foldL всё красиво, а со stream api плохо?
Ссылки на методы — следствие изначального дизайна Java. Имена классов, имена переменных/полей и имена методов — это три разных пространства имён, и из кода должно быть ясно, что имеется в виду. У вас может быть поле fetchLatestArticle типа Supplier<Article> и при этом метод fetchLatestArticle(), который возвращает Article. Тут и так сделали отступление, смешав классы и поля. Например foo::bar — это может быть статический метод bar в классе foo, а может быть метод bar у поля foo. Видимо, решили пойти на такой шаг, потому что классы обычно с большой буквы, а поля с маленькой.
Это misuse. Здесь вы никак не специфицируете, что сделать, если fetchLatestArticle вернёт пустой optional. Надо написать что-то типа
getFirstJavaArticle().orElseGet(() -> fetchLatestArticle().get())
илиgetFirstJavaArticle().orElseGet(() -> fetchLatestArticle().orElse(null))
или ещё что-нибудь. Спецификация обработки ошибки может быть разной, и надо её явно указать.lany
Кажется, понял, что вы имели в виду про apply. Java хранит ценное качество, что для понимания написанного требуется довольно узкий контекст. Благодаря этому тут не так много синтаксического сахара, как в других языках. Если перед круглыми скобками стоит идентификатор, это значит, что идентификатор — метод, а не десяток разных вещей в зависимости от контекста, как в некоторых других языках. Это очень хорошая фича, чтобы от неё взять и отказаться. Поэтому и нельзя написать
basedOnTag("Java")
. Это бы позволило писать код, где уже непонятно, что перед нами — объект или метод (в данном случае basedOnTag — объект). Ну и плюс та же проблема с пространствами имён: в текущем классе может быть метод с именем basedOnTag. Может, мы хотим его вызвать?grossws
Ответил здесь: habrahabr.ru/post/256057/#comment_8389073
grossws
Примеров для сравнения stream api и foldR/foldL не приведу, у меня нет кода со stream api (пока использую преимущественно java 7, местами ещё java 6). Но озвучу один важный момент: когда мы сравниваем stream api и foldR/foldL, то сравниваем их в соответствующих контекстах (java8 и scala), что убивает прямое сравнение на корню.
Когда мы сравнивали, то важным моментом был существенно более слабый вывод типов в java8, отсутствия в scala checked exceptions, что сильно сказывается на удобстве использования. Плюс, в случае scala часто бывает удобно тащить tuple с данными, что в java требует использования отдельных контейнеров (например, Map<K, V>.Entry). Или, например, более удобное преобразование Map[K, V] <-> List[(K, V)]. С одной стороны мелочи, но из них складывается общее удобство использования.
Что же до использования foldR/foldL, то я предпочитаю явную tail-recursive функцию вместо использования foldL/foldR, когда это возможно: оно нагляднее и проще для понимания.
Да, это и плюс (с точки зрения понимания человеком, удобства разбора и анализа), и минус (больше синтаксического шума, меньше гибкость языка).
Если смотреть дальше, то можно уйти в сторону языков типа smalltalk, ruby, python, где этот контекст не выводится статически (во время компиляции), но разрешается во время исполнения. Больше гибкости, больше метапрограммирования, больше шансов для выстрела в ногу.
lany
Что-то подобное я прикрутил в моём классе EntryStream: там можно тащить поток Entry, но при этом выполнять операции (map, filter и т. д.) только на ключах или только на значениях. Получается примерно такой код:
Приятнее, чем было бы на голых стримах.
Ну вот в том-то и дело, что у Java свой путь и своя философия, и статическое разрешение — часть её. Зачем создавать ещё один smalltalk или ruby, когда оно уже есть?
grossws
Незачем. Никто и не агитирует, вроде.
lany
Закоммитил, кстати, foldLeft. Подумаю ещё, потестирую, но наверно пусть будет. Теоретически он может даже что-то выиграть с параллельности, если, например, операция в foldLeft долгая и перед ней долгий map. Но в целом если можно использовать reduce, то стоит это делать.
Вот с foldRight засада: ему требуется память на весь поток. В Scala то же самое. Вон люди пишут, как выстрелить себе в ногу. Вдобавок пока предыдущие операции потока не выполнены, foldRight даже начать нельзя, то есть тут параллельность убивается полностью. То есть реализация foldRight должна быть вроде
List<T> list = toList(); IntStreamEx.range(list.size()).map(i -> list.get(list.size()-1-i)).foldLeft(...)
Причём второй поток должен выполняться в том же пуле, что и первый. Пока добавлять не буду.
На примитивных потоках foldLeft добавить можно, но там другая проблема: нет стандартного функционального интерфейса для функции, принимающей примитив и объект и возвращающей объект. Можно добавить такой интерфейс, но пока не хочется. Если кому-то сильно приспичит, пусть boxed() вызывают.
senia
В Scala с foldRight проблема не на всех коллекциях. На индексируемой ленивой дополнительна память не требуется. Ну а с параллельностью и foldLeft не дружит.
lany
Ну дело в том, что в Java потоки — это вообще не коллекции. А с параллельностью foldLeft чуть-чуть дружит. Например, такой бенчмарк:
Результаты:
Пока выполняется текущий шаг foldLeft, другой поток выполняет следующий map, из-за чего параллельная версия всё равно может быть быстрее. Но не быстрее, чем суммарное время выполнения всех шагов foldLeft, конечно.
senia
В этом отношении foldLeft от foldRight не должен отличаться в общем случае. Они вообще близнецы и разница только в конкретных реализациях. Стримы — не коллекции, несомненно, но они обладают информацией об исходной коллекции и для индексируемых коллеций left и right — условности.
lany
Ну вот в сорцах-то видно, что ни разу они не близнецы даже для линейных последовательностей с быстрым случайным доступом. Реализация foldLeft — обычный императивный цикл, а foldRight — рекурсия, причём не хвостовая. По ссылке, что я дал выше, как раз сравнивается
И получается, что они эквивалентны, но при этом второй вариант стек не жрёт.
С точки зрения стримов индексируемая коллекция — исключительно частный случай. Коллекция может быть конкатенацией нескольких коллекций, может быть результатом flatMap, может прийти после filter+limit, может вообще прийти из файла или из сокета. Left и right — далеко не условности. Left гораздо проще, чем right. У стримов, например, даже операции reverse нету.
senia
Смотря в каких сорцах — я же несколько раз написал, что зависит от импелментации.
Далее: мы говорили про параллельность, а это значит, что кроме прочего исходная коллекция должна быть разбиваемой на части, иначе накладные расходы могут сожрать все преимущества. В той же Scala collection.par для индексируемой коллекции создает обертку, а для не индексируемой — копирует в индексируемую.
Раз мы говорили про параллельные вычисления, то folrLeft может начать работу после завершения обработки первого чанка, foldRight — после последнего. Причем «первый» и «последний» здесь — не про порядок выполнения, ничто не мешает последнему выполниться раньше и даже, при определенных усилиях при реализации, первым начать выполняться.
Кстати: 10 элементов и слип — это крайне странный способ тестирования параллельных вычислоений.
lany
Ну так вы можете написать свой бенчмарк, который лучше и правильнее :-)
kerkomen
Да уж. И странно, что Haskell ещё не упомянули в комментариях…
Hixon10
Хм, увидел название статьи, думал, будет что-то новое, оригинальное. Прочитал статью, не увидел ничего нового. Потом подумал: «ага, это же перевод; тогда, наверное, оригинал написан весной 2014: именно тогда был бум простых примеров по Java 8». Ан нет, статья свежак — 13 апреля 2015 года.
В любом случае, спасибо за проделанную работу!
dkukushkin
Как C#-щик хочу поздравить всех Java-истов с разморозкой!
GrigoryPerepechko
А какая разница на чем вы пишете? Поздравлять вроде можно и без указания собственной принадлежности.
T-D-K
Он тонко намекал на это: en.wikipedia.org/wiki/Language_Integrated_Query
gurinderu
А я толсто намекну на www.querydsl.com и www.jooq.org
onikiychuka
Вы сравниваете совсем разные вещи.
Как минимум используя Linq можно писать запросы и к удаленному хранилищу и к любой локальной структуре данных в едином виде. Это очень полезно.
Jooq и querydsl — это возможность строить запросы к базе.
Тк я работал только с Jooq, то могу сказать только за него — синтаксис менее понятный чем у Linq.
Ну и с linq вся фишка в поддержке рантайма, а именно в том что вы можете получить доступ к AST лямбд которые вы передаете в Linq методы. Это позволяет писать куда более простой и читабельный код.
GrigoryPerepechko
Эээм, то есть теперь каждый человек который пишет на языке где поддерживается project/filter/fold может прийти и ехидно «поздравить с разморозкой»? Это детский говнизм от которого коробит, а никакой не намёк.
Danov
На C# своих заморочек хватает. Например, он очень медленно развивается. Идей много бродит, но добавляют очень медленно. Roslyn долгострой. XAML излишне тяжелый. Да и понять MS просто — они остерегаются резко увеличить порог вхождения в технологию.
LINQ — 2007г. А тут Java8 получила наконец-то! Дожили. 8 лет прошло. Я могу только порадоваться за Java разработчиков.
Мир очень быстро меняется и хочется много и прямо сейчас. Бывает и с C# народ уходит на новые языки.
Вот например, новое в C# 6.0 (синтаксический сахар, но приятный):
public double Sqr(double x) => x*x;
вместо
public double Sqr(double x) { return x*x; }
Еще добавили инициализацию свойств:
public int MyProp {get;set;} = 10;
Но вот сделали бы сразу:
public int MyProp {get => 2*x; set => x = value/2;} = 10; // с запуском сеттера
Впрочем, я не настолько прокаченный Computer Scientist. Может где и недосмотрел противоречия.
Yuuri
C# медленно развивается?..
stack_trace
del
asm0dey
Мне кажется, что вот это уже лишнее: basedOnTag.apply(«Java»).
Тут бы стоило сделать обычный метод, который возвращает предикат по тэгу. А то вот это вот apply выглядит достаточно уродлив. А было бы просто predicateByTag(«Java»).
Cobolorum
Примеры притянутые так за уши так что уши оторвались от головы.
В первом случаи читабильность упала просто на порядок. Или сделайте проверку на «Java» AND/OR «Basic». Я считаю что ваш пример зарефакторится сразу в классический вариант.
Во втором проверка на null это способ защиты от сбоев/ошибок в программе. А как известно 50 процентов работы любой программы это обработка ошибок. В вашем варианте все придется обрабатывать в исключениях и блок исключений у вас лопнет от объема кода.
В 3-ем примере с принтером вообще глупость. Принтер это объект и объект сам должен знать и уметь обрабатывать свое состояние.
stepik777
solver
Ну вообще-то это движуха в сторону FP.
Так что ваш ответ тоже за уши притянут.
В первом случае читабельность упала только у тех кто, извините, «в танке».
Ведь это самый простой случай. Как только добавится пару условий и пару функций обработчиков, классический Java код превратится в жуткий, километровый бойлерплейт.
Во втором случае от null никуда не уходят, проверки не убираются. Optional просто позволяет удобно работать с ними, избавляя от километров тупого java бойлерплейта, оборачивая самые простые случаи (коих большинство в таких проверках) в удобную, компактную конструкцию.
В третьем это не глупость, это называется функциональный подход, когда разделяют состояние и работу с ним.
Почитайте про чистые функции и вообще про принципы ФП что ли.
Хотя это далеко не самый лучший пример конечно.
Borz
на лямбдах всё вполне читабельно получается и лаконичнее.
Вот например преобразование массива строк-ролей в роли для Spring Security: gist.github.com/BorzdeG/675fac06a6338dad10a7
Develar
Java мертва, backward compatibility, как минимум, одна из причин. Kotlin (http://kotlinlang.org) позволяет писать гораздо более лаконичный и читабельный код.
defuz
Я совсем не понял часть про DRY — вместо простого решения вы написали кучу стремного и непонятного кода и радуетесь этому. В чем профит?
kalterfive Автор
Так там объясняются причины.
А можно подробнее? Стрёмный и непонятный код — понятие растяжимое.
agent10
Пока к Java 8 еще не притрагивался, но есть вопрос по первой части… А можно объединить «получение последней статьи» и «получить название первой Java-статьи»
Т.е. сделать как-то так:
getFirstJavaArticle().orElseGet(this::fetchLatestArticle).map(Article::getTitle);
Rasifiel
После orElseGet у вас будет объект класса Article, поэтому:
getFirstJavaArticle().orElseGet(this::fetchLatestArticle).getTitle()
fshp
Был код в Oracle джедаями писан.
Nagg
Вот если бы в андроид это дело всё. без уретралямбд всяких.
Yuuri
Без чего?!
Nagg
Опечатался, имел ввиду github.com/orfjackal/retrolambda
Terranz
андроид «на днях» перешёл на java7, а вы про 8 говорите…
r00tGER
не исправляйте )
vsb
Примеры не очень показательные, конечно.
Моё имхо — почти всегда простые конструкции читабельней ФП-конструкций. А если не читабельней, то и в ФП будет нечитабельная лапша и тут уже надо просто делать extract method для разделения логики на составные части с хорошими названиями. Я не против ФП, но у него очень узкая ниша. А его сейчас с радостными криками «Гип-гип-ура!» будут сувать во все дыры, как будто это что-то новое. Нет тут ничего нового, в лиспе это всё было 50 лет назад, а в хаскеле 20 лет назад и все интересующиеся уже это изучили и забыли.
Ну да ладно, больших минусов тут тоже нет. Разве что программы начнут работать на пару процентов медленней, ибо лишние выделения памяти, лишние конструкторы всякие, лишние косвенные вызовы и это везде будет.
А уж Article::getName насколько уродливый синтаксис! Ну почему _.getName() не сделали-то? Вот этого не понимаю.
solver
А можно поподробнее про очень узкую нишу у ФП?
А то мужики пилят понимаешь сервера, игры, графические редакторы, сайтики различные, и не в курсе, что это все очень узкая ниша…
vsb
Алгоритм обработки коллекции, записанный в функциональном виде, чаще всего проигрывает в читабельности, выразительности, расширяемости в сравнении с императивным видом.
Это я конкретно про Java сейчас говорю. Например в Scala есть for comprehensions, которые по сути те же map/filter/etc но выглядят гораздо красивее. В Python вроде тоже что-то похожее. В Haskell тоже. Это совсем другое дело. Это ФП с человеческим лицом. Но в Java такого нет и без этого обычные for и if лучше.
Можно придумать пример, где функциональный подход будет выигрывать. Но сложно.
solver
>Можно придумать пример, где функциональный подход будет выигрывать. Но сложно.
Facepalm…
Достаточно почитать про ФП и правильно переписать проект на Java8, что бы понять, что вы в корне ошибаетесь во всех высказываниях.
int19h
>> Алгоритм обработки коллекции, записанный в функциональном виде, чаще всего проигрывает в читабельности, выразительности, расширяемости в сравнении с императивным видом.
Это все, мягко говоря, субъективно. Когда в C# появились лямбды семь лет назад, многие комментировали это слово в слово как вы. А сейчас уже очень тяжело найти код без LINQ по объектам, и всем все понятно. Это вопрос привычки.
vsb
LINQ и набор функций это две большие разницы. Понятно, что под капотом оно всё похоже, но LINQ, For Comprehensions и тд это ФП в человеческом виде. В Java же просто дали набор функциональных примитивов.
int19h
На практике, 90% кода на шарпе, с которым я имею дело, не использует сахар. Т.е. в данном случае имелся в виду именно набор функциональных примитивов.
Hixon10
А можно поподробнее про мужиков, которые пилят сервера, игры, графические редакторы, сайтики различные на чистом FP?
solver
Что именно надо подробнее?
Мало что ли примеров на ФП или в гугле забанили?
Hixon10
Ну, вы вообще о каком ФП говорите? Об использовании функций высшего порядка в императивных языках, или о проектах, которые на чистом FP (e.g., haskell) написаны? Если (2), то да, примеров мало.
solver
Примеров мало по одной простой причине. Мейнстримом рулит ООП.
Но всем уже понятно, что в условиях когда производительность стала измеряться не гигагерцами, а ядрами, функциональный подход не просто удобнее, он надежнее.
Поэтому то и вспомнили о ФП. И ФП языки получили второе дыхание.
bobzer
Я 13 лет в Java EE (про мужиков, которые пилят сервера) и еще ни разу не встретил ни в одном проекте синтаксиса выше Java 5. Я сам хотя бы Generics активно пользую, а чужие проекты открываю, так там вообще на Java 1.4 остановились (и это не легаси, а свежие проекты, использующие Java 6-7, причем разработанные матерыми профи). Сам тоже смотрю нововведения и не вижу ничего, что сделало бы проект реально более читабельным/поддерживаемым/производительным/т.д… Да, это всё классные штуки, но мне доводилось разбирать код, в котором «классные штуки» применялись как вещь в себе, просто ради попробовать, и это была жуть… Синтаксический сахар — вещь на любителя, а если в проекте работает более одного человека, то тут уже можно начать и о вкусах спорить…
asm0dey
Я 6 лет в EE, сейчас пишем на 8ке.
bobzer
Это просто замечательно, но возникают 2 вопроса:
1. Писать на 8-ке понятие растяжимое — то же ФП, например, насколько активно используется?
2. Как насчет "что сделало бы проект реально более читабельным/поддерживаемым/производительным/т.д" — проекты на 8-ке чем лучше проектов на 7-ке/6-ке/5-ке? Производительность, трудозатраты, ошибки: если на конкретном проекте сменить синтаксис на 5-ку, например, станет ли он от этого набором жуткого говнокода?
asm0dey
1. Вполне активно. Стримы, опшналы и т.д.
2. Ну тут скорее производительность разработчиков, ошибок вроде из-за платформы не было практически (может 1-2, ничего заметного). У нас очень много работы с коллекциями — смотреться станет страшно. Есть некоторые места, где используется ФП — там, понятно, код тоже распухнет.
Yuuri
> почти всегда простые конструкции читабельней ФП-конструкций
Вы так говорите, будто второе исключает первое.
orcy
Заменили понятные 3 строчки кода на 3 непонятные + абзац объяснений что происходит и:
> Довольно круто, правда?
Если отбросить шутки, то у идеи «no raw loops» бывали хорошие примеры, хоть мне и кажется что это в целом какая-то потеря читаемости. На мой взгляд цикл сам по себе ничем не плох, если внутри не творится что-нибудь странное. Другой вариант когда это бы имело смысл если этот ваш Java стрим создовался бы сложным образом (на пример по каким-нибудь условиям или в разных функциях).
grossws
В плане читаемости хорошо выглядят for comprehensions в scala. Почти обычный foreach с виду, переписывается в map/flatMap/withFilter, которые реализованы у большого количества классов, что позволяет единообразно и наглядно производить различные итеративные вычисления.
Допустим, получения всех пар (i, j), где i не превосходит j выглядит так:
Первый вариант куда более удобен для чтения, сохраняет бонусы функциональных операций (например, lazyness, если оно реализуется в соответствующих типах) и также не оперирует изменяемым состоянием.
Semenych
Образ мышления автора — вот три строки проверяющие на nil а теперь три строки но уже без слова nil так что вам надо читать доку на функцию чтобы понять что тут делается — меня пугает. Я боюсь таких людей в проекте и боюсь что статья количество таких людей увеличит
Java 8 хороший релиз и в нем много полезного, но хочется статьи где будет показано как все это использовать не для запутывания кода, а наоборот для облегчения его понимания. Пока таких статей не видел.
yahorfilipcyk
Предлагаю посмотреть вот этот гайд: What's New in Java 8.
По-моему, отличное краткое руководство по нововведениям в Java 8, с простыми и понятными примерами, в которых видна разница (в сравнении с более старыми версиями) и практическая применимость.
Semenych
Спасибо толковый гайд
ttools
Вот не могу понять, в чем тут новый уровень абстракции (и, видимо, подразумевается что новый уровень абстракции-это абсолютное добро априори), и что тут круто?
greabock
Я вообще пишу на php (да простят меня боги)… и на php мы пользуемся «умными» коллекциями уже очень давно. Да, они не в ядре — это отдельные либы, и все же… Я все время слышу что php — недоязык и вообще очень плохой. А теперь я немного удивлен, что для многих в «крутой яве» такой подход явился откровением. Если я что-то не так сказал или не так понял, то поправьте меня.
Hixon10
В Крутой Джаве всегда была Крутая Гуава, в которой всё это давно уже есть (конечно же, по модулю того, какой синтаксис возможен с более старыми версиями JDK).
Throwable
Optional в Java — это по большей части пятое колесо, причем недоработанное.
Во-первых, он не предназначен для хранения данных. Optional не имлементирует Serializable, что делает невозможным использование в remote-интерфейсах, и persistence объектах. Поля объекта не рекомендуется делать Optionak. Также не рекомендуется использовать Optional в списках. Насчет аксессоров нет полной определенности, Java Beans спецификация обходит этот вопрос. До сих пор не утихают споры по поводу использования Optional. Вот хороший пост на этот счет: stackoverflow.com/questions/24547673/why-java-util-optional-is-not-serializable-how-to-serialize-the-object-with-suc
Во-вторых, основная проблема обертки, как и в Scala, что Optional[Integer] не является Integer, как например String? в Kotlin, что делает несовместимой всю семантику операций. Чтобы вставить поддержку Optional нужно перелопатить весь код ради сомнительной выгоды. В случае библиотеки, нужно выпускать радикально новую версию API.
В-третьих, Optional собственно не решает проблему null в Java. Он не гарантирует, что само возвращаемое значение Optional не будет null, равно как и что в коде с Optional возвращаемый не-Optional объект будет иметь ненулевое значение.
В-четвертых. Практически нет библиотек или API, которые использовли бы Optional. Даже сам Java RT API.
Optional — это чисто ФП фича, который пришел в Java вместе с лямбдами и прочей ФП-лабудой, и в традиционном контексте не имеет большого смысла. Есть более успешная попытка искоренить null в Java при помощи аннотаций @Nonnull/@Nullable, ее и нужно придерживаться. Что реально может решить проблему, так это value types, но это пока еще proposals.
Yuuri
Это не проблема, а одно из главных достоинств с точки зрения надёжности. Такую обёртку нельзя куда-то нечаянно передать, забыв обработать ошибочную ситуацию.
SomeType?
же в этом плане не лучшемиллиарднодолларовой ошибкиобычногоnull
с аннотациями.dougrinch
То, что можно передать обертку, забыв про ошибочную ситуацию — проблема реализации, а не самое идеи чтобы Optional[Integer] являлся Integer. Как раз в Kotlin это сделано очень грамотно. Нельзя передать String? туда, где ожидается String просто так. Однако, если предварительно проверить на null, то, внезапно, так сделать уже можно.
В итоге, одновременно имеем защиту от забывчивости + удобство использования.
int19h
Ну так по сути это все равно разные несовместимые типы, просто в Kotlin проверка на null меняет тип s со String? на String внутри соответствующей ветви. Т.е. претензия здесь не по адресу — виноват не Optional, виновато отсутствие удобного способа проверить и сузить тип.
dougrinch
Строго говоря, да, Вы правы. Мысль в том, что «я могу использовать String? везде, где ожидается String» (но только если компилятор знает что это безопасно). Т.е. снаружи я могу считать что «Optional[Integer] является Integer, но с дополнительными проверками». Если я правильно понял Throwable, то он имел ввиду именно это.
Собственно, если компилятор не гарантирует корректность приведения Optional[Integer] -> Integer, то я согласен, это получается косяк в безопасности и так делать нельзя.
Throwable
В Kotlin и подобных языках приведение nullables удобно закамуфлировано. Например, без лишних извратов можно передать non-null значение в функцию, принимающую nullable параметры.
Равно как и значение функции, возвращающей non-null значения, может быть присвоено nullable переменной. Плюс функция, возвращающая String? ковариантна к String, т.е. при переопределении можно String? заменить на String:
Основной плюс в том, что nullable types — это исключительно фишка компилятора, также как и generics. Не существует реального типа String?.. Напр. нельзя написать String?::class, нельзя унаследовать nullable type, etc. В итоге все транслируется в обычные ссылочные типы, которые по определению уже null. С Optional это не так — это реальный тип данных, который для каждого ссылочного значения, которое уже и так nullable, создает дополнительный объект в памяти. Так что я считаю Optional неудачной попыткой исправить nullable косяк при помощи системы типов, нежели изменениями в языке/компиляторе.
int19h
>> без лишних извратов можно передать non-null значение в функцию, принимающую nullable параметры.
То, что String — это подтип String?, это как раз хорошо и правильно, никакого камуфлирования. В C# аналогично int — это подтип Nullable, во всяком случае, с подстановочной т.з. (но не наоборот!)
>> Основной плюс в том, что nullable types — это исключительно фишка компилятора, также как и generics. Не существует реального типа String?.. Напр. нельзя написать String?::class, нельзя унаследовать nullable type, etc.
А вот это уже косяк. И непонятно, почему вы его приводите в качестве фичи. Понятно, что это ограничение рантайма, так же, как и type erasure у generics, но что в этом хорошего?
Ну и в любом случае это не значит, что такого типа нет. В TypeScript вот вообще все типы исключительно этапа компиляции, а в рантайме понятия «тип» нет вообще, но это же не значит, что типы там не настоящие.
>> итоге все транслируется в обычные ссылочные типы, которые по определению уже null. С Optional это не так — это реальный тип данных, который для каждого ссылочного значения, которое уже и так nullable, создает дополнительный объект в памяти.
Это уже косяк конкретной реализации Optional в Java. Хотя я могу их понять — из-за того, что внятной семантики у null не было, иногда им приходится различать «значение есть, но оно null» от «значения нет».
Но это, опять же, не проблема самой идеи с optional-типами, а данной конкретной её инкарнации. Те самые nullable-типы в Kotlin — это и есть optional, просто с более удобным сахаром и более внятной семантикой.
vsb
> В-третьих, Optional собственно не решает проблему null в Java. Он не гарантирует, что… в коде с Optional возвращаемый не-Optional объект будет иметь ненулевое значение.
А можете этот момент показать кодом, не совсем понятно. Вроде гарантирует.
grossws
Гарантирует:
dougrinch
Проблема в другом.
Вот и получается, что без Optional проверять на null все равно нужно, а с Optional вообще получаем три значения вместо двух. Так что, согласен, @Nonnull/@Nullable гораздо предпочтительнее (в контексте java).
grossws
Я не спорю с утверждением, что метод, возвращающий Optional может вернуть null, хотя это должно трактоваться, как ошибка и требовать устранения.
Optional.get() всегда вернет не-null или кинет исключение.Я спорю со второй частью утверждения Throwable:
Throwable
Я плохо выразился. Имелось ввиду, что там, где возвращается Optional можно сделать вывод, что значение nullable, тогда как там, где возвращается не-Optional никакого вывода сделать нельзя.
Проблема nullable не решается Optional, потому что его целью является ссылочный тип который изначально nullable по определению. Лишь дополнительно напрягает программиста распаковкой-запаковкой значений, и проверками, в которых как правило в 90% случаев нет необходимости.
Решение проблемы заключается в обратном: указать, что значение как раз не является nullable и оно safe для операций. Если идти путем Optional, то потребуется монада Some, и всего лишь переписать весь код, заменяя все non-nullable свободные ссылочные типы T на Some. Делов-то! Либо использовать другой способ, чтобы научить компилятор отличать non-nullable значения от всех остальных, например при помощи аннотаций. Или ввести в язык value-types.
P.S. Для любителей геморроя с Optional:
Borz
а чем плох такой Bean?
Throwable
Избыточностью.
Во-первых, задолбает писать для каждого поля safe-геттер. IDE такое не предлагает.
Во-вторых, если наша задача — минимизировать возможность ошибки, то здесь остается такая дверь ввиде традиционного геттера. Если я сделаю такой бин, угадайте какой из двух геттеров в 90% случаев будет использовать Вася?
Во-третьих, никто так не делает. Ни одна спецификация, рекомендация, или просто use-case не содержит подобного. Все библиотеки, фреймворки, работающие со свойствами, будут использовать традиционные аксессоры.
И, наконец, есть решение лучше и проще:
vayho
> Во-вторых, основная проблема обертки, как и в Scala, что Optional[Integer] не является Integer, как например String
В Scala это не проблема, куча механизмов как упростить ваш код pattern matching, implicit conversions, for comprehensive и так далее, это в Java Optional выглядит как собаке пятая нога, просто потому что когда получаешь объект этого типа пытаешься судорожно от него избавиться.
Наоборот в Scala радует что можно до последнего возиться с Option и распаковать значение уже тогда когда это действительно нужно.
Вот ваш код ниже на Scala
def foo(s: String): String =…
Option(«Hello world!»).map(foo(_)) match {
case Some(value) =>
case None =>…
}