Filtrar o Java Stream para 1 e apenas 1 elemento


230

Estou tentando usar o Java 8 Streams para encontrar elementos em um LinkedList. Quero garantir, no entanto, que haja uma e apenas uma correspondência com os critérios de filtro.

Pegue este código:

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

Este código encontra um com Userbase em seu ID. Mas não há garantias de quantos Users correspondem ao filtro.

Alterando a linha de filtro para:

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

Vai jogar um NoSuchElementException(bom!)

Eu gostaria que isso gerasse um erro se houver várias correspondências. Existe uma maneira de fazer isso?


count()é uma operação do terminal, então você não pode fazer isso. O fluxo não pode ser usado depois.
Alexis C.

Ok, obrigado @ZouZou. Eu não tinha certeza do que esse método fez. Por que não existe Stream::size?
ryvantage

7
@ryvantage Como um fluxo pode ser usado apenas uma vez: calcular seu tamanho significa "iterar" sobre ele e, depois disso, você não pode mais usá-lo.
assylias 27/03

3
Uau. Aquele comentário me ajudou a entender Streams muito mais do que eu fiz antes ...
ryvantage

2
É quando você percebe que precisava usar um LinkedHashSet(assumindo que deseja preservar o pedido de inserção) ou um o tempo HashSettodo. Se sua coleção é usada apenas para encontrar um único ID de usuário, por que você está coletando todos os outros itens? Se houver um potencial em que você sempre precisará encontrar algum ID de usuário que também precise ser exclusivo, por que usar uma lista e não um conjunto? Você está programando para trás. Use a coleção certa para o trabalho e salvar a si mesmo essa dor de cabeça
smac89

Respostas:


192

Crie um costume 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);
            }
    );
}

Usamos Collectors.collectingAndThenpara construir o nosso desejado Collectorpor

  1. Coletando nossos objetos em um Listcom o Collectors.toList()coletor.
  2. A aplicação de um finalizador extra no final, que retorna o elemento único - ou gera um IllegalStateExceptionif list.size != 1.

Usado como:

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

Você pode personalizar isso o Collectorquanto quiser, por exemplo, dar a exceção como argumento no construtor, ajustá-lo para permitir dois valores e mais.

Uma solução alternativa - sem dúvida menos elegante -:

Você pode usar uma 'solução alternativa' que envolva peek()e uma AtomicInteger, mas realmente não deveria estar usando isso.

O que você poderia fazer com istead é apenas coletá-lo em um List, assim:

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
As goiabas Iterables.getOnlyElementencurtariam essas soluções e forneceriam melhores mensagens de erro. Apenas uma dica para colegas leitores que já usam o Google Guava.
Tim Büthe

2
Enrolei essa idéia se em uma classe - gist.github.com/denov/a7eac36a3cda041f8afeabcef09d16fc
Denov

1
@LonelyNeuron Por favor, não edite meu código. Isso me coloca em uma situação em que preciso validar toda a minha resposta, que escrevi há quatro anos, e simplesmente não tenho tempo para isso no momento.
Skiwi 17/05/19

2
@skiwi: A edição da Lonely foi útil e correta, então reinstalei após a revisão. As pessoas que visitam esta resposta hoje não se preocupam com a resposta, não precisam ver a versão antiga, a nova versão e uma seção Atualizada . Isso torna sua resposta mais confusa e menos útil. É muito melhor colocar as postagens em um estado final e, se as pessoas quiserem ver como tudo aconteceu, poderão ver o histórico das postagens.
Martijn Pieters

1
@skiwi: O código na resposta é absolutamente o que você escreveu. Tudo o que o editor fez foi limpar sua postagem, removendo apenas uma versão anterior da singletonCollector()definição obsoleta pela versão que permanece na postagem e renomeando-a para toSingleton(). Minha experiência em fluxo Java está um pouco enferrujada, mas a renomeação parece útil para mim. Analisar essa alteração levou 2 minutos, no máximo. Se você não tiver tempo para revisar edições, posso sugerir que você peça a alguém para fazer isso no futuro, talvez na sala de bate-papo Java ?
Martijn Pieters

118

Por uma questão de exaustividade, eis o 'one-liner' correspondente à excelente resposta do @ prunge:

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

Isso obtém o único elemento correspondente do fluxo, lançando

  • NoSuchElementException caso o fluxo esteja vazio ou
  • IllegalStateException caso o fluxo contenha mais de um elemento correspondente.

