Lambda expression is all how we write code. It’s about how concise, smaller, and less boilerplate code we can write.
However, this aforementioned statement may not seem to be true in the case of Exception in Java, checked exception.
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.IOException;
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 to convert string URL to java.net.URL object and then pass it to 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, we didn’t handle the checked exceptions. The constructor of java.net.URL class throws MalformedURLException checked exception. And our private save method also throws a checked Exception which is 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();
}
});
}
Our Lambda expression is supposed to be concise, smaller, and crips, but none of these apply to the code above. That's a problem.
Let’s rewrite the whole program and make our lambda crisp.
import java.io.IOException;
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 method reference. It's now more concise, smaller, and crisp. However, we were not able to solve the problem of handling exceptions, 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 just re-throwing the checked exception using RuntimeException, which will work since we don’t have to handle runtime exceptions if we don’t want to and our lambda expression will remain crisp.
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 Function functional interface. We can write a similar functional interface which would use 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 which extends 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 return 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 use 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.
interface ThrowingConsumer {
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 remove the 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 that do we need checked exceptions or not. However, plenty of software project has been delivered without checked exceptions to date. Having said that, a few decisions that language designers made in the beginning when language was being created impact our way of writing code today which we just cannot ignore. Java 8 changed our way of writing code in a way that we can call a paradigm shift. For that, we can just ignore the debate and use the above-mentioned techniques when we need to deal with checked exceptions in the lambda expression.
Top comments (0)