Remover primeiro elemento (com índice zero) do fluxo condicionalmente

10

Eu tenho o seguinte código:

Stream<String> lines = reader.lines();

Se a primeira corda for igual "email", quero remover a primeira do fluxo. Para outras seqüências de caracteres do fluxo, não preciso dessa verificação.

Como eu consegui isso?

PS

Claro que posso transformá-lo na lista e, em seguida, usar o loop for old school, mas ainda preciso transmitir novamente.

gstackoverflow
fonte
3
E se o segundo elemento for "email", você não deseja descartá-lo?
Michalk
@michalk você está correto
gstackoverflow
Hmm ... há skip(long n)que ignora os primeiros nelementos, mas eu não sei se você pode condicioná-lo de alguma forma ...
deHaar
4
@gstackoverflow se for improvável que as duas primeiras cordas no fluxo igual a "e-mail" que eu acho que é o caso se você está falando de cabeçalho do arquivo CSV, você pode usarstream.dropWhile(s -> s.equals("email"));
Eritreia
11
@Eritrean ela só vai publicá-lo;)
YCF_L

Respostas:

6

Embora o leitor esteja em um estado não especificado depois que você construiu um fluxo de linhas a partir dele, ele está em um estado bem definido antes de você fazer isso.

Então você pode fazer

String firstLine = reader.readLine();
Stream<String> lines = reader.lines();
if(firstLine != null && !"email".equals(firstLine))
    lines = Stream.concat(Stream.of(firstLine), lines);

Qual é a solução mais limpa na minha opinião. Observe que isso não é o mesmo que o Java 9 dropWhile, que eliminaria mais de uma linha se corresponderem.

Holger
fonte
Concordo - esta solução melhor, mas dropWhile - é o segundo lugar. Infelizmente autor decidiu não publicar essa idéia como uma resposta separada
gstackoverflow
3

Se você não pode ter a lista e deve fazê-lo apenas com a Stream, pode fazê-lo com uma variável.

O problema é que você só pode usar uma variável se for "final" ou "efetivamente final", para que você não possa usar um booleano literal. Você ainda pode fazer isso com um AtomicBoolean:

Stream<String> stream  = Arrays.asList("test", "email", "foo").stream();

AtomicBoolean first = new AtomicBoolean(true);
stream.filter(s -> {
    if (first.compareAndSet(true, false)) {
        return !s.equals("email");
    }
    return true;
})
// Then here, do whatever you need
.forEach(System.out::println);

Nota: Não gosto de usar "variáveis ​​externas" Streamporque os efeitos colaterais são uma má prática no paradigma de programação funcional. Melhores opções são bem-vindas.

Arnaud Denoyelle
fonte
usando o compareAndSeté uma maneira muito elegante de definir e receber a bandeira ao mesmo tempo, legal.
F1sh 25/11/19
Poderia fazer stream.filter(!first.compareAndSet(true, false) || !s.equals("email"))embora discutível.
Joop Eggen
11
predicatedeve ser apátrida
ZhekaKozlov 25/11/19
3
Se o uso AtomicBooleanaqui for para atender fluxos paralelos (como compareAndSetsugere o uso de ), isso estará completamente errado, pois só funcionará se o fluxo for seqüencial. Se você usar a técnica no stream.sequential().filter(...)entanto, eu concordo que funcionará.
Daniel
3
@daniel, você está certo, essa abordagem está pagando as despesas de uma construção segura de thread (para cada elemento) sem oferecer suporte ao processamento paralelo. A razão pela qual tantos exemplos com filtros de estado usam essas classes é que não há equivalente sem segurança de encadeamento e as pessoas evitam a complexidade de criar uma classe dedicada em suas respostas ...
Holger
1

Para evitar verificar a condição em cada linha do arquivo, basta ler e verificar a primeira linha separadamente e, em seguida, executar o pipeline no restante das linhas sem verificar a condição:

String first = reader.readLine();
Stream<String> firstLines = Optional.of(first)
        .filter(s -> !"email".equals(s))
        .map(s -> Stream.of(s))
        .orElseGet(() -> Stream.empty());

Stream<String> lines = Stream.concat(firstLines, reader.lines());

Mais simples em Java 9+:

Stream<String> firstLines = Optional.of(first)
        .filter(s -> !"email".equals(s))
        .stream();

Stream<String> lines = Stream.concat(firstLines, reader.lines());
ernest_k
fonte
4
Java 9 é ainda mais simples:Stream.ofNullable(first).filter(not("email"::equals))
Holger
1

