image


От переводчика: LambdaMetafactory, пожалуй, один из самых недооценённых механизмов Java 8. Мы открыли его для себя совсем недавно, но уже по достоинству оценили его возможности. В версии 7.0 фреймворка CUBA улучшена производительность за счет отказа от рефлективных вызовов в пользу генерации лямбда выражений. Одно из применений этого механизма в нашем фреймворке — привязка обработчиков событий приложения по аннотациям, часто встречающаяся задача, аналог EventListener из Spring. Мы считаем, что знание принципов работы LambdaFactory может быть полезно во многих Java приложениях, и спешим поделиться с вами этим переводом.


В этой статье мы покажем несколько малоизвестных хитростей при работе с лямбда-выражениями в Java 8 и ограничения этих выражений. Целевая аудитория статьи — senior Java разработчики, исследователи и разработчики инструментария. Будет использоваться только публичный Java API без com.sun.* и других внутренних классов, поэтому код переносим между разными реализациями JVM.


Короткое предисловие


Лямбда-выражения появились в Java 8 как способ имплементации анонимных методов и,
в некоторых случаях, как альтернатива анонимным классам. На уровне байткода лямбда-выражение заменяется инструкцией invokedynamic. Эта инструкция используется для создания реализации функционального интерфейса и его единственный метод делегирует вызов фактическому методу, который содержит код, определенный в теле лямбда-выражения.


Например, у нас есть следующий код:


void printElements(List<String> strings){
    strings.forEach(item -> System.out.println("Item = %s", item));
}

Этот код будет преобразован компилятором Java во что-то похожее на:


private static void lambda_forEach(String item) { //сгенерировано Java компилятором
    System.out.println("Item = %s", item);
}
private static CallSite bootstrapLambda(Lookup lookup, String name, MethodType type) { //
    //lookup = предоставляется VM
    //name = "lambda_forEach", предоставляется VM
    //type = String -> void
    MethodHandle lambdaImplementation = lookup.findStatic(lookup.lookupClass(), name, type);
    return LambdaMetafactory.metafactory(lookup,
        "accept",
        MethodType.methodType(Consumer.class), //сигнатура фабрики лямбда-выражений
        MethodType.methodType(void.class, Object.class), //сигнатура метода Consumer.accept после стирания типов  
        lambdaImplementation, //ссылка на метод с кодом лямбда-выражения
        type);
}
void printElements(List<String> strings) {
    Consumer<String> lambda = invokedynamic# bootstrapLambda, #lambda_forEach
    strings.forEach(lambda);
}

Инструкция invokedynamic может быть примерно представлена как вот такой Java код:


private static CallSite cs;
void printElements(List<String> strings) {
    Consumer<String> lambda;
    //begin invokedynamic
    if (cs == null)
        cs = bootstrapLambda(MethodHandles.lookup(), 
                            "lambda_forEach", 
                            MethodType.methodType(void.class, String.class));
    lambda = (Consumer<String>)cs.getTarget().invokeExact();
    //end invokedynamic
    strings.forEach(lambda);
}

Как видно, LambdaMetafactory применяется для создания CallSite который предоставляет фабричный метод, возвращающий обработчик целевого метода,. Этот метод возвращает реализацию функционального интерфейса, используя invokeExact. Если в лямбда-выражении есть захваченные переменные, то invokeExact принимает эти переменные как фактические параметры.


В Oracle JRE 8 metafactory динамически генерирует Java класс, используя ObjectWeb Asm, который и создает класс-реализацию функционального интерфейса. К созданному классу могут быть добавлены дополнительные поля, если лямбда-выражение захватывает внешние переменные. Этот похоже на анонимные классы Java, но есть следующие отличия:


  • Анонимный класс генерируется компилятором Java.
  • Класс для реализации лямбда-выражения создается JVM во время выполнения.



Реализация metafactory зависит от вендора JVM и от версии




Конечно же, инструкция invokedynamic используется не только для лямбда-выражений в Java. В основном, она применяется при выполнении динамических языков в среде JVM. Движок Nashorn для исполнения JavaScript, который встроен в Java, интенсивно использует эту инструкцию.


Далее мы сфокусируемся на классе LambdaMetafactory и его возможностях. Следующий
раздел этой статьи исходит из предположения, что вы отлично понимаете как работают методы metafactory и что такое MethodHandle


Трюки с лямбда-выражениями


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


Проверяемые исключения и лямбды


Не секрет, что все функциональные интерфейсы, которые есть в Java, не поддерживают проверяемые исключения. Преимущества проверяемых исключений перед обычными — это очень давний (и до сих пор горячий) спор.


