I/O with Lambdas
So far, our exploration of lambdas and streams has existed in the well-controlled world of code. Sometimes,
however, your code has to talk to the outside world. This chapter is where we enter into lambdas in the
real world: instead of showing what is possible with lambdas in the abstract, we are getting into what
programming with lambdas looks like in the grittiness of real code. This is a world where you end up having
to work around a lot of unfortunate legacy code in Java, and where the nice theory and simple examples of
lambdas give way to some of the gritty complexity. The important part for you is to understand how to work
with that complexity in order to create a nicer development experience and more productive, stable code.
For our first case, we will be interacting with files. Through this chapter, we will implement a number of
useful file utilities. We will implement code that will handle all the boilerplate for constructing and cleaning
up a temporary file, allowing the user of the code to focus on working with the temporary file. And we will
implement a method that will read through all the lines of the file, and another that will read through all the
lines of certain files in the directory.
We chose files as a starting point because the file system is an obnoxiously unstable bit of reality.
Many inexperienced developers will write file system code with many undocumented assumptions about
the world outside the codebase and beyond the type system. These assumptions hold true throughout
development, but then explode on production. The developer, confused, then utters the infamous words:
“It works on my machine.”
To deal with this problem, Java introduced the idea of checked exceptions. A checked exception is
a way of stating that the method depends on some reality that is beyond the state of the program itself.
Unchecked exceptions are there for when the programmer screwed up and passed something they shouldn't
have or called a method at the wrong time. Checked exceptions, on the other hand, are for when something
went wrong beyond the control of the application itself. The IOException type hierarchy describes various
different ways that trying to perform I/O can fail for an application. The checked exception is the way of
requiring the programmer to deal with the instability of the outside world.
Languages with less expressive type systems ignore this instability, and unwise developers circumvent
the handling for these potential errors. This creates an illusion of clean code, but that illusion will be
brutally shattered when a virus scanner locks a file that your code is trying to write, or some other oddity
of the outside world deviates from the pristine presumptions of your code. Forcing you to deal with certain
exceptions was a wise design, although many developers seem quick to dismiss it. I would dismiss it, too,
except that I keep encountering programs that explode unhelpfully on I/O errors because people, languages,
and tools circumvented the safety check.
Checked exceptions exist for a reason: sometimes you really do need to be responsive to an error
condition. Unfortunately, they are not amenable to functional code at all. A mathematical function has an
input and an output and no side effects: functional code wants to be as close to a mathematical function
as possible. In Java 8 terms, this means that lambdas should not throw exceptions, and you cannot pass
methods throwing a checked exception as an implementation for a functional interface ( Function ,
Consumer , or Supplier ). Upon encountering this, many programmers will simply turn the checked exception
into an unchecked exception and move on. This is circumventing the safety of your system. Instead, we'd like
to retain the safety while still behaving nicely in our functional world.