Kotlin makes doing input/output (I/O) operations easy, but the style is different from what a Java developer may expect. Resources in Kotlin are frequently closed by employing a use
function that does so on the user’s behalf. This chapter includes a couple of recipes that focus on that approach, specifically for files, but that can be generalized to other resources.
Use the extension functions use
or useLines
on readers or input/output streams.
Java 1.7 introduced the try-with-resources construct, which allows a developer to open a resource inside parentheses between the try
keyword and its corresponding block; the JVM will automatically close the resource when the try
block ends. The only requirement is that the resource come from a class that implements the Closeable
interface. File
, Stream
, and many other classes implement Closeable
, as the example in Example 10-1 shows.
package
io
;
import
java.io.*
;
import
java.nio.file.Files
;
import
java.nio.file.Paths
;
import
java.util.stream.Stream
;
public
class
TryWithResourcesDemo
{
public
static
void
main
(
String
[
]
args
)
throws
IOException
{
String
path
=
"src/main/resources/book_data.csv"
;
File
file
=
new
File
(
path
)
;
String
line
=
null
;
try
(
BufferedReader
reader
=
new
BufferedReader
(
new
FileReader
(
file
)
)
)
{
while
(
(
line
=
reader
.
readLine
(
)
)
!
=
null
)
{
System
.
out
.
println
(
line
)
;
}
}
try
(
Stream
<
String
>
lines
=
Files
.
lines
(
Paths
.
get
(
path
)
)
)
{
lines
.
forEach
(
System
.
out
:
:
println
)
;
}
}
}
Both the BufferedReader
class and Stream
interface implement Closeable
, so each has a close
method that is automatically invoked when the try
block completes.
A few interesting features are worth noting:
In Java 10 or above, the declaration of BufferedReader
and Stream
could be replaced by the reserved word var
. In fact, this is one of the primary use cases for local variable type inference. It is not used here to avoid confusion with Kotlin’s var
keyword.
In Java 9 or above, you no longer have to create the Closeable
variable inside the parentheses. It can be supplied from outside. Again, there is no benefit to doing so here.
Since the main
method signature was modified to throw IOException
, the result is one of those rare moments when you have a try
block without either a catch
or a finally
.
This is all well and good, but unfortunately the try-with-resources construct is not supported by Kotlin. Instead, Kotlin adds the extension functions use
to Closeable
and useLines
to Reader
and File
.
The signature of useLines
is given by the following:
inline
fun
<
T
>
File
.
useLines
(
charset
:
Charset
=
Charsets
.
UTF_8
,
block
:
(
Sequence
<
String
>)
->
T
):
T
The optional first argument is a character set, which defaults to UTF-8. The second argument is a lambda that maps a Sequence
of lines from the file into a generic argument, T
. The implementation of the useLines
function automatically closes the reader after the processing is complete.
As an example, on Unix systems ultimately based on BSD (which include macOS), there is a file containing all the words from Webster’s Second International Dictionary, which is out of copyright. On a Mac, the file is located in the directory /usr/share/dict/words, and contains 238,000 words, one per line. Example 10-2 returns the 10 longest words in the dictionary.
fun
get10LongestWordsInDictionary
()
=
File
(
"/usr/share/dict/words"
).
useLines
{
line
->
line
.
filter
{
it
.
length
>
20
}
.
sortedByDescending
(
String
::
length
)
.
take
(
10
)
.
toList
()
}
The function filters out any line shorter than 20 characters (and since each line has a single word, that’s any word shorter than 20 characters), sorts them by length in descending order, selects the first 10, and returns them in a list.
To call this function, use the following:
get10LongestWordsInDictionary
().
forEach
{
word
->
println
(
"$word (${word.length})"
)
This prints the following:
formaldehydesulphoxylate (24) pathologicopsychological (24) scientificophilosophical (24) tetraiodophenolphthalein (24) thyroparathyroidectomize (24) anthropomorphologically (23) blepharosphincterectomy (23) epididymodeferentectomy (23) formaldehydesulphoxylic (23) gastroenteroanastomosis (23)
The implementation of File.useLines
in the standard library is given by Example 10-3.
useLines
as an extension function on File
inline
fun
<
T
>
Reader
.
useLines
(
block
:
(
Sequence
<
String
>)
->
T
):
T
=
buffered
().
use
{
block
(
it
.
lineSequence
())
}
Note that the implementation creates a buffered reader (returned by the buffered
function) and delegates to its use
function.
The corresponding signature of the use
function is given by Example 10-4.
use
extension function on Closeable
inline
fun
<
T
:
Closeable
?,
R
>
T
.
use
(
block
:
(
T
)
->
R
):
R
The implementation is complicated by the need for exception handling, but at its base it is similar to this:
try
{
return
block
(
this
)
}
catch
(
e
:
Throwable
)
{
// save the exception to be used later
throw
e
}
finally
{
close
()
// requires another try/catch block
}
The use
block is an example of the Execute Around Method design pattern, where the infrastructure code is built into the library, and a provided lambda does the actual work. This separation of infrastructure from business logic makes it easier to focus on the task at hand.
Recipe 10.2 shows how to code with the use
block directly. The use
function is also employed to shut down a Java thread pool in Recipe 13.4.
You want to write to a file.
Several extension functions have been added to Java’s java.io.File
class. You can iterate through a file by using the forEachLine
function. You can call readLines
on a file to get a collection containing all the lines in the file, which is useful if the file is not very large. The useLines
function, described in Recipe 10.1, allows you to supply a function that will be invoked on each line. If the file is small enough, you can use readText
or readBytes
to read the entire contents into a string or a byte array, respectively.
If you want to write to a file and replace all of its existing contents, use the writeText
function, as in Example 10-5.
File
(
"myfile.txt"
).
writeText
(
"My data"
)
It’s hard to be much simpler than that. The writeText
function takes an optional parameter to represent the character set, whose default value is UTF-8.
The File
class also has an extension function called appendText
, which can add data to a given file.
The writeText
and appendText
functions delegate to writeBytes
and appendBytes
, each of which takes advantage of the use
function to ensure that the file is closed when writing is finished.
You can also use the writer
(or printWriter
) and bufferedWriter
functions, which return an OutputStreamWriter
and a BufferedWriter
, as you might expect. With either of them, you can add a use
block to do the actual writing, as in Example 10-6.
use
functionFile
(
fileName
).
printWriter
().
use
{
writer
->
writer
.
println
(
data
)
}
Using bufferedWriter
with a use
block works exactly the same way.
Recipe 10.1 goes into detail about the use
and useLines
functions.
3.140.188.244