A resposta do @Arnouds está correta. Você pode criar um fluxo para a primeira linha e comparar como abaixo,

Stream<String> firstLineStream = reader.lines().limit(1).filter(line -> !line.startsWith("email"));;
Stream<String> remainingLinesStream = reader.lines().skip(1);
Stream.concat(firstLineStream, remainingLinesStream);
Code_Mode
fonte
mesmo se você usar um filtro, ele será calculado para cada linha. No caso de um arquivo grande, pode causar desempenho. Em caso de limite, seria comparado apenas uma vez.
Code_Mode 25/11/19
Além de não trabalhar por um leitor, limit(0)certamente deve ser limit(1)...
Holger
Eu editei a resposta para limitar 0 após o depoimento.
Code_Mode 25/11/19
11
Vejo que você mudou, mas .limit(0).filter(line -> !line.startsWith("email"))não faz sentido. O predicado nunca será avaliado. Para uma fonte de fluxo capaz de reproduzir fluxos, a combinação de limit(1)no primeiro e skip(1)no segundo estaria correta. Para uma fonte de fluxo como um leitor, limit(1)nem limit(0)funcionará. Você acabou de alterar qual linha é engolida incondicionalmente. Mesmo se você encontrar uma combinação que faz a coisa desejada, ela seria baseada no comportamento não especificado e dependente da implementação.
Holger
0

Para filtrar elementos com base em seu índice, você pode usar AtomicIntegerpara armazenar e incrementar o índice ao processar um Stream:

private static void filter(Stream<String> stream) {
  AtomicInteger index = new AtomicInteger();
  List<String> result = stream
      .filter(el -> {
        int i = index.getAndIncrement();
        return i > 0 || (i == 0 && !"email".equals(el));
      })
      .collect(toList());
  System.out.println(result);
}

public static void main(String[] args) {
  filter(Stream.of("email", "test1", "test2", "test3")); 
  //[test1, test2, test3]

  filter(Stream.of("test1", "email", "test2", "test3")); 
  //[test1, email, test2, test3]

  filter(Stream.of("test1", "test2", "test3")); 
  //[test1, test2, test3]
}

Essa abordagem permite filtrar elementos em qualquer índice, não apenas no primeiro.

Evgeniy Khyst
fonte
0

Um pouco mais complicado, obtendo alguma inspiração deste trecho .

Você pode criar um Stream<Integer>que represente índices e "compactá-lo" com o seu Stream<String>para criar umStream<Pair<String, Integer>>

Em seguida, filtre usando o índice e mapeie-o de volta para um Stream<String>

public static void main(String[] args) {
    Stream<String> s = reader.lines();
    Stream<Integer> indexes = Stream.iterate(0, i -> i + 1);

    zip(s, indexes)
        .filter(pair -> !(pair.getKey().equals("email") && pair.getValue() == 0))
        .map(Pair::getKey)
        .forEach(System.out::println);
}

private static Stream<Pair<String,Integer>> zip(Stream<String> stringStream, Stream<Integer> indexesStream){
    Iterable<Pair<String,Integer>> iterable = () -> new ZippedWithIndexIterator(stringStream.iterator(), indexesStream.iterator());
    return StreamSupport.stream(iterable.spliterator(), false);
}

static class ZippedWithIndexIterator implements Iterator<Pair<String, Integer>> {
    private final Iterator<String> stringIterator;
    private final Iterator<Integer> integerIterator;

    ZippedWithIndexIterator(Iterator<String> stringIterator, Iterator<Integer> integerIterator) {
        this.stringIterator = stringIterator;
        this.integerIterator = integerIterator;
    }
    @Override
    public Pair<String, Integer> next() {
        return new Pair<>(stringIterator.next(), integerIterator.next());
    }
    @Override
    public boolean hasNext() {
        return stringIterator.hasNext() && integerIterator.hasNext();
    }
}
Bentaye
fonte
0

Aqui está um exemplo com Collectors.reducing. Mas no final cria uma lista de qualquer maneira.

Stream<String> lines = Arrays.asList("email", "aaa", "bbb", "ccc")
        .stream();

List reduceList = (List) lines
        .collect(Collectors.reducing( new ArrayList<String>(), (a, v) -> {
                    List list = (List) a;
                    if (!(list.isEmpty() && v.equals("email"))) {
                        list.add(v);
                    }
                    return a;
                }));

reduceList.forEach(System.out::println);
lczapski
fonte
0

Tente o seguinte:

MutableBoolean isFirst = MutableBoolean.of(true);
lines..dropWhile(e -> isFirst.getAndSet(false) && "email".equals(e))
user_3380739
fonte