web-dev-qa-db-ja.com

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

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

このコードは、それらのIDに基づいてUserを見つけます。しかし、何個のUserがフィルタにマッチしたかについての保証はありません。

フィルタ行を次のように変更します。

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

NoSuchElementExceptionをスローします(いいね!)

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

182
ryvantage

カスタム 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. Collectors.toList()コレクターを使ってListにオブジェクトを集める。
  2. 最後に追加のフィニッシャを適用すると、単一の要素が返されます - またはlist.size != 1の場合はIllegalStateExceptionがスローされます。

使用されます:

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

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

代替案 - おそらくそれほどエレガントではない - 解決策:

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

その代わりにできることは、単に次のように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);
148
skiwi

完全を期すために、@ 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));
91
glts

カスタム 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);
}
78
Stuart Marks

Guava はここで正しいことをする 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を再利用することができます。

51
Louis Wasserman

Guavaの MoreCollectors.onlyElement()JavaDoc )を使ってください。

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

使用法:

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

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

ストリームではサポートされていない奇妙なことをできるようにする "escape hatch"操作は、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行を置き換えることができます。

28
Brian Goetz

更新

@ 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であることを確認し、唯一の要素を取得することです。

19
assylias

(この例では文字列を使用していますが、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();
11
prunge

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

そうすることで、これは呼び出し側にエラーを煩わすという負担をかけます。

5
Lonely Neuron

Guava には MoreCollectors.onlyElement() と呼ばれるCollectorがあります。

4
Hans

サードパーティのライブラリを使用しても構わない場合は、 cyclops-streams から SequenceM (および LazyFutureStream from simple-react )は両方ともsingle&singleOptional演算子を持ちます。

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

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

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

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

開示 - 私は両方の図書館の著者です。

1
John McClean

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
Arne Burmeister

Reduceを使う

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

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

このようにあなたが得る:

  • optional - あなたのオブジェクトといつものように。存在しない場合はOptional.empty()
  • 複数の要素がある場合はException(最終的にはあなたのカスタムタイプ/メッセージ付き)
1
Fabio Bonfante

私はこれら二つのコレクターを使っています:

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
Xavier Dury

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人以上のユーザーが見つかった場合に例外をスローします。

1
frhack

私は直接アプローチで行って、ただ事を実行しました:

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
gerardw