The call Files.lines(path).count() inside a try-with-resources block is the shortest way to count lines in a file in Java, and it reads the file as a lazy stream that never holds more than one line in memory. The count matches what a text editor shows, including a last line without a trailing newline.
We count lines to show progress during an import, to check that an export has the expected number of records, to reject uploads that are too large, or to report the size of a log file.
The following example writes a three-line file and counts its lines with Files.lines().
Path orders = Files.writeString(Files.createTempFile("orders", ".csv"), "id,item\n1,tea\n2,cake\n");
try (Stream<String> lines = Files.lines(orders)) {
long count = lines.count(); // 3
}
Notice that the stream is closed by try-with-resources, because an open stream keeps the file handle open. We also cover LineNumberReader, Files.readAllLines(), a fast byte-level count for very large files, the edge cases that make counts differ, and counting only the lines that match a condition.
1. Counting Lines Using a Stream of Lines
The method Files.lines() returns a Stream<String> that reads the file lazily as the stream pulls each line. The terminal operation count() pulls all lines and returns a long, so the method works for files of any size.

static long countLines(Path file) throws IOException {
try (Stream<String> lines = Files.lines(file)) {
return lines.count();
}
}
Path report = Files.writeString(Files.createTempFile("report", ".txt"), "a\nb\nc");
long count = countLines(report); // 3, the last line has no newline
The method decodes the file as UTF-8. When the file contains bytes that are not valid UTF-8, for example a Windows-1252 export with accented letters, the stream throws an UncheckedIOException caused by MalformedInputException. We pass the right charset as the second argument, such as Files.lines(file, StandardCharsets.ISO_8859_1).
Path legacy = Files.write(Files.createTempFile("legacy", ".txt"), new byte[] {'c', 'a', 'f', (byte) 0xE9, '\n'});
long strict = countLines(legacy); // UncheckedIOException: java.nio.charset.MalformedInputException: Input length = 1
long lenient = Files.lines(legacy, StandardCharsets.ISO_8859_1).count(); // 1
The lenient line leaves the stream open to keep the comparison short, so real code wraps it in try-with-resources like countLines() does.
2. Using LineNumberReader
The LineNumberReader class is a BufferedReader that counts line terminators as it reads. We skip to the end of the file with skip(Long.MAX_VALUE), and getLineNumber() returns the count without creating a String for each line.
static int countWithLineNumberReader(Path file) throws IOException {
try (LineNumberReader reader = new LineNumberReader(Files.newBufferedReader(file))) {
reader.skip(Long.MAX_VALUE);
return reader.getLineNumber();
}
}
Path noNewline = Files.writeString(Files.createTempFile("a", ".txt"), "a\nb\nc");
Path withNewline = Files.writeString(Files.createTempFile("b", ".txt"), "a\nb\nc\n");
int first = countWithLineNumberReader(noNewline); // 3
int second = countWithLineNumberReader(withNewline); // 3
We do not add 1 to getLineNumber(), because on Java 16 and later that returns 4 for both files. Since Java 16, LineNumberReader counts the end of the file as a terminator when the last line has no newline, so the result needs no adjustment. The method returns an int, which overflows after about 2.1 billion lines, whereas count() returns a long.
3. Using Files.readAllLines() for Small Files
When the program needs the lines anyway, for example to validate and import them, we read them once with Files.readAllLines() and take the size of the list. The method loads the whole file into memory, so it fits files of a few megabytes, not multi-gigabyte logs.
Path students = Files.writeString(Files.createTempFile("students", ".txt"), "Alex\nMaria\nJohn\n");
List<String> names = Files.readAllLines(students);
int count = names.size(); // 3
String last = names.get(count - 1); // "John"
Like Files.lines(), the method readAllLines() decodes the file as UTF-8 by default and throws a MalformedInputException when a byte sequence is not valid in that charset.
4. Counting Line Breaks in Bytes for Large Files
All the methods above decode bytes into characters and check each character for a line terminator. For multi-gigabyte files in UTF-8 or ASCII, we can skip the decoding and count the ‘\n’ bytes, because in these encodings the byte 0x0A appears only as a line feed.
static long countLineBreaks(Path file) throws IOException {
long lines = 0;
byte last = '\n';
byte[] buffer = new byte[64 * 1024];
try (InputStream in = Files.newInputStream(file)) {
int read;
while ((read = in.read(buffer)) != -1) {
for (int i = 0; i < read; i++) {
if (buffer[i] == '\n') {
lines++;
}
}
if (read > 0) {
last = buffer[read - 1];
}
}
}
return last == '\n' ? lines : lines + 1; // count a last line without newline
}
Path big = Files.writeString(Files.createTempFile("big", ".log"), "x\n".repeat(100_000) + "end");
long count = countLineBreaks(big); // 100001
Path empty = Files.createTempFile("empty", ".log");
long none = countLineBreaks(empty); // 0
The method counts \n only, so it gives the same result for Linux and Windows files, but not for old Mac files that end lines with a lone \r. It does not work for UTF-16 files, where every character takes two bytes. The command wc -l on Linux and macOS counts the same ‘\n’ bytes but does not add the last line when it has no newline, so its result can be one lower.
5. Edge Cases That Change the Count
Line counts differ between tools mostly because of the last line and of empty lines. The three Java methods agree on every case except the old Mac format, where the byte counter sees no \n at all.
| File content | Files.lines().count() | LineNumberReader | countLineBreaks() | wc -l |
|---|---|---|---|---|
| Empty file | 0 | 0 | 0 | 0 |
| a\nb\nc (no newline at the end) | 3 | 3 | 3 | 2 |
| a\nb\nc\n | 3 | 3 | 3 | 3 |
| a\n\nb\n (blank line) | 3 | 3 | 3 | 3 |
| a\r\nb\r\n (Windows) | 2 | 2 | 2 | 2 |
| a\rb (old Mac) | 2 | 2 | 1 | 0 |
A blank line counts as a line in every method. When the requirement is the number of records in a file, we often want to skip blank lines and a header row, which the next section shows.
6. Counting Non-Empty or Matching Lines
A stream of lines makes conditional counts a one-line change. We add a filter() before count(), and the file is still read only once.
Path export = Files.writeString(Files.createTempFile("export", ".csv"), "id,item\n1,tea\n\n2,cake\n# end\n");
try (Stream<String> lines = Files.lines(export)) {
long records = lines.skip(1) // skip the header
.filter(line -> !line.isBlank())
.filter(line -> !line.startsWith("#"))
.count(); // 2
}
For a CSV file whose quoted values can contain line breaks, a line count is not a record count. In that case we count records with a CSV parser, as shown in parsing CSV files in Java.
7. Picking a Way to Count Lines in a File
Each method fits a different file size and charset situation. For most code, Files.lines() is the default, and the others cover the cases where its UTF-8 check or its per-line String objects get in the way.
| Method | Memory | Bad bytes in the charset | Best for |
|---|---|---|---|
| Files.lines(path).count() | One line at a time | Throws UncheckedIOException | Default choice, conditional counts |
| LineNumberReader.skip() | One buffer | Throws from Files.newBufferedReader(), ignored with FileReader | Counting without creating strings |
| Files.readAllLines().size() | Whole file | Throws MalformedInputException | Small files whose lines we also use |
| countLineBreaks() | One byte buffer | Not affected, no decoding | Very large UTF-8 or ASCII files |
The article on reading a file line by line covers processing each line after counting, and the Java IO tutorial groups the related file tasks.
8. Real-World Example of Import Progress
Say a shop admin panel imports a product feed of a few hundred thousand lines each night. The import page shows “row 1,200 of 250,000”, so the job counts the data rows first and passes the total to the progress tracker before it starts the import loop.
record ImportPlan(Path file, long totalRows) {}
static ImportPlan planImport(Path feed) throws IOException {
long rows = countLineBreaks(feed) - 1; // minus the header line
if (rows <= 0) {
throw new IllegalArgumentException("Feed has no data rows: " + feed.getFileName());
}
return new ImportPlan(feed, rows);
}
Path feed = Files.writeString(Files.createTempFile("feed", ".csv"), "sku,price\n" + "p1,10\n".repeat(1_200));
ImportPlan plan = planImport(feed);
long total = plan.totalRows(); // 1200
The byte counter reads the file once more before the import, which adds a short pass over the data. In return, the job can reject an empty feed before it touches the database, and the progress bar has a real total.
9. Line Counting FAQs
Speed, memory use and the last line of the file are the points that come up next once a line count works.
9.1. What is the fastest way to count lines in a Java file?
Counting ‘\n’ bytes with a large byte buffer, as in countLineBreaks(), skips the character decoding that the other methods do. It works only for UTF-8, ASCII and similar encodings. For files smaller than a few hundred megabytes, Files.lines().count() is fast enough and shorter.
9.2. Does Files.lines().count() read every line of the file?
Yes. The stream has no size information, so count() pulls and decodes every line, but it keeps only the current line in memory. Only Files.readAllLines() keeps all lines at once.
9.3. Why does my count differ from wc -l by one?
The command wc -l counts newline characters, so a last line without a trailing newline is not counted. The Java methods in this article count that line, so for such a file they return one more than wc -l.
9.4. Is the result of LineNumberReader different on Java 8 and Java 17?
Yes, for files whose last line has no newline. On Java 8 to 15, getLineNumber() after skip(Long.MAX_VALUE) did not count that last line, and since Java 16 it does. Code that adds 1 for all files was wrong before Java 16 for files that end with a newline, and is wrong for all files since Java 16.
10. Conclusion
The shortest correct way to count lines is Files.lines(path).count() in try-with-resources, with an explicit charset when the file is not UTF-8. The LineNumberReader approach gives the same count without an extra 1 on Java 16 and later, and readAllLines() suits small files we read anyway.
For very large UTF-8 files, counting ‘\n’ bytes avoids decoding, and it needs the extra check for a last line without a newline. When blank lines, comments or a header must not count, a filter() before count() gives the record count in the same pass.
11. References
- Files.lines() Javadoc (Java 25)
- LineNumberReader Javadoc (Java 25)
- Files.readAllLines() Javadoc (Java 25)
- JDK-8241020, LineNumberReader end of stream (CSR)
Happy Learning !!