А что, если вам нужно использовать код с проверяемыми исключениями внутри лямбда-выражений в сочетании с Java Streams? Например, нужно преобразовать список строк в список URL как здесь:


Arrays.asList("http://localhost/", "https://github.com").stream()
        .map(URL::new)
        .collect(Collectors.toList())

В конструкторе URL(String) объявлено проверяемое исключение, таким образом, он не может быть использован напрямую в виде ссылки на метод в классе Functiion.


Вы скажете: "Нет, возможно, если использовать вот такую хитрость":


public static <T> T uncheckCall(Callable<T> callable) {
    try { return callable.call(); }
    catch (Exception e) { return sneakyThrow(e); }
}
private static <E extends Throwable, T> T sneakyThrow0(Throwable t) throws E { throw (E)t; }
public static <T> T sneakyThrow(Throwable e) {
    return Util.<RuntimeException, T>sneakyThrow0(e);
}
// Пример использования
//return s.filter(a -> uncheckCall(a::isActive))
//        .map(Account::getNumber)
//        .collect(toSet());

Это грязный хак. И вот почему:


  • Используется блок try-catch.
  • Исключение выбрасывается ещё раз.
  • Грязное использование стирания типов в Java.

Проблема может быть решена более "легальным" способом, с использованием знания следующих фактов:


  • Проверяемые исключения распознаются только на уровне Java компилятора.
  • Секция throws — это всего лишь метаданные для метода без семантического значения на уровне JVM.
  • Проверяемые и обычные исключения неразличимы на уровне байткода в JVM.

Решение — обернуть метод Callable.call в метод без секции throws:


static <V> V callUnchecked(Callable<V> callable){
    return callable.call();
}

Этот код не скомпилируется, потому что у метода Callable.call объявлены проверяемые исключения в секции throws. Но мы можем убрать эту секцию, используя динамически сконструированное лямбда-выражение.


Сначала нам нужно объявить функциональный интерфейс, в котором нет секции throws
но который сможет делегировать вызов к Callable.call:


@FunctionalInterface
interface SilentInvoker {
    MethodType SIGNATURE = MethodType.methodType(Object.class, Callable.class);//сигнатура метода INVOKE
    <V> V invoke(final Callable<V> callable);
}

Второй шаг — создать реализацию этого интерфейса, используя LambdaMetafactory и делегировать вызов метода SilentInvoker.invoke методу Callable.call. Как было сказано ранее, секция throws игнорируется на уровне байткода, таким образом, метод SilentInvoker.invoke сможет вызвать метод Callable.call без объявления исключений:


private static final SilentInvoker SILENT_INVOKER;
final MethodHandles.Lookup lookup = MethodHandles.lookup();
final CallSite site = LambdaMetafactory.metafactory(lookup,
                    "invoke",
                    MethodType.methodType(SilentInvoker.class),
                    SilentInvoker.SIGNATURE,
                    lookup.findVirtual(Callable.class, "call", MethodType.methodType(Object.class)),
                    SilentInvoker.SIGNATURE);
SILENT_INVOKER = (SilentInvoker) site.getTarget().invokeExact();

Третье — напишем вспомогательный метод, который вызывает Callable.call без объявления исключений:


public static <V> V callUnchecked(final Callable<V> callable) /*no throws*/ {
    return SILENT_INVOKER.invoke(callable);
}

Теперь можно переписать stream без всяких проблем с проверяемыми исключениями:


Arrays.asList("http://localhost/", "https://dzone.com").stream()
        .map(url -> callUnchecked(() -> new URL(url)))
        .collect(Collectors.toList());

Этот код скомпилируется без проблем, потому что в callUnchecked нет объявления проверяемых исключений. Более того, вызов этого метода может быть заинлайнен при помощи мономорфного инлайн кэширования, потому что это только один класс во всей JVM, который реализует интерфейс SilentOnvoker


Если реализация Callable.call выкинет исключение во время выполнения, то оно будет перехвачено вызывающей функцией без всяких проблем:


try{
    callUnchecked(() -> new URL("Invalid URL"));
} catch (final Exception e){
    System.out.println(e);
}

Несмотря на возможности этого метода, нужно всегда помнить про следующую рекомендацию:




Скрывайте проверяемые исключения при помощи callUnchecked только если уверены, что вызываемый код не выкинет никаких исключений




Следующий пример показывает пример такого подхода:


callUnchecked(() -> new URL("https://dzone.com")); //этот URL всегда правильный и конструктор никогда не выкинет MalformedURLException

Полная реализация этого метода находится здесь, это часть проекта с открытым кодом SNAMP.


Работаем с Getters и Setters


