Stream APIの問題集を解いていると、Collectors.joining()が出てくるたびに「あれ、引数は何個あったっけ」と手が止まってしまう方は多いのではないでしょうか。区切り文字を指定するパターンと、接頭辞・接尾辞まで指定するパターンが似ているため、選択肢を見た瞬間に混乱してしまい、本来なら確実に取れるはずの問題を落としてしまうケースが目立ちます。

この記事では、Collectors.joining()の3つのオーバーロードを整理したうえで、1Z0-826で実際に狙われやすいひっかけポイントをコード付きで解説します。読み終えたタイミングで、試験本番でも迷わず正解を選べる状態を目指しましょう。

Collectors.joining()とは何か

Collectors.joining()は、Stream内の要素を1つの文字列として連結するためのCollectorです。Stream<String>やStream<CharSequence>に対してcollect()メソッドの引数として渡すことで、ループ処理を書かずに文字列結合を実現できます。

ポイントは、この収集器が対象とするのはCharSequence型の要素であるという点です。StringはCharSequenceを実装しているため問題なく使えますが、Integerや独自クラスのStreamに対してそのまま使うとコンパイルエラーになります。この制約は後ほど詳しく取り上げます。

基本形:区切り文字なしの連結

引数なしのjoining()は、Stream内の文字列をそのまま順番につなげます。

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

public class JoiningBasic {
    public static void main(String[] args) {
        List<String> fruits = List.of("apple", "banana", "cherry");

        String result = fruits.stream()
                .collect(Collectors.joining());

        System.out.println(result);
        // applebananacherry
    }
}

区切り文字も接頭辞・接尾辞も指定しないため、単純に文字列が連結されるだけです。実務ではあまり使う機会がありませんが、試験では他のオーバーロードとの違いを問う問題の比較対象として登場します。

区切り文字を指定する:joining(delimiter)

実務でもっとも使用頻度が高いのが、区切り文字を指定するオーバーロードです。

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

public class JoiningWithDelimiter {
    public static void main(String[] args) {
        List<String> fruits = List.of("apple", "banana", "cherry");

        String result = fruits.stream()
                .collect(Collectors.joining(", "));

        System.out.println(result);
        // apple, banana, cherry
    }
}

ここで注意したいのは、区切り文字は要素の間にのみ挿入されるという点です。先頭や末尾には付与されません。「カンマ区切りにしたら最後にも余分なカンマが付くのでは」と誤解する方がいますが、実際には要素数が3つなら区切り文字は2回しか使われません。

接頭辞・接尾辞を追加する:joining(delimiter, prefix, suffix)

3引数版では、区切り文字に加えて全体の先頭と末尾に文字列を付与できます。配列やリストの内容を見た目よく出力したいときに便利です。

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

public class JoiningWithPrefixSuffix {
    public static void main(String[] args) {
        List<String> fruits = List.of("apple", "banana", "cherry");

        String result = fruits.stream()
                .collect(Collectors.joining(", ", "[", "]"));

        System.out.println(result);
        // [apple, banana, cherry]
    }
}

toString()を自作するときの定型パターンとしてもよく使われる書き方です。

3つのオーバーロードの比較表

メソッド区切り文字接頭辞接尾辞空のStreamでの結果
joining()なしなしなし""(空文字)
joining(delimiter)ありなしなし""(空文字)
joining(delimiter, prefix, suffix)ありありありprefix + suffix(例:"[]")

3行目の「空のStreamでの結果」が試験で特に問われやすいポイントです。次の章で具体的なコードとともに確認します。

試験でよく狙われる注意点

空のStreamを連結したときの挙動

要素が1つもないStreamに対してcollect(Collectors.joining())を実行すると、結果はnullではなく空文字列""になります。NullPointerExceptionが発生すると思い込んでいると失点しやすいポイントです。

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

public class JoiningEmptyStream {
    public static void main(String[] args) {
        List<String> empty = List.of();

        String result1 = empty.stream().collect(Collectors.joining());
        String result2 = empty.stream().collect(Collectors.joining(", "));
        String result3 = empty.stream().collect(Collectors.joining(", ", "[", "]"));

        System.out.println("[" + result1 + "]");
        // []
        System.out.println("[" + result2 + "]");
        // []
        System.out.println(result3);
        // []
    }
}

3引数版だけは、要素がなくても接頭辞と接尾辞は出力されるため、result3は"[]"になります。これは前章の表でも示した通りで、出力結果だけを見て「全部同じ空文字になるはず」と判断すると間違えます。

CharSequence型でなければコンパイルエラーになる

Collectors.joining()が受け付けるのはStream<CharSequence>です。Stream<Integer>のようにCharSequenceを実装していない型に対して直接使うと、コンパイルエラーになります。

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

public class JoiningTypeMismatch {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3);

        // 以下はコンパイルエラー
        // String result = numbers.stream().collect(Collectors.joining(", "));

        // 正しい書き方:mapで文字列に変換してから連結する
        String result = numbers.stream()
                .map(String::valueOf)
                .collect(Collectors.joining(", "));

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

試験では「このコードはコンパイルできるか」という形式で問われることが多く、map(String::valueOf)やmap(Object::toString)の有無が正解・不正解の分かれ目になります。Stream<Integer>をそのまま渡しているコードを見たら、まずコンパイルエラーを疑う癖をつけておくと安全です。

IntStreamなど基本データ型のStreamには直接使えない

IntStreamやLongStreamはStream<T>ではないため、Collectors.joining()をそのままcollect()に渡すことはできません。boxed()でStream<Integer>に変換するか、mapToObj()で文字列のStreamに変換する必要があります。

import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class JoiningIntStream {
    public static void main(String[] args) {
        String result = IntStream.rangeClosed(1, 5)
                .mapToObj(String::valueOf)
                .collect(Collectors.joining("-"));

        System.out.println(result);
        // 1-2-3-4-5
    }
}

mapToObjを経由する書き方を覚えておくと、IntStream系の問題で迷わなくなります。

String.join()との違い

似た名前のメソッドとしてString.join()があり、両者を混同した選択肢が試験に登場することがあります。

項目Collectors.joining()String.join()
所属クラスjava.util.stream.Collectorsjava.lang.String
使用場面Streamのcollect()内で使う配列やIterableに対して直接使う
Streamが必要か必要不要
import java.util.List;

public class StringJoinExample {
    public static void main(String[] args) {
        List<String> fruits = List.of("apple", "banana", "cherry");

        String result = String.join(", ", fruits);

        System.out.println(result);
        // apple, banana, cherry
    }
}

String.join()はStreamを経由せずにそのままリストや配列を渡せるため、シンプルな連結処理ではこちらの方が短く書けます。一方でCollectors.joining()は、フィルタリングやマッピングといった他のStream操作と組み合わせられる点が強みです。試験ではこの「使い分けの理由」も選択肢の根拠として問われることがあります。

まとめ

Collectors.joining()は引数の数によって挙動が変わるため、区切り文字だけのパターンと接頭辞・接尾辞付きのパターンを表で整理しておくことが理解の近道です。特に、空のStreamを連結したときの結果と、CharSequenceでない型をそのまま渡したときのコンパイルエラーは、1Z0-826で繰り返し問われる典型的なひっかけポイントです。

コードを実際に手元で動かしながら、map(String::valueOf)の有無や引数の数を変えて出力結果を確認しておくと、本番でも落ち着いて選択肢を判断できるようになります。