Read File Line By Line In Java

15 min read

Introduction

Reading a file line by line in Java is a fundamental task for many developers, from beginners processing simple text logs to seasoned engineers handling massive data streams. This guide walks you through the most common approaches, explains the underlying mechanics, and highlights best practices to ensure strong, efficient file reading. Which means whether you prefer the classic BufferedReader + FileReader combo, the more readable Scanner class, or modern Java 7+ resource management, you’ll find clear, step‑by‑step instructions that you can copy directly into your projects. By the end of this article you’ll understand not only how to read files line by line in Java, but also why each technique works the way it does, enabling you to choose the optimal solution for any scenario.

Understanding Java File I/O Basics

Java’s I/O (Input/Output) model is built around streams, which represent a sequence of data flowing from a source to a destination. For text files, the most relevant stream types are character streams (as opposed to byte streams) because they handle Unicode characters directly, eliminating the need for manual encoding conversions.

Key Classes

  • FileReader – a simple character stream that reads from a file using the default platform encoding.
  • BufferedReader – wraps a FileReader (or any other Reader) to provide efficient buffering, reducing the number of system calls.
  • Scanner – offers a higher‑level, token‑based interface, useful for parsing numbers, booleans, and delimited text.
  • BufferedReader + try‑with‑resources – ensures streams are closed automatically, even if an exception occurs.

These classes form the backbone of line‑by‑line reading in Java and are covered in detail below Small thing, real impact..

Step‑by‑Step Guide to Read a File Line by Line

Below is a practical recipe you can follow regardless of which class you decide to use. The steps are the same: locate the file, create the appropriate reader, loop over lines, and process each line.

