CodeGym /Courses /JAVA 25 SELF /join, concat methods: merging streams

join, concat methods: merging streams

JAVA 25 SELF
Level 32 , Lesson 1
Available

1. Stream.concat: merging two streams

In programming, a common situation arises: we have two (or more) data streams and want to merge them into one. For example, two lists of students from different groups — and we need to process them all at once. In the “old days” (before the Stream API) we would simply merge two lists using addAll. But if we are working with streams (Stream<T>), we want to do this lazily and expressively.

Syntax and how it works

The most basic way to merge two streams is to use the static method Stream.concat:

Stream<T> Stream.concat(Stream<? extends T> a, Stream<? extends T> b)

What happens:

  • It consumes the first stream first, then the second.
  • The result is a new stream that first yields all elements from the first, then all elements from the second.
  • The concatenation is lazy: until you start traversing the resulting stream with a terminal operation (for example, forEach or collect), nothing happens.

Example: merging two lists of names

import java.util.List;
import java.util.stream.Stream;

List<String> groupA = List.of("Anna", "Boris", "Victoria");
List<String> groupB = List.of("Gregory", "Daria");

Stream<String> allStudents = Stream.concat(groupA.stream(), groupB.stream());

allStudents.forEach(System.out::println);

Result:

Anna
Boris
Victoria
Gregory
Daria

Important: after concatenation, the resulting stream is single-use. Like any other Stream, it cannot be reused.

Features of Stream.concat

  • Only two streams at a time. For three or more — either chain concat calls or use other approaches (see below).
  • Order is preserved. First the elements of the first stream, then the second.
  • Lazy concatenation. If the first stream is infinite, execution will never reach the second.
  • Common bug: trying to concatenate a stream with itself (Stream.concat(stream, stream)) causes the same stream to be reused — this is prohibited.

2. Merging more than two streams: flatMap and Stream.of

When there are more than two streams, it’s more convenient to gather them using the combination of Stream.of + flatMap.

Using Stream.of + flatMap

import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;

List<String> groupA = List.of("Anna", "Boris");
List<String> groupB = List.of("Victoria");
List<String> groupC = List.of("Gregory", "Daria");

Stream<String> allStudents = Stream.of(groupA, groupB, groupC)
    .flatMap(Collection::stream);

allStudents.forEach(System.out::println);

What happens here:

  • Stream.of(groupA, groupB, groupC) creates a stream of three collections.
  • flatMap(Collection::stream) flattens each collection into a single stream of strings.
  • The result is one big stream of all students.

Advantage: you can merge as many streams as you like.

Another option: merging a list of streams

import java.util.List;
import java.util.stream.Stream;

List<Stream<String>> streams = List.of(
    Stream.of("a", "b"),
    Stream.of("c"),
    Stream.of("d", "e")
);

Stream<String> merged = streams.stream()
    .flatMap(s -> s);

merged.forEach(System.out::println);

Result:

a
b
c
d
e

3. Collectors.joining: join strings with a delimiter

Sometimes you don’t just need to merge streams, but to glue all elements into a single string — for example, to print names separated by commas. For this, there is the collector Collectors.joining.

Syntax

String result = stream.collect(Collectors.joining(", "));
  • No arguments: joins strings without a delimiter.
  • With a delimiter: the specified string will appear between elements.
  • With prefix and suffix: Collectors.joining(delimiter, prefix, suffix)

Example: join a student list into one string

import java.util.List;
import java.util.stream.Collectors;

List<String> students = List.of("Anna", "Boris", "Victoria");

String line = students.stream()
    .collect(Collectors.joining(", "));

System.out.println("Student list: " + line);

Result:

Student list: Anna, Boris, Victoria

Example with prefix and suffix

String line = students.stream()
    .collect(Collectors.joining(", ", "[", "]"));

System.out.println(line);

Result:

[Anna, Boris, Victoria]

Why is this useful?

  • Forming CSV lines.
  • Pretty printing of lists and reports.
  • Passing data as a single string (for example, in a URL or a log).

4. Comparing concat and flatMap: when to use which?

  • Stream.concat — when you have exactly two streams and order matters.
  • Stream.of + flatMap — when you have many streams or they are stored in a collection.
  • Collectors.joining — when elements are of type String and you need a single string as the result.

Comparison table

