Javaストリームを1つだけの要素にフィルターする


230

Java 8を使用Streamしての要素を検索しようとしていますLinkedList。ただし、フィルター条件に一致するのは1つだけであることを保証したいと思います。

このコードを取ります:

public static void main(String[] args) {

    LinkedList<User> users = new LinkedList<>();
    users.add(new User(1, "User1"));
    users.add(new User(2, "User2"));
    users.add(new User(3, "User3"));

    User match = users.stream().filter((user) -> user.getId() == 1).findAny().get();
    System.out.println(match.toString());
}

static class User {

    @Override
    public String toString() {
        return id + " - " + username;
    }

    int id;
    String username;

    public User() {
    }

    public User(int id, String username) {
        this.id = id;
        this.username = username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public int getId() {
        return id;
    }
}

このコードは、UserIDに基づいてを検索します。ただしUser、フィルターに一致したの数は保証されません。

フィルターラインを次のように変更します。

User match = users.stream().filter((user) -> user.getId() < 0).findAny().get();

スローされますNoSuchElementException(良いです!)

ただし、複数の一致がある場合はエラーをスローします。これを行う方法はありますか?


count()端末操作なので、それはできません。ストリームは後で使用できません。
Alexis C.

わかりました、@ ZouZouに感謝します。私はその方法が何をするか完全に確信していませんでした。なぜないのStream::sizeですか?
ryvantage 2014年

7
@ryvantageストリームは1回しか使用できないため、そのサイズを計算することは、そのストリームを「繰り返す」ことを意味し、その後、ストリームを使用できなくなります。
アッシリア14年

3
ワオ。その1つのコメントStreamは、以前よりもずっと理解を深めるのに役立ちました...
ryvantage 2014年

2
これは、LinkedHashSet(挿入順序を保持したい場合)またはHashSetずっと使用する必要があったことに気付いたときです。コレクションが単一のユーザーIDの検索にのみ使用されている場合、なぜ他のすべてのアイテムを収集するのですか?一意である必要があるユーザーIDを常に見つける必要がある可能性がある場合、なぜセットではなくリストを使用するのですか?逆方向にプログラミングしています。仕事に適したコレクションを使用して、この頭痛の
種を省いて

回答:


192

カスタムを作成する Collector

public static <T> Collector<T, ?, T> toSingleton() {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            list -> {
                if (list.size() != 1) {
                    throw new IllegalStateException();
                }
                return list.get(0);
            }
    );
}

私たちは、使用Collectors.collectingAndThen私たちは希望構築するCollectorことにより、

  1. で私たちのオブジェクトを収集ListしてCollectors.toList()コレクタ。
  2. 最後に追加のフィニッシャーを適用すると、単一の要素が返されます。または、IllegalStateExceptionifがスローされlist.size != 1ます。

使用されます:

User resultUser = users.stream()
        .filter(user -> user.getId() > 0)
        .collect(toSingleton());

その後Collector、必要に応じてこれをカスタマイズできます。たとえば、コンストラクターで引数として例外を指定したり、2つの値を許可するように微調整したりできます。

代替—おそらくエレガントさが劣る—ソリューション:

peek()とを含む「回避策」を使用できますAtomicIntegerが、実際には使用しないでください。

isteadでできることはList、次のように単にに収集することです。

LinkedList<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));
List<User> resultUserList = users.stream()
        .filter(user -> user.getId() == 1)
        .collect(Collectors.toList());
if (resultUserList.size() != 1) {
    throw new IllegalStateException();
}
User resultUser = resultUserList.get(0);

24
Guava Iterables.getOnlyElementはこれらのソリューションを短縮し、より適切なエラーメッセージを提供します。すでにGoogle Guavaを使用している他の読者のためのヒントとして。
TimBüthe、2015年

2
私はこのアイデアをクラスにまとめました-gist.github.com/denov/a7eac36a3cda041f8afeabcef09d16fc
denov

1
@LonelyNeuron私のコードは編集しないでください。それは、4年前に書いた私の答え全体を検証する必要がある状況に私を置きます。
スキーウィ

