web-dev-qa-db-ja.com

Java 8でフィルタリングを動的に行う方法は?

Java 8で、次のようにフィルタリングを行うことができます。

List<User> olderUsers = users.stream().filter(u -> u.age > 30).collect(Collectors.toList());

しかし、コレクションと半ダースのフィルタリング基準があり、基準の組み合わせをテストしたい場合はどうでしょうか?

たとえば、オブジェクトのコレクションと次の基準があります。

<1> Size
<2> Weight
<3> Length
<4> Top 50% by a certain order
<5> Top 20% by a another certain ratio
<6> True or false by yet another criteria

そして、次のような上記の基準の組み合わせをテストしたい:

<1> -> <2> -> <3> -> <4> -> <5>
<1> -> <2> -> <3> -> <5> -> <4>
<1> -> <2> -> <5> -> <4> -> <3>
...
<1> -> <5> -> <3> -> <4> -> <2>
<3> -> <2> -> <1> -> <4> -> <5>
...
<5> -> <4> -> <3> -> <3> -> <1>

テストの順序ごとに異なる結果が得られる場合、すべての組み合わせを自動的にフィルター処理するループを作成する方法は?

私が考えることができるのは、次のようなテスト順序を生成する別の方法を使用することです:

int[][] getTestOrder(int criteriaCount)
{
 ...
}

So if the criteriaCount is 2, it will return : {{1,2},{2,1}}
If the criteriaCount is 3, it will return : {{1,2,3},{1,3,2},{2,1,3},{2,3,1},{3,1,2},{3,2,1}}
...

しかし、次に、Java 8?

36
Frank

興味深い問題。ここでいくつかのことが行われています。 HaskellまたはLISPの半ページ未満でこれを解決できることは間違いありませんが、これはJavaなので、ここに行きます。

1つの問題は、可変数のフィルターがあることです。一方、示されている例のほとんどは固定パイプラインを示しています。

もう1つの問題は、OPの「フィルター」の一部が「特定の順序で上位50%」などのコンテキスト依存であることです。これは、ストリーム上の単純なfilter(predicate)コンストラクトでは実行できません。

重要なのは、ラムダを使用すると関数を引数として渡すことができるということです(良い効果のため)。また、データ構造に格納して計算を実行できることも意味します。最も一般的な計算は、複数の関数を取得して構成することです。

操作対象の値がWidgetのインスタンスであると想定します。Widgetは、いくつかの明らかなゲッターを持つPOJOです。

_class Widget {
    String name() { ... }
    int length() { ... }
    double weight() { ... }

    // constructors, fields, toString(), etc.
}
_

最初の問題から始めて、さまざまな数の単純な述語を操作する方法を考えてみましょう。次のような述語のリストを作成できます。

_List<Predicate<Widget>> allPredicates = Arrays.asList(
    w -> w.length() >= 10,
    w -> w.weight() > 40.0,
    w -> w.name().compareTo("c") > 0);
_

このリストがあれば、それらを並べ替えたり(順序に依存しないため、おそらく役に立たない)、必要なサブセットを選択できます。すべてを適用したいとしましょう。可変数の述語をストリームにどのように適用しますか? 2つの述語を取り、論理andを使用してそれらを結合し、単一の述語を返すPredicate.and()メソッドがあります。したがって、最初の述語を取得し、それを連続する述語と組み合わせて、すべての複合andである単一の述語を構築するループを作成できます。

_Predicate<Widget> compositePredicate = allPredicates.get(0);
for (int i = 1; i < allPredicates.size(); i++) {
    compositePredicate = compositePredicate.and(allPredicates.get(i));
}
_

これは機能しますが、リストが空の場合は失敗し、現在関数型プログラミングを行っているため、ループ内の変数の変更はdeclasséです。しかし、lo!これは削減です! and演算子のすべての述語を削減して、次のように単一の複合述語を取得できます。

_Predicate<Widget> compositePredicate =
    allPredicates.stream()
                 .reduce(w -> true, Predicate::and);
_

(クレジット: @ venkat_s からこのテクニックを学びました。機会があれば、彼がカンファレンスで講演するのを見に行ってください。彼は素晴らしいです。)

