Pull to refresh

Comments 94

Сам еще не перешел на Java 8, но глядя на это все стало весьма интересно: а кто-нибудь делал замеры производительности Stream API vs. for/foreach. И есть ли какие-то общие рекомендации когда и что лучше использовать.
Вот тут есть кое-какое сравнение

Итерация через Stream API задействует все ядра многоядерной системы, for/foreach нет.
Без вызова похоже нет :( Простите про parallel() я забыл.
Stream API имеет один недостаток — там для всех streams используется один и тот же пул так что если у вас в программе много много поточной обработки то могут быть очень странные побочные эффекты
При желании можно использовать кастомный пул. Если код, использующий Stream API, выполняется в кастомном пуле, то Stream API из него не выйдет.
А пример можно? Хотя бы гипотетический? Т.е. в чём будет заключаться возможные проблемы?
Ну пример вполне не гипотетический. Если все все программисты в мире понимают что ForkJoin пул можно использовать только для легковесных коротких операций то ОК.
Но предположим я хочу использовать stream API (оно же так здорово заменяет циклы) и для каждого элемента списка скажем сделать запрос в БД или к какому-то третьестороннему API. В общем-то это не такой уж редкий случай.
Логично использовать parallel stream чтобы выполнить всю связку запросов быстрее.
Но если не знать о том что пул один на все-все приложение, то пока выполняются эти запросы все остальные parallel streams будут стоять и ждать.
Причем это будет достаточно «весело» в одном месте приложения невинные операции будут занимать иногда 10 мс, а иногда 10,000 мс.
Опять же если все программисты в вашем проекте и все разработчики библиотек используют parallel streams правильным образом — все хорошо, но достаточно попасться одному джуниору недочившему доки и все пойдет криво.
Понятно. Спасибо за объяснение!
А в вашем решении есть какой-либо шанс сделать асинхронный API?
Да, после scala оно выглядит довольно жалко. Нет в мире совершенства.
Надо отдать должное и Java 8: ParView в Scala так и нет, тут Stream API впереди.
Видимо, исходили из разных задач. В 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>.
куча статических импортов, без которых с использованием stream api код перестаёт нормально выглядеть (конструкции слишком громоздкие, взгляд начинает цепляться за всякие Collectors.toList())

Это я в некоторой мере починил. Если есть дополнительные пожелания к либе, готов выслушать.

— отсутствие синтаксического сахара для apply приводит к более громоздким конструкциям,

Можно пример на эту тему?

foldR, foldL, насколько я понимаю, антипараллельны. Концепция стримов как раз была в том, чтобы ограничиться операциями, которые нормально параллелятся. По этой же причине zip убрали. Можно, кстати, пример кода, когда с foldR/foldL всё красиво, а со stream api плохо?

Ссылки на методы — следствие изначального дизайна Java. Имена классов, имена переменных/полей и имена методов — это три разных пространства имён, и из кода должно быть ясно, что имеется в виду. У вас может быть поле fetchLatestArticle типа Supplier<Article> и при этом метод fetchLatestArticle(), который возвращает Article. Тут и так сделали отступление, смешав классы и поля. Например foo::bar — это может быть статический метод bar в классе foo, а может быть метод bar у поля foo. Видимо, решили пойти на такой шаг, потому что классы обычно с большой буквы, а поля с маленькой.

getFirstJavaArticle().orElseGet(this::fetchLatestArticle)

Это misuse. Здесь вы никак не специфицируете, что сделать, если fetchLatestArticle вернёт пустой optional. Надо написать что-то типа getFirstJavaArticle().orElseGet(() -> fetchLatestArticle().get()) или getFirstJavaArticle().orElseGet(() -> fetchLatestArticle().orElse(null)) или ещё что-нибудь. Спецификация обработки ошибки может быть разной, и надо её явно указать.
Кажется, понял, что вы имели в виду про apply. Java хранит ценное качество, что для понимания написанного требуется довольно узкий контекст. Благодаря этому тут не так много синтаксического сахара, как в других языках. Если перед круглыми скобками стоит идентификатор, это значит, что идентификатор — метод, а не десяток разных вещей в зависимости от контекста, как в некоторых других языках. Это очень хорошая фича, чтобы от неё взять и отказаться. Поэтому и нельзя написать basedOnTag("Java"). Это бы позволило писать код, где уже непонятно, что перед нами — объект или метод (в данном случае basedOnTag — объект). Ну и плюс та же проблема с пространствами имён: в текущем классе может быть метод с именем basedOnTag. Может, мы хотим его вызвать?
foldR, foldL, насколько я понимаю, антипараллельны. Концепция стримов как раз была в том, чтобы ограничиться операциями, которые нормально параллелятся. По этой же причине zip убрали. Можно, кстати, пример кода, когда с foldR/foldL всё красиво, а со stream api плохо?
Да, они не могут выполняться параллельно, т. к. протаскивают состояния аккумулятора.

Примеров для сравнения 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, когда это возможно: оно нагляднее и проще для понимания.

Кажется, понял, что вы имели в виду про apply. Java хранит ценное качество, что для понимания написанного требуется довольно узкий контекст. Благодаря этому тут не так много синтаксического сахара, как в других языках. Если перед круглыми скобками стоит идентификатор, это значит, что идентификатор — метод, а не десяток разных вещей в зависимости от контекста, как в некоторых других языках. Это очень хорошая фича, чтобы от неё взять и отказаться. Поэтому и нельзя написать basedOnTag(«Java»). Это бы позволило писать код, где уже непонятно, что перед нами — объект или метод (в данном случае basedOnTag — объект). Ну и плюс та же проблема с пространствами имён: в текущем классе может быть метод с именем basedOnTag. Может, мы хотим его вызвать?
Да, это и плюс (с точки зрения понимания человеком, удобства разбора и анализа), и минус (больше синтаксического шума, меньше гибкость языка).

Если смотреть дальше, то можно уйти в сторону языков типа smalltalk, ruby, python, где этот контекст не выводится статически (во время компиляции), но разрешается во время исполнения. Больше гибкости, больше метапрограммирования, больше шансов для выстрела в ногу.
в java требует использования отдельных контейнеров (например, Map<K, V>.Entry). Или, например, более удобное преобразование Map[K, V] <-> List[(K, V)]

Что-то подобное я прикрутил в моём классе EntryStream: там можно тащить поток Entry, но при этом выполнять операции (map, filter и т. д.) только на ключах или только на значениях. Получается примерно такой код:

public List<Sequence> calculate(Map<String, List<ChipSeqPeak>> chromosomeToPeaks, GenomeDatabase db, int minLength) {
    return EntryStream.of(chromosomeToPeaks)
        .mapKeys( chr -> db.fetchSequence(chr) )
        .flatMapValues( List::stream )
        .mapKeyValue( (sequence, peak) -> peak.getSequenceRegion(sequence, minLength))
        .toList();
}

Приятнее, чем было бы на голых стримах.

Если смотреть дальше, то можно уйти в сторону языков типа smalltalk, ruby, python, где этот контекст не выводится статически (во время компиляции), но разрешается во время исполнения.

Ну вот в том-то и дело, что у Java свой путь и своя философия, и статическое разрешение — часть её. Зачем создавать ещё один smalltalk или ruby, когда оно уже есть?
Приятнее, чем было бы на голых стримах.
Я видел ваш пост про streamex, выглядит куда более удобоваримо, нежели голые стримы.

Ну вот в том-то и дело, что у Java свой путь и своя философия, и статическое разрешение — часть её. Зачем создавать ещё один smalltalk или ruby, когда оно уже есть?
Незачем. Никто и не агитирует, вроде.
Закоммитил, кстати, 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() вызывают.
В Scala с foldRight проблема не на всех коллекциях. На индексируемой ленивой дополнительна память не требуется. Ну а с параллельностью и foldLeft не дружит.
Ну дело в том, что в Java потоки — это вообще не коллекции. А с параллельностью foldLeft чуть-чуть дружит. Например, такой бенчмарк:

private void sleep() {
    try { Thread.sleep(10); } catch(InterruptedException e) {}
}

@Benchmark
public void foldLeftSeq(Blackhole bh) {
    bh.consume(IntStreamEx.range(0,10).boxed().map(x -> {sleep();return x;})
                .foldLeft(0, (a, b) -> {sleep();return a+b;}));
}

@Benchmark
public void foldLeftPar(Blackhole bh) {
    bh.consume(IntStreamEx.range(0,10).parallel().boxed().map(x -> {sleep();return x;})
                .foldLeft(0, (a, b) -> {sleep();return a+b;}));
}

Результаты:

Benchmark             Mode  Cnt          Score         Error  Units
FoldLeft.foldLeftPar  avgt   20  126046361,977 ± 2005404,512  ns/op
FoldLeft.foldLeftSeq  avgt   20  199882975,012 ±   16028,386  ns/op

Пока выполняется текущий шаг foldLeft, другой поток выполняет следующий map, из-за чего параллельная версия всё равно может быть быстрее. Но не быстрее, чем суммарное время выполнения всех шагов foldLeft, конечно.
В этом отношении foldLeft от foldRight не должен отличаться в общем случае. Они вообще близнецы и разница только в конкретных реализациях. Стримы — не коллекции, несомненно, но они обладают информацией об исходной коллекции и для индексируемых коллеций left и right — условности.
Ну вот в сорцах-то видно, что ни разу они не близнецы даже для линейных последовательностей с быстрым случайным доступом. Реализация foldLeft — обычный императивный цикл, а foldRight — рекурсия, причём не хвостовая. По ссылке, что я дал выше, как раз сравнивается

list.foldRight("X")((a,b) => a + b)
list.reverse.foldLeft("X")((b,a) => a + b)

И получается, что они эквивалентны, но при этом второй вариант стек не жрёт.

С точки зрения стримов индексируемая коллекция — исключительно частный случай. Коллекция может быть конкатенацией нескольких коллекций, может быть результатом flatMap, может прийти после filter+limit, может вообще прийти из файла или из сокета. Left и right — далеко не условности. Left гораздо проще, чем right. У стримов, например, даже операции reverse нету.
Смотря в каких сорцах — я же несколько раз написал, что зависит от импелментации.

Далее: мы говорили про параллельность, а это значит, что кроме прочего исходная коллекция должна быть разбиваемой на части, иначе накладные расходы могут сожрать все преимущества. В той же Scala collection.par для индексируемой коллекции создает обертку, а для не индексируемой — копирует в индексируемую.

Раз мы говорили про параллельные вычисления, то folrLeft может начать работу после завершения обработки первого чанка, foldRight — после последнего. Причем «первый» и «последний» здесь — не про порядок выполнения, ничто не мешает последнему выполниться раньше и даже, при определенных усилиях при реализации, первым начать выполняться.

Кстати: 10 элементов и слип — это крайне странный способ тестирования параллельных вычислоений.
Ну так вы можете написать свой бенчмарк, который лучше и правильнее :-)
Да уж. И странно, что Haskell ещё не упомянули в комментариях…
Хм, увидел название статьи, думал, будет что-то новое, оригинальное. Прочитал статью, не увидел ничего нового. Потом подумал: «ага, это же перевод; тогда, наверное, оригинал написан весной 2014: именно тогда был бум простых примеров по Java 8». Ан нет, статья свежак — 13 апреля 2015 года.

