Подтвердить что ты не робот

Lambdas, несколько для Each с отливкой

Нуждайтесь в помощи, думая в lambdas от моих друзей StackOverflow.

Стандартный случай выбора списка списка списка для сбора некоторых детей на глубине в графе. Какие удивительные способы могли бы помочь с этим шаблоном Lambdas?

public List<ContextInfo> list() {
    final List<ContextInfo> list = new ArrayList<ContextInfo>();
    final StandardServer server = getServer();

    for (final Service service : server.findServices()) {
        if (service.getContainer() instanceof Engine) {
            final Engine engine = (Engine) service.getContainer();
            for (final Container possibleHost : engine.findChildren()) {
                if (possibleHost instanceof Host) {
                    final Host host = (Host) possibleHost;
                    for (final Container possibleContext : host.findChildren()) {
                        if (possibleContext instanceof Context) {
                            final Context context = (Context) possibleContext;
                            // copy to another object -- not the important part
                            final ContextInfo info = new ContextInfo(context.getPath());
                            info.setThisPart(context.getThisPart());
                            info.setNotImportant(context.getNotImportant());
                            list.add(info);
                        }
                    }
                }
            }
        }
    }
    return list;
}

Обратите внимание, что сам список отправляется клиенту как JSON, поэтому не сосредотачивайтесь на том, что возвращается. Должно быть несколько опрятных способов, которыми я могу сократить петли.

Заинтересованы в том, что создают мои коллеги-эксперты. Поддерживается несколько подходов.

ИЗМЕНИТЬ

findServices и два метода findChildren возвращают массивы

EDIT - BONUS CHALLENGE

"Незначительная часть" оказалась важной. Мне действительно нужно скопировать значение, доступное только в экземпляре host. Кажется, это испортило все прекрасные примеры. Как переносить состояние вперед?

final ContextInfo info = new ContextInfo(context.getPath());
info.setHostname(host.getName()); // The Bonus Challenge
4b9b3361

Ответ 1

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