リダクションのID値として_w -> true_を使用していることに注意してください。 (これは、ループのcompositePredicateの初期値として使用することもできます。これにより、長さゼロのリストのケースが修正されます。)

複合述語ができたので、単純に複合述語をウィジェットに適用する短いパイプラインを書き出すことができます。

_widgetList.stream()
          .filter(compositePredicate)
          .forEach(System.out::println);
_

状況依存フィルター

ここで、「コンテキスト依存」フィルターと呼ぶものを考えてみましょう。これは、「特定の順序で上位50%」、たとえば重量で上位50%のウィジェットなどの例で表されます。 「コンテキストセンシティブ」はこれに最適な用語ではありませんが、現時点で私が得ているものであり、この時点までのストリーム内の要素の数に対して相対的であるという点で多少説明的です。

ストリームを使用してこのようなものをどのように実装しますか?誰かが本当に賢い何かを考え出さない限り、出力に最初の要素を出力する前に、最初にどこかで(たとえば、リストで)要素を収集する必要があると思います。これは、パイプラインのsorted()のようなもので、すべての入力要素を読み取ってソートするまで、最初に出力する要素を判別できません。

ストリームを使用して、重量でウィジェットの上位50%を見つける簡単なアプローチは、次のようになります。

_List<Widget> temp =
    list.stream()
        .sorted(comparing(Widget::weight).reversed())
        .collect(toList());
temp.stream()
    .limit((long)(temp.size() * 0.5))
    .forEach(System.out::println);
_

これは複雑ではありませんが、50%の計算でリストのサイズを使用するために、リストに要素を収集して変数に割り当てる必要があるため、少し面倒です。

ただし、これはこの種のフィルタリングの「静的な」表現であるという点で制限されています。述部で行ったように、可変数の要素(他のフィルターまたは基準)を持つストリームにこれをどのようにチェーンしますか?

重要な所見は、このコードは、ストリームの消費とストリームの送信の間に実際の作業を行うということです。たまたまコレクターが中央にありますが、ストリームをフロントにチェーンし、バックエンドからチェーンをチェーンした場合、誰も賢くありません。実際、mapfilterなどの標準ストリームパイプライン操作は、それぞれ入力としてストリームを受け取り、出力としてストリームを発行します。したがって、次のような関数を自分で記述できます。

_Stream<Widget> top50PercentByWeight(Stream<Widget> stream) {
    List<Widget> temp =
        stream.sorted(comparing(Widget::weight).reversed())
              .collect(toList());
    return temp.stream()
               .limit((long)(temp.size() * 0.5));
}
_

同様の例は、最短の3つのウィジェットを見つけることです。

_Stream<Widget> shortestThree(Stream<Widget> stream) {
    return stream.sorted(comparing(Widget::length))
                 .limit(3);
}
_

これで、これらのステートフルフィルターと通常のストリーム操作を組み合わせたものを作成できます。

_shortestThree(
    top50PercentByWeight(
        widgetList.stream()
                  .filter(w -> w.length() >= 10)))
.forEach(System.out::println);
_

これは機能しますが、「裏返し」で逆向きに表示されるため、ちょっとお粗末です。ストリームソースはwidgetListであり、通常の述語を介してストリーミングおよびフィルタリングされます。さて、逆に、上位50%のフィルターが適用され、次に最短の3つのフィルターが適用され、最後にストリーム操作forEachが最後に適用されます。これは機能しますが、読むのはかなり混乱します。そして、それはまだ静的です。本当に必要なのは、元の質問のように、すべての順列を実行するなど、操作できるデータ構造内にこれらの新しいフィルターを配置する方法があることです。

この時点での重要な洞察は、これらの新しい種類のフィルターは実際には単なる関数であり、関数をオブジェクトとして表現し、それらを操作し、データ構造に保存できるJavaの関数型インターフェイスを持っていることです。ある種の引数を取り、同じ型の値を返す機能的インターフェース型はUnaryOperatorです。この場合の引数と戻り値の型は_Stream<Widget>_です。 _this::shortestThree_や_this::top50PercentByWeight_などのメソッド参照を取得する場合、結果のオブジェクトの型は

_UnaryOperator<Stream<Widget>>
_

これらをリストに入れると、そのリストのタイプは

_List<UnaryOperator<Stream<Widget>>>
_

