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.Collectors | java.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)の有無や引数の数を変えて出力結果を確認しておくと、本番でも落ち着いて選択肢を判断できるようになります。