Skip to content

Commit 2503117

Browse files
committed
Modern I/O article (issue #26)
1 parent 50826e9 commit 2503117

File tree

1 file changed

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

1 file changed

+252
-0
lines changed
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# Common I/O Tasks in Modern Java
2+
3+
This article focuses on tasks that application programmers are likely to encounter, particularly in web applications, such as:
4+
5+
* Reading and writing text files
6+
* Reading text, images, JSON from the web
7+
* Visiting files in a directory
8+
* Reading a zip file
9+
* Creating a temporary file or directory
10+
11+
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/).
12+
13+
Modern, at the time of this writing, means features that are out of preview in Java 21. In particular:
14+
15+
* UTF-8 is the default for I/O since Java 18 ([JEP 400](https://openjdk.org/jeps/400))
16+
* The `java.nio.file.Files` class, which first appeared in Java 7, added useful methods in Java 8, 11, and 12
17+
* `java.io.InputStream` gained useful methods in Java 9, 11, and 12
18+
* The `java.io.File` and `java.io.BufferedReader` classes are now thoroughly obsolete, even though they appear frequently in web searches and AI chats.
19+
20+
## Reading Text Files
21+
22+
Tou can read a text file into a string like this:
23+
24+
```
25+
String content = Files.readString(path);
26+
```
27+
28+
Here, `path` is an instance of `java.nio.Path`, obtained like this:
29+
30+
```
31+
var path = Path.of("/usr/share/dict/words");
32+
```
33+
34+
If you want the file as a sequence of lines, call
35+
36+
```
37+
List<String> lines = Files.readAllLines(path);
38+
```
39+
40+
If the file is large, process the lines lazily as a `Stream<String>`:
41+
42+
```
43+
try (Stream<String> lines = Files.lines(path))
44+
{
45+
. . .
46+
}
47+
```
48+
49+
Also use `Files.lines` if you can naturally process lines with stream operations (such as `map`, `filter`) .
50+
51+
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.
52+
53+
There is no longer a good reason to use the `readLine` method of `java.io.BufferedReader`.
54+
55+
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:
56+
57+
```
58+
Stream<String> tokens = new Scanner(path).useDelimiter("\\PL+").tokens();
59+
```
60+
61+
## Writing Text Files
62+
63+
You can write a string to a text file with a single call:
64+
65+
```
66+
String content = . . .;
67+
Files.writeString(path, content);
68+
```
69+
70+
If you have a list of lines rather than a single string, use:
71+
72+
```
73+
List<String> lines = . . .;
74+
Files.write(path, lines);
75+
```
76+
77+
For more general output, use a `PrintWriter` so that you can use the `printf` method:
78+
79+
```
80+
var writer = new PrintWriter(path.toFile());
81+
writer.printf("Hello, %s, next year you'll be %d years old!%n", name, age + 1);
82+
```
83+
84+
Weirdly enough, as of Java 21, there is no `PrintWriter` constructor with a `Path` parameter.
85+
86+
The `BufferedWriter` class can only write strings without formatting them. That is ok if you use the `String.formatted` method:
87+
88+
```
89+
var writer = Files.newBufferedWriter(path);
90+
writer.write("Hello, %s, next year you'll be %d years old!%n".formatted(name, age + 1));
91+
```
92+
93+
Or, with the `FMT` template (which is still in preview):
94+
95+
```
96+
writer.write(FMT."Hello, %s\{name}, next year you'll be %d\{age + 1} years old!%n");
97+
```
98+
99+
Remember to close the `writer` when you are done.
100+
101+
## Reading From an Input Stream
102+
103+
Perhaps the most common reason to use a stream is to read something from a web site.
104+
105+
If you need to set request headers or read response headers, use the `HttpClient`:
106+
107+
```
108+
HttpClient client = HttpClient.newBuilder().build();
109+
HttpRequest request = HttpRequest.newBuilder()
110+
.uri(URI.create("https://horstmann.com/index.html"))
111+
.GET()
112+
.build();
113+
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
114+
String result = response.body();
115+
```
116+
117+
That is overkill if all you want is the data. Instead, use:
118+
119+
```
120+
InputStream in = new URI("https://horstmann.com/index.html").toURL().openStream();
121+
```
122+
123+
Then read the data into a byte array and optionally turn them into a string:
124+
125+
```
126+
byte[] bytes = in.readAllBytes();
127+
String result = new String(bytes);
128+
```
129+
130+
Or transfer the data to an output stream:
131+
132+
```
133+
OutputStream out = Files.newOutputStream(path);
134+
in.transferTo(out);
135+
```
136+
137+
Nowadays, there is no need to read or write bytes, or chunks of bytes, in a loop.
138+
139+
But do you really need an input stream? Many APIs give you the option to read from a file or URL.
140+
141+
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):
142+
143+
```
144+
URL url = new URI("https://dog.ceo/api/breeds/image/random").toURL();
145+
Map<String, Object> result = JSON.std.mapFrom(url);
146+
```
147+
148+
Here is how to read the dog image from the preceding call:
149+
150+
```
151+
url = new URI(result.get("message").toString()).toURL();
152+
BufferedImage img = javax.imageio.ImageIO.read(url)
153+
```
154+
155+
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.
156+
157+
## The Files API
158+
159+
The `java.nio.file.Files` class provides a comprehensive set of file operations, such as creating, copying, moving, and deleting fies 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.
160+
161+
### Traversing Entries in Directories and Subdirectories
162+
163+
For most situations you can use one of two methods. The `Files.list` method visits all entries (files, subdirectories, symbolic links) of a directory.
164+
165+
```
166+
try (Stream<Path> entries = Files.list(pathToDirectory))
167+
{
168+
. . .
169+
}
170+
```
171+
172+
Use a `try`-with-resources statement to ensure that the stream object, which keeps track of the iteration, will be closed.
173+
174+
If you also want to visit the entries of descendant directories, instead use the method
175+
176+
```
177+
Stream<Path> entries = Files.walk(pathToDirectory);
178+
```
179+
180+
Then simply use stream methods to home in on the entries that you are interested in, and to collect the results:
181+
182+
```
183+
try (Stream<Path> entries = Files.walk(pathToDirectory)) {
184+
List<Path> htmlFiles = entries.filter(p -> p.toString().endsWith("html")).toList();
185+
. . .
186+
}
187+
```
188+
189+
Here are the other methods for traversing directory entries:
190+
191+
* An overloaded version of `Files.walk` lets you limit the depth of the traversed tree.
192+
* 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 [=https://dev.java/learn/java-io/file-system/walking-tree/ Walking the File Tree](null) for details. Unless you need this control, use the simpler `Files.walk` method.
193+
* 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.
194+
* Two `Files.newDirectoryStream` methods yields `DirectoryStream` instances, which can be used in enhanced `for` loops. There is no advantage over using `Files.list`.
195+
* The legacy `File.list` or `File.listFiles` methods return file names or `File` objects. These are now obsolete.
196+
197+
### Working with Zip Files
198+
199+
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*:
200+
201+
```
202+
FileSystem fs = FileSystems.newFileSystem(pathToZipFile);
203+
```
204+
205+
You can then use the methods of the `Files` class. Here we get a list of all files in the zip file:
206+
207+
```
208+
try (Stream<Path> entries = Files.walk(fs.getPath("/"))) {
209+
List<Path> filesInZip = entries.filter(Files::isRegularFile).toList();
210+
}
211+
```
212+
213+
To read the file contents, just use `Files.readString` or `Files.readAllBytes`:
214+
215+
```
216+
String contents = Files.readString(fs.getPath("/LICENSE"));
217+
```
218+
219+
You can remove files with `Files.delete`. To add or replace files, simply use `Files.writeString` or `Files.write`.
220+
221+
You must close the file system so that the changes are written to the zip file. Call
222+
223+
```
224+
fs.close();
225+
```
226+
227+
or use a `try`-with-resources statement.
228+
229+
### Creating Temporary Files and Directories
230+
231+
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.
232+
233+
The calls
234+
235+
```
236+
Path filePath = Files.createTempFile("myapp", ".txt");
237+
Path dirPath = Files.createTempDirectory("myapp");
238+
```
239+
240+
create a temporary file or directory in a suitable location (`/tmp` in Linux) with the given prefix and, for a file, suffix.
241+
242+
## Conclusion
243+
244+
Web searches and AI chats can suggest needlessly complex code for common I/O operations. There are often better alternatives:
245+
246+
1. You don't need a loop to read or write strings or byte arrays.
247+
2. You may not even need a stream, reader or writer.
248+
3. Become familiar with the `Files` methods for creating, copying, moving, and deleting files and directories.
249+
4. Use `Files.list` or `Files.walk` to traverse directory entries.
250+
5. Use a zip file system for processing zip files.
251+
6. Stay away from the legacy `File` class.
252+

0 commit comments

Comments
 (0)