Method When to use Code example
Stream.concat(a, b)
Two streams
Stream.concat(a, b)
Stream.of(...).flatMap()
Many streams, a collection of streams
Stream.of(a, b, c).flatMap(x -> x)
Collectors.joining()
Join elements into one string
stream.collect(Collectors.joining(", "))

5. Practical examples in a learning app

Example 1. Merge students from two departments

import java.util.List;
import java.util.stream.Stream;

List<String> itStudents = List.of("Anna", "Boris");
List<String> mathStudents = List.of("Victoria", "Gregory");

Stream<String> all = Stream.concat(itStudents.stream(), mathStudents.stream());

all.forEach(System.out::println);

Example 2. Merge students from all groups and output as a single string

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

List<List<String>> allGroups = List.of(
    List.of("Anna", "Boris"),
    List.of("Victoria"),
    List.of("Gregory", "Daria")
);

String allNames = allGroups.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.joining("; "));

System.out.println("All students: " + allNames);

Result:

All students: Anna; Boris; Victoria; Gregory; Daria

Example 3. Merge streams of numbers

import java.util.stream.Stream;

Stream<Integer> s1 = Stream.of(1, 2, 3);
Stream<Integer> s2 = Stream.of(4, 5);

Stream<Integer> merged = Stream.concat(s1, s2);

merged.forEach(System.out::print); // 12345

6. Important nuances and caveats

concat works only with two streams

For three streams you end up with nested calls — it works but looks noisy. It’s better to use Stream.of + flatMap:

Stream<Integer> s1 = Stream.of(1);
Stream<Integer> s2 = Stream.of(2);
Stream<Integer> s3 = Stream.of(3);

Stream<Integer> merged = Stream.concat(Stream.concat(s1, s2), s3);

Streams are single-use

After a terminal operation, a stream cannot be used. Reusing it results in IllegalStateException.

The concat-produced stream is lazy

If the first stream is infinite (for example, created via Stream.generate(...)), execution will never reach the second.

Element order

Order is always preserved: first the first stream, then the second.

Collectors.joining works only with Stream<String>

For non-strings, convert elements to strings first, for example via map(Object::toString):

import java.util.stream.Collectors;
import java.util.stream.Stream;

Stream<Integer> numbers = Stream.of(1, 2, 3);
String line = numbers
    .map(Object::toString)
    .collect(Collectors.joining(", "));

System.out.println(line); // 1, 2, 3

Comparison with the “manual” approach

Before the Stream API:

import java.util.ArrayList;
import java.util.List;

List<String> merged = new ArrayList<>(listA);
merged.addAll(listB);

With the Stream API:

import java.util.stream.Collectors;

List<String> merged = Stream.concat(listA.stream(), listB.stream())
    .collect(Collectors.toList());

Benefit of streams: you can insert intermediate operations (filter, map, and others) before and after concatenation, and you can work with arbitrary data sources.

7. Common mistakes when merging streams

Mistake #1: reusing a stream. A stream can be used only once. You must not concatenate a stream with itself.

import java.util.stream.Stream;

Stream<String> s = Stream.of("a", "b");
Stream<String> merged = Stream.concat(s, s); // Error! s will be reused

Mistake #2: attempting to merge an infinite stream with a finite one. If the first stream is infinite, the second will never start.

import java.util.stream.Stream;

Stream<Integer> infinite = Stream.generate(() -> 1);
Stream<Integer> finite = Stream.of(2, 3);

Stream<Integer> merged = Stream.concat(infinite, finite);
// merged.limit(10).forEach(System.out::println); // finite will never be included in the result

Mistake #3: joining for non-strings. The Collectors.joining collector expects a stream of strings. For numbers, convert to strings first:

import java.util.List;
import java.util.stream.Collectors;

List<Integer> numbers = List.of(1, 2, 3);
// numbers.stream().collect(Collectors.joining(", ")); // Compilation error!

String joined = numbers.stream()
    .map(Object::toString)
    .collect(Collectors.joining(", "));

Mistake #4: breaking order. If order matters, use concat or an ordered flatMap. Methods like unordered() can break the deterministic output order.

1
Task
JAVA 25 SELF, level 32, lesson 1
Locked
Harvesting for the Grand Fruit Salad 🍎🍊
Harvesting for the Grand Fruit Salad 🍎🍊
1
Task
JAVA 25 SELF, level 32, lesson 1
Locked
Showcase design for celestial objects 🚀
Showcase design for celestial objects 🚀
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION