Skip to main content

    Lesson 33 • Advanced

    Java I/O and NIO

    Every real program reads and writes data — config files, logs, uploads, exports. By the end of this lesson you'll read and write files the clean, modern way, stream huge files without running out of memory, and know exactly when to drop down to channels and buffers.

    What You'll Learn in This Lesson

    • You'll be able to read and write text with Reader/Writer and buffered streams
    • You'll be able to auto-close streams with try-with-resources (no leaks)
    • You'll be able to read a whole file with Files.readString / readAllLines
    • You'll be able to write a file in one call with Files.write / writeString
    • You'll be able to stream a giant file safely with Files.lines
    • You'll know when to reach for channels, buffers, and non-blocking IO

    Before You Start

    You should already know Exception Handling (especially try-with-resources) and the Streams API, since the modern file methods return a Stream. A basic feel for the difference between bytes (raw data) and characters (text) helps too.

    A Real-World Analogy

    💡 Analogy: Classic IO is like drinking through a straw — data flows one direction, one sip at a time, and you wait for each sip (this is what "blocking" means). Modern NIO is like a bucket brigade: you fill a buffer (the bucket), pass it through a channel (the line of people), and you can fill and empty buckets at both ends. For most jobs you don't carry buckets yourself — you press a button (the Files helpers) and the brigade runs for you.

    Keep that picture in mind: streams = the straw, buffers + channels = the brigade, and the Files class = the button that hides the brigade behind a one-liner.

    1️⃣ Streams — the Classic Way (Reader, Writer, Buffered)

    A stream is a one-way flow of data. Java splits them in two: InputStream/OutputStream move raw bytes (images, zip files), while Reader/Writer move characters (text). For files you'd use FileReader/FileWriter.

    Reading one character at a time is slow, so you wrap a reader in a BufferedReader. "Buffered" means it grabs a big chunk into memory and hands it to you piece by piece — and it adds the handy readLine() method. The try (...) block below is try-with-resources: anything you open in those parentheses is closed automatically when the block ends.

    Read the fully-commented example, then run it. Note how readLine() returns null at the end of the stream — that's your signal to stop the loop.

    Worked Example: buffered Reader / Writer + try-with-resources
    import java.io.BufferedReader;
    import java.io.BufferedWriter;
    import java.io.IOException;
    import java.io.StringReader;
    import java.io.StringWriter;
    
    public class Main {
        public static void main(String[] args) throws IOException {
            // A "stream" is a one-way flow of data: bytes for binary,
            // characters for text. Reader/Writer handle TEXT; you wrap
            // them in Buffered* so you read/write a chunk at a time
            // instead of one slow character at a time.
            String config = "host=localhost\nport=8080\ndb=myapp";
    
            // WRITE — String-backed Writer so this runs anywhere.
            // With a real file you'd use new FileWriter("config.txt").
            StringWriter sink = new StringWriter();
            // try-with-resources auto-closes bw at the end of the block —
            // no manual close(), no finally. The () after try holds the
            // resource; anything AutoCloseable goes there.
            try (BufferedWriter bw = new BufferedWriter(sink)) {
                bw.write(config);              // buffered: held in memory...
            }                                  // ...flushed + closed here
            System.out.println("Wrote " + config.length() + " chars");
    
            // READ line by line — readLine() returns null at end of stream.
            try (BufferedReader br = new BufferedReader(new StringReader(sink.toString()))) {
                String line;                   // declared outside the loop
                int n = 1;
                while ((line = br.readLine()) != null) {  // null = no more lines
                    System.out.println("Line " + (n++) + ": " + line);
                }
            }
        }
    }
    Output
    Wrote 31 chars
    Line 1: host=localhost
    Line 2: port=8080
    Line 3: db=myapp
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    2️⃣ NIO.2 — the Modern Way (Path and Files)

    Since Java 7, java.nio.file replaced most of that ceremony. A Path is simply a location on disk (the modern stand-in for the legacy File class). The Files class is a toolbox of static helpers that turn whole tasks into one line.

    Files.readString(path) — read an entire small file into a String (Java 11+)

    Files.readAllLines(path) — read every line into a List<String>

    Files.writeString(path, s) — write a String, creating or overwriting the file

    Files.write(path, bytes) — write raw bytes

    Files.lines(path) — a lazy Stream<String>, safe for huge files

    The example below writes a file, reads it three different ways, then deletes it. The key habit: always pass StandardCharsets.UTF_8 so the same code reads the same text on every machine.

    Worked Example: Path, Files.writeString / readString / readAllLines / lines
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.util.List;
    import java.util.stream.Stream;
    
    public class Main {
        public static void main(String[] args) throws IOException {
            // Path = a location on disk (the modern replacement for File).
            // Files = a toolbox of static helpers that act on a Path.
            // We make a real temp file so the example is self-contained.
            Path file = Files.createTempFile("demo", ".log");
    
            // WRITE a whole small file in ONE call (Java 11+).
            // Always pass a charset so it behaves the same on every OS.
            Files.writeString(file,
                "ERROR boot failed\nINFO ready\nERROR disk full\nDEBUG tick\nERROR timeout",
                StandardCharsets.UTF_8);
            System.out.println("Wrote " + Files.size(file) + " bytes");
    
            // READ it all back as one String (Java 11+) — small files only.
            String text = Files.readString(file, StandardCharsets.UTF_8);
            System.out.println("readString got " + text.length() + " chars");
    
            // READ as a List of lines — handy when you want every line at once.
            List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
            System.out.println("readAllLines got " + lines.size() + " lines");
    
            // STREAM lines lazily — safe for HUGE files (one line in memory
            // at a time). Files.lines holds an open file, so close it with
            // try-with-resources.
            try (Stream<String> stream = Files.lines(file, StandardCharsets.UTF_8)) {
                long errors = stream.filter(l -> l.contains("ERROR")).count();
                System.out.println("Found " + errors + " ERROR lines");
            }
    
            Files.delete(file);                // tidy up
            System.out.println("Cleaned up: " + !Files.exists(file));
        }
    }
    Output
    Wrote 67 bytes
    readString got 67 chars
    readAllLines got 5 lines
    Found 3 ERROR lines
    Cleaned up: true
    This writes and reads a real temp file, so run it with a local JDK (Java 11+). The output is fixed except the temp filename, which is random each run.

    🎯 Your Turn #1 — Write Then Read a File

    Finish the program: write three lines, read them back as a List, and print the count. Replace each ___ using the // 👆 hints. Check your work against the expected output in the comments.

    🎯 Your Turn #1
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.util.List;
    
    public class Main {
        public static void main(String[] args) throws IOException {
            // 🎯 YOUR TURN — fill in the blanks marked with ___
            Path notes = Files.createTempFile("notes", ".txt");
    
            // 1) Write three lines to the file using Files.writeString.
            //    Separate lines with \n and pass the UTF_8 charset.
            Files.writeString(notes, "buy milk\nwalk dog\ncall mum", ___);
            //                                                         👆 the charset
    
            // 2) Read every line back as a List<String>.
            List<String> lines = Files.___(notes, StandardCharsets.UTF_8);
            //                         👆 the method that returns a List of lines
    
            // 3) Print how many lines you read.
            System.out.println("You have " + lines.___() + " notes");
            //                                       👆 List method for the count
    
            Files.delete(notes);
    
            // ✅ Expected output:
            // You have 3 notes
        }
    }
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    🎯 Your Turn #2 — Stream and Filter a Log

    Count the WARN lines in a log without loading the whole file into memory. Fill in the streaming method and the text to match, then compare with the expected output.

    🎯 Your Turn #2
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.util.stream.Stream;
    
    public class Main {
        public static void main(String[] args) throws IOException {
            // 🎯 YOUR TURN — count the WARN lines without loading the
            // whole file into memory. Fill in the blanks marked with ___
            Path log = Files.createTempFile("app", ".log");
            Files.writeString(log,
                "INFO start\nWARN low disk\nINFO ok\nWARN slow query\nINFO done",
                StandardCharsets.UTF_8);
    
            // 1) Open a lazy Stream of the file's lines.
            //    Use try-with-resources so the file is closed for you.
            try (Stream<String> lines = Files.___(log, StandardCharsets.UTF_8)) {
                //                             👆 the lazy streaming method
                // 2) Keep only lines that contain "WARN", then count them.
                long warnings = lines.filter(l -> l.contains("___")).count();
                //                                                👆 the text to match
                System.out.println("Warnings: " + warnings);
            }
    
            Files.delete(log);
    
            // ✅ Expected output:
            // Warnings: 2
        }
    }
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    3️⃣ Under the Hood — Channels and Buffers

    The convenient Files helpers are built on lower-level pieces you'll occasionally meet. A channel (FileChannel) is a two-way pipe to a file or socket. A buffer (ByteBuffer) is a fixed-size box of bytes that the channel fills from or drains into.

    The one trick people forget is buffer.flip(): after you fill a buffer you must flip it to switch from "write mode" to "read mode" so the channel knows where your data ends. You rarely need this layer directly — but it's what powers fast, large-file work like FileChannel.transferTo() (zero-copy) and memory-mapped files.

    Worked Example: FileChannel + ByteBuffer (and flip)
    import java.io.IOException;
    import java.nio.ByteBuffer;
    import java.nio.charset.StandardCharsets;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.StandardOpenOption;
    import java.nio.channels.FileChannel;
    
    public class Main {
        public static void main(String[] args) throws IOException {
            // A Channel is a two-way pipe to a file or socket. A ByteBuffer
            // is a fixed-size box of bytes the channel reads into / writes
            // from. This is the low-level engine Files.* uses for you — you
            // rarely need it, but it powers high-performance code.
            Path file = Files.createTempFile("channel", ".bin");
    
            byte[] data = "Hello, NIO!".getBytes(StandardCharsets.UTF_8);
            try (FileChannel ch = FileChannel.open(file, StandardOpenOption.WRITE)) {
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                buffer.put(data);          // fill the buffer (write mode)
                buffer.flip();             // FLIP: switch to read mode so the
                                           // channel knows where the data ends
                int written = ch.write(buffer);
                System.out.println("Wrote " + written + " bytes");
            }
    
            try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ)) {
                ByteBuffer buffer = ByteBuffer.allocate((int) ch.size());
                ch.read(buffer);           // fill the buffer from the file
                buffer.flip();             // flip before reading it back out
                String text = StandardCharsets.UTF_8.decode(buffer).toString();
                System.out.println("Read back: " + text);
            }
    
            Files.delete(file);
        }
    }
    Output
    Wrote 11 bytes
    Read back: Hello, NIO!
    FileChannel works against a real temp file, so run it with a local JDK. It writes 11 bytes, reads back Hello, NIO!, then deletes the file.

    🧩 Mini-Challenge — Word Counter

    No blanks this time — just an outline. Write a sentence to a temp file, read it back with Files.readString, split it on spaces, and print the word count. The starter has only comments; you write the code.

    🧩 Mini-Challenge
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.util.List;
    
    public class Main {
        public static void main(String[] args) throws IOException {
            // 🎯 MINI-CHALLENGE: word counter
            // 1. Create a temp file and write a sentence into it with
            //    Files.writeString (remember the UTF_8 charset).
            // 2. Read it back with Files.readString.
            // 3. Split the text on spaces:  text.split(" ")  gives a String[].
            // 4. Print: "Word count: N"   (an array's length is array.length).
            // 5. Delete the temp file when you're done.
            //
            // ✅ Expected (text = "the quick brown fox"): Word count: 4
    
            // your code here
        }
    }
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    Common Errors (and the Fix)

    • Not closing streams — leaving streams open leaks OS file handles until your program eventually throws java.io.IOException: Too many open files. Fix: open every stream in a try (...) (try-with-resources) so it closes automatically.
    • Charset issues — relying on the platform default makes the same file read as caf? on one OS and café on another, or throws MalformedInputException. Fix: always pass StandardCharsets.UTF_8.
    • Reading a huge file into memoryFiles.readAllLines on a multi-GB log dies with OutOfMemoryError: Java heap space. Fix: use Files.lines (lazy stream) and process line by line.
    • Blocking on a slow source — a plain read() waits forever for a slow file or network peer, freezing that thread. Fix: for high-concurrency network work use non-blocking channels with Selectors instead of one blocking thread per connection.
    • Forgetting buffer.flip() — you fill a ByteBuffer then write it and 0 bytes come out. Fix: call flip() after filling, before the channel reads from it.

    📋 Quick Reference — Old IO vs NIO.2

    TaskOld way (java.io)Modern way (java.nio.file)
    A location on disknew File("a.txt")Path.of("a.txt")
    Read whole fileBufferedReader loopFiles.readString(path)
    Read all linesreadLine() until nullFiles.readAllLines(path)
    Write textFileWriter + write()Files.writeString(path, s)
    Stream a huge filemanual readLine() loopFiles.lines(path)
    Copy a fileread/write byte loopFiles.copy(src, dst)
    Walk a directory treerecursive File.listFiles()Files.walk(dir)

    Frequently Asked Questions

    What is the difference between IO and NIO in Java?

    Classic IO (java.io) is stream-based: data flows one direction, blocking until each byte arrives. NIO.2 (java.nio.file) is the modern file API built on Paths, the Files toolbox, channels and buffers — it reads in blocks, can be non-blocking, and gives you convenient one-liners like Files.readString and Files.lines. For everyday file work, reach for the Files class first.

    Do I still need to close streams if I use try-with-resources?

    No — that is the whole point. Any resource declared in the try (...) parentheses is closed automatically when the block ends, even if an exception is thrown. Before try-with-resources (Java 7) you had to close streams by hand in a finally block, which was easy to get wrong and leaked file handles.

    Should I use Files.readAllLines or Files.lines?

    Use Files.readAllLines when the file is small and you want every line in a List at once. Use Files.lines for large files: it returns a lazy Stream that holds only one line in memory at a time, so a multi-gigabyte log will not blow up your heap. Files.lines keeps the file open, so wrap it in try-with-resources.

    Why do I need to specify a charset like StandardCharsets.UTF_8?

    If you do not pass a charset, older APIs use the platform default, which differs between machines and operating systems. The same code can then read different characters on Windows vs Linux and silently corrupt text. Always pass StandardCharsets.UTF_8 so your program behaves identically everywhere.

    What does buffer.flip() actually do?

    A ByteBuffer tracks a position and a limit. While you fill it (put), position moves forward. flip() sets the limit to the current position and resets position to 0, switching the buffer from write mode to read mode so the channel knows exactly which bytes to send. Forgetting flip() is the classic NIO bug — you end up writing zero bytes or garbage.

    🎉 Lesson Complete!

    Great work! You can now read and write files the classic way with buffered Reader/Writer, close them safely with try-with-resources, and do the same far more cleanly with the modern Path + Files API. You also know to stream huge files with Files.lines instead of loading them whole, and what channels, buffers, and non-blocking IO are for.

    Next up: JDBC — connecting your Java programs to databases so your data outlives a single run.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service

    Install LearnCodingFast

    Learn faster with the app on your home screen.