I've found that stream pipelines can be difficult to debug because stack traces involving lambda expressions are quite cryptic. Consider the following contrived example:
import java.util.Arrays; import java.util.List; import java.util.function.Function; public class Test { public static void main(final String[] args) { final List<String> list = Arrays.asList("foo", null, "bar"); list.stream() .map(Function.identity()) .filter(x -> true) .map(String::length) .forEach(System.out::println); } }
You may have already guessed that the code above will throw a NullPointerException
when String.length
is called on the null
element in the list. I've added extra map
and filter
operations, which do nothing, just to make the example a bit more interesting. In the real world, you will probably have a number of different operations in your stream pipeline.
Running the code, produces the following stack trace:
Exception in thread "main" java.lang.NullPointerException at Test$$Lambda$3/455659002.apply(Unknown Source) at java.util.stream.ReferencePipeline$3$1.accept(Unknown Source) at java.util.stream.ReferencePipeline$2$1.accept(Unknown Source) at java.util.stream.ReferencePipeline$3$1.accept(Unknown Source) at java.util.Spliterators$ArraySpliterator.forEachRemaining(Unknown Source) at java.util.stream.AbstractPipeline.copyInto(Unknown Source) at java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source) at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(Unknown Source) at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(Unknown Source) at java.util.stream.AbstractPipeline.evaluate(Unknown Source) at java.util.stream.ReferencePipeline.forEach(Unknown Source) at Test.main(Test.java:12)
The stack trace shows that a NullPointerException
occurred but it doesn't tell you which operation in the pipeline failed. What does Test$$Lambda$3/455659002.apply(Unknown Source)
mean and why is there no line number?! Since lambda expressions don't have a name, the compiler makes one up (similar to anonymous classes). In this case, it is Test$$Lambda$3
but that doesn't help us track the bug in our code.
So, what can we do? Let's go old-school and add some logging to our code! We can use peek
to print out each element before it is consumed by the next operation in the pipeline.
import java.util.Arrays; import java.util.List; import java.util.function.Function; public class Test { public static void main(final String[] args) { final List<String> list = Arrays.asList("foo", null, "bar"); list.stream() .peek(x -> System.out.println("Running identity on: " + x)) .map(Function.identity()) .peek(x -> System.out.println("Running filter on: " + x)) .filter(x -> true) .peek(x -> System.out.println("Running string length on: " + x)) .map(String::length) .peek(x -> System.out.println("Running print on: " + x)) .forEach(System.out::println); } }
Running it produces the following output:
Running identity map on: foo Running filter on: foo Running string length on: foo Running print on: 3 3 Running identity map on: null Running filter on: null Running string length on: null Exception in thread "main" java.lang.NullPointerException at Test$$Lambda$6/295530567.apply(Unknown Source) at java.util.stream.ReferencePipeline$3$1.accept(Unknown Source) at java.util.stream.ReferencePipeline$11$1.accept(Unknown Source) at java.util.stream.ReferencePipeline$2$1.accept(Unknown Source) at java.util.stream.ReferencePipeline$11$1.accept(Unknown Source) at java.util.stream.ReferencePipeline$3$1.accept(Unknown Source) at java.util.stream.ReferencePipeline$11$1.accept(Unknown Source) at java.util.Spliterators$ArraySpliterator.forEachRemaining(Unknown Source) at java.util.stream.AbstractPipeline.copyInto(Unknown Source) at java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source) at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(Unknown Source) at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(Unknown Source) at java.util.stream.AbstractPipeline.evaluate(Unknown Source) at java.util.stream.ReferencePipeline.forEach(Unknown Source) at Test.main(Test.java:16)
Great! Now we know that the NullPointerException
was thrown by the string length lambda!
In general, I think stack traces involving lambdas could be improved in future versions of Java.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.