Intermediate

I/O Operations in Java

Learn console input and output, formatted printing, and reading and writing files in Java with practical Docker-ready examples

Input and output (I/O) is how a program talks to the outside world — printing to the terminal, reading what a user types, and persisting data to files. You have already used System.out.println to print text; this tutorial goes deeper into formatted output, reading from standard input, and working with files.

As a class-based, object-oriented language, Java models I/O through objects: System.out is a PrintStream, Scanner wraps an input source, and the modern java.nio.file.Files class exposes static helpers for whole-file operations. Java also uses checked exceptions for I/O — the compiler forces you to acknowledge that reading or writing a file might fail. That verbosity is deliberate: it makes error handling part of the type system rather than an afterthought.

In this tutorial you will format console output with printf, read typed input with Scanner, write and read files with Files, and handle I/O errors with try/catch.

Formatted Console Output

println prints a value followed by a newline, and print prints without one. For anything more structured, printf (and its sibling String.format) use C-style format specifiers: %s for strings, %d for integers, %f for floating point, and %n for a platform-appropriate newline.

Create a file named FormattedOutput.java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FormattedOutput {
    public static void main(String[] args) {
        String lang = "Java";
        int year = 1995;
        double share = 0.153;

        // print does not add a newline; println does
        System.out.print("Language: ");
        System.out.println(lang);

        // printf uses format specifiers; %n is a portable newline
        System.out.printf("%s first appeared in %d.%n", lang, year);

        // %.1f rounds to one decimal; %% prints a literal percent sign
        System.out.printf("Market share: %.1f%%%n", share * 100);

        // String.format builds a string instead of printing it;
        // %-10s left-justifies within a field 10 characters wide
        String padded = String.format("[%-10s]", lang);
        System.out.println(padded);
    }
}

You can run this example with:

1
2
docker run --rm -v $(pwd):/app -w /app eclipse-temurin:21-jdk \
    sh -c 'javac FormattedOutput.java && java FormattedOutput'

It prints:

Language: Java
Java first appeared in 1995.
Market share: 15.3%
[Java      ]

Reading Console Input

To read what a user types, wrap System.in in a Scanner. Scanner parses input into the type you ask for: nextLine() reads a full line of text, while nextInt() reads the next integer token.

Create a file named ConsoleInput.java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import java.util.Scanner;

public class ConsoleInput {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("What is your name?");
        String name = scanner.nextLine();

        System.out.println("What year were you born?");
        int birthYear = scanner.nextInt();

        int age = 2026 - birthYear;
        System.out.printf("Hi %s, you are about %d this year.%n", name, age);

        scanner.close();
    }
}

Because this program reads from standard input, pipe answers into the container and add the -i (interactive) flag so Docker forwards stdin:

1
2
printf 'Ada\n1990\n' | docker run --rm -i -v $(pwd):/app -w /app eclipse-temurin:21-jdk \
    sh -c 'javac ConsoleInput.java && java ConsoleInput'

With Ada and 1990 as input, it prints:

What is your name?
What year were you born?
Hi Ada, you are about 36 this year.

Reading and Writing Files

Modern Java handles files through java.nio.file.Files and Path. The Files class offers concise static methods for the common cases: write dumps a list of lines to a file, readAllLines reads them all back, and writeString (with StandardOpenOption.APPEND) adds to an existing file. Because these operations can fail — a disk fills up, a path is missing — they throw the checked IOException, so the method must either declare throws IOException or catch it.

Create a file named IoOperations.java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;

public class IoOperations {
    public static void main(String[] args) throws IOException {
        // 1. Formatted console output
        String name = "Grace";
        int commits = 42;
        double coverage = 0.8756;
        System.out.printf("%s made %d commits (%.1f%% coverage).%n", name, commits, coverage * 100);

        // 2. Write a list of lines to a file (creates or overwrites it)
        Path log = Path.of("build.log");
        List<String> entries = List.of("compile: OK", "test: OK", "package: OK");
        Files.write(log, entries);
        System.out.println("Wrote " + entries.size() + " lines to " + log.getFileName());

        // 3. Read the whole file back into memory
        List<String> lines = Files.readAllLines(log);
        System.out.println("File contents:");
        for (int i = 0; i < lines.size(); i++) {
            System.out.printf("  line %d -> %s%n", i + 1, lines.get(i));
        }

        // 4. Append one more line without erasing the file
        Files.writeString(log, "deploy: OK" + System.lineSeparator(), StandardOpenOption.APPEND);
        System.out.println("After append, total lines: " + Files.readAllLines(log).size());

        // 5. Handle I/O errors with try-catch instead of crashing
        try {
            Files.readAllLines(Path.of("does-not-exist.txt"));
        } catch (NoSuchFileException e) {
            System.out.println("Handled missing file: " + e.getFile());
        }
    }
}

Running with Docker

The file I/O example above is the most complete, so use it to see everything working together. It compiles and runs entirely inside the container, writing build.log into your mounted directory:

1
2
3
4
5
6
# Pull the official Eclipse Temurin JDK image
docker pull eclipse-temurin:21-jdk

# Compile and run the file I/O example
docker run --rm -v $(pwd):/app -w /app eclipse-temurin:21-jdk \
    sh -c 'javac IoOperations.java && java IoOperations'

Expected Output

Running IoOperations produces:

Grace made 42 commits (87.6% coverage).
Wrote 3 lines to build.log
File contents:
  line 1 -> compile: OK
  line 2 -> test: OK
  line 3 -> package: OK
After append, total lines: 4
Handled missing file: does-not-exist.txt

After it finishes, a build.log file will remain in your working directory containing the four written lines.

Key Concepts

  • I/O is object-basedSystem.out is a PrintStream, Scanner wraps an input source, and Path represents a filesystem location. You compose these objects rather than calling free functions.
  • printf and String.format format output — use %s, %d, and %.1f for values, %n for a portable newline, and %% for a literal percent sign.
  • Scanner reads typed inputnextLine() reads a whole line while nextInt() reads a single number; always close() the scanner when you are done.
  • java.nio.file.Files is the modern file APIFiles.write, Files.readAllLines, and Files.writeString cover most everyday reading and writing without manual streams.
  • StandardOpenOption.APPEND adds to a file — without it, writing replaces the existing contents.
  • I/O uses checked exceptions — methods that touch the filesystem throw IOException, so you must either declare throws IOException or wrap the call in try/catch.
  • Catch specific exceptions — handling NoSuchFileException separately lets you react to a missing file distinctly from other I/O failures.

Running Today

All examples can be run using Docker:

docker pull eclipse-temurin:21-jdk
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining