Files Make Your Data Survive the Program
Everything your programs have stored so far lived in memory and disappeared the moment the program ended. Files are how data outlives a single run: a student record system that forgets everything on exit is not a system.
Java has two generations of file APIs and you will see both in tutorials, which is confusing. The old one is java.io, built around the File class. The modern one is java.nio.file, built around Path and Files, and it has been the recommended choice since Java 7. It has better error messages, real support for file attributes and directory trees, and far simpler one-line helpers. This lesson uses the modern API throughout.
The two central types are easy to keep straight. A Path is just a location — a name pointing at a place in the filesystem. Creating one touches nothing on disk and never fails, even if nothing exists there. Files is a class of static methods that do things: read, write, copy, delete, list.
One rule governs everything here: file operations are unreliable by nature. The file may be missing, locked, on a disconnected drive, or read-only. That is why almost every method in Files declares IOException, a checked exception. The compiler will not let you ignore it, and it is right not to.
import java.nio.file.*;
import java.io.IOException;
import java.util.List;
// A Path is only a location. Nothing happens on disk here.
Path p = Path.of("data", "marks.txt"); // builds data/marks.txt correctly
System.out.println(p); // data\marks.txt on Windows
System.out.println(p.getFileName()); // marks.txt
System.out.println(p.toAbsolutePath()); // full path from the drive root
// Files does the actual work — and can fail, so IOException is checked
try {
String content = Files.readString(p);
System.out.println(content);
} catch (NoSuchFileException e) { // more specific first
System.out.println("That file does not exist: " + e.getMessage());
} catch (IOException e) {
System.out.println("Could not read it: " + e.getMessage());
}
// Build paths with Path.of, never by gluing strings together
// Path bad = Path.of("data" + "/" + "marks.txt"); // fragile
Path good = Path.of("data", "marks.txt"); // correct on every OS - Never hard-code
/or\in a path. Passing the pieces separately toPath.of()lets Java insert the right separator for the operating system, so the same code works on a Windows laptop and a Linux server.
Reading a File, Three Ways
Which reading method to use depends entirely on how big the file is, and the wrong choice on a large file will exhaust memory.
Files.readString(path) reads the whole file into one String. It is the simplest option and the right one for configuration files, small data files and anything you know is modest in size.
Files.readAllLines(path) gives you a List<String>, one entry per line, which is usually what you want for records. Both of these load the entire file into memory at once, so a two-gigabyte log file will produce an OutOfMemoryError rather than a result.
For large files, read one line at a time so that only one line is in memory. Files.newBufferedReader gives you a reader whose readLine() returns null at end of file — the classic while ((line = reader.readLine()) != null) loop, which looks odd but reads as "assign the next line, and keep going while it is not null". Files.lines(path) does the same lazily as a stream.
Both of those hold an open handle on the file, so both must be closed. Use try-with-resources and it happens automatically, even if an exception is thrown mid-file. Forgetting to close file handles in a long-running program eventually exhausts the operating system's limit.
import java.nio.file.*;
import java.io.*;
import java.util.List;
import java.util.stream.Stream;
Path path = Path.of("marks.txt");
try {
// 1. Whole file as one String — small files only
String all = Files.readString(path);
System.out.println(all);
// 2. Whole file as a list of lines — small to medium files
List<String> lines = Files.readAllLines(path);
System.out.println("Line count: " + lines.size());
for (String line : lines) {
System.out.println(line);
}
// 3. One line at a time — safe for files of any size
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
// 3b. The same idea as a stream — also needs closing
try (Stream<String> stream = Files.lines(path)) {
long passing = stream.filter(l -> !l.isBlank())
.map(l -> l.split(",")[1].strip())
.mapToInt(Integer::parseInt)
.filter(m -> m >= 40)
.count();
System.out.println("Students passing: " + passing);
}
} catch (IOException e) {
System.out.println("File problem: " + e.getMessage());
} readLine()strips the line break, so the string you get does not end with\n. If you are writing lines back out, you must add the separator yourself — or useFiles.writewith a list, which adds them for you.
Writing Files, and the Truncation Trap
Writing mirrors reading. Files.writeString(path, text) writes a String, and Files.write(path, listOfLines) writes a list with a line break after each entry.
Here is the trap, and it destroys student project data regularly. By default these methods overwrite the file completely. If the file already exists, its entire contents are discarded before your new text is written. A loop that saves one record at a time to the same file leaves you with exactly one record at the end — the last one — and no warning that the rest were deleted.
To add to a file rather than replace it, pass StandardOpenOption.APPEND. Pass CREATE alongside it, because APPEND on its own fails if the file does not yet exist. Those two together give you the behaviour a log file needs.
For writing many lines in a loop, opening and closing the file on every iteration is slow. Open a BufferedWriter once with try-with-resources and write inside the loop. newBufferedWriter takes the same open options, so appending works the same way. Note that write() does not add a line break — call newLine(), which uses the correct separator for the platform.
One safety habit worth adopting: when replacing an important file, write to a temporary file first and move it over the original only after the write succeeds. If the program crashes halfway through, the original is still intact.
import java.nio.file.*;
import java.io.*;
import java.util.List;
Path out = Path.of("report.txt");
try {
// Overwrites any existing content — this is the default!
Files.writeString(out, "Marks report\n");
// Writes a list, adding a line break after each entry
Files.write(out, List.of("Ananya,92", "Rahul,87", "Meera,78"));
// ---- The trap ----
for (String record : List.of("a", "b", "c")) {
Files.writeString(out, record + "\n"); // each call wipes the file
}
System.out.println(Files.readString(out)); // just "c" — a and b are gone
// ---- Appending, done correctly ----
Path log = Path.of("app.log");
Files.writeString(log, "Server started\n",
StandardOpenOption.CREATE, // make it if missing
StandardOpenOption.APPEND); // add, do not replace
// ---- Many lines: open once, write in the loop ----
try (BufferedWriter writer = Files.newBufferedWriter(Path.of("marks.csv"))) {
writer.write("name,marks");
writer.newLine(); // correct line ending
for (String line : List.of("Ananya,92", "Rahul,87")) {
writer.write(line);
writer.newLine();
}
}
} catch (IOException e) {
System.out.println("Write failed: " + e.getMessage());
} Files.readString,Files.writeString,Files.readAllLinesandFiles.newBufferedReaderall use UTF-8. That is what you want — it handles Hindi, Tamil, Bengali and every other script correctly. The olderFileReaderandFileWriterclasses historically used the platform's default encoding instead, which is why files written on one machine sometimes displayed as question marks on another.
Where Is My File? Relative Paths and the Working Directory
The most common file-handling frustration is not an API problem at all. You write Path.of("marks.txt"), the file is clearly sitting right there in your project, and Java insists it does not exist.
A relative path is resolved against the program's current working directory, which is the folder you were in when you launched the program — not the folder containing your .java file, and not the folder containing the .class file. When you run from an IDE, the working directory is usually the project root, so a file next to your source will not be found.
Diagnose it in one line: print System.getProperty("user.dir"), which gives the working directory, or print path.toAbsolutePath(), which shows exactly where Java is looking. Nine times out of ten the answer is immediately obvious.
The fixes, in order of preference: put the data file in the working directory that gets printed; or pass the file location as a command-line argument so the user decides; or use an absolute path, which works but ties the program to one machine.
The remaining everyday operations are all one-liners on Files: checking existence, creating folders, copying, moving, deleting and listing. Note that Files.delete() throws if the file is not there, while Files.deleteIfExists() returns a boolean — the second is usually what you want.
import java.nio.file.*;
import java.io.IOException;
import java.util.stream.Stream;
// Diagnose "file not found" in two lines
System.out.println(System.getProperty("user.dir"));
System.out.println(Path.of("marks.txt").toAbsolutePath());
try {
Path data = Path.of("data", "marks.txt");
// Check before you act
System.out.println(Files.exists(data));
System.out.println(Files.isDirectory(Path.of("data")));
if (Files.exists(data)) {
System.out.println(Files.size(data) + " bytes");
}
// Create folders — makes every missing level, and does not fail if present
Files.createDirectories(Path.of("data", "backups"));
// Copy, move, delete
Files.copy(data, Path.of("data", "backups", "marks-backup.txt"),
StandardCopyOption.REPLACE_EXISTING);
Files.move(Path.of("old.txt"), Path.of("new.txt"),
StandardCopyOption.REPLACE_EXISTING);
Files.deleteIfExists(Path.of("temp.txt")); // returns false if absent
// Files.delete(Path.of("temp.txt")); // throws NoSuchFileException
// List the files in a folder — the stream must be closed
try (Stream<Path> entries = Files.list(Path.of("data"))) {
entries.filter(Files::isRegularFile)
.forEach(System.out::println);
}
} catch (IOException e) {
System.out.println("Operation failed: " + e.getMessage());
} Files.createDirectories()creates every missing parent folder and succeeds quietly if the folder already exists.Files.createDirectory()creates only one level and throws if the parent is missing. The plural version is almost always the one you want.
A Realistic Example: Loading Records From a CSV
Reading a comma-separated file into objects is the most common real file-handling task in a student project, and it is worth walking through once with all the failure cases handled, because that is what separates code that works on your laptop from code that works on the assessor's.
Four things go wrong with real data files and none of them are exotic. The file has a header row that is not a record. Some lines are blank, especially the last one. Some rows have the wrong number of columns because someone edited the file by hand. And some numeric fields contain text, so Integer.parseInt throws.
The version below handles all four and — importantly — reports the line number when a row is bad instead of stopping with a stack trace that tells the user nothing. Skipping a bad row with a warning is often better than abandoning the whole file, though which you choose depends on whether partial data is acceptable.
Note the use of strip() on every field. Spreadsheet exports frequently include stray spaces, and Integer.parseInt(" 92") throws NumberFormatException — which is a genuinely annoying half-hour to debug if you have not met it before.
import java.nio.file.*;
import java.io.IOException;
import java.util.*;
record Student(int rollNo, String name, int marks) { }
public class CsvLoader {
public static List<Student> load(Path file) throws IOException {
List<Student> students = new ArrayList<>();
List<String> lines = Files.readAllLines(file);
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
if (line.isBlank()) continue; // skip empty lines
if (i == 0 && line.startsWith("rollNo")) continue; // skip header
String[] parts = line.split(",");
if (parts.length != 3) {
System.out.println("Line " + (i + 1) + ": expected 3 columns, "
+ "found " + parts.length + " — skipped");
continue;
}
try {
int roll = Integer.parseInt(parts[0].strip());
String nm = parts[1].strip();
int marks = Integer.parseInt(parts[2].strip());
students.add(new Student(roll, nm, marks));
} catch (NumberFormatException e) {
System.out.println("Line " + (i + 1) + ": bad number — skipped");
}
}
return students;
}
public static void main(String[] args) {
try {
List<Student> students = load(Path.of("students.csv"));
students.forEach(System.out::println);
double avg = students.stream().mapToInt(Student::marks).average().orElse(0);
System.out.printf("Average: %.2f%n", avg);
} catch (NoSuchFileException e) {
System.out.println("students.csv not found in "
+ System.getProperty("user.dir"));
} catch (IOException e) {
System.out.println("Could not read the file: " + e.getMessage());
}
}
} split(",")is fine for simple files. It breaks the moment a field itself contains a comma inside quotes, such as"Sharma, Ananya". Proper CSV has a surprising number of edge cases — quoting, escaped quotes, embedded newlines — so for real data use a small library such as OpenCSV or Apache Commons CSV rather than extending your own parser.
