It might be better to say you are completing the pipeline. Just as whenever I am trying to lead water around the house in copper pipelines, I am not going to turn the water on at the source until I am confident I have connected the other end. When you build a Sahara Pipeline to carry oil across the desert, you don't turn the oil supply on until you are sure the other end is connected correctly.
JShell doesn't help by calling toString() on your Stream objects.
...but it is only worth creating parallel Streams when there is a lot of work to be done. And those Streams are not called sequential.
...it is a bit of confusing information JShell has given you.
...Start small and simple.
Streams do not “have” elements; they process them.
Also, Streams implement lazy execution; an element is only taken by the first Stream when the terminal operation requires it.
In the example code that you gave, nothing happens at all because there is no terminal operation. But let's assume that you added the terminal operation .forEachOrdered(System.out::println).
The forEachOrdered() method would call the tryAdvance() method of the map operation, which in turn calls the tryAdvance() method of the filter operation, which in turn calls the tryAdvance() method of the source. The source takes the integer 2 and passes it to the Consumer that the filter operation passed to it. That Consumer checks that 2 is greater than or equal to 3, determines that it's not, and so does nothing more. Control returns to the tryAdvance() method of the source stream, which just returns the value 'true' to indicate to the filter operation that there are more elements remaining, which the filter returns back up to the map operation, which the map operation returns to the forEachOrdered operation.
Because there are more elements remaining, the forEachOrdered operation calls the tryAdvance() method of the map operation a second time. This will call the tryAdvance() method of the filter operation. This will call the tryAdvance() method of the source. The source passes the integer 4 to the Consumer that was passed in by the filter operation. The Consumer checks that 4 is greater than or equal to 3, determines that it is, and so it passes 4 to the Consumer that was passed in by the map operation. The map's Consumer then applies the toString() method on the 4, and passes the String "4" to the Consumer that was passed in by the forEachOrdered operation. The forEachOrdered operation's Consumer then calls System.out.println() on "4".
... the convention that in Java, when specifying a range, the starting index is inclusive and the ending index is exclusive.
The range is cut in half, NOT the data source.
When trySplit() is called on the spliterator (by other code in the Stream API, not by you) and it succeeds, the original spliterator is modified so it will only be responsible for half of what it did before, and it will return a new spliterator that is responsible for iterating over the remaining elements.
Spliterators don't "contain" any elements at all, nor do they enclose a collection. They're just objects that have access to the internals of a collection that does contain the elements.