Первое наблюдение заключается в том, что если for-loop преобразуется в поток, вложенные for-loops могут быть "сплющены" в один поток с использованием flatMap. Эта операция принимает один элемент и возвращает произвольные числовые элементы в потоке. Я посмотрел вверх и обнаружил, что StandardServer.findServices() возвращает массив Service, поэтому мы превращаем его в поток с помощью Arrays.stream(). (Я делаю подобные предположения для Engine.findChildren() и Host.findChildren().

Затем логика в каждом цикле выполняет проверку instanceof и приведение. Это можно смоделировать с использованием потоков в качестве операции filter для выполнения instanceof, за которой следует операция map, которая просто выдает и возвращает ту же ссылку. На самом деле это no-op, но он позволяет системе статического ввода преобразовать Stream<Container> в Stream<Host>, например.

Применяя эти преобразования к вложенным циклам, мы получаем следующее:

public List<ContextInfo> list() {
    final List<ContextInfo> list = new ArrayList<ContextInfo>();
    final StandardServer server = getServer();

    Arrays.stream(server.findServices())
        .filter(service -> service.getContainer() instanceof Engine)
        .map(service -> (Engine)service.getContainer())
        .flatMap(engine -> Arrays.stream(engine.findChildren()))
        .filter(possibleHost -> possibleHost instanceof Host)
        .map(possibleHost -> (Host)possibleHost)
        .flatMap(host -> Arrays.stream(host.findChildren()))
        .filter(possibleContext -> possibleContext instanceof Context)
        .map(possibleContext -> (Context)possibleContext)
        .forEach(context -> {
            // copy to another object -- not the important part
            final ContextInfo info = new ContextInfo(context.getPath());
            info.setThisPart(context.getThisPart());
            info.setNotImportant(context.getNotImportant());
            list.add(info);
        });
    return list;
}

Но подождите, там еще.

Последняя операция forEach представляет собой несколько более сложную операцию map, которая преобразует a Context в ContextInfo. Кроме того, они просто собираются в List, поэтому мы можем использовать сборщиков, чтобы сделать это, вместо того, чтобы создавать и пустить список вперед, а затем заполнять его. Применение этих рефакторингов приводит к следующему:

public List<ContextInfo> list() {
    final StandardServer server = getServer();

    return Arrays.stream(server.findServices())
        .filter(service -> service.getContainer() instanceof Engine)
        .map(service -> (Engine)service.getContainer())
        .flatMap(engine -> Arrays.stream(engine.findChildren()))
        .filter(possibleHost -> possibleHost instanceof Host)
        .map(possibleHost -> (Host)possibleHost)
        .flatMap(host -> Arrays.stream(host.findChildren()))
        .filter(possibleContext -> possibleContext instanceof Context)
        .map(possibleContext -> (Context)possibleContext)
        .map(context -> {
            // copy to another object -- not the important part
            final ContextInfo info = new ContextInfo(context.getPath());
            info.setThisPart(context.getThisPart());
            info.setNotImportant(context.getNotImportant());
            return info;
        })
        .collect(Collectors.toList());
}

Обычно я стараюсь избегать многострочных lambdas (например, в финальной операции map), поэтому я реорганизую его в небольшой вспомогательный метод, который принимает Context и возвращает ContextInfo. Это не сокращает код вообще, но я думаю, что он делает его более ясным.

UPDATE

Но подождите, еще больше.

Позвольте извлечь вызов service.getContainer() в свой собственный элемент конвейера:

    return Arrays.stream(server.findServices())
        .map(service -> service.getContainer())
        .filter(container -> container instanceof Engine)
        .map(container -> (Engine)container)
        .flatMap(engine -> Arrays.stream(engine.findChildren()))
        // ...

Это предоставляет повторение фильтрации на instanceof, за которым следует сопоставление с литой. Это делается три раза. Похоже, что другому коду нужно будет делать подобные вещи, поэтому было бы неплохо извлечь этот бит логики в вспомогательный метод. Проблема состоит в том, что filter может изменять количество элементов в потоке (отбрасывая те, которые не совпадают), но не может изменять их типы. И map может изменять типы элементов, но не может изменить их число. Может ли что-то изменить как число, так и типы? Да, это наш старый друг flatMap снова! Поэтому наш вспомогательный метод должен взять элемент и вернуть поток элементов другого типа. Этот обратный поток будет содержать один литой элемент (если он совпадает) или он будет пустым (если он не совпадает). Вспомогательная функция будет выглядеть так:

<T,U> Stream<U> toType(T t, Class<U> clazz) {
    if (clazz.isInstance(t)) {
        return Stream.of(clazz.cast(t));
    } else {
        return Stream.empty();
    }
}

(Это свободно основано на конструкции С# OfType, упомянутой в некоторых комментариях.)

Пока мы находимся в нем, дайте возможность извлечь метод для создания ContextInfo:

ContextInfo makeContextInfo(Context context) {
    // copy to another object -- not the important part
    final ContextInfo info = new ContextInfo(context.getPath());
    info.setThisPart(context.getThisPart());
    info.setNotImportant(context.getNotImportant());
    return info;
}

После этих выходов конвейер выглядит следующим образом:

    return Arrays.stream(server.findServices())
        .map(service -> service.getContainer())
        .flatMap(container -> toType(container, Engine.class))
        .flatMap(engine -> Arrays.stream(engine.findChildren()))
        .flatMap(possibleHost -> toType(possibleHost, Host.class))
        .flatMap(host -> Arrays.stream(host.findChildren()))
        .flatMap(possibleContext -> toType(possibleContext, Context.class))
        .map(this::makeContextInfo)
        .collect(Collectors.toList());

Ник, я думаю, и мы удалили страшный многострочный оператор lambda.

ОБНОВЛЕНИЕ: ВЫЗОВ БОНУСА

Еще раз, flatMap - ваш друг. Возьмите хвост потока и перенесите его в последний flatMap перед хвостом. Таким образом, переменная host все еще находится в области видимости, и вы можете передать ее вспомогательному методу makeContextInfo, который был изменен для принятия host.

    return Arrays.stream(server.findServices())
        .map(service -> service.getContainer())
        .flatMap(container -> toType(container, Engine.class))
        .flatMap(engine -> Arrays.stream(engine.findChildren()))
        .flatMap(possibleHost -> toType(possibleHost, Host.class))
        .flatMap(host -> Arrays.stream(host.findChildren())
                               .flatMap(possibleContext -> toType(possibleContext, Context.class))
                               .map(ctx -> makeContextInfo(ctx, host)))
        .collect(Collectors.toList());

Ответ 2

Это будет моя версия вашего кода с использованием потоков JDK 8, ссылок на методы и лямбда-выражения:

server.findServices()
    .stream()
    .map(Service::getContainer)
    .filter(Engine.class::isInstance)
    .map(Engine.class::cast)
    .flatMap(engine -> Arrays.stream(engine.findChildren()))
    .filter(Host.class::isInstance)
    .map(Host.class::cast)
    .flatMap(host -> Arrays.stream(host.findChildren()))
    .filter(Context.class::isInstance)
    .map(Context.class::cast)
    .map(context -> {
        ContextInfo info = new ContextInfo(context.getPath());
        info.setThisPart(context.getThisPart());
        info.setNotImportant(context.getNotImportant());
        return info;
    })
    .collect(Collectors.toList());

В этом подходе я заменяю ваши if-утверждения для предикатов фильтра. Учтите, что проверка instanceof может быть заменена на Predicate<T>

Predicate<Object> isEngine = someObject -> someObject instanceof Engine;

который также может быть выражен как

Predicate<Object> isEngine = Engine.class::isInstance

Аналогично, ваши приведения могут быть заменены на Function<T,R>.

Function<Object,Engine> castToEngine = someObject -> (Engine) someObject;

Это почти то же самое, что и

Function<Object,Engine> castToEngine = Engine.class::cast;

И добавление элементов вручную в список в цикле for может быть заменено коллекционером. В производственном коде лямбда, которая преобразует Context в ContextInfo, может (и должна) быть извлечена в отдельный метод и использована как ссылка метода.

Ответ 3

Решение проблемы с бонусом

Вдохновленный ответом @EdwinDalorzo.

public List<ContextInfo> list() {
    final List<ContextInfo> list = new ArrayList<>();
    final StandardServer server = getServer();

    return server.findServices()
            .stream()
            .map(Service::getContainer)
            .filter(Engine.class::isInstance)
            .map(Engine.class::cast)
            .flatMap(engine -> Arrays.stream(engine.findChildren()))
            .filter(Host.class::isInstance)
            .map(Host.class::cast)
            .flatMap(host -> mapContainers(
                Arrays.stream(host.findChildren()), host.getName())
            )
            .collect(Collectors.toList());
}

private static Stream<ContextInfo> mapContainers(Stream<Container> containers,
    String hostname) {
    return containers
            .filter(Context.class::isInstance)
            .map(Context.class::cast)
            .map(context -> {
                ContextInfo info = new ContextInfo(context.getPath());
                info.setThisPart(context.getThisPart());
                info.setNotImportant(context.getNotImportant());
                info.setHostname(hostname); // The Bonus Challenge
                return info;
            });
}

Ответ 4

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

Обратите внимание, что методы findChildren возвращают массивы, которые, конечно, работают с синтаксисом for (N n: array), но не с новым методом Iterable.forEach. Пришлось обернуть их Arrays.asList

public List<ContextInfo> list() {
    final List<ContextInfo> list = new ArrayList<ContextInfo>();
    final StandardServer server = getServer();

    asList(server.findServices()).forEach(service -> {

        if (!(service.getContainer() instanceof Engine)) return;

        final Engine engine = (Engine) service.getContainer();

        instanceOf(Host.class, asList(engine.findChildren())).forEach(host -> {

            instanceOf(Context.class, asList(host.findChildren())).forEach(context -> {

                // copy to another object -- not the important part
                final ContextInfo info = new ContextInfo(context.getPath());
                info.setThisPart(context.getThisPart());
                info.setNotImportant(context.getNotImportant());
                list.add(info);
            });
        });
    });

    return list;
}

Полезные методы

public static <T> Iterable<T> instanceOf(final Class<T> type, final Collection collection) {
    final Iterator iterator = collection.iterator();
    return () -> new SlambdaIterator<>(() -> {
        while (iterator.hasNext()) {
            final Object object = iterator.next();
            if (object != null && type.isAssignableFrom(object.getClass())) {
                return (T) object;
            }
        }
        throw new NoSuchElementException();
    });
}

И, наконец, реализуемая Lambda реализация Iterable

public static class SlambdaIterator<T> implements Iterator<T> {
    // Ya put your Lambdas in there
    public static interface Advancer<T> {
        T advance() throws NoSuchElementException;
    }
    private final Advancer<T> advancer;
    private T next;

    protected SlambdaIterator(final Advancer<T> advancer) {
        this.advancer = advancer;
    }

    @Override
    public boolean hasNext() {
        if (next != null) return true;

        try {
            next = advancer.advance();

            return next != null;
        } catch (final NoSuchElementException e) {
            return false;
        }
    }

    @Override
    public T next() {
        if (!hasNext()) throw new NoSuchElementException();

        final T v = next;
        next = null;
        return v;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

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