2
@skiwi:Lonelyの編集は有用で正しかったので、レビュー後にそれを元に戻しました。今日この回答にアクセスした人は、あなたが回答にたどり着いた方法を気にしません。古いバージョンと新しいバージョン、および更新されたセクションを見る必要はありません。それはあなたの答えをより混乱させ、あまり役に立ちません。投稿を最終的な状態にすることをお勧めします。すべてがどのように行われたかを確認したい場合は、投稿の履歴を表示できます。
Martijn Pieters

1
@skiwi:答えのコードがある絶対にあなたが書かれているもの。エディターが行ったのは、投稿をクリーンアップすることだけでした。投稿にsingletonCollector()残っているバージョンによって廃止された以前のバージョンの定義のみを削除し、名前をに変更しましたtoSingleton()。私のJavaストリームの専門知識は少し錆びていますが、名前の変更は私に役立つようです。この変更を確認するには、2分かかりました。編集をレビューする時間がない場合は、おそらくJavaチャットルームで、他の誰かにこれを行うよう依頼することをお勧めできますか?
Martijn Pieters

118

完全を期すために、@ prungeの優れた回答に対応する「ワンライナー」を以下に示します。

User user1 = users.stream()
        .filter(user -> user.getId() == 1)
        .reduce((a, b) -> {
            throw new IllegalStateException("Multiple elements: " + a + ", " + b);
        })
        .get();

これは、ストリームから唯一の一致する要素を取得し、

  • NoSuchElementException ストリームが空の場合、または
  • IllegalStateException ストリームに複数の一致する要素が含まれている場合。

このアプローチのバリエーションは、例外の早期スローを回避し、代わりにOptional単一の要素を含むか、要素がゼロまたは複数の場合は何もない(空)として結果を表します。

Optional<User> user1 = users.stream()
        .filter(user -> user.getId() == 1)
        .collect(Collectors.reducing((a, b) -> null));

3
この回答の最初のアプローチが気に入っています。カスタマイズのために、最後に変換することが可能であるget()orElseThrow()
ARIN

1
私はこれが簡潔であること、そして呼び出されるたびに不要なListインスタンスが作成されないことを気に入っています。
LordOfThePigs 2018年

83

カスタムの作成に関連する他の回答のCollector方がおそらく効率的です(Louis Wassermanの +1など)。

List<User> result = users.stream()
    .filter(user -> user.getId() == 1)
    .limit(2)
    .collect(Collectors.toList());

次に、結果リストのサイズを確認します。

if (result.size() != 1) {
  throw new IllegalStateException("Expected exactly one user but got " + result);
User user = result.get(0);
}

5
limit(2)このソリューションのポイントは何ですか?結果のリストが2であるか100であるかによって、どのような違いがありますか?1より大きい場合
ryvantage 2014年

18
2番目の一致が見つかるとすぐに停止します。これは、より多くのコードを使用するだけで、すべてのファンシーコレクターが行うことです。:-)
スチュアートマークス

10
追加はどうですかCollectors.collectingAndThen(toList(), l -> { if (l.size() == 1) return l.get(0); throw new RuntimeException(); })
Lukas Eder

1
Javadocは、limitのパラメーターについてこれを述べています:maxSize: the number of elements the stream should be limited to。では、.limit(1)代わりにすべきではない.limit(2)でしょうか?
alexbt

5
@alexbt問題ステートメントは、一致する要素が正確に1つ(これ以上、それ以下)ないことを確認することです。私のコードの後で、1であることresult.size()を確認するためにテストできます。2の場合、複数の一致があるため、エラーになります。代わりにコードがそうした場合limit(1)、複数の一致が単一の要素になり、厳密に1つの一致があることと区別できません。これは、OPが懸念していたエラーケースを見逃してしまいます。
スチュアートマーク、

67

グアバが提供MoreCollectors.onlyElement()するのは、ここで正しいことです。しかし、自分で行う必要がある場合は、これを自分で行うことができますCollector

