Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Fundamentals › Java

File I/O

What do you exchange when you write to and read from a file?

Code that writes text to a file and reads it back is made of three methods of the Files class. The Files.writeString method writes text to a file, the Files.readAllBytes method reads the bytes stored in the file as they are, and the Files.readString method turns the bytes of the file back into text. A file stores not text but bytes (values you can see as numbers from 0 to 255). So when you write text, it is turned into bytes by a set rule (character encoding), and when you read, the bytes are turned back into text by a rule. The StandardCharsets.UTF_8 part of the code is where that rule is written, and you write it on the writing line and on the reading line. In this lab you change the text to write and the way to read, and the same code is compiled and run, to show the bytes actually stored in the file and the text that was read. (Path.of("memo.txt") is taken to mean the file memo.txt in the folder where the program was run, and other path rules are not covered.)

Checked against real execution · Compiled 9 combinations with javax.tools on JDK 21 and ran each in a new Java process, then checked the stored file bytes against the characters and exceptions that were read

Concepts in this lab

Byte
A byte is the basic unit that makes up what is stored in a file, and it is one 8-bit value. The Java byte type shows values from -128 to 127, but the same 8 bits can also be read as a value from 0 to 255 (Byte.toUnsignedInt). A file stores not the text or the picture itself but such bytes one after another, and which character they become depends on the rule by which you read those bytes. In Java, a byte[] array is a container that holds the contents of a file as they are, with no rule, and Files.readAllBytes reads the bytes of a file into this array. The size of a file is also counted in bytes, not in characters.
Character encoding and UTF-8
A character encoding is a rule that defines which bytes each character is converted to when it is stored. UTF-8 is the most widely used encoding, and the length of a character varies from 1 byte to 4 bytes depending on the character. English letters and digits take 1 byte, and one Korean character takes 3 bytes. Text is garbled when the encoding used for writing differs from the one used for reading, or when the bytes are cut off in the middle of a character, so it is safer to state the encoding explicitly in code that reads and writes text.
ISO_8859_1
ISO_8859_1 is one of the character encodings that the Java StandardCharsets class provides as a constant, and the documentation describes it as ISO Latin Alphabet No. 1 (ISO-LATIN-1). When all 256 byte values from 0 to 255 were read on this JDK, one byte became one character, and the number of that character was the same as the byte value. So any sequence of bytes turns into characters without an error, but text in which one character is several bytes, such as Korean stored as UTF-8, breaks because each byte becomes a wrong character. The bytes from 0x00 to 0x7F become the same English letters and digits as in UTF-8.
NoSuchFileException
NoSuchFileException is a checked exception thrown when an attempt is made to access a file that does not exist. It is a subclass of java.nio.file.FileSystemException, and so it is a kind of IOException. On this JDK, when Files.readAllBytes tried to read a file whose name was not in the folder, this exception was thrown, and the exception message contained the path that was not found. If the exception is not handled and leaves main, the program ends there with an error.

Try these

Running with the default settings

  1. The line that calls the Files.writeString method runs. At this point the file memo.txt does not exist yet. A file stores not text but bytes (values you can see as numbers from 0 to 255), so the 2 characters to write must first be turned into bytes. The rule for turning them (character encoding) is the StandardCharsets.UTF_8 rule passed as the third argument, that is, UTF-8.
  2. The text is turned into bytes by the UTF-8 rule. "가": 3 bytes (EA B0 80), "A": 1 byte (41). In this rule, a Korean character is 3 bytes and an English letter is 1 byte. There are 2 characters, but the bytes come to 4. These bytes are stored in the file memo.txt. These numbers are all that is stored in the file.
  3. The line that calls the Files.readAllBytes method reads the file memo.txt. This method has no rule for reading; it puts the bytes stored in the file into the variable bytes as they are. The bytes put in are 4 (EA B0 80 41), the same as the bytes just stored.
  4. The line that calls the Files.readString method turns the bytes of the file memo.txt back into text. The rule for reading is the StandardCharsets.UTF_8 rule, the same as when writing. Because the bytes are grouped and read by the same rule, the original characters come back as they were and are put in the variable text: EA B0 80 → "가", 41 → "A".
  5. The two System.out.println lines print the number of bytes in the variable bytes and the text in the variable text. The first line is 4, and the second line is 가A. There are 2 characters, but the number on the first line is 4, so the value of bytes.length is the number of bytes, not the number of characters.

Result of this run

Code

public class Main {
    public static void main(String[] args) throws IOException {
        // Write: turn the text into bytes by a set rule and store them in the file
        Files.writeString(Path.of("memo.txt"), "가A",
                StandardCharsets.UTF_8);
        // Read 1: read the bytes stored in the file as they are
        byte[] bytes = Files.readAllBytes(Path.of("memo.txt"));
        // Read 2: turn the bytes of the file back into text by a set rule
        String text = Files.readString(Path.of("memo.txt"),
                StandardCharsets.UTF_8);
        System.out.println(bytes.length);
        System.out.println(text);
    }
}

Assumptions of this model

The compiler and runtime are JDK 21.0.12.1 (javac 21). All 9 combinations were checked against real runs: the code on the screen (with only the four import lines at the top added) was compiled with javax.tools and run in a new Java process (java -cp . Main) in an empty folder for each combination. After the process ended, the memo.txt in the folder was opened directly to read the bytes actually stored (in hexadecimal) and the size, and I confirmed the number of bytes the program printed, the code points of the characters it read, the exit code, the name of the exception and the line where it occurred (which line of the source), the list of files in the folder after the run (none.txt does not exist and memo.txt does), and that memo.txt did not exist before the run. The four import lines were left out of the screen. That 가 is stored as EA B0 80, 나 as EB 82 98, and the English letters A and B as 41 and 42, and that the ISO_8859_1 rule turns each of the 256 byte values from 0 to 255 into exactly one character, were also checked by a separate run on the same JDK. "Broken characters" is the number of characters read with ISO_8859_1 whose number is 128 or more, and English letters that are the same as the original characters are not counted. Characters that cannot be seen on the screen, such as numbers 128 to 159 (for example U+0080), were actually read, but in the diagram and the step sentences they are written by their number (U+0080 and so on). I opened the documentation and checked it. These are the Java SE 21 API documentation for Files (readAllBytes reads bytes without a rule, readString decodes bytes into characters with the specified character set, and writeString encodes characters into bytes with the specified character set and, with no options, creates the file or empties the existing content and writes), StandardCharsets (the constants UTF_8 and ISO_8859_1), NoSuchFileException (a checked exception thrown when an attempt is made to access a file that does not exist), and Byte (a byte is 8 bits, and how to read it as an unsigned value), and sections 1 and 3 of RFC 3629 (in UTF-8, U+0000 to U+007F take 1 byte, and U+0800 to U+FFFF take 3 bytes). However, the Files documentation does not list NoSuchFileException separately as an exception of these methods and lists only IOException, so that this exception occurs when you read a missing file was observed by running on this JDK, not from the documentation. In the same way, that Path.of("memo.txt") points to a file in the folder where the program was run was only observed by running, and I did not open the documentation to check it. Beyond that, the case where there are invalid bytes when reading as UTF-8 (MalformedInputException), other encodings such as UTF-16, line break characters, file path rules, streams and buffers that write to a file one byte at a time (in-depth lab: I/O streams and buffers), and serialization, which turns an object into bytes to store it (in-depth lab: Serialization), are not covered.

Previous lab: Compilation and the JVM · Next lab: Libraries and dependencies

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these