Этот раздел будет полезен тем, кто пишет сериализацию/десериализацию для различных форматов данных, таких как JSON, Thrift и т.д. Более того, он может быть довольно полезен, если ваш код сильно полагается на рефлексию для Getters и Setters в JavaBeans.


Getter, объявленный в JavaBean — это метод с именем getXXX без параметров и возвращаемым типом данных, отличным от void. Setter, объявленный в JavaBean — метод с именем setXXX, с одним параметром и возвращающий void. Эти две нотации могут быть представленв как функциональные интерфейсы:


  • Getter может быть представлен классом Function, в котором аргумент — значение this.
  • Setter может быть представлен классом BiConsumer, в котором первый аргумент — this, а второй — значение, которое передается в Setter.

Теперь мы создадим два метода, которые смогут преобразовать любой getter или setter в эти
функциональные интерфейсы. И неважно, что оба интерфейса — generics. После стирания типов
реальный тип данных будет Object. Автоматическое приведение возвращаемого типа и аргументов может быть сделано при помощи LambdaMetafactory. В дополнение, библиотека Guava поможет с кэшированием лямбда-выражений для одинаковых getters и setters.


Первый шаг: необходимо создать кэш для getters и setters. Класс Method из Reflection API представляет реальный getter или setter и используется в качестве ключа.
Значение кэша — динамически сконструированный функциональный интерфейс для определенного getter'а или setter'а.


private static final Cache<Method, Function> GETTERS = CacheBuilder.newBuilder().weakValues().build();
private static final Cache<Method, BiConsumer> SETTERS = CacheBuilder.newBuilder().weakValues().build();

Во-вторых, создадим фабричные методы, которые создают экземпляр функционального интерфейса на основе ссылок на getter или setter.


private static Function createGetter(final MethodHandles.Lookup lookup,
                                         final MethodHandle getter) throws Exception{
        final CallSite site = LambdaMetafactory.metafactory(lookup, "apply",
                MethodType.methodType(Function.class),
                MethodType.methodType(Object.class, Object.class), //signature of method Function.apply after type erasure
                getter,
                getter.type()); //actual signature of getter
        try {
            return (Function) site.getTarget().invokeExact();
        } catch (final Exception e) {
            throw e;
        } catch (final Throwable e) {
            throw new Error(e);
        }
}
private static BiConsumer createSetter(final MethodHandles.Lookup lookup,
                                           final MethodHandle setter) throws Exception {
        final CallSite site = LambdaMetafactory.metafactory(lookup,
                "accept",
                MethodType.methodType(BiConsumer.class),
                MethodType.methodType(void.class, Object.class, Object.class), //signature of method BiConsumer.accept after type erasure
                setter,
                setter.type()); //actual signature of setter
        try {
            return (BiConsumer) site.getTarget().invokeExact();
        } catch (final Exception e) {
            throw e;
        } catch (final Throwable e) {
            throw new Error(e);
        }
}

Автоматическое приведение типов между аргументами типа Object в функциональных интерфейсах (после стирания типов) и реальными типами аргументов и возвращамого значения достигается при помощи разницы между samMethodType и instantiatedMethodType (третий и пятый аргументы метода metafactory, соответственно). Тип созданного экземпляра метода — это и есть специализация метода, который предоставляет реализацию лямбда-выражения.


В-третьих, создадим фасад для этих фабрик с поддержкой кэширования:


public static Function reflectGetter(final MethodHandles.Lookup lookup, final Method getter) throws ReflectiveOperationException {
        try {
            return GETTERS.get(getter, () -> createGetter(lookup, lookup.unreflect(getter)));
        } catch (final ExecutionException e) {
            throw new ReflectiveOperationException(e.getCause());
        }
}
public static BiConsumer reflectSetter(final MethodHandles.Lookup lookup, final Method setter) throws ReflectiveOperationException {
        try {
            return SETTERS.get(setter, () -> createSetter(lookup, lookup.unreflect(setter)));
        } catch (final ExecutionException e) {
            throw new ReflectiveOperationException(e.getCause());
        }
}

Информация о методе, полученная из экземпляра класса Method с использованием Java Reflection API может быть легко преобразована в MethodHandle. Примите во внимание, что у методов экземпляров класса, всегда есть скрытый первый аргумент, используемый для передачи this в этот метод. У статических методов такого параметра нет. Например, реальная сигнатура метода Integer.intValue() выглядит как int intValue(Integer this). Эта хитрость используется в нашей имплементации функциональных оберток для getters и setters.


А теперь — время тестировать код:


final Date d = new Date();
final BiConsumer<Date, Long> timeSetter = reflectSetter(MethodHandles.lookup(), Date.class.getDeclaredMethod("setTime", long.class));
timeSetter.accept(d, 42L); //the same as d.setTime(42L);
final Function<Date, Long> timeGetter = reflectGetter(MethodHandles.lookup(), Date.class.getDeclaredMethod("getTime"));
System.out.println(timeGetter.apply(d)); //the same as d.getTime()
//output is 42

