File & Path Basics
File & Path Basics
Before a Java program can read or write anything, it needs a way to represent a location on the filesystem. Two APIs exist for this: java.io.File, part of Java since version 1.0, and java.nio.file.Path, introduced in Java 7 as part of NIO.2. This article covers what each represents, how to construct one, and how to query basic filesystem information — reading and writing actual file content is covered in this section's dedicated Reading Files and Writing Files articles.
What Is a File or Path?
Both are objects that represent a location — a file or a directory — without guaranteeing that location actually exists. java.io.File reports most failures as a boolean return value, which makes it easy to miss a failure if the return value is not checked. java.nio.file.Path, combined with the Files utility class, represents the same idea but reports failures as real exceptions.
1// File: BeforeFileBasics.java
2import java.io.File;
3
4public class BeforeFileBasics {
5 public static void main(String[] args) {
6 File dir = new File("reports");
7 File file = new File(dir, "summary.txt");
8
9 System.out.println("Path: " + file.getPath().replace('\\', '/'));
10 System.out.println("Name: " + file.getName());
11 System.out.println("Parent: " + file.getParent());
12 }
13}Output:
Path: reports/summary.txt
Name: summary.txt
Parent: reports
1// File: AfterFileBasics.java
2import java.nio.file.*;
3
4public class AfterFileBasics {
5 public static void main(String[] args) {
6 Path dir = Path.of("reports");
7 Path file = dir.resolve("summary.txt");
8
9 System.out.println("Path: " + file.toString().replace('\\', '/'));
10 System.out.println("Name: " + file.getFileName());
11 System.out.println("Parent: " + file.getParent());
12 }
13}Output:
Path: reports/summary.txt
Name: summary.txt
Parent: reports
Both produce the same result here — the difference in practice shows up once error handling and building paths from several dynamic pieces are involved.
How It Works Internally
Neither constructing a File nor constructing a Path touches the filesystem at all — both are just in-memory representations of a location until a method like exists() or createFile() is actually called.
One sentence before the diagram: a File or Path object is nothing more than a wrapped string until something forces an actual filesystem lookup.
new File("reports/summary.txt") Path.of("reports/summary.txt")
| |
just a string wrapper just a string wrapper
in memory, no disk access in memory, no disk access
| |
v v
file.exists() -----------------------> actual filesystem call happens
Files.exists(path) HERE, not at construction time
That distinction matters because a File or Path can be freely built, passed around, and compared long before the location it points to is ever checked, or even created. Path.of() accepts any number of segments and joins them correctly for the current platform.
1// File: PathConstructionExample.java
2import java.io.File;
3import java.nio.file.*;
4
5public class PathConstructionExample {
6 public static void main(String[] args) {
7 Path multiSegment = Path.of("data", "2026", "orders.csv");
8 System.out.println(multiSegment.toString().replace('\\', '/'));
9
10 Path fromFile = new File("logs/app.log").toPath();
11 System.out.println(fromFile.toString().replace('\\', '/'));
12
13 File backToFile = multiSegment.toFile();
14 System.out.println(backToFile.getClass().getSimpleName());
15 }
16}Output:
data/2026/orders.csv
logs/app.log
File
resolve() and Path.of() insert the platform's separator correctly, exactly once, between every segment — never build a path by concatenating raw strings with a hardcoded / or \.
toPath() / toFile() convert between the two APIs whenever a mix of legacy and modern code needs to interoperate, and this same lazy-until-checked behavior is what makes listing a directory with Files.list() or converting between File and Path entirely free of filesystem cost until a real query actually runs.
Real-World Example
An invoice batch processor needs to validate each uploaded file before attempting to process it — confirming it exists, is an actual file rather than a directory, and is not empty, exactly the kind of existence-and-type check this article's internals section sets up.
1// File: InvoiceFileValidator.java
2import java.nio.file.*;
3import java.io.IOException;
4
5public class InvoiceFileValidator {
6
7 public String validate(Path invoiceFile) throws IOException {
8 if (!Files.exists(invoiceFile)) {
9 return invoiceFile.getFileName() + ": missing";
10 }
11 if (!Files.isRegularFile(invoiceFile)) {
12 return invoiceFile.getFileName() + ": not a regular file";
13 }
14 if (Files.size(invoiceFile) == 0) {
15 return invoiceFile.getFileName() + ": empty file";
16 }
17 return invoiceFile.getFileName() + ": valid (" + Files.size(invoiceFile) + " bytes)";
18 }
19}1// File: InvoiceFileValidatorDemo.java
2import java.nio.file.*;
3import java.io.IOException;
4
5public class InvoiceFileValidatorDemo {
6 public static void main(String[] args) throws IOException {
7 InvoiceFileValidator validator = new InvoiceFileValidator();
8
9 Path tempDir = Files.createTempDirectory("invoices");
10
11 Path validInvoice = tempDir.resolve("invoice-2001.csv");
12 Files.writeString(validInvoice, "id,amount\n1,4500\n2,3200\n");
13
14 Path emptyInvoice = tempDir.resolve("invoice-2002.csv");
15 Files.createFile(emptyInvoice);
16
17 Path missingInvoice = tempDir.resolve("invoice-9999.csv");
18
19 System.out.println(validator.validate(validInvoice));
20 System.out.println(validator.validate(emptyInvoice));
21 System.out.println(validator.validate(missingInvoice));
22
23 Files.delete(validInvoice);
24 Files.delete(emptyInvoice);
25 Files.delete(tempDir);
26 }
27}Output:
invoice-2001.csv: valid (24 bytes)
invoice-2002.csv: empty file
invoice-9999.csv: missing
A mistake that appears often in fresher pull requests is assuming a path that exists() is automatically safe to read — a path can exist and still be a directory, a zero-byte placeholder, or something else entirely unusable as an invoice. Checking existence, regular-file status, and size as three separate, explicit steps, exactly as InvoiceFileValidator does here, produces a message that actually explains what went wrong instead of failing deeper inside the processing logic with a confusing error. Every meaningful Files method here also declares throws IOException, tying this topic directly to exception handling — a path operation that can fail due to permissions, a missing parent directory, or a full disk always needs a real plan for that failure, not just a validation message.
Best Practices
Prefer Path and Files over File in new code — exceptions surface failures immediately instead of a boolean that is easy to forget to check, and Path methods like resolve() avoid manual separator handling entirely.
Build paths with Path.of() and resolve() instead of concatenating strings with a hardcoded / or \, exactly as PathConstructionExample demonstrates.
Check a path's existence and type explicitly before depending on it, rather than assuming a successful earlier step (like an upload completing) guarantees the file is still there and readable later.
Keep toFile() and toPath() as the bridge between the two APIs only where genuinely necessary — mixing File and Path throughout the same class is harder to follow than committing to one.
Common Mistakes
Concatenating path segments manually, rather than using resolve(), easily produces a doubled separator when both pieces already include one.
1// File: PathConcatenationMistake.java
2import java.nio.file.*;
3
4public class PathConcatenationMistake {
5 public static void main(String[] args) {
6 String base = "reports/";
7 String fileName = "/summary.txt";
8
9 String concatenated = base + fileName;
10 System.out.println("Concatenated: " + concatenated);
11
12 Path resolved = Path.of("reports").resolve("summary.txt");
13 System.out.println("Resolved: " + resolved.toString().replace('\\', '/'));
14 }
15}Output:
Concatenated: reports//summary.txt
Resolved: reports/summary.txt
Assuming File's boolean-returning methods behave like Files's exception-throwing equivalents is a second, easy mistake — File.delete() on a missing file silently returns false, while Files.delete() on the same missing path throws.
1// File: SilentFailureMistake.java
2import java.io.File;
3import java.io.IOException;
4import java.nio.file.*;
5
6public class SilentFailureMistake {
7 public static void main(String[] args) {
8 File missingFile = new File("does-not-exist-12345.txt");
9 boolean deleted = missingFile.delete();
10 System.out.println("File.delete() returned: " + deleted);
11
12 try {
13 Files.delete(Path.of("does-not-exist-12345.txt"));
14 } catch (IOException e) {
15 System.out.println("Files.delete() threw: " + e.getClass().getSimpleName());
16 }
17 }
18}Output:
File.delete() returned: false
Files.delete() threw: NoSuchFileException
Code that checks File.delete()'s return value only in a debugger, and ignores it in production, can fail to delete a file for months without anyone noticing — Files.delete()'s thrown exception is much harder to silently ignore.
Interview Questions
Q1. What is the difference between java.io.File and java.nio.file.Path?
File, part of Java since version 1.0, reports most failures as a boolean return value. Path, introduced in Java 7's NIO.2, is used together with the Files utility class, which reports failures as checked exceptions and offers richer operations like resolve(), directory streaming, and file attribute views. The nuance interviewers are listening for is whether you can name the exception-based error handling as the actual reason Path is preferred, not just recite "Path is newer."
Q2. Does constructing a File or Path object perform any filesystem I/O?
No. Both are purely in-memory representations of a location — no filesystem access happens until a method like exists(), Files.size(), or createFile() is actually called on them. This question is often a filter for whether you understand that a stale Path object can silently point at nothing, since holding the reference never confirms the location is real.
Q3. How do you convert between a File and a Path object?
file.toPath() converts a File to a Path, and path.toFile() converts a Path back to a File, letting the two APIs interoperate when part of a codebase still expects the older type. Interviewers listening closely want to hear that this is a zero-cost, in-memory conversion, not something that touches disk.
Q4. What is the difference between getName() and getPath() on a File object?
getName() returns only the final segment of the path — just the file or directory's own name. getPath() returns the full path string as it was constructed, including every parent segment and the platform's separator character. The nuance being tested is whether you know getPath() reflects exactly how the File was constructed, not a normalized or absolute form.
Q5. Why is Path.resolve() generally preferred over manual string concatenation for building paths?
resolve() correctly inserts the platform's separator exactly once between segments, avoiding both the missing-separator and doubled-separator bugs that manual string concatenation can introduce, as shown in this article's Common Mistakes section. A strong answer connects this directly to platform independence, since a hardcoded separator silently breaks on the other operating system.
Q6. Does File.delete() throw an exception when the file does not exist?
No, it returns false silently. Files.delete(), the NIO.2 equivalent, throws a NoSuchFileException in the same situation, which is much harder to accidentally ignore than an unchecked boolean. Product-company interviewers often follow up asking how this affects error handling strategy in a real deletion pipeline — the answer is that silent booleans need to be checked explicitly everywhere, while exceptions propagate on their own.
Q7. What does Files.exists() actually check, and can it be affected by filesystem permissions?
It checks whether the filesystem currently reports the path as present, and its result is immediately outdated the moment it returns, since another process could change the filesystem at any time. It can also return false for a path that exists but the JVM lacks permission to access, since Files.exists() suppresses the underlying IOException in that case rather than throwing. The nuance interviewers are listening for is awareness of this time-of-check-to-time-of-use gap, a classic source of race conditions in file-handling code.
FAQs
Is java.io.File deprecated?
No. It carries no @Deprecated annotation and is still fully supported — Path and Files are simply the recommended choice for new code because of their exception-based error handling and richer feature set.
Can a Path represent a location that doesn't exist yet?
Yes. A Path is just a representation of a location, valid or not — it is entirely normal to build a Path for a file that will only be created later, such as an output file a program is about to write.
What is the difference between a relative and an absolute Path?
A relative path, like Path.of("reports/summary.txt"), is resolved against the JVM's current working directory when used. An absolute path, like Path.of("/var/log/app.log") or a Windows path starting with a drive letter, identifies a location independent of the working directory. path.toAbsolutePath() converts a relative path to an absolute one.
Does Path.of() work the same as the older Paths.get()?
Yes, they behave identically. Path.of() was added in Java 11 as a shorter, static-factory-method equivalent defined directly on the Path interface itself, and is generally preferred over the older Paths.get() in new code.
Can File and Path be mixed in the same codebase?
Yes, and toFile() / toPath() exist specifically to bridge the two — a common situation when integrating with an older library that still expects a File while the rest of the codebase has moved to Path.
What does toAbsolutePath() do if the path is already absolute?
It returns an equivalent absolute path unchanged in meaning — calling toAbsolutePath() on an already-absolute path is safe and does not alter what location it represents.
Is File.separator the same on every operating system?
No, it is platform-dependent — a backslash on Windows and a forward slash on Unix-based systems. This is exactly why building paths with resolve() or Path.of() is preferred over hardcoding either character directly in a string.
Summary
File and Path both represent a location on the filesystem without performing any I/O until a method is actually called on them, but they differ sharply in how they report failure — File through boolean return values that are easy to overlook, Path and Files through checked exceptions that are much harder to ignore. Building a path from several pieces with resolve() avoids both the missing-separator and doubled-separator bugs manual string concatenation invites.
The habit worth carrying forward from this article's invoice validator is checking existence, type, and size as separate, explicit steps rather than assuming one successful check implies the others — exactly the discipline that keeps a validation failure message actually useful instead of a generic downstream error.