Using Java String lines() to Split Text into Lines
Learn how to use Java's String.lines() method to split text into a stream of lines, including line terminator handling and practical examples.
Java's String.lines() method splits a string into a Stream<String> of lines, with line terminators removed. It was introduced in Java 11 and is useful for processing multiline text without manually handling \n, \r\n, or \r separators.
How String.lines() Works
Java defines the recognized line terminators as \n (LF), \r\n (CRLF), and \r (CR). String.lines() recognizes all three. The returned stream is lazily populated: it does not build an array of all lines upfront, but creates lines as the stream is consumed.
Here is a minimal example:
String text = "first line\nsecond line\r\nthird line\r"; text.lines().forEach(System.out::println);
Output:
first line
second line
third line
Notice that the trailing \r after third line is treated as a terminator, but the stream does not include an empty line after it. If the string ends with a terminator, that final terminator does not produce an extra empty line. An empty string returns an empty stream.
Practical Usage Examples
A common use case is reading a file's content as a single string and then processing each line. For example:
String content = Files.readString(Path.of("data.txt")); long count = content.lines().count();
This counts the lines in the file without manually splitting on \n. Another example is filtering lines that match a pattern:
List<String> errors = content.lines() .filter(line -> line.startsWith("ERROR")) .collect(Collectors.toList());
The stream is sequential by default, but you can call .parallel() if you need parallel processing. However, for typical line processing, sequential is usually sufficient and avoids overhead.
Comparing lines() with split() and BufferedReader
Before Java 11, developers often used String.split("\\r?\\n|\\r") or a BufferedReader to handle lines. Each approach has different characteristics.
| Approach | Returns | Handles Empty Lines | Lazy Evaluation | Best Fit |
|---|---|---|---|---|
String.lines() | Stream<String> | Yes, empty lines are preserved | Yes | In-memory strings, functional processing |
String.split() | String[] | Depends on regex; trailing empty strings removed by default | No (creates full array) | Small strings, when an array is needed |
BufferedReader.readLine() | String (one at a time) | Yes, returns empty string for blank lines | Yes | Reading from files or streams, memory efficiency |
split() uses a regular expression and by default discards trailing empty strings. For example, "a\nb\n".split("\n") returns ["a", "b"], not ["a", "b", ""]. lines() preserves the empty line if it appears in the middle, but does not add one after a trailing terminator.
BufferedReader.readLine() is more efficient when reading from a file because it does not load the entire file into memory. lines() requires the whole string to be present, so it is best suited for strings that are already in memory.
Performance and Memory Considerations
Because lines() is lazy, it does not build an array of all lines upfront. This means that, for a large string, you can process lines without a large intermediate array. However, the original string itself must remain in memory for the duration of the stream processing.
If you are processing a very large file, reading it entirely into a String with Files.readString() may cause high memory usage. In such cases, a BufferedReader is a better choice because it reads lines incrementally. For moderate-sized strings that are already in memory, lines() is convenient and avoids the overhead of a regex-based split().
Handling Edge Cases
Understanding how lines() handles edge cases prevents subtle bugs.
- Empty string:
"".lines().count()returns0. - String with only terminators:
"\n\r\n".lines().count()returns2because the string has two line terminators and an empty line before each; the trailing terminator does not create a third line. - Trailing terminator:
"a\n".lines().count()returns1for the linea, not2. The trailing terminator does not create an extra empty line. - Mixed terminators:
"a\rb\nc\r\nd"yields linesa,b,c,dbecause each terminator is recognized. - Null input:
lines()throwsNullPointerExceptionif the string isnull. This is expected since it is an instance method.
Common Mistakes and How to Avoid Them
A frequent mistake is assuming lines() trims whitespace from each line. It does not. If you need to remove leading or trailing spaces, you must call .trim() or strip() on each line:
content.lines().map(String::trim).forEach(...)
Another mistake is using lines() on a string that contains only a single line without any terminator. That works fine; it returns a stream with one element.
Some developers expect lines() to handle \n only, but it also handles \r and \r\n. This is usually an advantage, but if you need to preserve the exact line separator, lines() does not provide it. You would need to use split() with a capturing group or a custom scanner.
Compatibility and Version Requirements
String.lines() was added in Java 11. If you are working with Java 8 or 9, you need an alternative. One common workaround is to use a BufferedReader on a StringReader:
new BufferedReader(new StringReader(text)) .lines() .forEach(System.out::println);
This works in Java 8 because BufferedReader.lines() was added in Java 8. However, it is less direct than String.lines().
When migrating to Java 11 or later, note that lines() is an instance method on String. It returns a Stream<String>, so if you declare the stream type explicitly, import java.util.stream.Stream; if you just chain stream operations, you can use it without an explicit import.