Этот подход с закэшированными getters и setters можно эффективно использовать в библиотеках для сериализации/десериализации (таких, как Jackson), которые используют getters и setters во время сериализации и десериализации.




Вызовы функциональных интерфейсов с динамически сгенерированными реализациями с использованием LambdaMetaFactory значительно быстрее, чем вызовы через Java Reflection API




Полную версию кода можно найти здесь, это часть библиотеки SNAMP.


Ограничения и баги


В этом разделе мы рассмотрим некоторые баги и ограничения, связанные с лямбда-выражениями в компиляторе Java и JVM. Все эти ограничения можно воспроизвести в OpenJDK и Oracle JDK с javac версии 1.8.0_131 для Windows и Linux.


Создание лямбда-выражений из обработчиков методов


Как вы знаете, лямбда-выражение можно сконструировать динамически, используя LambdaMetaFactory. Чтобы это сделать, нужно определить обработчик — класс MethodHandle, который указывает на реализацию единственного метода, который определен в функциональном интерфейсе. Давайте взглянем на этот простой пример:


final class TestClass {
            String value = "";
            public String getValue() {
                return value;
            }
            public void setValue(final String value) {
                this.value = value;
            }
        }
final TestClass obj = new TestClass();
obj.setValue("Hello, world!");
final MethodHandles.Lookup lookup = MethodHandles.lookup();
final CallSite site = LambdaMetafactory.metafactory(lookup,
                "get",
                MethodType.methodType(Supplier.class, TestClass.class),
                MethodType.methodType(Object.class),
                lookup.findVirtual(TestClass.class, "getValue", MethodType.methodType(String.class)),
                MethodType.methodType(String.class));
final Supplier<String> getter = (Supplier<String>) site.getTarget().invokeExact(obj);
System.out.println(getter.get());

Этот код эквивалентен:


final TestClass obj = new TestClass();
obj.setValue("Hello, world!");
final Supplier<String> elementGetter = () -> obj.getValue();
System.out.println(elementGetter.get());

Но что, если мы заменим обработчик метода, который указывает на getValue на обработчик, который представляет getter поля:


final CallSite site = LambdaMetafactory.metafactory(lookup,
                "get",
                MethodType.methodType(Supplier.class, TestClass.class),
                MethodType.methodType(Object.class),
                lookup.findGetter(TestClass.class, "value", String.class), //field getter instead of method handle to getValue
                MethodType.methodType(String.class));

Этот код должен, ожидаемо, работать, потому что findGetter возвращает обработчик, который указывает на getter поля и у него правильная сигнатура. Но, если вы запустите этот код, то увидите следующее исключение:


java.lang.invoke.LambdaConversionException: Unsupported MethodHandle kind: getField

Что интересно, getter для поля работает нормально, если будем использовать MethodHandleProxies:


final Supplier<String> getter = MethodHandleProxies
                                       .asInterfaceInstance(Supplier.class, lookup.findGetter(TestClass.class, "value", String.class)
                                       .bindTo(obj));

Нужно отметить, что MethodHandleProxies — не очень хороший способ для динамического создания лямбда-выражений, потому что этот класс просто оборачивает MethodHandle в прокси-класс и делегирует вызов InvocationHandler.invoke методу MethodHandle.invokeWithArguments. Этот подход использует Java Reflection и работает очень медленно.


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




Только несколько типов обработчиков методов могут быть использованы для динамического создания лямбда-выражений




Вот они:


  • REF_invokeInterface: может быть создан при помощи Lookup.findVirtual для методов интерфейсов
  • REF_invokeVirtual: может быть создан с помощью Lookup.findVirtual для виртуальных методов класса
  • REF_invokeStatic: создается при помощи Lookup.findStatic для статических методов
  • REF_newInvokeSpecial: может быть создан при помощи Lookup.findConstructor для конструкторов
  • REF_invokeSpecial: может быть создан с помощью Lookup.findSpecial
    для приватных методов и раннего связывания с виртуальными методами класса

Остальные типы обработчиков вызовут ошибку LambdaConversionException.


Generic исключения


Этот баг связан с компилятором Java и возможностью объявлять generic исключения в секции throws. Следующий пример кода демонстрирует это поведение:


interface ExtendedCallable<V, E extends Exception> extends Callable<V>{
        @Override
        V call() throws E;
}
final ExtendedCallable<URL, MalformedURLException> urlFactory = () -> new         
    URL("http://localhost");
    urlFactory.call();

Этот код должен скомпилироваться, потому что конструктор класса URL выбрасывает MalformedURLException. Но он не компилируется. Выдается следующее сообщение об ошибке:


Error:(46, 73) java: call() in <anonymous Test$CODEgt; cannot implement call() in ExtendedCallable
overridden method does not throw java.lang.Exception

Но, если мы заменим лямбда-выражение анонимным классом, то код скомпилируется:



final ExtendedCallable<URL, MalformedURLException> urlFactory = new ExtendedCallable<URL, MalformedURLException>() {
            @Override
            public URL call() throws MalformedURLException {
                return new URL("http://localhost");
            }
        };
urlFactory.call();

Из этого следует:




Вывод типов для generic исключений не работет корректно в сочетании с лямбда-выражениями




Ограничения типов параметризации


Можно сконструировать generic объект с несколькими ограничениями типов, используя знак &: <T extends A & B & C & ... Z>.
Такой способ определения generic параметров редко используется, но определенным образом влияет на лямбда-выражения в Java из-за некоторых ограничений:


  • Каждое ограничение типа, кроме первого, должно быть интерфейсом.
  • Чистая версия класса с таким generic учитывает только первое ограничение типа из списка.

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


final class MutableInteger extends Number implements IntSupplier, IntConsumer { //mutable container of int value
    private int value;
    public MutableInteger(final int v) {
        value = v;
    }
    @Override
    public int intValue() {
        return value;
    }
    @Override
    public long longValue() {
        return value;
    }
    @Override
    public float floatValue() {
        return value;
    }
    @Override
    public double doubleValue() {
        return value;
    }
    @Override
    public int getAsInt() {
        return intValue();
    }
    @Override
    public void accept(final int value) {
        this.value = value;
    }
}
static <T extends Number & IntSupplier> OptionalInt findMinValue(final Collection <T> values) {
    return values.stream().mapToInt(IntSupplier::getAsInt).min();
}
final List <MutableInteger> values = Arrays.asList(new MutableInteger(10), new MutableInteger(20));
final int mv = findMinValue(values).orElse(Integer.MIN_VALUE);
System.out.println(mv);

Этот код абсолютно корректный и успешно компилируется. Класс MutableInteger удовлетворяет ограничениям обобщенного типа T:


  • MutableInteger наследуется от Number.
  • MutableInteger реализует IntSupplier.

Но код упадет с исключением во время выполнения:


java.lang.BootstrapMethodError: call site initialization exception
    at java.lang.invoke.CallSite.makeSite(CallSite.java:341)
    at java.lang.invoke.MethodHandleNatives.linkCallSiteImpl(MethodHandleNatives.java:307)
    at java.lang.invoke.MethodHandleNatives.linkCallSite(MethodHandleNatives.java:297)
    at Test.minValue(Test.java:77)
Caused by: java.lang.invoke.LambdaConversionException: Invalid receiver type class java.lang.Number; not a subtype of implementation type interface java.util.function.IntSupplier
    at java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:233)
    at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:303)
    at java.lang.invoke.CallSite.makeSite(CallSite.java:302)

Так получается, потому что конвейер JavaStream захватывает только чистый тип, который, в нашем случае — класс Number и он не реализует интерфейс IntSupplier. Эту проблему можно исправить явным объявлением типа параметра в отдельном методе, используемом в качестве ссылки на метод:


private static int getInt(final IntSupplier i){
        return i.getAsInt();
}
private static <T extends Number & IntSupplier> OptionalInt findMinValue(final Collection<T> values){
        return values.stream().mapToInt(UtilsTest::getInt).min();
}

Этот пример демонстрирует некорректный вывод типов в компиляторе и среде исполнения.




Обработка нескольких ограничений типов generic параметров в сочетании с использованием лямбда-выражений во время компиляции и выполнения — неконсистентна