うん!ネストされたジェネリックの3つのレベルは、私には多すぎます。 (しかし Aleksey Shipilev は、ネストされたジェネリックの4つのレベルを使用したコードを一度表示しました。)ジェネリックが多すぎる場合の解決策は、独自の型を定義することです。新しいものの1つを基準と呼びましょう。新しい関数型をUnaryOperatorに関連付けることで得られる価値はほとんどないことがわかります。そのため、定義は次のようになります。

_@FunctionalInterface
public interface Criterion {
    Stream<Widget> apply(Stream<Widget> s);
}
_

これで、次のような基準のリストを作成できます。

_List<Criterion> criteria = Arrays.asList(
    this::shortestThree,
    this::lengthGreaterThan20
);
_

(このリストの使用方法については、以下で説明します。)これは、リストを動的に操作できるようになったため、一歩前進していますが、それでも多少制限があります。まず、通常の述語と組み合わせることはできません。次に、最短の3つなど、多くのハードコードされた値があります。2つまたは4つはどうでしょうか。長さとは異なる基準はどうですか?本当に必要なのは、これらのCriterionオブジェクトを作成する関数です。これはラムダで簡単です。

これにより、コンパレータを指定すると、上位N個のウィジェットを選択する基準が作成されます。

_Criterion topN(Comparator<Widget> cmp, long n) {
    return stream -> stream.sorted(cmp).limit(n);
}
_

これにより、コンパレータを指定すると、ウィジェットの上位pパーセントを選択する基準が作成されます。

_Criterion topPercent(Comparator<Widget> cmp, double pct) {
    return stream -> {
        List<Widget> temp =
            stream.sorted(cmp).collect(toList());
        return temp.stream()
                   .limit((long)(temp.size() * pct));
    };
}
_

そして、これは通常の述語から基準を作成します:

_Criterion fromPredicate(Predicate<Widget> pred) {
    return stream -> stream.filter(pred);
}
_

これで、非常に柔軟な方法で基準を作成し、それらをリストに入れることができます。ここで、それらをサブセット化または置換することができます。

_List<Criterion> criteria = Arrays.asList(
    fromPredicate(w -> w.length() > 10),                    // longer than 10
    topN(comparing(Widget::length), 4L),                    // longest 4
    topPercent(comparing(Widget::weight).reversed(), 0.50)  // heaviest 50%
);
_

Criterionオブジェクトのリストを取得したら、それらすべてを適用する方法を見つける必要があります。ここでも、友人reduceを使用して、それらすべてを単一のCriterionオブジェクトに結合できます。

_Criterion allCriteria =
    criteria.stream()
            .reduce(c -> c, (c1, c2) -> (s -> c2.apply(c1.apply(s))));
_

識別関数_c -> c_は明確ですが、2番目の引数は少し注意が必要です。ストリームsが与えられると、まずCriterion c1を適用し、次にCriterion c2を適用します。これは2つのCriterionオブジェクトc1およびc2を取り、c1およびc2の合成をストリームに適用するラムダを返し、結果のストリームを返します。

すべての基準を作成したので、次のようにウィジェットのストリームに適用できます。

_allCriteria.apply(widgetList.stream())
           .forEach(System.out::println);
_

これはまだ少し裏返しですが、かなりよく制御されています。最も重要なことは、基準を動的に組み合わせる方法である元の質問に対処することです。 Criterionオブジェクトがデータ構造になったら、選択、サブセット化、並べ替え、または必要に応じて任意のオブジェクトを選択できます。また、すべてのオブジェクトを単一の基準に結合し、上記の手法を使用してストリームに適用できます。

関数型プログラミングの達人は、おそらく「彼はちょうど再発明した...!」と言っているでしょう。おそらく本当です。これはおそらくどこかですでに発明されていると思いますが、ラムダ以前はこれらの手法を使用するJavaコードを書くことは実行不可能であったため、Javaにとって新しいものです。

更新2014-04-07

完全に サンプルコード をクリーンアップして投稿しました。

78
Stuart Marks

マップにカウンターを追加して、フィルターの後にある要素の数を知ることができます。渡された同じオブジェクトをカウントして返すメソッドを持つヘルパークラスを作成しました。

