Search code examples
javafunctionlambdajava-8consumer

Multiple Java Consumers for an Action


Is there a way I can have the following action actually be two actions in one?

static int process(String[] cmd, File dir, Consumer<? super String> action) throws IOException {
    ProcessBuilder pb = new ProcessBuilder(cmd);
    pb.directory(dir);
    pb.redirectErrorStream(true);
    Stopwatch sw = Stopwatch.createStarted();
    Process p = pb.start();
    int exit = -1;
    try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
        br.lines().forEach(action);
    } finally {
        log.debug("Ending process {} with exist code {} in time {}", pb.command(),
                exit = p.destroyForcibly().exitValue(), sw);
    }
    return exit;
}

So the Consumer<? super String> action I typically specify it to be log::debug or a custom function like AtomicReference::set however what if I wanted to be both, or a third? How can I have the action perform a few independent actions?

I know I could just create a custom Consumer that does whatever I want, but I think it would be very convenient if I could almost treat the action like an nvarargs equivalent, but for functions/actions.


Solution

  • There is a few alternatives here, some of which have already been mention by others.

    Perhaps the most lambda like approach would be to use the consumers own aggregation method andThen (defined in Consumer itself):

    Stream<X> exes = ....
    exes.forEach(action1
        .andThen(action2)
        .andThen(action3)
        .andThen(...)
        .andThen(actionN));
    

    Where all action? are declared as Consumer<X> or Consumer<? super X>.

    Without looking at the docs to confirm. I guess that actions 1 to N are executed one right after the other on the same thread for every element in the stream.

    Another possibility here is to use peek that basically passes the elements of the stream to an action without consuming them as it returns a stream that would contain the same elements.

    Stream<X> exes = ...
    exes.peek(action1)
     .peek(action2)
     .peek(action3)
     .peek(...)
     .forEach(actionN);
    

    Without looking at the docs I bet that:

    • you need to actually call to a final consuming action like forEach (or count, empty, etc.) in order to get the different peeks executed.
    • that the only restriction in execution order is that action-i will go before action-j for any given element in the stream as long as i < j. No other assumption can be made unless we know something else about the stream (is it parallel/serial, sorted, etc.).

    I reckon that you may want to use Consumer.andThen based on the content of your question however peek seems like a plausible solution if there is some action whose execution is not central/main to the task at hand but rather like a desirable side-effect.

    Perhaps you want to pass a modified stream that would "spy" on what objects are being processed later by the code that the stream reference has been passed to. In that case peek is the better if not the only option.

    Of course you could also do a combination of both.