web-dev-qa-db-ja.com

ラムダ、複数のキャスト付きforEach

私の仲間の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としてクライアントに送られることに注意してください。そのため、返されるものに注目しないでください。ループを削減できるいくつかのきちんとした方法でなければなりません。

私の仲間の専門家が作成するものを見て興味があります。複数のアプローチが推奨されます。

[〜#〜] edit [〜#〜]

findServicesおよび2つのfindChildrenメソッドは配列を返します

編集-ボーナスチャレンジ

「重要ではない部分」が重要であることが判明しました。実際には、Hostインスタンスでのみ使用可能な値をコピーする必要があります。これは、すべての美しい例を台無しにしているようです。国家をどのように進めますか?

final ContextInfo info = new ContextInfo(context.getPath());
info.setHostname(Host.getName()); // The Bonus Challenge
27
David Blevins

かなり深くネストされていますが、例外的に難しくはないようです。

最初の観察は、forループがストリームに変換される場合、flatMapを使用してネストされたforループを単一のストリームに「フラット化」できることです。この操作は単一の要素を取り、ストリーム内の任意の数の要素を返します。調べてみると、StandardServer.findServices()Serviceの配列を返すので、Arrays.stream()を使用してこれをストリームに変換します。 (Engine.findChildren()Host.findChildren()についても同様の仮定をしています。

次に、各ループ内のロジックはinstanceofチェックとキャストを実行します。これは、ストリームを使用してfilter操作としてモデル化でき、instanceofの後に、同じ参照をキャストして返すだけのmap操作が続きます。これは実際には何もしませんが、静的型付けシステムが例えば_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操作は、mapContextに変換する少し複雑な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());
}
_

私は通常、複数行のラムダ(最後の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のフィルタリングと、それに続くキャストによるマッピングが繰り返されます。これは合計3回行われます。他のコードも同様のことをする必要があるようですので、このビットのロジックをヘルパーメソッドに抽出すると良いでしょう。問題は、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();
    }
}
_

(これは、コメントのいくつかで言及されているC#の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());
_
36
Stuart Marks

これは、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ループのリストに手動で項目を追加することは、コレクターに置き換えることができます。実動コードでは、ContextContextInfoに変換するラムダを別のメソッドに抽出でき(またそうすべきです)、メソッド参照として使用します。

26
Edwin Dalorzo

ボーナスチャレンジの解決策

@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;
            });
}
2
user11153

い以上の最初の試み。これが判読できるようになるまでには何年もかかるでしょう。より良い方法でなければなりません。

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();
    });
}

そして最後に、IterableのLambda対応の実装

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倍。より良い方法でなければなりません。

1
David Blevins