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 map
method 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.