class DoNothingButCount<T> {
    AtomicInteger i;
    public DoNothingButCount() {
        i = new AtomicInteger(0);
    }
    public T pass(T p) {
        i.incrementAndGet();
        return p;
    }
}

public void runDemo() {
    List<Person>persons = create(100);
    DoNothingButCount<Person> counter = new DoNothingButCount<>();

    persons.stream().filter(u -> u.size > 12).filter(u -> u.weitght > 12).
            map((p) -> counter.pass(p)).
            sorted((p1, p2) -> p1.age - p2.age).
            collect(Collectors.toList()).stream().
            limit((int) (counter.i.intValue() * 0.5)).
            sorted((p1, p2) -> p2.length - p1.length).
            limit((int) (counter.i.intValue() * 0.5 * 0.2)).forEach((p) -> System.out.println(p));
}

それ以外の場合は制限が初期カウントを使用するため、ストリームをリストに変換し、途中でストリームに戻す必要がありました。すべてが「ハック」ではないが、私が考えることができるすべてです。

マッピングされたクラスの関数を使用して、少し異なる方法でそれを行うことができます:

class DoNothingButCount<T > implements Function<T, T> {
    AtomicInteger i;
    public DoNothingButCount() {
        i = new AtomicInteger(0);
    }
    public T apply(T p) {
        i.incrementAndGet();
        return p;
    }
}

ストリームで変更されるのは次のとおりです。

            map((p) -> counter.pass(p)).

となります:

            map(counter).

2つの例を含む私の完全なテストクラス:

import Java.util.*;
import Java.util.concurrent.atomic.AtomicInteger;
import Java.util.function.Function;
import Java.util.stream.Collectors;

public class Demo2 {
    Random r = new Random();
    class Person {
        public int size, weitght,length, age;
        public Person(int s, int w, int l, int a){
            this.size = s;
            this.weitght = w;
            this.length = l;
            this.age = a;
        }
        public String toString() {
            return "P: "+this.size+", "+this.weitght+", "+this.length+", "+this.age+".";
        }
    }

    public List<Person>create(int size) {
        List<Person>persons = new ArrayList<>();
        while(persons.size()<size) {
            persons.add(new Person(r.nextInt(10)+10, r.nextInt(10)+10, r.nextInt(10)+10,r.nextInt(20)+14));
        }
        return persons;
    }

    class DoNothingButCount<T> {
        AtomicInteger i;
        public DoNothingButCount() {
            i = new AtomicInteger(0);
        }
        public T pass(T p) {
            i.incrementAndGet();
            return p;
        }
    }

    class PDoNothingButCount<T > implements Function<T, T> {
        AtomicInteger i;
        public PDoNothingButCount() {
            i = new AtomicInteger(0);
        }
        public T apply(T p) {
            i.incrementAndGet();
            return p;
        }
    }

    public void runDemo() {
        List<Person>persons = create(100);
        PDoNothingButCount<Person> counter = new PDoNothingButCount<>();

        persons.stream().filter(u -> u.size > 12).filter(u -> u.weitght > 12).
                map(counter).
                sorted((p1, p2) -> p1.age - p2.age).
                collect(Collectors.toList()).stream().
                limit((int) (counter.i.intValue() * 0.5)).
                sorted((p1, p2) -> p2.length - p1.length).
                limit((int) (counter.i.intValue() * 0.5 * 0.2)).forEach((p) -> System.out.println(p));
    }

    public void runDemo2() {
        List<Person>persons = create(100);
        DoNothingButCount<Person> counter = new DoNothingButCount<>();

        persons.stream().filter(u -> u.size > 12).filter(u -> u.weitght > 12).
                map((p) -> counter.pass(p)).
                sorted((p1, p2) -> p1.age - p2.age).
                collect(Collectors.toList()).stream().
                limit((int) (counter.i.intValue() * 0.5)).
                sorted((p1, p2) -> p2.length - p1.length).
                limit((int) (counter.i.intValue() * 0.5 * 0.2)).forEach((p) -> System.out.println(p));
    }

    public static void main(String str[]) {
        Demo2 demo = new Demo2();
        System.out.println("Demo 2:");
        demo.runDemo2();
        System.out.println("Demo 1:");
        demo.runDemo();

    }
}
2
Raul Guiu