Uma variação dessa abordagem evita lançar uma exceção antecipadamente e, em vez disso, representa o resultado como Optionalcontendo o elemento único ou nada (vazio) se houver zero ou vários elementos:

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

3
Eu gosto da abordagem inicial nesta resposta. Para fins de personalização, é possível converter o último get()paraorElseThrow()
arin

1
Gosto da brevidade deste e do fato de evitar criar uma instância List desnecessária toda vez que for chamada.
LordOfThePigs

83

As outras respostas que envolvem a escrita de um costume Collectorsão provavelmente mais eficientes (como o de Louis Wasserman , +1), mas se você quiser concisão, sugiro o seguinte:

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

Em seguida, verifique o tamanho da lista de resultados.

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

5
Qual é o objetivo limit(2)desta solução? Que diferença faria se a lista resultante fosse 2 ou 100? Se for maior que 1.
ryvantage 28/03

18
Para imediatamente se encontrar uma segunda correspondência. É isso que todos os colecionadores elegantes fazem, apenas usando mais código. :-)
Stuart Marks

10
Que tal adicionarCollectors.collectingAndThen(toList(), l -> { if (l.size() == 1) return l.get(0); throw new RuntimeException(); })
Lukas Eder

1
Javadoc diz param deste limite sobre: maxSize: the number of elements the stream should be limited to. Então, não deveria ser em .limit(1)vez de .limit(2)?
Alexbt

5
@alexbt A declaração do problema é garantir que exista exatamente um (nem mais, nem menos) elemento correspondente. Após o meu código, é possível testar result.size()para garantir que seja igual a 1. Se for 2, haverá mais de uma correspondência, portanto, é um erro. Se o código o fizesse limit(1), mais de uma correspondência resultaria em um único elemento, que não pode ser diferenciado de haver exatamente uma correspondência. Isso perderia um caso de erro com o qual o OP estava preocupado.
Stuart Marks

67

A goiaba fornece o MoreCollectors.onlyElement()que faz a coisa certa aqui. Mas se você tiver que fazer isso sozinho, poderá fazer o seu próprio Collectorpara isso:

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

... ou usando seu próprio Holdertipo em vez de AtomicReference. Você pode reutilizar isso o Collectorquanto quiser.


O singletonCollector de @ skiwi era menor e mais fácil de seguir do que isso, foi por isso que dei o cheque a ele. Mas é bom ver consenso na resposta: um costume Collectorera o caminho a seguir.
ryvantage

1
Justo. Eu estava principalmente buscando velocidade, não concisão.
Louis Wasserman

1
Sim? Por que o seu é mais rápido?
ryvantage

3
Principalmente porque alocar um all-up Listé mais caro que uma única referência mutável.
Louis Wasserman

1
@LouisWasserman, a última frase de atualização sobre MoreCollectors.onlyElement()deve realmente ser a primeira (e talvez a única :))
Piotr Findeisen

46

Use goiabas MoreCollectors.onlyElement()( JavaDoc ).

Ele faz o que você deseja e lança um IllegalArgumentExceptionse o fluxo consistir em dois ou mais elementos e um NoSuchElementExceptionse o fluxo estiver vazio.

Uso:

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

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

2
Nota para outros usuários: MoreCollectorsfaz parte do ainda inéditos (em 2016-12) unreleased versão 21.
qerub

2
Esta resposta deve ser superior.
Emdadul Sawon

31

A operação "escape hatch" que permite fazer coisas estranhas que não são suportadas por fluxos é solicitar um 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();
}

A goiaba possui um método de conveniência para Iteratorobter e obter o único elemento, lançando se houver zero ou vários elementos, o que poderia substituir as linhas n-1 inferiores aqui.


4
Método da goiaba: Iterators.getOnlyElement (Iterator <T> iterador).
ANRE

23

Atualizar

Boa sugestão no comentário de @Holger:

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

Resposta original

A exceção é lançada Optional#get, mas se você tiver mais de um elemento que não ajudará. Você pode coletar os usuários em uma coleção que aceita apenas um item, por exemplo:

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

que lança um java.lang.IllegalStateException: Queue full, mas parece muito hacky.

Ou você pode usar uma redução combinada com uma opcional:

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

A redução retorna essencialmente:

  • null se nenhum usuário for encontrado
  • o usuário se apenas um for encontrado
  • lança uma exceção se mais de um for encontrado

O resultado é então envolvido em um opcional.

