I don't particularly recommend the following approach, because it's probably a bit overkill, and there are more efficient ways to get this data than reading the entire file a line at a time.
However, if you really want to get the first and last elements in a single pass over the stream, one solution would be to create a custom Gatherer using the JEP 485: Stream Gatherers Java 24 language feature (available as a preview language feature since Java 22) to capture the first and last elements of the stream, and throw away the rest.
public class FirstAndLast {
// For production-quality code, add and use getters, setters, and constructors
public String first, last;
}
Gatherer<String, ?, FirstAndLast> firstAndLastGatherer =
Gatherer.of(
FirstAndLast::new,
Gatherer.Integrator.ofGreedy((state, element, downstream) -> {
if (state.first == null) {
state.first = Objects.requireNonNull(element);
}
state.last = Objects.requireNonNull(element);
return true;
}),
(s1, s2) -> {
s1.last = s2.last;
return s1;
},
(state, downstream) -> downstream.push(state)
);
FirstAndLast firstAndLast =
br.lines().gather(firstAndLastGatherer).findFirst().orElseThrow();
String firstLine = firstAndLast.first;
String lastLine = firstAndLast.last;
This custom gatherer stores the first and most recently encountered elements as first and last, overwriting the last variable for each new element. It then emits a single FirstAndLast element to the stream, resulting in a stream with just the one single element containing the first and last items.
Alternatively, a gatherer could be written that keeps the first and last elements, and discards the elements in between, resulting in a 2-element stream:
class State {
String first, last;
}
Gatherer<String, ?, String> firstAndLastGatherer = Gatherer.of(
State::new,
Gatherer.Integrator.ofGreedy((state, element, downstream) -> {
if (state.first == null) {
state.first = Objects.requireNonNull(element);
} else {
state.last = Objects.requireNonNull(element);
}
return true;
}),
(s1, s2) -> {
s1.last = s2.last;
return s1;
},
(state, downstream) -> {
if (state.first != null) {
downstream.push(state.first);
}
if (state.last != null) {
downstream.push(state.last);
}
}
);
List<String> firstAndLast = list.stream().gather(firstAndLastGatherer).toList();
String first = firstAndLast.getFirst();
String last = firstAndLast.getLast();
Javadocs
Gatherer:
An intermediate operation that transforms a stream of input elements into a stream of output elements, optionally applying a final action when the end of the upstream is reached. […]
[…]
There are many examples of gathering operations, including but not limited to: grouping elements into batches (windowing functions); de-duplicating consecutively similar elements; incremental accumulation functions (prefix scan); incremental reordering functions, etc. The class Gatherers provides implementations of common gathering operations.
API Note:
A Gatherer is specified by four functions that work together to process input elements, optionally using intermediate state, and optionally perform a final action at the end of input. They are:
Stream.gather(Gatherer<? super T,?,R> gatherer):
Returns a stream consisting of the results of applying the given gatherer to the elements of this stream.
Gatherer.of(initializer, integrator, combiner, finisher)
Returns a new, sequential, Gatherer described by the given initializer, integrator, combiner and finisher.
lines()stream method.lines()method (after all, it doesn't really read anything at this point), but in the loop implied by thereduce(). UsingStream.skip(Stream.count()-1)may be a cleaner (but probably not faster) solution. It's a pity Stream.skip() doesn't support negative arguments. The other option would be to go low level using aSeekableByteChannel().