To read a specific line from a file in Java, we skip the lines before it in Files.lines(path) and take the next one with findFirst(), which returns an empty Optional when the file has fewer lines. The stream reads the file lazily and stops at the requested line, so the method works for files of any size.
We need a single line when an error message points to line 57 of an upload, when a config format stores a value at a fixed line, or when a tool shows one entry of a large log by its number.
The following example reads line 3 of a five-line file, using the line number a user would see in an editor.
Path songs = Files.writeString(Files.createTempFile("songs", ".txt"), "Yesterday\nImagine\nHey Jude\nLet It Be\nHelp\n");
int lineNumber = 3;
try (Stream<String> lines = Files.lines(songs)) {
Optional<String> line = lines.skip(lineNumber - 1).findFirst(); // Optional[Hey Jude]
}
Notice the lineNumber – 1, because editors count lines from 1 while skip() counts how many lines to drop. We cover small files with readAllLines(), reading a range of lines, a BufferedReader loop, a line index for repeated lookups, and what happens when the line does not exist.
1. Line Numbers vs Zero-Based Indexes
People and editors number lines from 1, whereas List.get() and Stream.skip() work with zero-based positions. Every off-by-one bug in this task comes from mixing the two, so the helper methods in this article take the line number as the user sees it and convert it once.

