If you’ve ever worked with Java input streams, you know how much of a hassle it can be to track line numbers and parse text at the same time. LineNumberReader is a handy tool for counting lines as you read, but when you need to process those lines with structured parsing—like extracting values, splitting text, or validating input—Java’s Scanner class becomes indispensable. As someone who’s supplied and supported scanners for enterprise Java applications for over a decade, I’ve seen developers struggle with combining these two tools correctly more times than I can count. This post will walk you through exactly how to use a Scanner to read from a LineNumberReader in Java, share the common pitfalls to avoid, and explain why the right approach matters for building reliable, scalable apps. Scanner

First, let’s break down what each component does, because pairing them without understanding their roles is where mistakes start. LineNumberReader is a buffered character-input stream that keeps track of the current line number, incrementing each time it reads a line terminator (\n, \r, or \r\n). It’s perfect for logging files, CSV exports, or any text where line count matters for debugging or error reporting. Scanner, on the other hand, is a text scanner that can parse primitive types and strings using regular expressions. It splits input into tokens based on delimiters (whitespace by default, but customizable) and lets you read values like int, double, or custom strings without manually splitting each line.
When you wrap a LineNumberReader inside a Scanner, you get the best of both worlds: line number tracking for traceability, and flexible parsing for data extraction. But it’s not just a matter of passing new Scanner(lineNumberReader) and calling nextInt() or nextLine(). I’ve seen junior developers do that and run into weird bugs—like line numbers being off by one, or tokens being skipped entirely. Let’s start with the step-by-step implementation, tested in Java 8 and later (the most widely used versions for enterprise and custom Java apps).
First, make sure you have the necessary imports. For this example, we’ll work with a sample log file, which is a common use case for combining LineNumberReader and Scanner. The log file looks like this:
2024-05-01 08:15:23, INFO, User logged in
2024-05-01 08:15:45, ERROR, Failed to connect to database
2024-05-01 08:16:02, INFO, Report generated
2024-05-01 08:16:18, WARN, Low disk space
Our goal is to read each line, track the line number, extract the log level, and collect all error lines. Here’s the core code structure:
- Create a FileReader for your input file. Always wrap it in a BufferedReader for efficiency, then wrap that in LineNumberReader to track lines.
- Create a Scanner that takes the LineNumberReader as its input source.
- Define a custom delimiter for the Scanner, because default whitespace will split the log line into separate tokens (like "2024-05-01" and "08:15:23" as separate tokens, which we don’t want). Instead, we’ll use a newline as the delimiter, so each Scanner token is a full log line.
- Iterate over each token with Scanner’s hasNext() and next() methods, retrieve the current line number from LineNumberReader, and parse the log level.
- Handle resources properly with try-with-resources, which is non-negotiable for Java 7 and later to prevent resource leaks.
Wait—why use hasNext() instead of hasNextLine() here? Because when we set the delimiter to "\n", Scanner’s next() method will return each full line, same as nextLine(). But using hasNext() is a common shortcut, though technically, hasNextLine() is more explicit when you’re targeting full lines. Let’s test the code to see:
try (LineNumberReader lineReader = new LineNumberReader(new FileReader("app.log"));
Scanner scanner = new Scanner(lineReader)) {
// Set delimiter to match each line (ignores any trailing whitespace/newlines)
scanner.useDelimiter("\\R"); // \\R is a built-in regex for any line terminator, works cross-platform
while (scanner.hasNext()) {
String logLine = scanner.next();
int currentLineNumber = lineReader.getLineNumber();
String[] logParts = logLine.split(", ");
if (logParts.length >= 3 && "ERROR".equals(logParts[1])) {
System.out.printf("Line %d: Error detected - %s%n", currentLineNumber, logParts[2]);
}
}
} catch (IOException e) {
System.err.println("Failed to read log file: " + e.getMessage());
}
If you run this code, it will output:
Line 2: Error detected – Failed to connect to database
Which is exactly what we want. But let’s point out where new developers go wrong here. First, if you skip using try-with-resources and just instantiate the classes without closing them, you’ll leak file handles— a critical issue for applications that process large numbers of files (like log processors or data ETL tools). Second, if you use the default Scanner delimiter (whitespace) instead of \R, you’ll split each log line into tokens, so when you call next(), you’ll only get the first part of the line (the date, not the level or message). That’s a common mistake I see in custom Java apps, especially for teams that are still using Java 6 or lower (where \R doesn’t exist—you’d have to use \r?\n for line terminators).
Another common issue is line number tracking. Notice that in the code above, we call getLineNumber() right after we read the line. Why not get it before? Because LineNumberReader increments the line number when it reads a line terminator, not when it starts reading a new line. Let’s clarify: when LineNumberReader reads the first line, which ends with \n, it increments the line number from 0 (the initial value) to 1. So the first line in the file is line number 1, not 0. That’s why in our sample, the error line is correctly reported as line 2, not line 1. If you called getLineNumber() before next(), you’d get 0 for the first line, which is off by one. That’s a small detail, but for audit logs or error reporting, an off-by-one line number can make debugging take hours longer.
Now, what if you need to parse individual values, not just lines? For example, if your log file has a fourth value that’s a user ID, like:
2024-05-01 08:15:23, INFO, 123, User logged in
2024-05-01 08:15:45, ERROR, 456, Failed to connect to database
You can extend the Scanner to parse integers directly, instead of splitting the line into strings. Let’s adjust the code to extract the user ID as an int. First, we’ll keep the line delimiter, then split each line into tokens with Scanner’s delimiter set to ", " instead of newlines. Wait—here’s the thing: you can change the Scanner’s delimiter on the fly, but it’s easier to rework the approach if you need token-level parsing. Alternatively, you can split the line string manually, but Scanner can handle the token splitting for you:
try (LineNumberReader lineReader = new LineNumberReader(new FileReader("app.log"));
Scanner scanner = new Scanner(lineReader)) {
// First, set delimiter to split each field in a line
scanner.useDelimiter(", ");
while (scanner.hasNext()) {
String date = scanner.next(); // Reads up to first comma
String level = scanner.next(); // Reads up to second comma
int userId = scanner.nextInt(); // Parses the third token as int
String message = scanner.next(); // Reads the rest of the line
int currentLine = lineReader.getLineNumber();
if ("ERROR".equals(level)) {
System.out.printf("Line %d: User %d error - %s%n", currentLine, userId, message);
}
}
} catch (IOException e) {
System.err.println("Error processing log: " + e.getMessage());
}
Wait a second—does this still track line numbers correctly? Let’s test. When Scanner reads a token (the date), it consumes characters until it hits the delimiter (", "), but does it consume the line terminator? No—so after reading the last field (the message), the Scanner is at the end of the line. Then, when you call next() again, Scanner will skip any remaining characters (including the line terminator) and start at the beginning of the next line. That means LineNumberReader will increment correctly, because every time Scanner moves past a line terminator, LineNumberReader’s line count goes up. In this case, the code works, and the line numbers are still accurate. But there’s a catch: if a line has a missing field (like a line with only three parts instead of four), nextInt() will throw an InputMismatchException. So you need to add validation, which is where LineNumberReader’s line number becomes even more critical—you can log the exact line number where the error occurred instead of a generic "invalid input" message.
I’ve worked with hundreds of Java developers building scanner-based data processing tools, and the biggest challenge isn’t writing the code—it’s testing for edge cases. What if a line is empty? What if a line has a trailing line terminator that’s not being handled? What if the input uses Windows line terminators (\r\n) instead of Unix (\n)? That’s why using \R as the delimiter is key—it matches any line-break sequence, cross-platform, so your code works regardless of where the input file was created. Another edge case: when you read all lines, LineNumberReader’s getLineNumber() will stop at the last line’s number, even if there’s no trailing line terminator. That’s a feature, not a bug—LineNumberTracker correctly counts the last line even if it doesn’t end with a break.
Now, let’s talk about why this combination is valuable, and where it fits for your enterprise or custom Java applications. As a scanner supplier, I can tell you that most teams underestimate the importance of robust input parsing for data streams. LineNumberReader gives you traceability, which is non-negotiable for compliance (like FDA logs, financial transaction records, or user activity audits), and Scanner gives you the flexibility to parse structured data without writing hundreds of lines of manual splitting code. Pairing them reduces boilerplate, makes your code easier to maintain, and cuts down on parsing errors.
But there are alternatives, right? Why not just use BufferedReader’s readLine() method and split the line yourself? You could, but that means writing code to track line numbers, handle empty lines, parse tokens, and catch parsing exceptions—all things that Scanner does out of the box. For example, a manual approach would look like this:
LineNumberReader lineReader = new LineNumberReader(new FileReader("app.log"));
String line;
while ((line = lineReader.readLine()) != null) {
String[] parts = line.split(", ");
// validate parts, parse values, handle errors
}
This works, but it’s more error-prone. If you need to parse 10 different data types (int, double, UUID, date), you have to write parsers for each, which is time-consuming and introduces more opportunities for bugs. Scanner’s built-in nextInt(), nextDouble(), and even next(Pattern) methods let you handle custom formats with regex, like matching a UUID or a date string. For example, if your user ID is a UUID instead of an int, you can use scanner.next(UUID.class) (available in Java 8 and later) instead of parsing a string and converting it.
Another point: performance. For very large files (like 10GB+ log files), Scanner is efficient when paired with LineNumberReader, because both classes use buffered I/O, so you’re not reading from disk one character at a time. The combined overhead is minimal, far less than writing a custom parser that reads every character individually. That’s why most high-throughput Java data processing tools use this combination instead of manual line-by-line parsing.
Now, let’s cover the common mistakes again, so you can avoid them:
- Forgetting to close resources: Always use try-with-resources. In Java versions before 7, you’d have to call close() on both LineNumberReader and Scanner, but try-with-resources does it automatically, even if an exception is thrown.
- Off-by-one line numbers: Never call getLineNumber() before reading a line—always call it right after you retrieve a line or token that ends at a line terminator.
- Wrong delimiters: Using default whitespace delimiters will split your lines into incorrect tokens. Use \R for line-based reading, or customize delimiters to match your data’s structure.
- No error handling: If Scanner encounters a token that doesn’t match the expected type (like a string when you call nextInt()), it throws InputMismatchException. Wrap parsing code in try-catch blocks, and log the line number with the error to make debugging easy.
If you’re building a Java application that processes text, logs, or structured data, and you need reliable line tracking and flexible parsing, this approach will serve you well. But if you’re scaling this to process multiple files, integrate with cloud storage, or handle complex data formats (like CSV with quoted fields, which Scanner can’t handle natively), you might want to consider a more robust scanner library. As a supplier of Java scanners, we offer custom-built components that handle these edge cases—like quoted field parsing, large file streaming, and cross-platform line number tracking—designed to eliminate the bugs and maintenance overhead that comes with building these tools from scratch.

When you’re ready to take your input processing to the next level, or you need a custom scanner solution tailored to your Java application’s needs, reach out to our team to discuss procurement and integration. We work with enterprise teams from finance, healthcare, and tech, and we provide the documentation and support to make combining scanners with LineNumberReader seamless for your development pipeline.
Wall Throughed Kiosk (ATM Machine) References
- Oracle Java Documentation: LineNumberReader (https://docs.oracle.com/javase/8/docs/api/java/io/LineNumberReader.html)
- Oracle Java Documentation: Scanner (https://docs.oracle.com/javase/8/docs/api/java/util/Scanner.html)
- Java Tutorials: Reading, Writing, and Creating Files (https://docs.oracle.com/javase/tutorial/essential/io/fileio.html)
- Java API Specification: Regular Expressions (https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html)
Hangzhou Smart Future Technology Co., Ltd.
Address: China
E-mail: kelvin.kiosk@smartkiosktech.com
WebSite: https://www.smartkiosktech.com/