Using BufferedReader and FileReader

  1. Import the required packages

    import java.FileReader;
    import java.BufferedReader;
    import java.That said, io. io.io.
    
    
  2. Create the reader inside a try‑with‑resources block – this guarantees the stream is closed.

    try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
        // reading logic
    } catch (IOException e) {
        // handle exception
    }
    
  3. Read lines in a loop – br.readLine() returns null when the end of the stream is reached.

    String line;
    while ((line = br.Worth adding: readLine()) ! = null) {
        System.out.
    
    
  4. Process each line – you can parse, transform, or store the line as needed.

Using Scanner

  1. Import

    import java.Scanner;
    import java.io.Still, util. On top of that, file;
    import java. io.
    
    
  2. Create a Scanner over the file

    try (Scanner scanner = new Scanner(new File("example.txt"))) {
        while (scanner.hasNextLine()) {
            String line = scanner.
    
    *Scanner* internally uses a buffer, but it also provides convenient methods for tokenizing lines.  
    
    

Using Java 7+ try‑with‑resources for Automatic Cleanup

The try‑with‑resources syntax introduced in Java 7 simplifies resource management. Consider this: any class that implements AutoCloseable (including BufferedReader, FileReader, and Scanner) can be used directly. The JVM automatically calls close() when exiting the block, even if an exception propagates Most people skip this — try not to. But it adds up..

Processing Each Line with a Loop

Whether you choose BufferedReader or Scanner, the loop pattern remains the same:

while ((line = reader.readLine()) != null) {
    // your business logic here
}

or

while (scanner.hasNextLine()) {
    String line = scanner.nextLine();
    // your business logic here
}

Both approaches guarantee that every line is visited exactly once, and the loop terminates cleanly when the file ends Worth knowing..

Scientific Explanation of How the Stream Works

Understanding the internal mechanics helps you diagnose performance issues and choose the right tool.

Internal Buffering

When you wrap a raw FileReader with a BufferedReader, Java allocates a buffer (default size 8 KB). Worth adding: each read operation pulls a chunk of bytes from the OS into this buffer, then the BufferedReader hands out characters line by line without repeatedly invoking the OS. This drastically reduces I/O overhead, especially for large files But it adds up..

Character Encoding Considerations

FileReader uses the platform default encoding (often UTF‑8 on modern systems, but could be ISO‑8859‑1 on others). If your file uses a different encoding, you must explicitly specify it, for example:

FileReader fr = new FileReader("example.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(
    new FileInputStream("example.txt"), StandardCharsets.UTF_8));

Choosing the correct charset prevents mojibake (garbled text) and ensures accurate line reading.

Best Practices and Common Pitfalls

Handling Exceptions

Always catch IOException (or its subclasses) because file operations are prone to failures such as missing files, permission issues, or disk errors. Logging the exception stack trace helps with debugging Not complicated — just consistent..

Managing Large Files

Reading an entire file into memory is rarely advisable. The line‑by‑line approach keeps memory usage constant, regardless of file size. g.Still, be aware that extremely large lines (e.This leads to , multi‑megabyte log entries) can still cause memory spikes. In such cases, consider splitting lines or processing them in chunks The details matter here..

Choosing the Right Tool

  • Use BufferedReader when you need raw line strings and prioritize speed.
  • Use Scanner when you also need to parse numbers, dates, or delimited values within each line.
  • Avoid FileReader alone for anything beyond tiny files because it lacks buffering.

Common Mistakes

  • Forgetting to close streams (prevented by try‑with‑resources).

More Gotchas to Watch Out For

Pitfall Why It Hurts How to Avoid It
Assuming every line ends with \n Files created on Windows (\r\n) or old macOS (\r) can cause the line‑reading logic to treat the trailing carriage‑return as part of the content, corrupting data. Use a StringBuilder (or StringJoiner) and append each line, then convert to a string once the loop finishes. parseInt()will throwInputMismatchExceptionorNumberFormatExceptionif the line contains non‑numeric characters, crashing the pipeline. For retry scenarios, open a new reader each time or store the lines in a temporary collection (e.If you need both raw lines and tokenised values, read the line withBufferedReaderand hand it toScanner (new Scanner(line)`) for parsing.
Parsing numbers without validation Scanner.In real terms, a FileReader/BufferedReader` cannot be “rewound” without reopening the file. Wrap the parsing in a try/catch block, log the offending line, and decide whether to skip it, default to a sentinel value, or abort the whole process. Also,
Assuming readLine() is thread‑safe If you share a single BufferedReader across multiple threads and call readLine() concurrently, the internal buffer can be corrupted, throwing IOException or returning garbled lines.
Mixing Scanner and BufferedReader in the same method Scanner internally wraps a BufferedReader (or InputStreamReader) and adds its own tokenisation layer. Worth adding: Use `BufferedReader. g.On the flip side,
Ignoring the cost of String concatenation in a loop Building a result string with += inside the loop creates a new String object each iteration, leading to O(n²) memory copying for long files. g., List<String>) after the first pass. That said,
Not resetting the stream for retry If an exception occurs mid‑processing, you might want to re‑attempt the operation from the beginning. Keep the file open only for the duration of a single pass. Using both on the same source double‑wraps the stream, degrading performance and confusing resource management. Think about it: readLine()– it normalises all three line‑break conventions – or, when using a custom delimiter, explicitly strip\r if present. nextInt() or Integer., synchronized` block).

Testing Your File‑Reading Logic

  1. Unit‑test with an in‑memory source – Use ByteArrayInputStream wrapped in InputStreamReader → BufferedReader. This eliminates I/O side‑effects and lets you assert exact line processing.
  2. Edge‑case files – Create test files that contain:
    • Empty lines
    • Very long lines (several megabytes)
    • Mixed line endings (\n, \r\n, \r)
    • Non‑UTF‑8 byte sequences (if you deliberately test encoding handling)
  3. Exception scenarios – Simulate FileNotFoundException, AccessDeniedException, and IOException by mocking FileReader or FileInputStream. Verify that your try‑with‑resources block still releases resources.

A Real‑World Example: Parsing a CSV Log

Path logFile = Paths.get("logs", "events.csv");

try (BufferedReader reader = Files.newBufferedReader(logFile,
        StandardCharsets.UTF_8)) {

    // Header line – skip or use as schema
    String header = reader.On top of that, readLine(); // e. g. 

    while (true) {
        String raw = reader.readLine();
        if (raw == null) break; // EOF

        // Simple CSV split – beware of quoted commas in production code
        String[] parts = raw.split(",", -1);
        if (parts.length < 3) {
            log.

        long ts = Long.parseLong(parts[0]);
        String level = parts[1];
        String message = parts[2];

        // Business logic – e.g. aggregate per level
        counters.

} catch (IOException e) {
    // Log and possibly re‑throw as a runtime wrapper
    throw new UncheckedIOException("Failed to process CSV log", e);
}

Leveraging the Stream API for Declarative Processing

Since Java 8, Files.readLine()) !Even so, lines() has turned line‑oriented reading into a first‑class stream operation. This approach eliminates the explicit while ((line = reader.= null) boilerplate and composes naturally with filter, map, and collect.

Path path = Paths.get("data", "sensor-readings.txt");

try (Stream lines = Files.But uTF_8)) {
    Map errorCounts = lines
        . trim())   // extract error code
        .Day to day, lines(path, StandardCharsets. In real terms, contains("ERROR"))
        . collect(Collectors.groupingBy(
            Function.Because of that, split("\\|")[1]. Worth adding: filter(line -> line. map(line -> line.identity(),
            Collectors.

    errorCounts.forEach((code, count) -> 
        System.out.

**Why this shines**  
* **Lazy evaluation** – Lines are read only as the pipeline demands them, so memory stays constant even for multi‑gigabyte files.  
* **Parallelism** – Swapping `.stream()` for `.parallelStream()` (on a splittable source) can use multiple cores, though for pure I/O‑bound work the gain is often marginal.  
* **Exception transparency** – `Files.lines()` wraps checked `IOException` in an `UncheckedIOException`, letting you keep business logic free of `try/catch` clutter while still propagating failures.

**Caveat**: The stream *must* be closed. The `try‑with‑resources` block above guarantees that the underlying `BufferedReader` is released even if the pipeline throws.

---

### Handling Very Large Files: Memory‑Mapped I/O & Chunked Reading  

When a single line can span hundreds of megabytes (e.g., a minified JSON dump or a base64‑encoded blob), `readLine()` becomes a liability because it materializes the entire line in heap memory. 

| Technique | How it works | When to choose it |
|-----------|--------------|-------------------|
| **`FileChannel.map()` (MappedByteBuffer)** | Maps a region of the file directly into virtual memory; the OS pages data in/out on demand. | Random access, read‑only scans, or when you need to hand a `ByteBuffer` to a parser that works off‑heap (e.g., Jackson’s `ByteSource`). Which means |
| **Custom `Spliterator` over fixed‑size buffers** | Reads `N` bytes at a time, splits on a delimiter without allocating a full-line `String`. | Streaming parsers (CSV, log formats) where you control the tokenization loop. 

A minimal memory‑mapped line iterator looks like this:

```java
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
    MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
    CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
    ByteBuffer lineBuf = ByteBuffer.allocate(8192); // reusable line buffer

    while (buffer.Also, get();
            if (b == '\n') break;
            if (lineBuf. clear();
        // Fill lineBuf until '\n' or buffer exhausted
        while (buffer.Think about it: hasRemaining()) {
        lineBuf. hasRemaining()) {
            byte b = buffer.Practically speaking, remaining() == 0) lineBuf = expand(lineBuf); // grow if needed
            lineBuf. Practically speaking, flip();
        String line = decoder. put(b);
        }
        lineBuf.decode(lineBuf).

Counterintuitive, but true.

> **Tip**: For production‑grade off‑heap parsing, consider libraries such as **Apache Commons IO** (`LineIterator`), **Okio** (`BufferedSource`), or **Chronicle Bytes**—they battle‑test edge cases like surrogate pairs, BOM handling, and direct‑memory cleanup.

---

### Performance Checklist for High‑Throughput Pipelines  

| ✅ Item | Reason |
|--------|--------|
| **Reuse `CharsetDecoder` / `CharsetEncoder`** | Avoids per‑line charset lookup and internal buffer allocation. |
| **Pre‑allocate `StringBuilder` / `ByteBuffer`** | Eliminates repeated resizing when lines are consistently long. Consider this: |
| **Disable `BufferedReader`’s default 8 KB buffer if you control buffering** | `Files. newBufferedReader(path, StandardCharsets.UTF_8, 65536)` lets you tune for SSD block sizes. But |
| **Batch `Files. Now, write()` calls** | Accumulate output in a `StringBuilder` and flush every *N* lines instead of writing line‑by‑line. |
| **Profile with `-XX:+PrintCompilation -XX:+PrintInlining`** | Confirms hot paths (e.That said, g. , `split`, `parseLong`) are JIT‑compiled and inlined. 

---

### Concurrency: When Parallel Streams Meet I/O  

`Files.l

`Files.And the stream’s spliterator hands off chunks of lines to the common `ForkJoinPool`, which is designed for CPU‑bound work, not blocking disk reads. parallel()` looks like an easy win, but it is usually the wrong tool for file I/O. Also, on a spinning disk, multiple threads issuing concurrent reads to the same file cause head thrashing; on SSDs, you still contend for the same file descriptor and kernel page cache, often ending up slower than a single well‑buffered sequential pass. This leads to lines(path, charset). Worse, `BufferedReader` is not thread‑safe, so the parallel stream internally serializes access anyway, negating any theoretical speedup.

A safer concurrency model is to parallelize **across files**, not within one file:

```java
ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

List> futures = files.On top of that, stream()
    . map(file -> CompletableFuture.runAsync(() -> {
        try (BufferedReader reader = Files.newBufferedReader(file, UTF_8)) {
            reader.lines().forEach(this::processLine);
        }
    }, pool))
    .

CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
pool.shutdown();

If you truly must process one enormous file concurrently, split it into disjoint byte ranges and assign each range to its own thread using FileChannel or MappedByteBuffer. Day to day, each worker must scan forward to the next line boundary after its start offset and stop before the next worker’s start offset, so no line is processed twice or split in the middle of a UTF‑8 sequence. This adds significant complexity and is only justified when the per‑line work is CPU‑intensive and the file is too large to fit in page cache Less friction, more output..

For non‑blocking reads, AsynchronousFileChannel lets you issue read operations without dedicating a thread per connection, but the callback‑driven model rarely pays off for simple line‑by‑line text processing; the bookkeeping usually outweighs the gains unless you are already building an NIO‑based server Took long enough..

Bottom line: profile before you parallelize. A single thread with a tuned BufferedReader (64 KB–1 MB buffer), a reused CharsetDecoder, and batched writes will often outperform a naive parallel() stream on a single file.


Conclusion

Java offers a spectrum of tools for reading files, and the right choice depends on access pattern, file size, and what you do with the data:

  • BufferedReader / Files.lines() – the default for most line‑oriented tasks; easy to use, safe, and fast enough when paired with a appropriately sized buffer.
  • FileChannel.map() – ideal for very large files that need random access or off‑heap parsing, provided you manage decoding and buffer boundaries yourself.
  • Custom Spliterator over fixed buffers – best when you are building a streaming parser (CSV, logs) and want to avoid materializing full String objects for every line

When the per‑line work is CPU‑intensive — e.put(line), while worker threads take()and process the payload. Here's the thing — g. Still, a common pattern is to read lines in a dedicated I/O thread and hand them off to a worker pool via aBlockingQueue. The producer thread blocks on queue., parsing JSON, running regexes, or performing calculations — it can be worthwhile to decouple the I/O thread from the computation thread. This eliminates contention on the reader and lets you size the computation pool independently of the number of available CPU cores.

If you are dealing with compressed source files, wrap the FileInputStream in a GZIPInputStream (or another java.util.Which means zip wrapper) before creating the BufferedReader. The wrapper streams the decompressed bytes directly into the reader’s buffer, so you still benefit from a single buffered read loop without materialising an intermediate byte array Simple, but easy to overlook..

For scenarios where the file size exceeds the maximum heap size, a direct‑buffered approach can keep memory pressure low. Then wrap the mapped buffer in a CharBuffer using a CharsetDecoder that reads from the buffer without copying data into a heap‑allocated String for each line. Use FileChannel#map to obtain a MappedByteBuffer that maps a contiguous region of the file into virtual memory. This technique is most effective when the processing logic can work on chunks of bytes rather than full lines, because the decoder must be instructed to keep state across buffer boundaries.

Batch writes are another low‑effort performance win. Because of that, instead of calling Files. On the flip side, println for every line, accumulate output in a StringBuilder (or a List<String>) and flush to a BufferedWriter after a configurable number of lines or when the buffer reaches a few megabytes. Now, writeStringorPrintWriter. This reduces the number of system calls and the associated context switches Took long enough..

A practical checklist for high‑throughput line processing:

  1. Choose the right reader – start with Files.lines or a manually‑managed BufferedReader with a 256 KB–1 MB buffer; reuse the same CharsetDecoder instance.
  2. Profile first – use a micro‑benchmark (e.g., JMH) to measure latency per line; only introduce concurrency if the per‑line cost is a noticeable fraction of the total runtime.
  3. Parallelise across files – create a fixed thread pool sized to the number of logical cores and submit a CompletableFuture for each file; combine the futures with allOf.
  4. Avoid splitting a single file unless the per‑line work is heavy and the file is too large to fit comfortably in the page cache; the added complexity of managing line boundaries often outweighs any speed gain.
  5. Use batch I/O for output – buffer writes, flush periodically, and close resources in a try‑with‑resources block to guarantee cleanup.
  6. Handle exceptions centrally – wrap the line‑processing lambda in a try/catch that logs or re‑throws, so a single malformed line does not abort the whole stream.

By following these guidelines, you can keep the codebase simple while still extracting the maximum possible throughput from Java’s I/O libraries. The most effective solution is usually the one that matches the access pattern to the tooling that was designed for it, and the only reliable way to know for sure is to measure the real‑world workload.

New on the Blog

New Arrivals

Similar Ground

Other Perspectives

Thank you for reading about Read File Line By Line In Java. We hope the information has been useful. Feel free to contact us if you have any questions. See you next time — don't forget to bookmark!
⌂ Back to Home