split と join
split と join とは
Java の文字列を扱う上で必須の
String.splitとString.joinを学び、CSV を逆順に並べ替えるメソッドを実装してみよう。
文字列を「分けて」「つなぐ」二大メソッド
業務アプリでも Android アプリでも、外部から渡ってくるデータの多くは カンマ区切り タブ区切り スラッシュ区切り といった「区切り文字でつながった 1 本の文字列」の形をしています。CSV ファイルの 1 行、URL のパス、ユーザー入力のタグ一覧、JSON では表現しきれないシンプルなログ、どれもこの形式です。こうした文字列を扱うとき、Java で最初に思い出すべきなのが String.split と String.join の 2 つのメソッドです。
splitは「区切り文字で切り分けて配列にする」、joinは「区切り文字で配列をつないで 1 本の文字列にする」、ちょうど逆の操作と覚えると整理しやすいです。
本章では、この 2 つを軸に「文字列 → 配列 → 加工 → 文字列」というデータ変換の基本パターンを身につけます。一見地味ですが、Java の現場で最も登場頻度が高い文字列操作のひとつであり、Web API の URL を組み立てたり、ログを集計したりするときに必ず役立ちます。
String.split で配列に切り分ける
まずは split から見ていきます。split は String クラスのインスタンスメソッドで、呼び出した文字列を引数で指定した区切り文字で分割し、結果を String[] (文字列配列) として返します。
Java
String csv = "apple,banana,cherry";
String[] parts = csv.split(",");
System.out.println(parts.length); // 3
System.out.println(parts[0]); // apple
System.out.println(parts[1]); // banana
System.out.println(parts[2]); // cherry見た目はとてもシンプルですが、split には覚えておくべき重要な性質がいくつかあります。
- 戻り値は
String[]であってList<String>ではない。リストとして扱いたいときはArrays.asList(parts)などで変換する - 元の文字列が空文字列
""の場合、戻り値は要素 1 個の配列[""]になる (空配列ではない) - 区切り文字が連続している場合 (
"a,,b"を,でsplitするなど) は途中に空文字列の要素が入る - 引数は 正規表現 として解釈される。これが最大の落とし穴
3 番目の特性は意外と知られていません。たとえば "a,,b".split(",") は ["a", "", "b"] という 3 要素の配列になります。CSV パースで「ユーザーが何も入力していない列」を扱うときに重要なので覚えておきましょう。
splitのもうひとつの注意点として、末尾の空要素は省略される という仕様があります。"a,b,,".split(",")の結果は["a", "b"](2 要素) になります。これを避けたいときはsplit(",", -1)のように第 2 引数に負の数を渡すと、末尾の空要素も保持されます。
String.join で配列をつなぐ
次に join です。join は String クラスの 静的メソッド で、第 1 引数に区切り文字、第 2 引数以降に連結したい文字列を並べて渡します。配列や List をそのまま渡すことも可能です。
Java
String[] words = {"red", "green", "blue"};
String joined = String.join("-", words);
System.out.println(joined); // red-green-blue
List<String> names = List.of("佐藤", "鈴木", "高橋");
String line = String.join(", ", names);
System.out.println(line); // 佐藤, 鈴木, 高橋join の便利なところは、区切り文字を要素と要素の間にしか入れない点です。"a" + "," + "b" + "," + "c" のように自前で連結すると、ループの中で末尾の余計な , を取り除く処理が必要になりがちですが、join ならそんな心配は不要です。
Java 8 で導入された
String.joinは、それ以前はStringBuilderでループしながら自前で連結していた処理を一気にスッキリ書けるようにしてくれました。Pythonの",".join(list)とほぼ同じ感覚で使えます。引数の順番が逆 (Javaは区切り文字が先) になっている点だけ注意してください。
split の引数は「正規表現」
split を使うときに最も多いトラブルが、引数を「ただの区切り文字」だと思って渡してしまうことです。実は split の引数は 正規表現 として解釈されます。たとえばパスを . で分割しようとして次のように書くと、期待と違う結果になります。
Java
String path = "a.b.c";
String[] bad = path.split("."); // [] (空配列)
String[] good = path.split("\\."); // ["a", "b", "c"]正規表現の世界では . は「任意の 1 文字」を表す特殊文字なので、"a.b.c".split(".") は「任意の 1 文字で区切る」となり、すべての文字を区切り文字とみなして空配列を返します。文字どおりの . で区切りたいなら、\\. のようにバックスラッシュ 2 つでエスケープする必要があります。
同様に、| ? * + ( ) [ ] { } ^ $ も正規表現の特殊文字なので、そのまま区切り文字に使うと意図しない動作になります。エスケープが面倒なときは java.util.regex.Pattern.quote(".") を使うと「文字どおりの文字列」として扱えるようになるので便利です。
一方の
String.joinには正規表現の概念はなく、第 1 引数の文字列はそのまま区切り文字として挿入されます。String.join(".", parts)は素直に.を挟むだけです。splitとjoinで挙動が違うところは混乱しやすいポイントなので意識しておきましょう。
Collectors.joining という選択肢
Stream API を使っているコードでは、String.join の代わりに Collectors.joining を見かけることがあります。これは Stream<String> を文字列に集約するためのコレクタで、String.join と似た役割を果たします。
Java
import java.util.List;
import java.util.stream.Collectors;
List<String> tags = List.of("java", "backend", "web");
String result = tags.stream()
.map(String::toUpperCase)
.collect(Collectors.joining(","));
System.out.println(result); // JAVA,BACKEND,WEBCollectors.joining には joining(",", "[", "]") のように接頭辞と接尾辞を指定できるオーバーロードもあり、"[JAVA,BACKEND,WEB]" のような形を一発で組み立てられます。とはいえ単純に既に手元にある配列や List をつなぎたいだけなら、わざわざ Stream に変換せずに String.join を使うほうがシンプルです。ストリーム処理の途中で文字列化したいときは Collectors.joining、最終結果をつなぐだけなら String.join、と使い分けるとよいでしょう。
よくある間違い
split と join を組み合わせるときに、初学者がハマりやすいポイントを 3 つ整理しておきます。
splitの引数に正規表現特殊文字をそのまま渡す —".""|""?"などはエスケープしないと意図どおりに動きません。split("\\.")split("\\|")のようにバックスラッシュ 2 つでエスケープするか、Pattern.quoteを使いましょう- 空文字列を
splitした結果を空配列だと思い込む —"".split(",")は要素 1 個の配列[""]を返します。空入力に対して空文字列を返したいときは、csv.isEmpty()で先に判定するのが安全です - 末尾の空要素が消える仕様を知らない —
"a,b,,".split(",")は 2 要素しか返しません。CSV の列数を厳密に揃えたいときはsplit(",", -1)を使って末尾の空要素を保持してください
もうひとつ、String.join の第 1 引数に null を渡すと NullPointerException が飛びます。区切り文字を変数で渡すときは null チェックを忘れずに行いましょう。
String[] parts = csv.split(",");のあとにparts[0].trim()のようにtrim()を使うと、" apple , banana "のように区切り文字の周りに空白がある CSV にも対応できます。Web フォームから受け取った値には空白が混ざりがちなので、現場ではsplitの直後にtrimするのがほぼ定番です。
やってみよう
今回の課題では、CSV を受け取って要素を逆順に並べ替えてからつなぎ直すメソッド Solution.swapJoin(String csv) を作ります。手順は次のとおりです。
- 引数
csvが空文字列""のときは、そのまま空文字列を返す csv.split(",")でString[]に分割する- 配列を逆順に並べ替える (新しい配列を作ってもよいし、ループでインデックスを逆順に回してもよい)
String.join(",", ...)で,を挟みながらつなぎ直してreturnする
動作例は次のとおりです。
swapJoin("a,b,c")→"c,b,a"swapJoin("hello")→"hello"(区切り文字が含まれないので 1 要素のままで結果は同じ)swapJoin("")→""(空入力)swapJoin("1,2,3,4")→"4,3,2,1"
空文字列のハンドリングを忘れると、"".split(",") が [""] を返してしまい、String.join(",", new String[]{""}) の結果が "" にはなるものの「空チェックなしでも動く」と勘違いしやすいので、明示的に csv.isEmpty() で分岐しておくのがおすすめです。配列の逆順は、Java 標準には「配列をそのまま逆順にする 1 行メソッド」がないので、for (int i = parts.length - 1; i >= 0; i--) のようにループで StringBuilder に詰めるか、List に変換して Collections.reverse を使うか、新しい配列に逆順で詰め直すなど、好きな書き方で OK です。String.join は配列も List も両方受け取れるので、どちらの中間データでも同じ書き方でつなげます。
書き終えたら、テストで "a,b,c" "hello" "" "1,2,3,4" の 4 パターンを確認します。エッジケースを意識して書くと、現場の CSV パースでもバグの少ないコードが書けるようになります。
よくある質問
Q. INNER JOIN と LEFT JOIN の違いは?
A. INNER は両側に一致するレコードのみ、LEFT は左側を全て残し右側に一致するものを結合します。LEFT で右側が無いと NULL になります。「ユーザーごとに購入履歴を、購入が無いユーザーも含めて出したい」場合は LEFT JOIN が必須です。
Q. 3 テーブル以上を JOIN できますか?
A. できます。a JOIN b ON ... JOIN c ON ... のように繋げます。JOIN は順番に評価されるため、絞り込みの強い条件を先に置くとパフォーマンスが上がります。EXPLAIN で実行計画を確認しながら順序を見直すのが定石です。
Q. JOIN したら行数が増えたのはなぜ?
A. 右側に複数の一致レコードがある(多対多)場合、その分だけ行が増えます。意図しない重複が出たら GROUP BY や DISTINCT で集約するか、サブクエリで先に集計してから JOIN すると行数が安定します。
次のレッスン
次は 文字列操作 まとめクイズ で、Java の文字列を扱う上で必須の `String を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- split/join の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. split/join とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
csv.split(",")を使って文字列を,で分割すること- 分割した配列を逆順に並べ替えてから
String.join(",", ...)でつなぎ直すこと csvが空文字列""のときは空文字列をそのまま返すこと
入出力例
test-cases.txt
swapJoin("a,b,c") → "c,b,a"
swapJoin("hello") → "hello"
swapJoin("") → ""
swapJoin("1,2,3,4") → "4,3,2,1"
swapJoin("foo,bar") → "bar,foo"