An Incomplete Guide to Modern Java I/O Idioms

If you ask StackOverflow or ChatGPT, how to convert an InputStream to a String in Java, you get archaic constructs with buffered readers and tedious loops. In modern Java, you achieve this task and similar ones with a single line of code. Apparently, these one-liners are not common knowledge, so I list a few. Maybe it'll get me a mention in the the JetBrains Java Annotated Monthly newsletter.

JetBrains has a nice monthly newsletter with Java tidbits. Mostly, these links are really useful. But the April 2023 edition included A Complete Guide on How to Convert InputStream to String In Java with the caption: “Eden Allen gives step-by-step instructions on understanding Inputstream [sic] and converting it to String using BufferedReader.”

.jpg

Huh? Why do you need to use BufferedReader for this in 2023? And why is the author's web page listed as https://cheapsslweb.com? Maybe ChatGPT had a hand in this? When I asked OpenAI Chat “How to Convert InputStream to String In Java”, it gave me a five-step procedure with exactly the same code as in the article:

public static String convertInputStreamToString(InputStream inputStream) throws IOException {
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
    StringBuilder stringBuilder = new StringBuilder();
    String line;
    while ((line = bufferedReader.readLine()) != null) {
        stringBuilder.append(line).append("\n");
    }
    bufferedReader.close();
    return stringBuilder.toString();
}

Hello, it's 2023

Most of the time, when I want to read a string, it's in a file. Java 11, released in 2018, has methods

public static String readString(Path path) // UTF-8
public static String readString(Path path, Charset cs)

in the java.nio.file.Files class. No need for buffered readers or loops.

What if I read the string from a URL? Of course, I can use HttpClient, also a part of Java 11:

HttpClient client = HttpClient.newBuilder().build();
HttpRequest request = HttpRequest.newBuilder()
   .uri(URI.create("https://horstmann.com/index.html"))
   .GET()
   .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
String result = response.body();

Ok, maybe that's a bit heavy-handed. I could just call

InputStream in = new URL("https://horstmann.com/index.html").openStream();

And then I need to turn an InputStream into a string. But not with a loop. In 2017, Java 9 was released, and InputStream gained a method readAllBytes:

var result = new String(in.readAllBytes(), StandardCharsets.UTF_8);

And, thanks to JEP 400, as of Java 18, you don't need StandardCharsets.UTF_8 anymore. (Assuming, of course, that the contents is actually in UTF-8. But hey, it's 2023.)

Common I/O Tasks Without Loops

To read text from a file, use one of these methods:

String contents = Files.readString(path);
List<String> lines = Files.readAllLines(path);
Stream<String> lineStream = Files.lines(path);

If you need to read numbers, words, or characters instead of lines, then use a scanner:

var in = new Scanner(path);
in.useDelimiter("\\PL+");
Stream<String> words = in.tokens();

If you already have your output in a string or a collection of lines, use:

Files.writeString(path, contents);
Files.write(path, lines); // lines is any Iterable<String>

With binary data, there are

byte[] bytes = Files.readAllBytes(path);
Files.write(path, bytes);

The “Complete Guide” article proposes a second method for reading a string from an input stream:

public static String convertInputStreamToString(InputStream inputStream) throws IOException {
    ByteArrayOutputStream result = new ByteArrayOutputStream();
    byte[] buffer = new byte[1024];
    int length;
    while ((length = inputStream.read(buffer)) != -1) {
        result.write(buffer, 0, length);
    }
    return result.toString("UTF-8");
}

This is also unnecessarily complex. Since Java 9, InputStream has a method transferTo, and the loop can be replaced with:

in.transferTo(result);

What about BufferedReader and BufferedWriter?

It is unlikely that you want to use these classes in 2023. If you write your own lexer and read input a character at a time, a BufferedReader still makes sense. But that's a very specialized task.

For most text processing tasks, it is simpler to read the entire text into a string or a list of strings, with one of the convenience methods that you just saw. If you need to break up each line into fields, call line.split(delimiterRegex) or construct a new Scanner(line) for each line.

I cannot imagine ever needing a BufferedWriter. For hand-crafted output, you are better off with a PrintWriter since it has methods for formatting. Weirdly enough, you still need a File, not a Path, to construct one:

var writer = new PrintWriter(path.toFile());

Conclusion

  1. Apparently, there is a myth that Java has been frozen in time somewhere around 2010. It ain't so. The API is tweaked all the time, and the language evolves in small and large ways.
  2. Before writing a loop to transfer blocks of characters or bytes, look whether there is a convenience method.
  3. You probably don't need a BufferedReader or BufferedWriter.
  4. If a common task seems hard to do in Java, check the modern API. The https://javaalmanac.io site is your friend. Or check out Core Java, which, all modesty aside, is generally up to date.
  5. If your task is still onerous with the latest Java API, kvetch to the developers. Find the appropriate mailing list, read through the archives, and then post your suggestion. In my experience, thoughtful requests receive thoughtful responses.

Comments powered by Talkyard.