<E> Collector<E, ?, Optional<E>> getOnly() {
  return Collector.of(
    AtomicReference::new,
    (ref, e) -> {
      if (!ref.compareAndSet(null, e)) {
         throw new IllegalArgumentException("Multiple values");
      }
    },
    (ref1, ref2) -> {
      if (ref1.get() == null) {
        return ref2;
      } else if (ref2.get() != null) {
        throw new IllegalArgumentException("Multiple values");
      } else {
        return ref1;
      }
    },
    ref -> Optional.ofNullable(ref.get()),
    Collector.Characteristics.UNORDERED);
}

...またはのHolder代わりに独自のタイプを使用しますAtomicReference。好きなだけ再利用できCollectorます。


@skiwiのsingletonCollectorはこれよりも小さく、追跡が容易だったので、私は彼にチェックを与えました。しかし、答えでコンセンサスを見るのは良いことです:習慣Collectorは行くべき道でした。
ryvantage 2014年

1
けっこうだ。私は、簡潔さではなく、主にスピードを目指していました。
Louis Wasserman 2014年

1
うん?なぜあなたの方が速いのですか?
ryvantage 2014年

3
全部を割り当てるのListは、単一の変更可能な参照よりもコストがかかるためです。
Louis Wasserman 2014年

1
@LouisWasserman、最後の更新文MoreCollectors.onlyElement()は実際には最初(そしておそらく唯一:)でなければなりません)
Piotr Findeisen 2017

46

Guava MoreCollectors.onlyElement()JavaDoc)を使用します。

IllegalArgumentExceptionストリームが2つ以上の要素で構成されているNoSuchElementException場合は必要な処理を実行し、ストリームが空の場合はをスローします。

使用法:

import static com.google.common.collect.MoreCollectors.onlyElement;

User match =
    users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());

2
他のユーザーのための注:MoreCollectors(2016から12までのような)、まだ未発表未発表バージョン21の一部である
qerub

2
この答えは高くなるはずです。
Emdadul Sawon 2017

31

ストリームでサポートされていない奇妙なことを実行できる「エスケープハッチ」操作は、を要求することですIterator

Iterator<T> it = users.stream().filter((user) -> user.getId() < 0).iterator();
if (!it.hasNext()) 
    throw new NoSuchElementException();
else {
    result = it.next();
    if (it.hasNext())
        throw new TooManyElementsException();
}

Guavaには、Iteratorを取得して唯一の要素を取得する便利なメソッドがあり、ゼロまたは複数の要素がある場合にスローします。これにより、ここで下のn-1行を置き換えることができます。


4
Guavaのメソッド:Iterators.getOnlyElement(Iterator <T> iterator)。
2016

23

更新

@Holgerからのコメントで素晴らしい提案:

Optional<User> match = users.stream()
              .filter((user) -> user.getId() > 1)
              .reduce((u, v) -> { throw new IllegalStateException("More than one ID found") });

元の答え

例外はによってスローされOptional#getますが、役に立たない要素が複数ある場合。たとえば、1つのアイテムのみを受け入れるコレクションにユーザーを収集できます。

User match = users.stream().filter((user) -> user.getId() > 1)
                  .collect(toCollection(() -> new ArrayBlockingQueue<User>(1)))
                  .poll();

これはをスローしますがjava.lang.IllegalStateException: Queue full、ハックしすぎると感じます。

または、オプションと組み合わせて削減を使用することもできます。

User match = Optional.ofNullable(users.stream().filter((user) -> user.getId() > 1)
                .reduce(null, (u, v) -> {
                    if (u != null && v != null)
                        throw new IllegalStateException("More than one ID found");
                    else return u == null ? v : u;
                })).get();

削減は基本的に次の結果を返します。

  • ユーザーが見つからない場合はnull
  • 1つしか見つからない場合のユーザー
  • 複数が見つかると例外をスローします

結果はオプションでラップされます。

しかし、最も簡単な解決策は、おそらくコレクションに収集し、そのサイズが1であることを確認して、唯一の要素を取得することです。