| We want | readAllLines() call | Stream call |
|---|---|---|
| Line 1 | lines.get(0) | skip(0).findFirst() |
| Line 3 | lines.get(2) | skip(2).findFirst() |
| Line n | lines.get(n – 1) | skip(n – 1).findFirst() |
2. Reading a Line From a Small File
For files of a few megabytes, Files.readAllLines() loads every line into a List<String>, and get() returns any line by index. The list is handy when the program reads several lines from the same file, because the file is read only once.
Path songs = Files.writeString(Files.createTempFile("songs", ".txt"), "Yesterday\nImagine\nHey Jude\n");
List<String> lines = Files.readAllLines(songs);
String second = lines.get(1); // "Imagine"
String tenth = lines.get(9); // IndexOutOfBoundsException: Index 9 out of bounds for length 3
An index past the end throws an IndexOutOfBoundsException, so the safe version checks the size first and returns an Optional. The caller decides whether a missing line is an error or a default.
static Optional<String> lineOf(List<String> lines, int lineNumber) {
if (lineNumber < 1 || lineNumber > lines.size()) {
return Optional.empty();
}
return Optional.of(lines.get(lineNumber - 1));
}
List<String> lines = List.of("Yesterday", "Imagine", "Hey Jude");
Optional<String> third = lineOf(lines, 3); // Optional[Hey Jude]
Optional<String> tenth = lineOf(lines, 10); // Optional.empty
String shown = lineOf(lines, 10).orElse("(no such line)"); // "(no such line)"
Code that catches the IOException, prints it and calls get() on a null list turns a missing file into a NullPointerException. Letting the IOException propagate keeps the real cause visible.
3. How to Read a Specific Line From a Large File
The method Files.lines() returns a lazy stream, and skip() with findFirst() stops pulling lines as soon as it has the requested line. Memory use stays at one buffer no matter how large the file is, but the lines before the target are still read and decoded.
static Optional<String> readLine(Path file, int lineNumber) throws IOException {
if (lineNumber < 1) {
throw new IllegalArgumentException("Line numbers start at 1: " + lineNumber);
}
try (Stream<String> lines = Files.lines(file)) {
return lines.skip(lineNumber - 1).findFirst();
}
}
Path log = Files.writeString(Files.createTempFile("app", ".log"), "line 1\n".repeat(50_000) + "the target\n");
Optional<String> target = readLine(log, 50_001); // Optional[the target]
Optional<String> missing = readLine(log, 60_000); // Optional.empty
Optional<String> zero = readLine(log, 0); // IllegalArgumentException: Line numbers start at 1: 0
Calling findFirst().get() instead throws a NoSuchElementException when the file is shorter than expected. Returning the Optional pushes that decision to the caller, which can show a message instead of crashing.
4. Reading a Range of Lines
A range is a skip followed by a limit(). For lines 3 to 5, we skip 2 lines and keep 3, and the stream stops after the fifth line.
Path songs = Files.writeString(Files.createTempFile("songs", ".txt"), "Yesterday\nImagine\nHey Jude\nLet It Be\nHelp\nSomething\n");
int from = 3;
int to = 5;
try (Stream<String> lines = Files.lines(songs)) {
List<String> range = lines.skip(from - 1).limit(to - from + 1).toList(); // [Hey Jude, Let It Be, Help]
}
When the file ends before to, the list is shorter, and it is empty when the file ends before from. No exception is thrown, so we check range.size() when the full range is required.
5. Reading a Line With BufferedReader
A plain loop over BufferedReader.readLine() does the same work as the stream and suits code that also needs to inspect the skipped lines, for example to count comments. It works with checked exceptions inside the loop, which stream lambdas do not.
static Optional<String> readLineWithReader(Path file, int lineNumber) throws IOException {
try (LineNumberReader reader = new LineNumberReader(Files.newBufferedReader(file))) {
String line;
while ((line = reader.readLine()) != null) {
if (reader.getLineNumber() == lineNumber) {
return Optional.of(line);
}
}
}
return Optional.empty();
}
Path songs = Files.writeString(Files.createTempFile("songs", ".txt"), "Yesterday\nImagine\nHey Jude\n");
Optional<String> second = readLineWithReader(songs, 2); // Optional[Imagine]
Optional<String> fifth = readLineWithReader(songs, 5); // Optional.empty
We use LineNumberReader here because its getLineNumber() returns the 1-based number of the line that readLine() has returned, so no separate counter is needed.
6. Jumping to a Line Without Reading the Earlier Lines
Text files have no table of contents, so no method can find line 1,000,000 without scanning the bytes before it at least once. When a program reads lines from the same large file many times, for example a log viewer that pages through a file, we scan it once, store the byte offset where each line starts, and seek to that offset on every lookup.
static long[] indexLines(Path file) throws IOException {
List<Long> starts = new ArrayList<>(List.of(0L));
long position = 0;
try (InputStream in = new BufferedInputStream(Files.newInputStream(file))) {
int b;
while ((b = in.read()) != -1) {
position++;
if (b == '\n') {
starts.add(position); // the next line starts after '\n'
}
}
}
starts.add(position + 1); // end marker for the last line
return starts.stream().mapToLong(Long::longValue).toArray();
}
static String lineAt(Path file, long[] index, int lineNumber) throws IOException {
long start = index[lineNumber - 1];
int length = (int) (index[lineNumber] - start - 1); // without the '\n'
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {
ByteBuffer bytes = ByteBuffer.allocate(Math.max(length, 0));
while (bytes.hasRemaining() && channel.read(bytes, start + bytes.position()) > 0) {
// a positional read can return fewer bytes than requested
}
return new String(bytes.array(), StandardCharsets.UTF_8).stripTrailing();
}
}
Path log = Files.writeString(Files.createTempFile("app", ".log"), "first\nsecond\nthird");
long[] index = indexLines(log);
String line2 = lineAt(log, index, 2); // "second"
String line3 = lineAt(log, index, 3); // "third"
The line number must be between 1 and index.length – 1, and the stripTrailing() call removes the \r of Windows line endings. Reading by byte offset is safe for UTF-8, because ‘\n’ never appears inside a multi-byte character. The method RandomAccessFile.readLine() looks like a shortcut here, but it decodes each byte as one character, so it breaks accented letters and other non-ASCII text. The index costs 8 bytes per line, about 8 MB for a million lines, and it becomes stale when the file changes.
7. Real-World Example of Showing an Error in Context
Say a payroll app validates an uploaded CSV file and finds a bad amount on line 4. A message that shows the line and its neighbors lets the user fix the file without opening it in an editor, so the validator reads lines 3 to 5 with the range technique from section 4.
static String errorContext(Path file, int badLine) throws IOException {
int from = Math.max(1, badLine - 1);
try (Stream<String> lines = Files.lines(file)) {
List<String> nearby = lines.skip(from - 1).limit(3).toList();
StringBuilder message = new StringBuilder();
for (int i = 0; i < nearby.size(); i++) {
int number = from + i;
message.append(number == badLine ? "> " : " ").append(number).append(": ").append(nearby.get(i)).append('\n');
}
return message.toString();
}
}
Path payroll = Files.writeString(Files.createTempFile("payroll", ".csv"), "name,amount\nAlex,1200\nMaria,1500\nJohn,12OO\nRita,1100\n");
String context = errorContext(payroll, 4);
List<String> shown = context.lines().toList(); // [ 3: Maria,1500, > 4: John,12OO, 5: Rita,1100]
Line numbers in the message include the header line, because that is what the user sees when they open the file. If the app reports record numbers instead, the message must say so, since record 3 is line 4 in a file with a header.
8. Specific Line FAQs
Off-by-one errors and line numbers past the end of the file cause most follow-up questions on this task.
8.1. Is the first line of a file line 0 or line 1?
Line 1 for people and editors, and index 0 for List.get() and skip(). The helpers in this article accept the 1-based number and subtract 1 internally.
8.2. What happens if the line number is larger than the file?
Each method reports it in its own way. The call readAllLines().get() throws an IndexOutOfBoundsException, findFirst() returns an empty Optional, and a readLine() loop reaches null. The Optional versions in this article return Optional.empty() in every case.
8.3. What is the fastest way to read one line of a large file?
For a single lookup, Files.lines().skip(n – 1).findFirst(), because it stops at the line and keeps memory low. For many lookups in the same file, a byte offset index read once makes each later lookup a single seek.
8.4. How do we read the last line of a file?
For small files, lines.get(lines.size() – 1) after readAllLines(). For large files, Files.lines(path).reduce((first, second) -> second) returns the last line as an Optional while reading the file once.
9. Conclusion
To read a specific line, we convert the 1-based line number to a skip count and use Files.lines(path).skip(n – 1).findFirst() in try-with-resources. It returns an Optional, works for any file size and stops reading at the line.
For small files read several times, readAllLines() with a size check is simpler, and skip() with limit() returns a range. When the same large file is queried again and again, an index of byte offsets turns each lookup into one read. The article on reading a file line by line covers processing every line, and the Java IO tutorial lists the other file tasks.
10. References
- Stream Javadoc (Java 25)
- Files.lines() Javadoc (Java 25)
- FileChannel Javadoc (Java 25)
- RandomAccessFile.readLine() Javadoc (Java 25)
Happy Learning !!