Java 스트림을 1 및 1 요소로 필터링


229

Java 8을 사용 Stream하여의 요소를 찾으려고합니다 LinkedList. 그러나 필터 기준과 일치하는 항목이 하나만 있음을 보증하고 싶습니다.

이 코드를 보자 :

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

7
@ryvantage 스트림은 한 번만 사용할 수 있기 때문에, 크기를 계산한다는 것은 "반복"을 의미하고 그 이후에는 더 이상 스트림을 사용할 수 없습니다.
— assylias

3
와. 그 한 가지 의견은 내가 Stream전보다 훨씬 더 많은 것을 이해하도록 도와주었습니다 .
— ryvantage

2
이것은 LinkedHashSet(삽입 순서를 유지하려는 가정) 또는 HashSet모든 것을 사용해야한다는 것을 알았을 때 입니다. 컬렉션이 단일 사용자 ID를 찾기 위해서만 사용된다면 왜 다른 모든 항목을 수집합니까? 고유해야 할 사용자 ID를 항상 찾아야 할 가능성이있는 경우 왜 세트가 아닌 목록을 사용합니까? 뒤로 프로그래밍하고 있습니다. 작업에 적합한 모음을 사용하여 자신이 두통 저장
— smac89

답변:


190

맞춤 만들기 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예를 들어, 예외를 생성자에서 인수로 지정하고 두 값을 허용하도록 조정하십시오.

대체로 우아하지 않은 대안 :

당신은 관련된 '해결'사용 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);

23
구아바 Iterables.getOnlyElement는 이러한 솔루션을 단축하고 더 나은 오류 메시지를 제공합니다. 이미 Google Guava를 사용하는 동료 독자를위한 팁입니다.
— Tim Büthe


1
@LonelyNeuron 내 코드를 편집하지 마십시오. 4 년 전에 작성한 전체 답변의 유효성을 검사 해야하는 상황에 빠졌으며 지금은 그럴 시간이 없습니다.
— skiwi

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단독 요소를 포함하거나 0 또는 다중 요소가있는 경우 아무것도 없음 (빈)으로 결과를 나타냅니다 .

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

3
이 답변의 초기 접근 방식이 마음에 듭니다. 커스터마이징 목적으로, 마지막 get()을orElseThrow()
— arin

1
나는 이것의 간결함을 좋아하며, 호출 될 때마다 불필요한 List 인스턴스를 생성하지 않는다는 사실을 좋아한다.
— LordOfThePigs

83

사용자 정의 작성과 관련된 다른 대답 Collector은 아마도 더 효율적일 것입니다 (예 : Louis Wasserman 's , +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

18
두 번째 일치를 찾으면 즉시 중지됩니다. 이것은 더 많은 코드를 사용하여 모든 멋진 수집가가하는 일입니다. :-)
— Stuart Marks

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과 result.size()같은지 테스트 할 수 있습니다 . 2이면 일치하는 것이 두 개 이상이므로 오류입니다. 코드가 대신을 수행 limit(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

1
그럴 수 있지. 나는 간결함이 아니라 속도를 목표로하고있었습니다.
— Louis Wasserman

1
네? 왜 더 빠른가요?
— ryvantage

3
대체로 all-up을 할당하는 List것이 단일 가변 참조보다 비싸기 때문입니다.
— Louis Wasserman

1
@LouisWasserman, 최종 업데이트 문장 MoreCollectors.onlyElement()은 실제로 첫 번째 (그리고 아마도 유일한 것)해야합니다
— Piotr Findeisen

46

구아바 MoreCollectors.onlyElement()( JavaDoc )를 사용하십시오 .

IllegalArgumentException스트림이 둘 이상의 요소로 구성되어 NoSuchElementException있고 스트림이 비어 있으면 원하는 것을 수행하고 throw합니다 .

용법:

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

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

구아바는 Iterator유일한 요소 를 가져 와서 유일한 요소 를 가져 와서 0 개 또는 여러 개의 요소가있는 경우 던지는데 여기에서 맨 아래 n-1 줄을 바꿀 수 있습니다.


4
구아바의 방법 : Iterators.getOnlyElement (Iterator <T> iterator).
— anre

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하지만 도움이되지 않는 요소가 두 개 이상인 경우. 하나의 항목 만 허용하는 컬렉션의 사용자를 수집 할 수 있습니다 (예 :

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
null사용하지 못하도록 identity 요소 ( )를 추가합니다 get(). 슬프게도 당신 reduce이 생각하는대로 작동하지 않습니다. 요소 Stream가 있는 것을 고려 null하십시오. 어쩌면 당신이 그것을 덮었다 고 생각할 수도 있습니다. 그러나 내가 할 수는 있습니다 [User#1, null, User#2, null, User#3].
— skiwi

2
@Skiwi null 요소가 있으면 필터가 먼저 NPE를 발생시킵니다.
— assylias

2
당신이 스트림이 통과 할 수 있다는 것을 알고 있기 때문에 null로 전체 거래를 렌더링 할 ID 값 인수를 제거, 감소 기능에 null사용되지 않는 기능의 : reduce( (u,v) -> { throw new IllegalStateException("More than one ID found"); } )작업을 수행하고 더 나은, 그것은 이미를 반환 Optional호출하기위한 필요성을 eliding, Optional.ofNullable온 결과.
— Holger

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
처음에는 발견되었지만 두 개 이상일 때 예외를 처리하는 것이
— 었습니다

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정확히 하나의 요소를 포함 한다고 가정 할 수 없으므로를 반환합니다 . 이 경우에 대해 이미 알고 있다면 다음으로 전화하십시오.

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

단일 작업자는 어떠한 사용자 또는이 두 개 이상의 사용자가 발견되지 않는 경우 예외를 던진다.


정답, 블로킹 스트림 또는 수집을 초기화하는 것은 아마도 리소스 측면에서 그리 저렴하지는 않습니다.
— Karl Richter

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

1

나는 그 두 수집기를 사용하고 있습니다 :

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

산뜻한! onlyOne()슬로우 IllegalStateException1 개 요소>에 대해, 그리고 NoSuchElementException` (의 Optional::get0 원소).
— simon04

simon04 @ 당신은 취할 수있는 방법에 과부하 수 Supplier의를 (Runtime)Exception.
— Xavier Dury

1

당신이 제 3 자 라이브러리를 사용 괜찮다면 SequenceM에서 외눈 박이 스트림 (과 LazyFutureStream에서 단순 반응 ) 모두 단일 및 singleOptional 연산자를 가지고있다.

singleOptional()에 요소 0이상이 있으면 예외를 throw 1하고 Stream, 그렇지 않으면 단일 값을 반환합니다.

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

이것이 실제로 인용문이라면, 출처를 추가하십시오
— Neuron
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.