1
null使用を防ぐために、identity要素()を追加しget()ます。悲しいことに、あなたreduceが思っているように機能していない、Streamそれがnull要素を持っていると考えてください、おそらくあなたはそれをカバーしたと思うかもしれませんが、私はそうすることができます[User#1, null, User#2, null, User#3]、私がここで間違っていない限り、私は思う例外を投げません
skiwi 2014年

2
@Skiwi null要素がある場合、フィルターは最初にNPEをスローします。
アッシリアス14年

2
あなたは、ストリームを渡すことができないことを知っているので、nullと全体取引をレンダリングするでしょうアイデンティティ値の引数を削除し、リダクション機能にnull廃止された機能で:reduce( (u,v) -> { throw new IllegalStateException("More than one ID found"); } )仕事をしても良く、それはすでに返しOptional呼び出すための必要性をeliding、Optional.ofNullable上結果。
Holger、2016年

15

別の方法は、簡約を使用することです(この例では文字列を使用していますが、を含むすべてのオブジェクトタイプに簡単に適用できますUser)。

List<String> list = ImmutableList.of("one", "two", "three", "four", "five", "two");
String match = list.stream().filter("two"::equals).reduce(thereCanBeOnlyOne()).get();
//throws NoSuchElementException if there are no matching elements - "zero"
//throws RuntimeException if duplicates are found - "two"
//otherwise returns the match - "one"
...

//Reduction operator that throws RuntimeException if there are duplicates
private static <T> BinaryOperator<T> thereCanBeOnlyOne()
{
    return (a, b) -> {throw new RuntimeException("Duplicate elements found: " + a + " and " + b);};
}

したがって、Userあなたとの場合のために:

User match = users.stream().filter((user) -> user.getId() < 0).reduce(thereCanBeOnlyOne()).get();

8

削減の使用

これは私が見つけたより簡単で柔軟な方法です(@prungeの回答に基づく)

Optional<User> user = users.stream()
        .filter(user -> user.getId() == 1)
        .reduce((a, b) -> {
            throw new IllegalStateException("Multiple elements: " + a + ", " + b);
        })

このようにして、以下を取得します。

  • オプション-常にオブジェクトと同じように、またはOptional.empty()存在しない場合
  • 要素が複数ある場合の例外(最終的にはあなたのカスタムタイプ/メッセージ)

6

この方法はもっと簡単だと思います:

User resultUser = users.stream()
    .filter(user -> user.getId() > 0)
    .findFirst().get();

4
最初にしか見つかりませんが、1つ以上の場合は例外をスローすることもありました
lczapski

5

を使用するCollector

public static <T> Collector<T, ?, Optional<T>> toSingleton() {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.size() == 1 ? Optional.of(list.get(0)) : Optional.empty()
    );
}

使用法:

Optional<User> result = users.stream()
        .filter((user) -> user.getId() < 0)
        .collect(toSingleton());

Optionalは通常、Collectionに要素が1つだけ含まれているとは想定できないため、を返します。これが事実であることをすでに知っている場合は、次の番号に電話してください。

User user = result.orElseThrow();

これにより、呼び出し元にエラーを処理する負担がかかります-必要に応じて。



1

RxJava(非常に強力なリアクティブ拡張ライブラリ)を使用できます

LinkedList<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));

User userFound =  Observable.from(users)
                  .filter((user) -> user.getId() == 1)
                  .single().toBlocking().first();

単一の オペレータがユーザーまたは複数次いで、一人のユーザが見つからない場合に例外をスロー。


正解ですが、ブロッキングストリームまたはコレクションの初期化は、(リソースの観点から)おそらくそれほど安くはありません。
カールリヒター

1

Collectors.toMap(keyMapper, valueMapper)同じキーを持つ複数のエントリを処理するために使用する投げ合併それは簡単です:

List<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));

int id = 1;
User match = Optional.ofNullable(users.stream()
  .filter(user -> user.getId() == id)
  .collect(Collectors.toMap(User::getId, Function.identity()))
  .get(id)).get();

IllegalStateException重複キーのを取得します。しかし、最終的には、を使用してコードをさらに読みやすくするかどうかはわかりませんif


1
ファインソリューション!そして、あなたがそうするなら.collect(Collectors.toMap(user -> "", Function.identity())).get("")、あなたはより一般的な行動をします。
glglgl 2017年

1

私はこれら2つのコレクターを使用しています。

