Using Streams and StreamSupplier: forEach closes StreamSupplier instead of instance of stream

Tags: , , , ,



I am making a obj file loader for an engine that I’m writing and I am trying to use Streams to load vertex index, uvcoord, and normals from this file. The way I intended to do this was to create a new stream from a stream supplier for each type I want to load.

Right now I am just trying to get the bare minimum, vertex and index data. The problem is I can only get one or the other.

After a lot of testing I boiled my problem down to this

        obj = new BufferedReader(new InputStreamReader(is));
        ss = () -> obj.lines();

        Stream<String> stream2 = ss.get().filter(line -> line.startsWith("v "));
        Stream<String> stream1 = ss.get().filter(line -> line.startsWith("f "));
        stream2.forEach(verts::add);
        stream1.forEach(ind::add);

Here I would only get the output from Stream2 but, If I switch the order of

        stream2.forEach(verts::add);
        stream1.forEach(ind::add);

to

        stream1.forEach(ind::add);
        stream2.forEach(verts::add);

Than I only get the output of stream1

Now, to my understanding these streams should be completely separate and one should not close the other but the forEach closes both streams and I wind up with an empty array for the other.

Answer

Now, to my understanding these streams should be completely separate and one should not close the other but the forEach closes both streams and I wind up with an empty array for the other.

The two Stream objects are indeed independent of each other. The problem is that they are both using the same source, and that source is single-use1. Once you execute forEach on one of the Stream objects it consumes the BufferedReader. By the time you call forEach on the second Stream the BufferedReader has reached the end of its input and has nothing else to give.

You need to either open multiple BufferedReader objects or do all the processing in a single Stream. Here’s an example of the second:

Map<Boolean, List<String>> map;
try (BufferedReader reader = ...) {
  map =
      reader
          .lines()
          .filter(line -> line.startsWith("v ") || line.startsWith("f "))
          .collect(Collectors.partitioningBy(line -> line.startsWith("v ")));
}
verts.addAll(map.getOrDefault(true, List.of()));
ind.addAll(map.getOrDefault(false, List.of()));

The above closes the BufferedReader when done with it. Your current code does not do that.

The use of streams and maps here may be more trouble than it’s worth. The above can be refactored into:

try (BufferedReader reader = ...) {
  String line;
  while ((line = reader.readLine()) != null) {
    if (line.startsWith("f ")) {
      ind.add(line);
    } else if (line.startsWith("v ")) {
      verts.add(line);
    }
  }
}

Personally I find that much easier to read and understand.

If you really want or need to use a Supplier<Stream<String>> then you can make a slight modification to your current code:

// if you're reading a file then this can be simplified to
// List<String> lines = Files.readAllLines(file);
List<String> lines;
try (BufferedReader reader = ...) {
  lines = reader.lines().collect(Collectors.toList());
}
Supplier<Stream<String>> supplier = lines::stream;

A List can be iterated more than once. Note this will buffer the entire file into memory.


1. You could try to make use of mark and reset but that seems overly complicated for what you’re trying to do. Doing so would also cause you to buffer the entire file into memory.



Source: stackoverflow