Java stream apply different logic to last element in a single pass

Viewed 80

I have a stream of data from database using Spring Data Jpa that needs to be Json serialized and write to a Http response, without storing in memory. This is the sample code.

try (Stream<Employee> dataStream = empRepo.findAllStream()) {
        response.setHeader("content-type", "application/json");
        PrintWriter respWriter = response.getWriter();
        respWriter.write("[");     // array begin
        dataStream.forEach(data -> {
            try {
                respWriter.write(jsonSerialize(data));
                respWriter.write(",");
            } catch (JsonProcessingException e) {
                log(e);
            }
            entityManager.detach(data);
        });
        respWriter.write("]");    // array end
        respWriter.flush();
    } catch (IOException e) {
        log(e);
    }
}

But this logic will write an extra comma after the last element. How can I not to do respWriter.write(",");, if it is the last element?

There are solutions with stream operators - peek, reduce etc, but what's the most optimized solution? Is there something like Stream.hasNext() so that I can use an if condition inside forEach?

2 Answers

First I'd like to say that I don't think that your problem is a good fit for a single pipeline stream. You are performing side effects both with the write call and the detach call. Maybe you are better of with a normal for-loop? Or using multiple streams instead?

That being said, you can use the technique that Eran describes in an answer to this question: Interleave elements in a stream with separator

try (Stream<Employee> dataStream = empRepo.findAllStream()) {
    response.setHeader("content-type", "application/json");
    PrintWriter respWriter = response.getWriter();
    respWriter.write("[");     // array begin
    dataStream.map(data -> {
        try {
            String json = jsonSerialize(data);

            // NOTE! It is confusing to have side effects like this in a stream!
            entityManager.detach(data);
            return json;
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    })
    .flatMap(json -> Stream.of(",", json))
    .skip(1)
    .forEach(respWriter::write);

    respWriter.write("]");    // array end
    respWriter.flush();
} catch (IOException e) {
    log(e);
}

For this specific scenario, you can use Collectors.joining

printWriter.write(dataStream
   .map(this::deserializeJson)
   .peek(entityManager::detach)
   .collect(Collectors.joining(","));

However since you are performing side-effects which are discouraged in streams, and since you specifically asked about a hasNext() operation, and since this streaming solution would build a large string in memory, you might instead prefer converting the stream to an iterator and using an imperative loop:

Iterator<Employee> it = dataStream.iterator();
while(it.hasNext()) {
  Employee data = it.next();
  ...
  // skip writing delimiter on last entry
  if (it.hasNext()){
    respWriter.write(",")
  }
}
Related