public static <T> Collector<T, ?, Optional<T>> zeroOrOne() {
    return Collectors.reducing((a, b) -> {
        throw new IllegalStateException("More than one value was returned");
    });
}

public static <T> Collector<T, ?, T> onlyOne() {
    return Collectors.collectingAndThen(zeroOrOne(), Optional::get);
}

きちんと!> 1要素の場合onlyOne()はスローIllegalStateExceptionOptional::get、0要素の場合はNoSuchElementException`()をスローします。
simon04 2018年

@ simon04あなたは取るメソッドをオーバーロードする可能性Supplierのを(Runtime)Exception
Xavier Dury

1

サードパーティのライブラリを使用しても構わない場合はSequenceMcyclops-streamsLazyFutureStreamから(およびsimple-reactから)、両方に単一および単一のオプションの演算子があります。

singleOptional()に要素がある0か、それ以上の場合は例外をスローします。それ以外の場合は、単一の値を返します。1Stream

String result = SequenceM.of("x")
                          .single();

SequenceM.of().single(); // NoSuchElementException

SequenceM.of(1, 2, 3).single(); // NoSuchElementException

String result = LazyFutureStream.fromStream(Stream.of("x"))
                          .single();

singleOptional()Optional.empty()値がない場合、または複数の値がある場合に返されますStream

Optional<String> result = SequenceM.fromStream(Stream.of("x"))
                          .singleOptional(); 
//Optional["x"]

Optional<String> result = SequenceM.of().singleOptional(); 
// Optional.empty

Optional<String> result =  SequenceM.of(1, 2, 3).singleOptional(); 
// Optional.empty

開示-私は両方のライブラリの著者です。


0

私は直接的なアプローチで行き、それを実装しました:

public class CollectSingle<T> implements Collector<T, T, T>, BiConsumer<T, T>, Function<T, T>, Supplier<T> {
T value;

@Override
public Supplier<T> supplier() {
    return this;
}

@Override
public BiConsumer<T, T> accumulator() {
    return this;
}

@Override
public BinaryOperator<T> combiner() {
    return null;
}

@Override
public Function<T, T> finisher() {
    return this;
}

@Override
public Set<Characteristics> characteristics() {
    return Collections.emptySet();
}

@Override //accumulator
public void accept(T ignore, T nvalue) {
    if (value != null) {
        throw new UnsupportedOperationException("Collect single only supports single element, "
                + value + " and " + nvalue + " found.");
    }
    value = nvalue;
}

@Override //supplier
public T get() {
    value = null; //reset for reuse
    return value;
}

@Override //finisher
public T apply(T t) {
    return value;
}


} 

JUnitテスト:

public class CollectSingleTest {

@Test
public void collectOne( ) {
    List<Integer> lst = new ArrayList<>();
    lst.add(7);
    Integer o = lst.stream().collect( new CollectSingle<>());
    System.out.println(o);
}

@Test(expected = UnsupportedOperationException.class)
public void failOnTwo( ) {
    List<Integer> lst = new ArrayList<>();
    lst.add(7);
    lst.add(8);
    Integer o = lst.stream().collect( new CollectSingle<>());
}

}

この実装スレッドセーフではありません


0
User match = users.stream().filter((user) -> user.getId()== 1).findAny().orElseThrow(()-> new IllegalArgumentException());

5
このコードは問題を解決する可能性がありますが、これが問題を解決する方法と理由の説明含めると、投稿の品質が向上し、おそらくより多くの投票が得られます。あなたが今尋ねている人だけでなく、あなたが将来の読者のための質問に答えていることを忘れないでください。回答を編集して説明を追加し、適用される制限と前提を示してください。
David Buck

-2

これを試しましたか

long c = users.stream().filter((user) -> user.getId() == 1).count();
if(c > 1){
    throw new IllegalStateException();
}

long count()
Returns the count of elements in this stream. This is a special case of a reduction and is equivalent to:

     return mapToLong(e -> 1L).sum();

This is a terminal operation.

ソース:https : //docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html


3
count()端末操作なので使い勝手が悪いとのことでした。
ryvantage 2014年

これが本当に引用である場合は、ソースを追加してください
Neuron
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.