Комментарии (34)


  1. Px2
    11.12.2018 10:27

    После прочтения ваших статей у меня складывается ощущение, что в компании Холмонт задались целью писать как можно более неподдерживаемый код.

    Почему бы вместо этого

    Arrays.asList(«localhost», «github.com»)
    .stream()
    .map(URL::new)
    .collect(Collectors.toList())

    не использовать обычный цикл для прохода по коллекции? Придумали проблему на пустом месте, обязательно вам захотелось использовать стримы, вместо того, чтобы написать код, понятный, не машине, а человеку.


    1. a_belyaev Автор
      11.12.2018 10:44
      +1

      Это пример кода, который показывает потенциальные проблемы использования лямбд. Это не фрагмент кода CUBA :-)

      А проблема читабельности кода Java после введения лямбда-выражений в 8-ке — это отдельная тема, достойная большой статьи. Если этими вещами злоупотреблять — то получится страшное.

      Просто заметка — у так назывемых «внутренних» циклов, есть одно преимущество — можно поставить
      .parallelStream

      и обрабатывать коллекцию в несколько потоков. Но, конечно, это тоже можно превратить в недостаток, если пихать параллельность бездумно :-) Тут хорошо написано про то, что стримы могут код замедлять в 5 раз.


      1. AstarothAst
        11.12.2018 10:54

        Это пример кода, который показывает потенциальные проблемы использования лямбд

        У вас проблема с лямбдами, но в примерах вы делаете еще больше проблем с пониманием написанного, что бы-таки применить лямбды… В скором времени после релиза восьмерки я видел замечательно правило по этому поводу: «Если в вашем коде лямбды больше мешают, нежели помогают — просто не используйте лямбды. Любая задача решаемая через лямбды может быть решена без них.»


        1. a_belyaev Автор
          11.12.2018 11:12
          +1

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


        1. sshikov
          11.12.2018 16:57

          >У вас проблема с лямбдами
          > что бы-таки применить лямбды…
          Проблема не с лямбдами. Если вы вглядитесь, вы поймете, что изначально проблема в checked exceptions. Которые плохо совмещаются с функциями — это чистая правда.

          Просто посмотрите на описанные тут приемы с иной точки зрения. Представьте, что вы это вот все не руками пишете, а генерируете. Или создали на базе этого знания библиотеку, что вполне возможно. Если взять тот же vavr.io, то приведенный пример будет выглядеть как:

          Arrays.asList(«localhost», «github.com»)
          .stream()
          .map(s->Try.of(()->new URL(s)))
          .collect(Collectors.toList())

          и все. На выходе вы получите List<Try>>. А захотите — можете сразу тут отфильтровать невалидные URL, и это будет так же просто и кратко.


    1. a_e_tsvetkov
      11.12.2018 10:45
      +2

      Ну это же только минимальный пример. Или вы считаете что стримы сложнее в принципе и использовать их не стоит?


      1. Px2
        11.12.2018 10:54

        Я писал не об этом, а о том, что использование разных трюков и хитростей, как написано в статье, — это плохо и ведет к появлению трудноподдерживаемого кода.
        Сегодня этим кодом занимается сеньор и никто ему не мешает самовыражаться (за чужой счет). Завтра его переведут на другой проект и на поддержку кода придет джуниор, который будет неделями разбираться, что это за хаки и как они работают. Проще надо быть, проще. А самовыражаться в домашних проектах. Все равно их никто не увидит.


        1. a_belyaev Автор
          11.12.2018 11:42

          Самовыражаться в коде боевого проекта — вообще плохая идея :-) А хаки, трюки и хитрости должны быть локализованы и подробно задокументированы, иногда без них нельзя. А ещё, некоторые задачи нельзя передавать недостаточно опытным разработчикам, если они недостаточно четко понимают, как оно внутри работает, как бы оскорбительно это не звучало. Пока разработчик не понимает, зачем нужен synchronized, отдавать ему код по многопоточной обработке данных не надо.


      1. a_belyaev Автор
        11.12.2018 11:00
        +1

        Лично меня не сильно пугает синтаксис стримов, если аккуратно писать — то все читаемо получается. И стримы не сложнее «понятного человеку» кода. До Java 8, лямбд и стримов я видел примеры нечитаемого кода. Как обычно — все сводится к тому, как человек пишет.

        На coursera есть курс Kotlin for Java developers там задачка про таксопарк. Так вот, там, если решать с использованием стримов — то все получается очень красиво. Если решать в более «императивном» стиле — то кода будет в три раза больше, мне кажется :-)


        1. ruslanys
          12.12.2018 11:35
          +1

          Строго говоря, в Kotlin не совсем стримы, вернее, совсем не стримы) Там используются функциональные вызовы, выполняющие императивные операции. Но в целом, да, согласен, понимаю о чем идет речь.

          Со своей стороны всем, кто не понимает зачем нужны стримы, могу порекомендовать прекрасный, на мой взгляд, Kotlin Koans Collections.


    1. poxvuibr
      11.12.2018 18:59

      Почему бы вместо этого… не использовать обычный цикл для прохода по коллекции?

      Потому что обычный цикл хуже читается и содержит больше бойлерплейта.


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

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


    1. 0x1000000
      11.12.2018 20:43
      +1

      Не думаю, что у человека знакомого (хоть немного) с функциональным программированием возникнет проблема с пониманием этого кода. На мой взгляд, такая запись куда понятнее чем куча вложенных циклов.


      1. tuxi
        11.12.2018 22:47

        void printElements(List<String> strings){
            strings.forEach(item -> System.out.println("Item = %s", item));
        }

        Ну как бы тут нет вложенных циклов.


  1. sshikov
    11.12.2018 16:48

    >Я писал не об этом,
    Но получилось в итоге именно об этом. Все свелось к:

    >Почему бы вместо этого… не использовать обычный цикл для прохода по коллекции?

    >понятный, не машине, а человеку.
    Ну, это ваше личное мнение. Мне вот по большей части все равно, какой стиль читать, но при этом стримы и сопутствующий стиль (map/reduce) имеют и очевидные преимущества. Одно из них — как раз большая очевидность выражения «намерений».

    Если использовать обычный цикл, то exception на new URL никуда не денется, и его так или иначе придется обработать. Это можно сделать разными способами, многие из которых неправильные. А уж от «намерений» как правило вообще мало что остается.

    Вы попробуйте, напишите, только try не забудьте — вот тогда и будет видно, насколько у вас получится понятно человеку. Я практически уверен, что настолько же лаконично вряд ли выйдет.


  1. ValDubrava
    11.12.2018 19:24

    При все моем уважении к автору, но это выглядит как темнейшая магия! Мне кажется, нет никаких причин, чтоб писать код таким образом. Всегда есть другие решение: сделайте метод-обертку (без магии), хелпер какой-нибудь. Напишите явный код try-catch, да, может не так красиво, но зато просто и понятно. Ну или — используйте обычный цикл. Нельзя в гонке за лаконичностью кода переступать на темную сторону. Оно того не стоит.


    1. a_belyaev Автор
      11.12.2018 22:16
      +2

      Это не совсем магия. При перекладывании данных из формочки в базу и обратно, вы, скорее всего, ЯВНО с этим не встретитесь. Но если вы пишете фреймворк, то рано или поздно столкнетесь с необходимостью использовать reflection, например. Reflection — магия? А если захотите, чтобы фреймворк работал ещё быстрее, то будете искать варианты, как ускорить reflection. И придете к MethodHandle, лямбдам и вот этому вот всему. В рамках формата перевода много от себя не напишешь, можно почитать пример тут, если не страшно :-) А ещё можно глянуть в исходники одного из классов библиотеки spring-data-commons — ClassGeneratingPropertyAccessorFactory, там тоже есть чему порадоваться. А этим кодом многие люди пользуются и он как-то поддерживается.


      1. ValDubrava
        11.12.2018 23:21
        +1

        Я допускаю использование reflection в таких вещах, как spring. Знаю, что hibernate в базу и jackson на фронт используют рефлексию — ок, это вещи кажутся вынужденными. Хотя, я знаю людей, которые не пользуются hibernate в том числе и из-за рефлексии. В spring же можно ограничится рефлексие только на init phase (когда, обычно, производительность не так критична). В jackson — писать кастомные сериализаторы. Но судя по данной статье, вы используете черную магию, просто потому, что хотите добится более красивового и локаничного кода. Отсюда и мое возмущение. Иными словами, рефлекся — это сила, и пользоваться ей надо с большой осторожностью и умом. Надеюсь, что вы так и делаете, а в статье просто описаны вырожденные примеры.


        1. Sap_ru
          12.12.2018 05:45
          +2

          Ну, вот, как-то мне нужно было сделать такую простую вещь, как разбор INI-файла. Но хотелось автоматизации, т.к. сложная структура с разделами и подразделами, проверка валидности значений, автоподастановка значений по-умолчанию, контроль изменений, кэширование и т.п.
          Сначала это был просто код. Но когда данный стало много, то код стал нечитаем.
          Было переписано на набор классов, которые делали основную автоматизацию. Но по мере разрастания количество и сложности данных это опять стало нечитаемым — куча дублирующегося когда, в частности.
          В результате было переписано с рефлекшином, когда класс данных динамически разбирается и под него формируются метода работы с данным. Кода стало в четрые раза меньше, основной код стал неизмеримо понятнее. А если бы это были не статические, а динамические данные, то могли возникнуть вопросы быстродейтсвия и пришлось лезть в LambdaFactory.
          Рефлекшн уже не магия. На нём много чего основано и есть такое ощущение, что в случае работы с со сложными динамическими данными без него на определённом этапе уже не обойдёшься — слишком много дурного кода придётся писать. Его уже нужно знать и уметь применять.


          1. BugM
            12.12.2018 10:18
            -1

            Любой ваш код разбирающий ini файл плохой. И исправить его можно только выкинув вообще и заменив на сериализацию.


            1. a_e_tsvetkov
              12.12.2018 10:23
              +4

              Отличная идея. Придется правда пользователей научить править бинарный файл с сериализованными объектами. Но ничего, они вытерпят.


              1. BugM
                12.12.2018 13:00
                -1

                Вы правда не знаете про сериализацию в текст?
                Печально это…


                1. a_e_tsvetkov
                  12.12.2018 13:38
                  +2

                  Я знаю много сериализаций, поэтому когда не оговаривается специально, я подразумеваю что речь идет о стандартной сериализации.

                  Если же вы имели ввиду сериализацию в текст то ini это текст и смысл вашего комментария становится совсем туманным.


                1. aleksandy
                  12.12.2018 13:43
                  +1

                  А с каких пор ini-файл перестал быть текстом? Или под текстом имеется ввиду сугубо xml/yaml/json/properties/conf/etc.?


            1. Sap_ru
              12.12.2018 18:58
              +1

              Вау! А мужики-то и не знают, уж, простите!
              А исходные данные в сериализацию вы как забивать будете? А править их потом ручками в случае сложной структуры? INI-файлы, они, как бы, очень простой человеко-ориентированный кросс-платформенный формат.
              Только не говорите про XML, сами его редактируйте в чистом поле средствами какого-нибудь notepad.


              1. BugM
                13.12.2018 11:49

                Вы на полном серьезе говорите что отредактировать xml это проблема?

                Что лучше писать велосипед по парсингу кастомных конфигов, чем воспользоваться стандартным решением которое вообще не надо писать?

                Что не знаете откуда взять классы для хранения ваших конфигурацинных данных?

                А потом еще удивляемся неработающим игрушкам из-за опечатки в конфиге. Они тоже небось считают что кастомный ini это круто.


                1. Sap_ru
                  13.12.2018 14:27

                  Вы в документации как будете формат XML описывать? Удачи. Как неподготовленный пользователь должен достаточно сложный XML править?
                  Всё равно нужно решать задачу валидации данных, структуры данных, значений по-умолчанию ии т.п. «стандартного решения» не будет.
                  И самое главное, вы какую задачу применением XML решаете? Формата данных, который может править неподготовленный пользователь? Нет. Тогда какую?


                  1. BugM
                    13.12.2018 15:54

                    Проблема описания XML в документации решена уже столько раз… Даже не сосчитать. Вы точно не первые кто xml использует.

                    Не нужно. Десериализуем файл. Ситуация бинарная: десериализовалось или нет. Во втором случае конфиг некорректен вообще. Выводим ошибку не стартуем.
                    В первом валидируем поля наших конфиг классов. Задача стандартнее некуда. Тесты простейшие и пишутся без проблем. Вероятность ошибки минимальная.

                    Отсутствие кода лучше чем любой код. Если задачу можно решить без написания кода это великолепно и ее только так и надо решать. Я решаю задачу упрощения приложения и уменьшения вероятности ошибок.


                    1. Sap_ru
                      13.12.2018 16:33

                      Так как вы будете в документации XML описывать для обычного пользователя, далёкого от IT? Описание формата INI-файла занимает одну страницу.
                      И я же говорю, вам всё равно придётся прикручивать проверки правильности значений и структуры, значения по умолчанию и т.п. Количество кода изменится не сильно, но с точки зрения пользователя результат будет намного хуже.


                1. a_e_tsvetkov
                  14.12.2018 06:55

                  Создается впечатление что вы не в курсе о существовании библиотек парсинга ini файлов.


    1. Maccimo
      12.12.2018 22:14
      +1

      Как метко заметил товарищ Кларк, любая достаточно развитая технология неотличима от магии. Из чего прямо следует, что встреча с магией это повод заняться самообразованием, а вовсе не причина остановить прогресс.


  1. PqDn
    12.12.2018 11:38

    Спасибо за статью.
    Хотелось, чтобы этот автор написал статью про MethodHandle, если есть желание и интерес


    1. a_belyaev Автор
      12.12.2018 11:55
      +1

      Статья — перевод. А про MethodHandles мы недавно писали.


  1. dim2r
    12.12.2018 15:29

    Даешь теорию ламбда исчилений и теорему Чёрча-Росса в массы!!!
    wiki: Лямбда-исчисление


  1. RomanKhomyshynets
    13.12.2018 10:18

    Спасибо за статью.
    Есть небольшой вопрос по поводу проверяемых исключений в лямбдах. В чем, собственно, заключается зло при оборачивании проверяемых исключений в непроверяемые?
    Для себя нахожу более удобным для этих целей синтаксис JOOL.
    Например, Ваш код:

    .map(url -> callUnchecked(() -> new URL(url)))

    Аналогично, с использованием JOOL:
    import static org.jooq.lambda.Unchecked.function;
    ...
    .map(function(URL::new))