Mas a solução mais simples provavelmente seria coletar apenas uma coleção, verificar se o tamanho é 1 e obter o único elemento.


1
Eu adicionaria um elemento de identidade ( null) para impedir o uso get(). Infelizmente, o seu reducenão está funcionando como você pensa, considere um Streamque contenha nullelementos, talvez você pense que o cobriu, mas eu posso estar [User#1, null, User#2, null, User#3], agora não lançará uma exceção, a menos que eu esteja enganado aqui.
skiwi

2
@Skiwi Se houver elementos nulos, o filtro lançará um NPE primeiro.
assylias 27/03

2
Como você sabe que o fluxo não pode passar nullpara a função de redução, remover o argumento do valor da identidade tornaria obsoleto todo o trato nullna função: reduce( (u,v) -> { throw new IllegalStateException("More than one ID found"); } )faz o trabalho e, melhor ainda, ele já retorna um Optional, eliminando a necessidade de chamar Optional.ofNullableo resultado.
Holger

15

Uma alternativa é usar a redução: (este exemplo usa seqüências de caracteres, mas pode ser facilmente aplicado a qualquer tipo de objeto, inclusive 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);};
}

Então, para o caso com Uservocê, você teria:

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

8

Usando reduzir

Esta é a maneira mais simples e flexível que encontrei (com base na resposta @prunge)

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

Dessa forma, você obtém:

  • o opcional - como sempre com seu objeto ou Optional.empty()se não estiver presente
  • a exceção (eventualmente com SEU tipo / mensagem personalizada) se houver mais de um elemento

6

Eu acho que dessa maneira é mais simples:

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

4
É encontrar apenas o primeiro, mas o caso foi também para lançar uma exceção quando se é mais do que um
lczapski

5

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

Uso:

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

Retornamos um Optional, já que geralmente não podemos assumir Collectionque ele contenha exatamente um elemento. Se você já sabe que é esse o caso, ligue para:

User user = result.orElseThrow();

Isso coloca o ônus de tratar o erro no chamador - como deveria.



1

Podemos usar RxJava ( biblioteca de extensão reativa muito poderosa )

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

O operador único lança uma exceção se nenhum usuário ou mais de um usuário for encontrado.


Resposta correta: inicializar um fluxo ou coleção de bloqueio provavelmente não é muito barato (em termos de recursos).
19618 Karl Richter

1

Como Collectors.toMap(keyMapper, valueMapper)usa uma fusão de lançamento para lidar com várias entradas com a mesma chave, é fácil:

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

Você receberá um IllegalStateExceptionpara chaves duplicadas. Mas no final não tenho certeza se o código não seria ainda mais legível usando um if.


1
Solução fina! E se o fizer .collect(Collectors.toMap(user -> "", Function.identity())).get(""), você tem um comportamento mais genérico.
glglgl

1

Estou usando esses dois colecionadores:

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

Arrumado! onlyOne()lança IllegalStateExceptionpara> 1 elementos e NoSuchElementException` (in Optional::get) para 0 elementos.
precisa saber é

@ simon04 Você poderia sobrecarregar os métodos para tirar um Supplierde (Runtime)Exception.
Xavier Dury

1

Se você não se importa em usar uma biblioteca de terceiros, tanto SequenceMdo cyclops-streams (como LazyFutureStreamdo simple- react), ambos têm operadores únicos e únicos opcionais.

singleOptional()lança uma exceção se houver 0ou mais de 1elementos no Stream, caso contrário, ele retornará o valor único.

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()retorna Optional.empty()se não houver valores ou mais de um valor no 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

Divulgação - sou o autor de ambas as bibliotecas.


0

Eu fui com a abordagem direta e apenas implementei a coisa:

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


} 

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

}

Esta implementação não é segura.


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

5
Embora esse código possa resolver a questão, incluir uma explicação de como e por que isso resolve o problema realmente ajudaria a melhorar a qualidade da sua postagem e provavelmente resultaria em mais votos positivos. Lembre-se de que você está respondendo à pergunta dos leitores no futuro, não apenas à pessoa que está perguntando agora. Edite sua resposta para adicionar explicações e dar uma indicação de quais limitações e suposições se aplicam.
David Buck

-2

Você já tentou isso?

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.

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


3
Foi dito que count()não é bom usar porque é uma operação terminal.
ryvantage

Se esta é realmente uma citação, adicione suas fontes
Neuron
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.