Warunkowo usuń pierwszy element (z indeksem zerowym) ze strumienia

10

Mam następujący kod:

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

Jeśli ciąg pięści jest równy "email", chcę usunąć pierwszy ciąg ze strumienia. W przypadku innych ciągów ze strumienia nie potrzebuję tego sprawdzania.

Jak mogę to osiągnąć?

PS

Jasne, że mogę przekształcić go w listę, a następnie użyć starej pętli dla starej szkoły, ale dalej potrzebuję strumienia ponownie.

gstackoverflow
źródło
3
A jeśli drugim elementem jest „e-mail”, nie chcesz go upuścić?
michalk
@michalk masz rację
gstackoverflow
Hmm ... jest tak, skip(long n)że pomija pierwsze nelementy, ale nie wiem, czy można to jakoś uwarunkować ...
deHaar,
4
@gstackoverflow, jeśli jest mało prawdopodobne, aby pierwsze dwa ciągi w strumieniu były równe „email”, tak myślę, jeśli chodzi o nagłówek pliku csv, możesz użyćstream.dropWhile(s -> s.equals("email"));
Erytrei
1
@Eritrean po prostu to opublikuje;)
YCF_L,

Odpowiedzi:

6

Podczas gdy czytnik będzie w nieokreślonym stanie po zbudowaniu z niego strumienia linii, jest w dobrze zdefiniowanym stanie, zanim to zrobisz.

Więc możesz to zrobić

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

Co jest moim zdaniem najczystszym rozwiązaniem. Zauważ, że nie jest to to samo co Java 9 dropWhile, które spadłyby więcej niż jedną linię, jeśli się zgadzają.

Holger
źródło
Zgadzam się - to rozwiązanie lepiej, ale dropWhile - to drugie miejsce. Niestety autor postanowił nie publikować tego pomysłu jako oddzielnej odpowiedzi
gstackoverflow
3

Jeśli nie możesz mieć listy i musisz to zrobić tylko za pomocą a Stream, możesz to zrobić za pomocą zmiennej.

Chodzi o to, że można użyć zmiennej tylko wtedy, gdy jest ona „ostateczna” lub „efektywnie ostateczna”, więc nie można użyć literalnej wartości logicznej. Nadal możesz to zrobić za pomocą 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);

Uwaga: Nie lubię używać „zmiennych zewnętrznych”, Streamponieważ skutki uboczne są złą praktyką w paradygmacie programowania funkcjonalnego. Lepsze opcje są mile widziane.

Arnaud Denoyelle
źródło
używanie tego compareAndSetjest bardzo eleganckim sposobem ustawiania i zdobywania flagi w tym samym czasie, miło.
f1sh
Mógłby to zrobić stream.filter(!first.compareAndSet(true, false) || !s.equals("email"))choćby dyskusyjne.
Joop Eggen,
1
predicatemusi być bezpaństwowcem
ZhekaKozlov
3
Jeśli użycie AtomicBooleantutaj służy do obsługi równoległych strumieni (jak compareAndSetsugeruje użycie ), jest to całkowicie błędne, ponieważ zadziała tylko wtedy, gdy strumień jest sekwencyjny. Jeśli jednak użyjesz tej techniki stream.sequential().filter(...), zgadzam się, że zadziała.
Daniel
3
@ Daniel masz rację, to podejście pokrywa koszty bezpiecznej konstrukcji wątku (dla każdego elementu), nie wspierając przetwarzania równoległego. Powodem, dla którego tak wiele przykładów z filtrami stanowymi używa tych klas, jest to, że nie ma odpowiednika bez bezpieczeństwa wątków i ludzie unikają złożoności tworzenia dedykowanej klasy w swoich odpowiedziach
Holger
1

Aby uniknąć sprawdzania warunku w każdej linii pliku, po prostu czytam i sprawdzam pierwszy wiersz osobno, a następnie uruchamiam potok na pozostałych liniach bez sprawdzania warunku:

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

Prostsze w Javie 9+:

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

Stream<String> lines = Stream.concat(firstLines, reader.lines());
ernest_k
źródło
4
Java 9 jest jeszcze prostsza:Stream.ofNullable(first).filter(not("email"::equals))
Holger
1

Odpowiedź @Arnouds jest prawidłowa. Możesz utworzyć jeden strumień dla pierwszej linii, a następnie porównać jak poniżej,

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
źródło
nawet jeśli użyjesz filtru, zostanie obliczony dla każdej linii. W przypadku dużego pliku może to spowodować wydajność. W przypadku limitu byłby porównywany tylko raz.
Code_Mode
Poza tym, że nie pracuje dla czytelnika, na limit(0)pewno powinien być limit(1)
Holger
Zredagowałem odpowiedź do limitu 0 po testimg.
Code_Mode,
1
Widzę, że to zmieniłeś, ale .limit(0).filter(line -> !line.startsWith("email"))nie ma sensu. Predykat nigdy nie zostanie oceniony. W przypadku źródła strumienia zdolnego do odtwarzania strumieni kombinacja limit(1)pierwszego i skip(1)drugiego będzie poprawna. W przypadku źródła strumienia, takiego jak czytnik, ani limit(1)nie limit(0)będzie działać. Właśnie zmieniłeś, która linia zostanie bezwarunkowo połknięta. Nawet jeśli znajdziesz kombinację, która przyniesie pożądaną czynność, będzie to wynikać z nieokreślonego zachowania zależnego od implementacji.
Holger
0

Aby filtrować elementy na podstawie ich indeksu, możesz użyć AtomicIntegerdo przechowywania i zwiększania indeksu podczas przetwarzania 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]
}

Takie podejście pozwala filtrować elementy według dowolnego indeksu, nie tylko pierwszego.

Evgeniy Khyst
źródło
0

Trochę bardziej skomplikowany, czerpiąc inspirację z tego fragmentu .

Możesz utworzyć plik, Stream<Integer>który będzie reprezentował indeksy i „spakować” go, Stream<String>aby utworzyć plikStream<Pair<String, Integer>>

Następnie filtruj za pomocą indeksu i zamapuj go z powrotem na 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
źródło
0

Oto przykład z Collectors.reducing. Ale ostatecznie i tak tworzy listę.

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);
Łczapski
źródło
0

Spróbuj tego:

MutableBoolean isFirst = MutableBoolean.of(true);
lines..dropWhile(e -> isFirst.getAndSet(false) && "email".equals(e))
użytkownik_3380739
źródło