Skip to content

Commit a5166d8

Browse files
authored
Merge pull request #66 from java/modern-io
Modern I/O article (issue #26)
2 parents 862c781 + f2677b1 commit a5166d8

File tree

1 file changed

+263
-0
lines changed
  • app/pages/learn/01_tutorial/04_mastering-the-api/02_modern_io

1 file changed

+263
-0
lines changed
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
---
2+
id: api.modernio
3+
title: "Common I/O Tasks in Modern Java"
4+
type: tutorial
5+
category: api
6+
category_order: 2
7+
layout: learn/tutorial.html
8+
subheader_select: tutorials
9+
main_css_id: learn
10+
last_update: 2024-04-24
11+
description: "This article focuses on tasks that application programmers are likely to encounter, particularly in web applications, such as reading and writing text files, reading text, images, JSON from the web, and more."
12+
author: ["CayHorstmann"]
13+
---
14+
15+
<a id="commonio">&nbsp;</a>
16+
17+
## Introduction
18+
19+
This article focuses on tasks that application programmers are likely to encounter, particularly in web applications, such as:
20+
21+
* Reading and writing text files
22+
* Reading text, images, JSON from the web
23+
* Visiting files in a directory
24+
* Reading a ZIP file
25+
* Creating a temporary file or directory
26+
27+
The Java API supports many other tasks, which are explained in detail in the [Java I/O API tutorial](https://dev.java/learn/java-io/).
28+
29+
This article focuses on API improvements since Java 8. In particular:
30+
31+
* UTF-8 is the default for I/O since Java 18 ([JEP 400](https://openjdk.org/jeps/400))
32+
* The `java.nio.file.Files` class, which first appeared in Java 7, added useful methods in Java 8, 11, and 12
33+
* `java.io.InputStream` gained useful methods in Java 9, 11, and 12
34+
* The `java.io.File` and `java.io.BufferedReader` classes are now thoroughly obsolete, even though they appear frequently in web searches and AI chats.
35+
36+
## Reading Text Files
37+
38+
You can read a text file into a string like this:
39+
40+
```java
41+
String content = Files.readString(path);
42+
```
43+
44+
Here, `path` is an instance of `java.nio.Path`, obtained like this:
45+
46+
```java
47+
var path = Path.of("/usr/share/dict/words");
48+
```
49+
50+
Before Java 18, you were strongly encouraged to specify the character encoding with any file operations that read or write strings. Nowadays, by far the most common character encoding is UTF-8, but for backwards compatibility, Java used the "platform encoding", which can be a legacy encoding on Windows. To ensure portability, text I/O operations needed parameters `StandardCharsets.UTF_8`. This is no longer necessary.
51+
52+
If you want the file as a sequence of lines, call
53+
54+
```java
55+
List<String> lines = Files.readAllLines(path);
56+
```
57+
58+
If the file is large, process the lines lazily as a `Stream<String>`:
59+
60+
```java
61+
try (Stream<String> lines = Files.lines(path)) {
62+
. . .
63+
}
64+
```
65+
66+
Also use `Files.lines` if you can naturally process lines with stream operations (such as `map`, `filter`). Note that the stream returned by `Files.lines` needs to be closed. To ensure that this happens, use a `try`-with-resources statement, as in the preceding code snippet.
67+
68+
There is no longer a good reason to use the `readLine` method of `java.io.BufferedReader`.
69+
70+
To split your input into something else than lines, use a `java.util.Scanner`. For example, here is how you can read words, separated by non-letters:
71+
72+
```java
73+
Stream<String> tokens = new Scanner(path).useDelimiter("\\PL+").tokens();
74+
```
75+
76+
The `Scanner` class also has methods for reading numbers, but it is generally simpler to read the input as one string per line, or a single string, and then parse it.
77+
78+
Be careful when parsing numbers from text files, since their format may be locale-dependent. For example, the input `100.000` is 100.0 in the US locale but 100000.0 in the German locale. Use `java.text.NumberFormat` for locale-specific parsing. Alternatively, you may be able to use `Integer.parseInt`/`Double.parseDouble`.
79+
80+
## Writing Text Files
81+
82+
You can write a string to a text file with a single call:
83+
84+
```java
85+
String content = . . .;
86+
Files.writeString(path, content);
87+
```
88+
89+
If you have a list of lines rather than a single string, use:
90+
91+
```java
92+
List<String> lines = . . .;
93+
Files.write(path, lines);
94+
```
95+
96+
For more general output, use a `PrintWriter` if you want to use the `printf` method:
97+
98+
```java
99+
var writer = new PrintWriter(path.toFile());
100+
writer.printf(locale, "Hello, %s, next year you'll be %d years old!%n", name, age + 1);
101+
```
102+
103+
Note that `printf` is locale-specific. When writing numbers, be sure to write them in the appropriate format. Instead of using `printf`, consider `java.text.NumberFormat` or `Integer.toString`/`Double.toString`.
104+
105+
Weirdly enough, as of Java 21, there is no `PrintWriter` constructor with a `Path` parameter.
106+
107+
If you don't use `printf`, you can use the `BufferedWriter` class and write strings with the `write` method.
108+
109+
```java
110+
var writer = Files.newBufferedWriter(path);
111+
writer.write(line); // Does not write a line separator
112+
writer.newLine();
113+
```
114+
115+
Remember to close the `writer` when you are done.
116+
117+
## Reading From an Input Stream
118+
119+
Perhaps the most common reason to use a stream is to read something from a web site.
120+
121+
If you need to set request headers or read response headers, use the `HttpClient`:
122+
123+
```java
124+
HttpClient client = HttpClient.newBuilder().build();
125+
HttpRequest request = HttpRequest.newBuilder()
126+
.uri(URI.create("https://horstmann.com/index.html"))
127+
.GET()
128+
.build();
129+
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
130+
String result = response.body();
131+
```
132+
133+
That is overkill if all you want is the data. Instead, use:
134+
135+
```java
136+
InputStream in = new URI("https://horstmann.com/index.html").toURL().openStream();
137+
```
138+
139+
Then read the data into a byte array and optionally turn them into a string:
140+
141+
```java
142+
byte[] bytes = in.readAllBytes();
143+
String result = new String(bytes);
144+
```
145+
146+
Or transfer the data to an output stream:
147+
148+
```java
149+
OutputStream out = Files.newOutputStream(path);
150+
in.transferTo(out);
151+
```
152+
153+
Note that no loop is required if you simply want to read all bytes of an input stream.
154+
155+
But do you really need an input stream? Many APIs give you the option to read from a file or URL.
156+
157+
Your favorite JSON library is likely to have methods for reading from a file or URL. For example, with [Jackson jr](https://github.com/FasterXML/jackson-jr):
158+
159+
```java
160+
URL url = new URI("https://dog.ceo/api/breeds/image/random").toURL();
161+
Map<String, Object> result = JSON.std.mapFrom(url);
162+
```
163+
164+
Here is how to read the dog image from the preceding call:
165+
166+
```java
167+
url = new URI(result.get("message").toString()).toURL();
168+
BufferedImage img = javax.imageio.ImageIO.read(url)
169+
```
170+
171+
This is better than passing an input stream to the `read` method, because the library can use additional information from the URL to determine the image type.
172+
173+
## The Files API
174+
175+
The `java.nio.file.Files` class provides a comprehensive set of file operations, such as creating, copying, moving, and deleting files and directories. The [File System Basics](https://dev.java/learn/java-io/file-system/) tutorial provides a thorough description. In this section, I highlight a few common tasks.
176+
177+
### Traversing Entries in Directories and Subdirectories
178+
179+
For most situations you can use one of two methods. The `Files.list` method visits all entries (files, subdirectories, symbolic links) of a directory.
180+
181+
```java
182+
try (Stream<Path> entries = Files.list(pathToDirectory)) {
183+
. . .
184+
}
185+
```
186+
187+
Use a `try`-with-resources statement to ensure that the stream object, which keeps track of the iteration, will be closed.
188+
189+
If you also want to visit the entries of descendant directories, instead use the method
190+
191+
```java
192+
Stream<Path> entries = Files.walk(pathToDirectory);
193+
```
194+
195+
Then simply use stream methods to home in on the entries that you are interested in, and to collect the results:
196+
197+
```java
198+
try (Stream<Path> entries = Files.walk(pathToDirectory)) {
199+
List<Path> htmlFiles = entries.filter(p -> p.toString().endsWith("html")).toList();
200+
. . .
201+
}
202+
```
203+
204+
Here are the other methods for traversing directory entries:
205+
206+
* An overloaded version of `Files.walk` lets you limit the depth of the traversed tree.
207+
* Two `Files.walkFileTree` methods provide more control over the iteration process, by notifying a `FileVisitor` when a directory is visited for the first and last time. This can be occasionally useful, in particularly for emptying and deleting a tree of directories. See the tutorial [Walking the File Tree](https://dev.java/learn/java-io/file-system/walking-tree) for details. Unless you need this control, use the simpler `Files.walk` method.
208+
* The `Files.find` method is just like `Files.walk`, but you provide a filter that inspects each path and its `BasicFileAttributes`. This is slightly more efficient than reading the attributes separately for each file.
209+
* Two `Files.newDirectoryStream` methods yields `DirectoryStream` instances, which can be used in enhanced `for` loops. There is no advantage over using `Files.list`.
210+
* The legacy `File.list` or `File.listFiles` methods return file names or `File` objects. These are now obsolete.
211+
212+
### Working with ZIP Files
213+
214+
Ever since Java 1.1, the `ZipInputStream` and `ZipOutputStream` classes provide an API for processing ZIP files. But the API is a bit clunky. Java 8 introduced a much nicer *ZIP file system*:
215+
216+
```java
217+
try (FileSystem fs = FileSystems.newFileSystem(pathToZipFile)) {
218+
. . .
219+
}
220+
```
221+
222+
The `try`-with-resources statement ensures that the `close` method is called after the ZIP file operations. That method updates the ZIP file to reflect any changes in the file system.
223+
224+
You can then use the methods of the `Files` class. Here we get a list of all files in the ZIP file:
225+
226+
```java
227+
try (Stream<Path> entries = Files.walk(fs.getPath("/"))) {
228+
List<Path> filesInZip = entries.filter(Files::isRegularFile).toList();
229+
}
230+
```
231+
232+
To read the file contents, just use `Files.readString` or `Files.readAllBytes`:
233+
234+
```java
235+
String contents = Files.readString(fs.getPath("/LICENSE"));
236+
```
237+
238+
You can remove files with `Files.delete`. To add or replace files, simply use `Files.writeString` or `Files.write`.
239+
240+
### Creating Temporary Files and Directories
241+
242+
Fairly often, I need to collect user input, produce files, and run an external process. Then I use temporary files, which are gone after the next reboot, or a temporary directory that I erase after the process has completed.
243+
244+
The calls
245+
246+
```java
247+
Path filePath = Files.createTempFile("myapp", ".txt");
248+
Path dirPath = Files.createTempDirectory("myapp");
249+
```
250+
251+
create a temporary file or directory in a suitable location (`/tmp` in Linux) with the given prefix and, for a file, suffix.
252+
253+
## Conclusion
254+
255+
Web searches and AI chats can suggest needlessly complex code for common I/O operations. There are often better alternatives:
256+
257+
1. You don't need a loop to read or write strings or byte arrays.
258+
2. You may not even need a stream, reader or writer.
259+
3. Become familiar with the `Files` methods for creating, copying, moving, and deleting files and directories.
260+
4. Use `Files.list` or `Files.walk` to traverse directory entries.
261+
5. Use a ZIP file system for processing ZIP files.
262+
6. Stay away from the legacy `File` class.
263+

0 commit comments

Comments
 (0)