Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8: Applying Stream map and filter in one go

I am writing a parser for a file in Java 8. The file is read using Files.lines and returns a sequential Stream<String>.

Each line is mapped to a data object Result like this:

Result parse(String _line) {
  // ... code here
  Result _result = new Result().
  if (/* line is not needed */) {
    return null;
  } else {
    /* parse line into result */
   return _result;
  }
}

Now we can map each line in the stream to its according result:

public Stream<Result> parseFile(Path _file) {
  Stream<String> _stream = Files.lines(_file);
  Stream<Result> _resultStream = _stream.map(this::parse);
}

However the stream now contains null values which I want to remove:

parseFile(_file).filter(v -> v != null);

How can I combine the map/filter operation, as I already know in parseLine/_stream.map if the result is needed?

like image 438
user3001 Avatar asked Nov 06 '14 12:11

user3001


2 Answers

As already pointed out in the comments the stream will be processed in one pass, so there isn't really a need to change anything. For what it's worth you could use flatMap and let parse return a stream:

Stream<Result> parse(String _line) {
  .. code here
  Result _result = new Result().
  if (/* line is not needed */) {
    return Stream.empty();
  } else {
    /** parse line into result */
   return Stream.of(_result);
  }
}  

public Stream<Result> parseFile(Path _file) {
  return Files.lines(_file)
              .flatMap(this::parse);
}

That way you won't have any null values in the first place.

like image 114
a better oliver Avatar answered Sep 29 '22 18:09

a better oliver


Updating for Java 9:

Using Stream<Result> seems like the wrong return type for the parse() function. A stream can contain many, many values, so the user of parse() either has to assume there will be at most one value in the stream, or use something like collect to extract and use the results of the parse() operation. If the function and its usage are only separated by a few lines of code, this may be fine, but if the distance increases, such as in a completely different file for JUnit testing, the interface contract isn't clear from the return value.

Instead of returning a Stream, it would be a better interface contract to return an empty Optional when the line is not needed.

Optional<Result> parse(String _line) {
   ... code here
   Result _result = null;
   if (/* line needed */) {
      /** parse line into result */
   }
   return Optional.ofNullable(_result);
}

Unfortunately, now _stream.map(this::parse) returns a stream of Optional values, so with Java 8, again you'd need to filter and map this with .filter(Optional::isPresent).map(Optional::get), and the question was looking for a solution which could do this "in one go".

This question was posted 3 years ago. With Java 9, we now have the option (pun intended) of using the Optional::stream method, so we can instead write:

public Stream<Result> parseFile(Path _file) {
  return Files.lines(_file)
      .map(this::parse)
      .flatMap(Optional::stream)
}

to transform the stream of Optional values into a stream of Result values, without any of the empty optionals.

like image 32
AJNeufeld Avatar answered Sep 29 '22 18:09

AJNeufeld