Programming

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

procodes 2020. 5. 9. 17:07
반응형

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


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(좋은!)

그래도 여러 개의 일치 항목이 있으면 오류가 발생하도록하고 싶습니다. 이것을 할 수있는 방법이 있습니까?


맞춤 만들기 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);

완전성을 위해 다음은 @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));

사용자 정의 작성과 관련된 다른 대답 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);
}

구아바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만큼 재사용 할 수 있습니다 .


구아바 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());

스트림에서 지원하지 않는 이상한 작업을 수행 할 수있는 "이스케이프 해치"작업은 다음을 요청하는 것입니다 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 줄을 바꿀 수 있습니다.


최신 정보

@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인지 확인하고 유일한 요소를 얻는 것입니다.


대안은 축소를 사용하는 것입니다 (이 예에서는 문자열을 사용하지만을 포함하여 모든 객체 유형에 쉽게 적용 할 수 있음 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();

사용하여 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();

이로 인해 발신자에게 오류를 처리해야하는 부담이 발생합니다.


구아바는 있다 Collector라고 이것을 MoreCollectors.onlyElement().


감소 사용

이것은 내가 찾은 더 간단하고 유연한 방법입니다 (@prunge 답변 기반)

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

이렇게하면 얻을 수 있습니다 :

  • 옵션-항상 개체와 함께 또는 Optional.empty()없는 경우
  • 하나 이상의 요소가있는 경우 예외 (결국 사용자 정의 유형 / 메시지 포함)

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

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


으로 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.


I am using those two collectors:

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

If you don't mind using a 3rd party library, SequenceM from cyclops-streams (and LazyFutureStream from simple-react) both a have single & singleOptional operators.

singleOptional() throws an exception if there are 0 or more than 1 elements in the Stream, otherwise it returns the single value.

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() returns Optional.empty() if there are no values or more than one value in the 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

Disclosure - I am the author of both libraries.


I think this way is more simple:

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

I went with the direct-approach and just implemented the thing:

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


} 

with the JUnit test:

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

}

This implementation not threadsafe.


Have you tried this

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.

Source: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html

참고URL : https://stackoverflow.com/questions/22694884/filter-java-stream-to-1-and-only-1-element

반응형