|
| 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