В любом случае, спасибо за проделанную работу!
Довольно круто, правда?

Как C#-щик хочу поздравить всех Java-истов с разморозкой!
А какая разница на чем вы пишете? Поздравлять вроде можно и без указания собственной принадлежности.
А я толсто намекну на www.querydsl.com и www.jooq.org
Вы сравниваете совсем разные вещи.
Как минимум используя Linq можно писать запросы и к удаленному хранилищу и к любой локальной структуре данных в едином виде. Это очень полезно.
Jooq и querydsl — это возможность строить запросы к базе.
Тк я работал только с Jooq, то могу сказать только за него — синтаксис менее понятный чем у Linq.
Ну и с linq вся фишка в поддержке рантайма, а именно в том что вы можете получить доступ к AST лямбд которые вы передаете в Linq методы. Это позволяет писать куда более простой и читабельный код.
Эээм, то есть теперь каждый человек который пишет на языке где поддерживается project/filter/fold может прийти и ехидно «поздравить с разморозкой»? Это детский говнизм от которого коробит, а никакой не намёк.
На 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. Может где и недосмотрел противоречия.
C# медленно развивается?..
Мне кажется, что вот это уже лишнее: basedOnTag.apply(«Java»).
Тут бы стоило сделать обычный метод, который возвращает предикат по тэгу. А то вот это вот apply выглядит достаточно уродлив. А было бы просто predicateByTag(«Java»).
Примеры притянутые так за уши так что уши оторвались от головы.
В первом случаи читабильность упала просто на порядок. Или сделайте проверку на «Java» AND/OR «Basic». Я считаю что ваш пример зарефакторится сразу в классический вариант.
Во втором проверка на null это способ защиты от сбоев/ошибок в программе. А как известно 50 процентов работы любой программы это обработка ошибок. В вашем варианте все придется обрабатывать в исключениях и блок исключений у вас лопнет от объема кода.
В 3-ем примере с принтером вообще глупость. Принтер это объект и объект сам должен знать и уметь обрабатывать свое состояние.
UFO just landed and posted this here
Ну вообще-то это движуха в сторону FP.
Так что ваш ответ тоже за уши притянут.
В первом случае читабельность упала только у тех кто, извините, «в танке».
Ведь это самый простой случай. Как только добавится пару условий и пару функций обработчиков, классический Java код превратится в жуткий, километровый бойлерплейт.
Во втором случае от null никуда не уходят, проверки не убираются. Optional просто позволяет удобно работать с ними, избавляя от километров тупого java бойлерплейта, оборачивая самые простые случаи (коих большинство в таких проверках) в удобную, компактную конструкцию.
В третьем это не глупость, это называется функциональный подход, когда разделяют состояние и работу с ним.
Почитайте про чистые функции и вообще про принципы ФП что ли.
Хотя это далеко не самый лучший пример конечно.
на лямбдах всё вполне читабельно получается и лаконичнее.
Вот например преобразование массива строк-ролей в роли для Spring Security: gist.github.com/BorzdeG/675fac06a6338dad10a7
Java мертва, backward compatibility, как минимум, одна из причин. Kotlin (http://kotlinlang.org) позволяет писать гораздо более лаконичный и читабельный код.
Я совсем не понял часть про DRY — вместо простого решения вы написали кучу стремного и непонятного кода и радуетесь этому. В чем профит?
Так там объясняются причины.

А можно подробнее? Стрёмный и непонятный код — понятие растяжимое.
Пока к Java 8 еще не притрагивался, но есть вопрос по первой части… А можно объединить «получение последней статьи» и «получить название первой Java-статьи»
Т.е. сделать как-то так:
getFirstJavaArticle().orElseGet(this::fetchLatestArticle).map(Article::getTitle);
После orElseGet у вас будет объект класса Article, поэтому:
getFirstJavaArticle().orElseGet(this::fetchLatestArticle).getTitle()
orElseGet

Был код в Oracle джедаями писан.
Вот если бы в андроид это дело всё. без уретралямбд всяких.
андроид «на днях» перешёл на java7, а вы про 8 говорите…
UFO just landed and posted this here
А можно поподробнее про очень узкую нишу у ФП?
А то мужики пилят понимаешь сервера, игры, графические редакторы, сайтики различные, и не в курсе, что это все очень узкая ниша…
UFO just landed and posted this here
>Можно придумать пример, где функциональный подход будет выигрывать. Но сложно.

Facepalm…

Достаточно почитать про ФП и правильно переписать проект на Java8, что бы понять, что вы в корне ошибаетесь во всех высказываниях.
>> Алгоритм обработки коллекции, записанный в функциональном виде, чаще всего проигрывает в читабельности, выразительности, расширяемости в сравнении с императивным видом.

Это все, мягко говоря, субъективно. Когда в C# появились лямбды семь лет назад, многие комментировали это слово в слово как вы. А сейчас уже очень тяжело найти код без LINQ по объектам, и всем все понятно. Это вопрос привычки.
UFO just landed and posted this here
На практике, 90% кода на шарпе, с которым я имею дело, не использует сахар. Т.е. в данном случае имелся в виду именно набор функциональных примитивов.
А можно поподробнее про мужиков, которые пилят сервера, игры, графические редакторы, сайтики различные на чистом FP?
Что именно надо подробнее?
Мало что ли примеров на ФП или в гугле забанили?
Ну, вы вообще о каком ФП говорите? Об использовании функций высшего порядка в императивных языках, или о проектах, которые на чистом FP (e.g., haskell) написаны? Если (2), то да, примеров мало.
Примеров мало по одной простой причине. Мейнстримом рулит ООП.

Но всем уже понятно, что в условиях когда производительность стала измеряться не гигагерцами, а ядрами, функциональный подход не просто удобнее, он надежнее.
Поэтому то и вспомнили о ФП. И ФП языки получили второе дыхание.
Я 13 лет в Java EE (про мужиков, которые пилят сервера) и еще ни разу не встретил ни в одном проекте синтаксиса выше Java 5. Я сам хотя бы Generics активно пользую, а чужие проекты открываю, так там вообще на Java 1.4 остановились (и это не легаси, а свежие проекты, использующие Java 6-7, причем разработанные матерыми профи). Сам тоже смотрю нововведения и не вижу ничего, что сделало бы проект реально более читабельным/поддерживаемым/производительным/т.д… Да, это всё классные штуки, но мне доводилось разбирать код, в котором «классные штуки» применялись как вещь в себе, просто ради попробовать, и это была жуть… Синтаксический сахар — вещь на любителя, а если в проекте работает более одного человека, то тут уже можно начать и о вкусах спорить…
Я 6 лет в EE, сейчас пишем на 8ке.
Это просто замечательно, но возникают 2 вопроса:
1. Писать на 8-ке понятие растяжимое — то же ФП, например, насколько активно используется?
2. Как насчет "что сделало бы проект реально более читабельным/поддерживаемым/производительным/т.д" — проекты на 8-ке чем лучше проектов на 7-ке/6-ке/5-ке? Производительность, трудозатраты, ошибки: если на конкретном проекте сменить синтаксис на 5-ку, например, станет ли он от этого набором жуткого говнокода?
1. Вполне активно. Стримы, опшналы и т.д.
2. Ну тут скорее производительность разработчиков, ошибок вроде из-за платформы не было практически (может 1-2, ничего заметного). У нас очень много работы с коллекциями — смотреться станет страшно. Есть некоторые места, где используется ФП — там, понятно, код тоже распухнет.
> почти всегда простые конструкции читабельней ФП-конструкций
Вы так говорите, будто второе исключает первое.
Заменили понятные 3 строчки кода на 3 непонятные + абзац объяснений что происходит и:
> Довольно круто, правда?

Если отбросить шутки, то у идеи «no raw loops» бывали хорошие примеры, хоть мне и кажется что это в целом какая-то потеря читаемости. На мой взгляд цикл сам по себе ничем не плох, если внутри не творится что-нибудь странное. Другой вариант когда это бы имело смысл если этот ваш Java стрим создовался бы сложным образом (на пример по каким-нибудь условиям или в разных функциях).
В плане читаемости хорошо выглядят for comprehensions в scala. Почти обычный foreach с виду, переписывается в map/flatMap/withFilter, которые реализованы у большого количества классов, что позволяет единообразно и наглядно производить различные итеративные вычисления.

Допустим, получения всех пар (i, j), где i не превосходит j выглядит так:
for (i <- 1 to n; j <- 1 to n if i <= j) yield (i, j)
// или так:
(1 to n).flatMap({ i => (1 to n).filter(i <= _).map({ j => (i, j) }) })

Первый вариант куда более удобен для чтения, сохраняет бонусы функциональных операций (например, lazyness, если оно реализуется в соответствующих типах) и также не оперирует изменяемым состоянием.
Образ мышления автора — вот три строки проверяющие на nil а теперь три строки но уже без слова nil так что вам надо читать доку на функцию чтобы понять что тут делается — меня пугает. Я боюсь таких людей в проекте и боюсь что статья количество таких людей увеличит

Java 8 хороший релиз и в нем много полезного, но хочется статьи где будет показано как все это использовать не для запутывания кода, а наоборот для облегчения его понимания. Пока таких статей не видел.
Предлагаю посмотреть вот этот гайд: What's New in Java 8.
По-моему, отличное краткое руководство по нововведениям в Java 8, с простыми и понятными примерами, в которых видна разница (в сравнении с более старыми версиями) и практическая применимость.
Новый уровень абстракции

Довольно круто, правда?

Вот не могу понять, в чем тут новый уровень абстракции (и, видимо, подразумевается что новый уровень абстракции-это абсолютное добро априори), и что тут круто?
Я вообще пишу на php (да простят меня боги)… и на php мы пользуемся «умными» коллекциями уже очень давно. Да, они не в ядре — это отдельные либы, и все же… Я все время слышу что php — недоязык и вообще очень плохой. А теперь я немного удивлен, что для многих в «крутой яве» такой подход явился откровением. Если я что-то не так сказал или не так понял, то поправьте меня.
В Крутой Джаве всегда была Крутая Гуава, в которой всё это давно уже есть (конечно же, по модулю того, какой синтаксис возможен с более старыми версиями JDK).
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.
основная проблема обертки, как и в Scala, что Optional[Integer] не является Integer

Это не проблема, а одно из главных достоинств с точки зрения надёжности. Такую обёртку нельзя куда-то нечаянно передать, забыв обработать ошибочную ситуацию. SomeType? же в этом плане не лучше миллиарднодолларовой ошибки обычного null с аннотациями.
То, что можно передать обертку, забыв про ошибочную ситуацию — проблема реализации, а не самое идеи чтобы Optional[Integer] являлся Integer. Как раз в Kotlin это сделано очень грамотно. Нельзя передать String? туда, где ожидается String просто так. Однако, если предварительно проверить на null, то, внезапно, так сделать уже можно.

fun foo(s: String)
val s: String?

//вот это не скомпилируется
foo(s)

//а вот это уже скомпилируется
if (s != null)
    foo(s)


В итоге, одновременно имеем защиту от забывчивости + удобство использования.
Ну так по сути это все равно разные несовместимые типы, просто в Kotlin проверка на null меняет тип s со String? на String внутри соответствующей ветви. Т.е. претензия здесь не по адресу — виноват не Optional, виновато отсутствие удобного способа проверить и сузить тип.
Строго говоря, да, Вы правы. Мысль в том, что «я могу использовать String? везде, где ожидается String» (но только если компилятор знает что это безопасно). Т.е. снаружи я могу считать что «Optional[Integer] является Integer, но с дополнительными проверками». Если я правильно понял Throwable, то он имел ввиду именно это.

Собственно, если компилятор не гарантирует корректность приведения Optional[Integer] -> Integer, то я согласен, это получается косяк в безопасности и так делать нельзя.
В Kotlin и подобных языках приведение nullables удобно закамуфлировано. Например, без лишних извратов можно передать non-null значение в функцию, принимающую nullable параметры.
fun doSomething(param:String?);
val s : String = "aaa";
doSomething(s);

Равно как и значение функции, возвращающей non-null значения, может быть присвоено nullable переменной. Плюс функция, возвращающая String? ковариантна к String, т.е. при переопределении можно String? заменить на String:
open class Class1 {
open fun doSomething():String?;
}
open class Class2 : Class1() {
override open fun doSomething():String;
}

Основной плюс в том, что nullable types — это исключительно фишка компилятора, также как и generics. Не существует реального типа String?.. Напр. нельзя написать String?::class, нельзя унаследовать nullable type, etc. В итоге все транслируется в обычные ссылочные типы, которые по определению уже null. С Optional это не так — это реальный тип данных, который для каждого ссылочного значения, которое уже и так nullable, создает дополнительный объект в памяти. Так что я считаю Optional неудачной попыткой исправить nullable косяк при помощи системы типов, нежели изменениями в языке/компиляторе.
>> без лишних извратов можно передать 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, просто с более удобным сахаром и более внятной семантикой.

UFO just landed and posted this here
Гарантирует:
private Optional(T value) {
  this.value = Objects.requireNonNull(value);
}

public static <T> Optional<T> of(T value) {
  return new Optional<>(value);
}

public static <T> Optional<T> ofNullable(T value) {
  return value == null ? empty() : of(value);
}
Проблема в другом.

public Optional<String> foo(); //может вернуть null, вместо empty()

public String bar(); //может вернуть null


Вот и получается, что без Optional проверять на null все равно нужно, а с Optional вообще получаем три значения вместо двух. Так что, согласен, @Nonnull/@Nullable гораздо предпочтительнее (в контексте java).
Я не спорю с утверждением, что метод, возвращающий Optional может вернуть null, хотя это должно трактоваться, как ошибка и требовать устранения.

Я спорю со второй частью утверждения Throwable:
В-третьих, Optional собственно не решает проблему null в Java. Он не гарантирует, что само возвращаемое значение Optional не будет null, равно как и что в коде с Optional возвращаемый не-Optional объект будет иметь ненулевое значение.
Optional.get() всегда вернет не-null или кинет исключение.
Я плохо выразился. Имелось ввиду, что там, где возвращается Optional можно сделать вывод, что значение nullable, тогда как там, где возвращается не-Optional никакого вывода сделать нельзя.
// мне нравятся Optinal и теперь я буду их везде использовать
public Optional<String> getMyValue();
// а это мой старый код, который используется в куче мест и мне лень все это переписывать. Может ли он возвращать nullable, я уже и забыл.
public String getMyAnotherValue();
// а это код Васи, который поклялся, что тоже будет везде использовать Optional, но почему-то здесь иногда возвращает null.
public String getHisValue();


Проблема nullable не решается Optional, потому что его целью является ссылочный тип который изначально nullable по определению. Лишь дополнительно напрягает программиста распаковкой-запаковкой значений, и проверками, в которых как правило в 90% случаев нет необходимости.

Решение проблемы заключается в обратном: указать, что значение как раз не является nullable и оно safe для операций. Если идти путем Optional, то потребуется монада Some, и всего лишь переписать весь код, заменяя все non-nullable свободные ссылочные типы T на Some. Делов-то! Либо использовать другой способ, чтобы научить компилятор отличать non-nullable значения от всех остальных, например при помощи аннотаций. Или ввести в язык value-types.

P.S. Для любителей геморроя с Optional:
class MyBean {
// надо optional поле
private Optional<String> value;
// это сгенерит любая IDE
public Optional<String> getValue() {return value;}
public void setValue(Optional<String> value) {this.value = value;}
}
// где-то в коде
MyBean b = new MyBean();
// так можно, компилятор не матюгнется
b.setValue(null);
b.getValue().orElse(""); // NPE

// теперь сделаем все корректно:
class MyBean {
private String value;  // чтобы bean был Serializable поля не должны быть Optional
// для каждого поля надо доработать аксессоры ручками
public Optional<String> getValue() {return Optional.ofNullable(value);}
public void setValue(Optional<String> value) {
if (value == null) this.value = null; // можно кинуть NPE в этом случае
this.value = value.orElse(null);
}
}
а чем плох такой Bean?

class MyBean {
  private String value;

  public Optional<String> getValueSafe() { return Optional.ofNullable(value); }

  public String getValue() { return this.value; }

  public void setValue(String value) {
    this.value = value;
  }
}
Избыточностью.
Во-первых, задолбает писать для каждого поля safe-геттер. IDE такое не предлагает.
Во-вторых, если наша задача — минимизировать возможность ошибки, то здесь остается такая дверь ввиде традиционного геттера. Если я сделаю такой бин, угадайте какой из двух геттеров в 90% случаев будет использовать Вася?
Во-третьих, никто так не делает. Ни одна спецификация, рекомендация, или просто use-case не содержит подобного. Все библиотеки, фреймворки, работающие со свойствами, будут использовать традиционные аксессоры.
И, наконец, есть решение лучше и проще:
class MyBean {
@Nullable
private String value;
// а это сгенерит умная IDE
@Nullable
public String getValue() {return value;}
public void setValue(@Nullable value) {this.value = value;}
}
> Во-вторых, основная проблема обертки, как и в 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 =>…
}
Sign up to leave a comment.

Articles

Change theme settings