Back to Blog
Java

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.

JavaString APIStreamsText ProcessingJava 11
Illustration of Java String lines() splitting a text into multiple lines, showing a stream of lines.

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.

ApproachReturnsHandles Empty LinesLazy EvaluationBest Fit
String.lines()Stream<String>Yes, empty lines are preservedYesIn-memory strings, functional processing
String.split()String[]Depends on regex; trailing empty strings removed by defaultNo (creates full array)Small strings, when an array is needed
BufferedReader.readLine()String (one at a time)Yes, returns empty string for blank linesYesReading 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() returns 0.
  • String with only terminators: "\n\r\n".lines().count() returns 2 because 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() returns 1 for the line a, not 2. The trailing terminator does not create an extra empty line.
  • Mixed terminators: "a\rb\nc\r\nd" yields lines a, b, c, d because each terminator is recognized.
  • Null input: lines() throws NullPointerException if the string is null. 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.

Java String lines(): Split Text into Lines with Examples | RYUSLOG DEV