How to Handle Checked Exceptions With Lambda Expression

Posted on by

Categories:     

Lambda expressions are all about how we write code. It’s about how we can write more concise, smaller, and less boilerplate code. However, this aforementioned statement may not seem to be true in the case of exceptions in Java.

In Java, we can only handle exceptions through the try-catch block, and this hasn’t changed for the lambda expression.

Let’s say we’re going to develop a simple web crawler. The crawler will take a list of URLs in a string as an argument and save the content of the text file in a text file. Let’s do this.

    import java.io.InputStream;
    import java.net.URL;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    import java.nio.file.StandardCopyOption;
    import java.util.List;
    import java.util.UUID;
    
    public class WebCrawler {
        public static void main(String[] args) {
            List<String> urlsToCrawl = List.of(“https://masterdevskills.com");
    
            WebCrawler webCrawler = new WebCrawler();
            webCrawler.crawl(urlsToCrawl);
        }
    
        public void crawl(List<String> urlsToCrawl) {
            urlsToCrawl.stream()
                    .map(urlToCrawl -> new URL(urlToCrawl))
                    .forEach(url -> save(url));
        }
    
        private void save(URL url) throws IOException {
            String uuid = UUID.randomUUID().toString();
            InputStream inputStream = url.openConnection().getInputStream();
            Files.copy(inputStream, Paths.get(uuid + ".txt"), StandardCopyOption.REPLACE_EXISTING);
        }
    }

The above code is simple and intuitive. We used Stream and the first map method converts string URL to java.net.URL object and then passed it to the forEach method, which saves it in a text file. We have used a lambda expression in the crawl method. However, the above code won’t compile. The reason is that we didn’t handle the checked exceptions. The constructor of java.net.URL class throws the MalformedURLException checked exception. And, our private save method also throws checked exceptions, which is the IOException.

Let’s handle the exceptions.

    public void crawl(List<String> urlsToCrawl) {
        urlsToCrawl.stream()
                .map(urlToCrawl -> {
                    try {
                        return new URL(urlToCrawl);
                    } catch (MalformedURLException e) {
                        e.printStackTrace();
                    }
                    return null;
                })
                .forEach(url -> {
                    try {
                        save(url);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
    }

Lambda expressions are supposed to be concise, smaller, and crisp, but none of these apply to the aforementioned code. So here, we have a problem.

Let’s rewrite the whole program and make our lambda crisp.

    import java.io.InputStream;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    import java.nio.file.StandardCopyOption;
    import java.util.List;
    import java.util.Objects;
    import java.util.UUID;
    
    public class WebCrawler {
        public static void main(String[] args) {
            List<String> urlsToCrawl = List.of("https://masterdevskills.com");
            WebCrawler webCrawler = new WebCrawler();
            webCrawler.crawl(urlsToCrawl);
        }
    
        public void crawl(List<String> urlsToCrawl) {
            urlsToCrawl.stream()
                    .map(this::createURL)
                   .forEach(this::save);
        }
    
        private URL createURL(String urlToCrawl) {
            try {
                return new URL(urlToCrawl);
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        private void save(URL url) {
            try {
                String uuid = UUID.randomUUID().toString();
                InputStream inputStream = url.openConnection().getInputStream();
                Files.copy(inputStream, Paths.get(uuid + ".txt"), StandardCopyOption.REPLACE_EXISTING);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

Now look carefully at the crawl() method. We replaced the lambda expression with the method reference. It’s now more concise, smaller, and crisp. However, we were not able to solve the problem of handling the exception, we just moved it to a different place.

We have another problem here, we handle the exception in the method with the try-catch block in places but did not delegate the exception up the stack of the method call where we actually called the crawl() method.

We can solve this problem by re-throwing the checked exception using RuntimeException, which will work since we don’t have to handle runtime exception if we don’t want to and our lambda expression will remain concise.

Let’s do that:

    public void crawl(List<String> urlsToCrawl) {
            urlsToCrawl.stream()
                    .map(this::createURL)
                    .forEach(this::save);
        }
    
        private URL createURL(String urlToCrawl) {
            try {
                return new URL(urlToCrawl);
            } catch (MalformedURLException e) {
                throw new RuntimeException(e);
            }
        }
    
        private void save(URL url) {
            try {
                String uuid = UUID.randomUUID().toString();
                InputStream inputStream = url.openConnection().getInputStream();
                Files.copy(inputStream, Paths.get(uuid + ".txt"), StandardCopyOption.REPLACE_EXISTING);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

The solution seems to work, but the amount of boilerplate code didn’t reduce. Let’s work on that now.

The map() method of stream takes a functional interface. We can write a similar functional interface, which would use a checked exception. Let’s do that.

    @FunctionalInterface
    public interface ThrowingFunction<T, R, E extends Throwable> {
        R apply(T t) throws E;
    }

This functional interface has three generic types, including one that extends a Throwable. Since Java 8, an interface can have static methods, let’s write one here.

    @FunctionalInterface
    public interface ThrowingFunction<T, R, E extends Throwable> {
        R apply(T t) throws E;
    
        static <T, R, E extends Throwable> Function<T, R> unchecked(ThrowingFunction<T, R, E> f) {
            return t -> {
                try {
                    return f.apply(t);
                } catch (Throwable e) {
                    throw new RuntimeException(e);
                }
            };
        }
    }

The above, unchecked method takes a ThrowingFunction and handles the exception, which, in turn, throws a RuntimeException and returns a Function.

Let’s use in our lambda expression:

    public void crawl(List<String> urlsToCrawl) {
            urlsToCrawl.stream()
                    .map(ThrowingFunction.unchecked(urlToCrawl -> new URL(urlToCrawl)))
                    .forEach(this::save);
    }

In the map method, the ThrowingFunction.unchecked() handles the exception inside it and returns a Function and mapmethod that uses it. This solves no more boilerplate around, and we can easily reuse this new ThrowingFunction functional interface anywhere we want.

Now, let’s take care of the forEach method of the stream API. It takes a Consumer. Here, we can also have a new ThrowingConsumer similar to the previous one.

    public interface ThrowingConsumer<T, E extends Throwable> {
        void accept(T t) throws E;
    
        static <T, E extends Throwable> Consumer<T> unchecked(ThrowingConsumer<T, E> consumer) {
        return (t) -> {
            try {
                consumer.accept(t);
            } catch (Throwable e) {
                throw new RuntimeException(e);
            }
        };
      }
    }

Let’s use it.

    public void crawl(List<String> urlsToCrawl) {
            urlsToCrawl.stream()
                    .map(ThrowingFunction.unchecked(urlToCrawl -> new URL(urlToCrawl)))
                    .forEach(ThrowingConsumer.unchecked(url -> save(url)));
    }
    
    private void save(URL url) throws IOException {
            String uuid = UUID.randomUUID().toString();
            InputStream inputStream = url.openConnection().getInputStream();
            Files.copy(inputStream, Paths.get(uuid + ".txt"), StandardCopyOption.REPLACE_EXISTING);
    }

Now in our code, there is no try-catch block, no boilerplate code. We can use method reference to make it crisper.

    public void crawl(List<String> urlsToCrawl) {
            urlsToCrawl.stream()
                    .map(ThrowingFunction.unchecked(URL::new))
                    .forEach(ThrowingConsumer.unchecked(this::save));
    }

In conclusion, it’s still debatable whether or not we need a checked exception. However, plenty of software projects have been delivered without checked exceptions till date. Having said that, the decisions that developers made when the language was being created impact our way of writing code today, which we cannot ignore. Java 8 changed our way of writing code. For that, we can just ignore the debate and use the above techniques when we need to deal with checked exceptions in the lambda expression.

Happy coding!

This article was published at DZone, See the original article here.

             

Share on:

Author: A N M Bazlur Rahman

Java Champion | Software Engineer | JUG Leader | Book Author | InfoQ & Foojay.IO Editor | Jakarta EE Ambassadors| Helping Java Developers to improve their coding & collaboration skills so that they can meet great people & collaborate

100daysofcode 100daysofjava access advance-java agile algorithm arraylist article bangla-book becoming-expert biginteger book calculator checked checked-exceptions cloning code-readability code-review coding coding-convention collection-framework compact-strings completablefuture concatenation concurrency concurrentmodificationexception concurrentskiplistmap counting countingcollections critical-section daemon-thread data-race data-structure datetime day002 deliberate-practice deserialization design-pattern developers duration execute-around executors export fibonacci file file-copy fork/join-common-pool functional future-java-developers groupby hash-function hashmap history history-of-java how-java-performs-better how-java-works http-client image import inspiration io itext-pdf java java-10 java-11 java-17 java-8 java-9 java-developers java-performance java-programming java-thread java-thread-programming java11 java16 java8 lambda-expression learning learning-and-development linkedlist list local-type-inference localdatetime map methodology microservices nio non-blockingio null-pointer-exception object-cloning optional packaging parallel pass-by-reference pass-by-value pdf performance prime-number programming project-loom race-condition readable-code record refactoring review scheduler scrum serialization serversocket simple-calculator socket software-development softwarearchitecture softwareengineering sorting source-code stack string string-pool stringbuilder swing thread threads tutorial unchecked vector virtual-thread volatile why-java zoneid