3

Have a look at the following snippet that tries to convert a list of strings into a list of class objects:

public static List<Class<?>> f1(String... strings) {
    return
        Stream.of(strings)
        .map(s -> {
            try {
                return Class.forName(s);
            }
            catch (ClassNotFoundException e) {
                System.out.println(e.getMessage());
            }
            return null;
        })
        .collect(Collectors.toList());
}

Because of the way Java handles checked exceptions in streams (as has been discussed at length here - where I also blatantly stole my example snippet from), you have to have the additional return statement after the try-catch block. This will result in an unwanted null reference being added to the result of type List<Class>.

To avoid this extra null reference, I came up with the following function, achieving a better result with a plain old procedural loop:

public static List<Class<?>> f2(String... strings) {
    List<Class<?>> classes = new ArrayList<>();
    for (String s : strings) {
        try {
            classes.add(Class.forName(s));
        }
        catch (ClassNotFoundException e) {
            // Handle exception here
        }
    }
    return classes;
}

Does this observation allow me to draw a conclusion in the form of a best-practice advice, that could be expressed like the following?

If you need to call a function that throws an exception inside a stream and you cannot make use of stream.parallel(), better use a loop, because:

  1. You'll need the try-catch block anyway (notwithstanding the tricky solutions involving some kind of wrapper around the throwing function provided in the aforementioned discussion)

  2. Your code will not be less concise (mainly because of 1.)

  3. Your loop will not break in case of an exception

What do you think?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Angle.Bracket
  • 1,438
  • 13
  • 29

2 Answers2

2

As @Patrick wrote, you could filter your null classes. Just add a filter after the map in your stream:

 .filter(Objects::nonNull)
 .collect(Collectors.toList());
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Schidu Luca
  • 3,897
  • 1
  • 12
  • 27
  • Right, but that's one extra line of code for the stream solution, thus making the loop even more concise in comparison. – Angle.Bracket Mar 28 '18 at 15:10
  • Right, streams aren't for everything. Also putting try/catch inside a lambda looks ugly... it would looks much nicer with a helper method. Still, there are advantages such as easy parrallelization (useful for very large lists). – Patrick Parker Mar 28 '18 at 19:53
  • So the parallel option would be the only argument in favor of the stream solution then, -> see my edits to the original post. – Angle.Bracket Mar 29 '18 at 08:54
  • Besides, `Objects::requireNonNull` will throw a NullPointerException if called with a null reference from upstream. – Angle.Bracket Mar 29 '18 at 09:14
  • Apparently, you are mixing up `Objects::nonNull` and `Objects::requireNonNull`. – Holger Mar 29 '18 at 11:37
  • Right, wasn't aware of `Objects::nonNull` in Java 8. – Angle.Bracket Mar 29 '18 at 12:31
2

You can do this without introducing elements which you have to filter in a subsequent step:

public static List<Class<?>> f1(String... strings) {
    return Arrays.stream(strings)
        .flatMap(s -> {
            try { return Stream.of(Class.forName(s)); }
            catch(ClassNotFoundException e) { return null; }
        })
        .collect(Collectors.toList());
}

Still, this isn’t more concise than a loop solution.

But it should be noted that this has nothing to do with checked exceptions. You want to continue in the exceptional case, omitting the failed element. That always requires you to catch the exception to implement this alternative behavior (the default would be to propagate the exception to the caller), whether the exception is checked or unchecked. The checked case has the advantage of reminding you that you have to do this.

In other words, the Stream API does not allow you to implement the behavior of propagating checked exceptions to the caller in a simple way, but that’s not what you want here anyway. If Class.forName(String) was designed to throw unchecked exceptions only, omitting the try … catch block in map would cause the entire operation to abort in the exceptional case by relaying the exception to the caller. But, as said, that’s not what you want here.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Good answer because it points to the Javadoc of `Stream.flatMap()` where it says "(If a mapped stream is null an empty stream is used, instead.)". I wasn't aware of that behavior. But what have we got here: in any case we're mapping exactly one element of a stream to another stream only to benefit from the fact that an empty stream is produced if an exception was thrown. As you say, still no more concise than the loop variant. And being more concise was the reason to use the Stream API in the first place (apart from seemingly easy parallel operations). – Angle.Bracket Mar 29 '18 at 12:57
  • The single element stream created by `Stream.of(T)` is very lightweight and nothing to worry about. Besides that, as already mentioned, if conciseness is all that matters, you may stay with the loop. It’s irrelevant here, which you use. It would be more interesting, if the method returned the `Stream>`, as then, the caller could decide whether to collect into a `List` or to use `findFirst()` to find the first non-failed class (mind the lazy nature of streams). That would be another example of an advantage besides parallel processing. If you don’t use/need this, the loop is fine. – Holger Mar 29 '18 at 13:03