6

Given the following Java 8 Stream:

scheduleService.list().stream()
                      .filter(Schedule::getEnabled)
                      .filter(this::runnable)
                      .flatMap(s -> s.getJobs().stream())
                      // .doSomethingArbitrary(System.out.println("A single message. The total number of 
                      // elements in the stream after filtering is " + this::count))
                      .forEach(this::invoke);

After the filtering has been applied to the stream and after the first terminal operation has been applied, I would like to either log a debug message if the stream is empty or if it isn't, call the invoke method on each element in the stream. Is this possible?

Yassin Hajaj
  • 21,337
  • 9
  • 51
  • 89
crmepham
  • 4,676
  • 19
  • 80
  • 155
  • 1
    Could you explain what do you mean by *after the first terminal operation has been applied*? There is only one terminal operation in the above stream i.e. `forEach`. How about a Java7 code corresponding to what your desired task is? Would make things much clear imho. – Naman Jul 10 '19 at 19:20
  • @Naman I have modified my example code. Does that make it clearer? – crmepham Jul 11 '19 at 10:11
  • 1
    @crmepham no it does not. what we are confused of is this _after the first terminal operation_ - since there is a single terminal operation... It seems to me that what you are trying to explain here is: if the Stream is empty _after_ that `filter` - you want to log a message; otherwise you want to process all jobs from `s.getJobs().stream() ` by calling the invoke method; if so simply: – Eugene Jul 11 '19 at 12:26
  • 2
    `List list = scheduleService.list() .stream() .filter(Schedule::getEnabled) .filter(this::runnable) .flatMap(s -> s.getJobs().stream()) .collect(Collectors.toList()); if(list.isEmpty()) {log that message;} else {process via invoke}` – Eugene Jul 11 '19 at 12:26
  • 1
    Totally agree with @Eugene. That's the cleanest solution. No matter what else you try, it won't be as clean as what he suggested. – Pateman Apr 26 '22 at 12:04

4 Answers4

1

You can create a custom Collector (here called StreamInterceptor), even though this does not really fit the purpose of a collector.


What will the custom collector do?

  1. Convert the Stream<T> to a List<T>
  2. Call the Consumer<List>, which will in your case print the length of the list.
  3. Return a new Stream<T> of the List<T>

Main method

Here I've just broken down your problem into the filtering of a simple string list and printing them to the console at the end.

    public static void main(String[] args) {
        List<String> myList = List.of("first", "second", "third");
        myList.stream()
                .filter(string -> !string.equals("second"))
                .collect(printCount())
                .forEach(System.out::println);
    }

    /**
     * Creates a StreamInterceptor, which will print the length of the stream 
     */
    private static <T> StreamInterceptor<T> printCount() {
        Consumer<List<T>> listSizePrinter = list -> System.out.println("Stream has " + list.size() + " elements");
        return new StreamInterceptor<>(listSizePrinter);
    }

When initializing the StreamInterceptor you can define a Consumer, that takes in the intermediate list constructed from the stream and performs some action on it. In your case it will just print the size of the list.

New StreamInterceptor class

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.*;
import java.util.stream.Collector;
import java.util.stream.Stream;

class StreamInterceptor<T> implements Collector<T, List<T>, Stream<T>> {

    private final Consumer<List<T>> listConsumer;

    public StreamInterceptor(Consumer<List<T>> listConsumer) {
        this.listConsumer = listConsumer;
    }

    @Override
    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return List::add;
    }

    @Override
    public BinaryOperator<List<T>> combiner() {
        return (list1, list2) -> {
            list1.addAll(list2);
            return list1;
        };
    }

    @Override
    public Function<List<T>, Stream<T>> finisher() {
        return list -> {
            listConsumer.accept(list);
            return list.stream();
        };
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }
}

Resources

Florian Hartung
  • 134
  • 1
  • 6
0

You could wrap your Stream into a custom method like the following

Stream<???> stream = scheduleService.list().stream()
                                           .filter(Schedule::getEnabled)
                                           .filter(this::runnable)
                                           .flatMap(s -> s.getJobs().stream());

forEachOrElse(stream, this::invoke, () -> System.out.println("The stream was empty"));

With forEachOrElse being

public <T> void forEachOrElse(Stream<T> inStream, Consumer<T> consumer, Runnable orElse) {
    AtomicBoolean wasEmpty = new AtomicBoolean(true);
    inStream.forEach(e -> {
        wasEmpty.set(false);
        consumer.accept(e);
    });

    if (wasEmpty.get())
        orElse.run();
}

I can't test it right now but it should do its magic

Yassin Hajaj
  • 21,337
  • 9
  • 51
  • 89
0

This isn't really "nice" at all, but you could use peek to look into your stream and set an AtomicBoolean:

AtomicBoolean empty = new AtomicBoolean(true);

scheduleService.list().stream()
                      .filter(Schedule::getEnabled)
                      .filter(this::runnable)
                      .flatMap(s -> s.getJobs().stream())
                      .peek(s -> ab.set(false);)
                      .forEach(this::invoke);

if(empty.get()){
   // is Empty
}
Christian
  • 22,585
  • 9
  • 80
  • 106
  • How is this setting the `empty` boolean to true if the stream is empty? – crmepham Jul 12 '19 at 15:55
  • 1
    The boolean is true at start. but if any job exists, it will set it to false and then if the boolean is set to true, the log will happen. – Christian Jul 12 '19 at 15:57
0

You can add a peek before converting the list to stream.

public static void main(String[] args) {
    Stream.of(Arrays.asList(1, 2, 3), Arrays.asList(4, 5), Collections.emptyList())
            .filter(x -> x.size() % 2 == 0)
            .peek(s -> System.out.println(s.isEmpty()))
            .flatMap(Collection::stream)
            .forEach(System.out::println);
}

Output

false
4
5
true
Butiri Dan
  • 1,759
  • 5
  • 12
  • 18