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.