Java Read Files
›Short versionOpen if you only have a minute.
- A text read decodes bytes with a charset. Pass StandardCharsets.UTF_8 when the file was written as UTF-8.
- Scanner on a File and FileReader on a path string use Charset.defaultCharset(), which was platform-specific before Java 18.
- Files.readString loads the whole file into one String. A large log is a BufferedReader loop that keeps one line.
- Illegal UTF-8 is malformed input. newBufferedReader throws MalformedInputException. The file is still there.
- A user-supplied name still has to resolve inside a root you chose. Decode only after that check accepts the path.
Reading recovers characters from bytes. The path says where the bytes are. The says what they mean. Both have to match the write.
The whole note, or one line
fits a short file whose entire text is the value you want.
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class ReadNote {
public static void main(String[] args) throws IOException {
Path file = Path.of("notes", "today.txt");
String text = Files.readString(file, StandardCharsets.UTF_8);
System.out.println(text);
}
}$ java ReadNotecafé
Those bytes were 63 61 66 c3 a9 0a from the write page. Decoded as UTF-8 they are the line café. The same call with the charset omitted also uses UTF-8. Writing the charset at the call keeps the agreement next to the path.
A is the other shape. This prints each line and holds one line at a time, which is the way through a log that does not fit comfortably in one String.
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class ReadLines {
public static void main(String[] args) throws IOException {
Path file = Path.of("notes", "today.txt");
try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
}When the bytes are someone else’s charset
A Scanner built on new File("notes/today.txt") decodes with the . Feed it the UTF-8 bytes of café on a runtime whose default is Windows-1252 and the line prints as café, with a copyright sign in place of é. Byte C3 is à in that table, and byte A9 is the copyright sign. The file was read. It was read with the other mapping.
new Scanner(path, StandardCharsets.UTF_8) sets the table. So does Files.newBufferedReader. Prefer those when the file’s charset is a decision you made on the write page.
Some files are simply not valid in the charset you asked for. A byte that was cut off, or a file that was actually Windows-1252, is for a UTF-8 reader. readLine throws MalformedInputException. Catch it as IOException if you only need to report the failure. The path still names the original bytes, so a later read with the charset that wrote them can succeed.
readString was given UTF-8, and the call failed because a byte sequence is illegal in UTF-8. What did the reader refuse to invent?
A character. The NIO read reports the coding failure instead of inserting a replacement and continuing. The bytes remain in the file. The charset argument and the bytes disagree, or the write was cut off inside a multi-byte character, which is one result of a partial write.
Read a name the user typed
Decode after the path has been pinned inside a root. toRealPath() follows symlinks, so a link inside the root that points at a file outside it fails startsWith on the real path. The file has to exist; a missing path throws NoSuchFileException from toRealPath().
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
static String readUnderRoot(Path root, String userInput) throws IOException {
Path realRoot = root.toAbsolutePath().normalize().toRealPath();
Path candidate = realRoot.resolve(userInput).normalize();
if (!candidate.startsWith(realRoot) || candidate.equals(realRoot)) {
throw new IllegalArgumentException("path escapes the root");
}
Path real = candidate.toRealPath();
if (!real.startsWith(realRoot) || !Files.isRegularFile(real)) {
throw new IllegalArgumentException("path escapes the root");
}
return Files.readString(real, StandardCharsets.UTF_8);
}resolve of an absolute user string such as /etc/passwd returns that absolute path. normalize then startsWith rejects it. The second check, on toRealPath(), is what rejects a relative path that stays lexically inside the root and follows a symlink out of it.
Java Delete Files uses the same boundary before it removes anything, and it has to deal with a directory that still contains entries.