FizzBuzz 完成

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

FizzBuzz 完成 とは

前章の FizzBuzz 判定と for ループを組み合わせて、1 から n までの FizzBuzz を空白区切りで返す完成版を作ろう。

FizzBuzz 完成 ─ for と判定を組み合わせる

前のセクションで作った fizzbuzz(int n) は、ひとつの数字を受け取って "Fizz" "Buzz" "FizzBuzz" "7" のような文字列を返すだけのメソッドでした。今回はそこに for ループを足して、1 から n までを一気に処理する 本物の FizzBuzz を完成させます。これが書けると、面接の定番問題が文字どおり「完答」できる状態になります。

前章の fizzbuzz判定 だけを担当する純粋な関数でした。今回作る fizzbuzzList は、その判定を for ループで n 回呼び出して、結果をひとつの文字列につないでいきます。「小さい関数を組み合わせて大きい関数を作る」というのは、Java に限らずあらゆる言語に共通する基本姿勢です。

仕様を確認しておきましょう。fizzbuzzList(int n)1 から n までを FizzBuzz ルールで変換した文字列を、半角スペース区切り で返します。具体例は次の通りです。

  • fizzbuzzList(5)"1 2 Fizz 4 Buzz"
  • fizzbuzzList(15)"1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz"
  • fizzbuzzList(3)"1 2 Fizz"
  • fizzbuzzList(1)"1" (要素 1 個なので空白なし)

全体像をつかむ

処理の流れはとてもシンプルです。空っぽの文字列を用意して、i1 から n まで動かしながら、その i に対応する FizzBuzz 文字列を末尾に足していくだけです。

diagram (will load when visible)

ポイントは「for の中で前章の判定ロジックを呼び出す」「result という変数に少しずつ追加していく」「最後の要素のあとにスペースを残さない」の 3 つです。順番に見ていきましょう。

シンプル版 ─ 文字列連結で書いてみる

まずは一番素直な書き方です。String+= でつないでいき、最後に trim() で末尾の余分なスペースを取り除きます。

Java

public class Solution { public static String fizzbuzzList(int n) { String result = ""; for (int i = 1; i <= n; i++) { if (i % 15 == 0) { result += "FizzBuzz"; } else if (i % 3 == 0) { result += "Fizz"; } else if (i % 5 == 0) { result += "Buzz"; } else { result += i; } result += " "; } return result.trim(); } }

for (int i = 1; i <= n; i++)i1, 2, 3, ..., n と動きます。条件式が i < n ではなく i <= n であることに注意してください。n まで 含めて 処理したいので、ここは <= が正解です。

判定の中身は前章とまったく同じです。i % 15 == 0FizzBuzz を最初に判定し、その次に Fizz、最後に Buzz、どれにも当てはまらなければ i を文字列として追加します。result += i; のように int をそのまま足しても、Java が自動で文字列に変換してくれます。

String result = "";""空の文字列 であって、null ではありません。String result = null; から始めると result += "..."; のときに "null..." という文字列ができてしまうので注意してください。初期値は必ず "" です。

各要素を追加したあとに result += " "; で半角スペースを足しています。これだと最終ループでも末尾にスペースが残るので、最後に result.trim() で前後の空白文字をまとめて削除しています。trim()String クラスに最初から用意されているメソッドで、両端の半角スペース・タブ・改行を取り除いてくれます。

Mermaid でループの動きをもう一度見る

for ループの内部を別の角度で書くと、判定 → 追加 → スペース追加、をひたすら繰り返している構造がよく見えます。

diagram (will load when visible)

for の 3 つの部分 (初期化 条件式 更新式) は、それぞれ「最初に 1 回だけ」「毎回チェック」「毎回最後に」走ります。この構造はあらゆる for ループに共通するので、頭の中で必ずこの図が再生できるようになっておくと安心です。

大きい n では StringBuilder を使うべき理由

シンプル版の result += "..."; はとても読みやすい一方で、n が大きくなると一気に遅くなる という欠点があります。なぜでしょうか?

JavaStringイミュータブル (一度作ったら変更できない) です。つまり result += "Fizz"; は、内部では「新しい String を作ってその中に result"Fizz" を全部コピーしてから、result をその新しい String に置き換える」という処理が走っています。n100 くらいなら気になりませんが、10000 100000 と増えてくると、毎回コピーが走るのでループ回数の 2 乗に比例して遅くなります。

そこで登場するのが StringBuilder です。StringBuilder可変 な文字列バッファで、append で末尾に追加するときに毎回コピーを取らずに済みます。書き換えた版は次のとおりです。

Java

public class Solution { public static String fizzbuzzList(int n) { StringBuilder sb = new StringBuilder(); for (int i = 1; i <= n; i++) { if (i > 1) { sb.append(" "); } if (i % 15 == 0) { sb.append("FizzBuzz"); } else if (i % 3 == 0) { sb.append("Fizz"); } else if (i % 5 == 0) { sb.append("Buzz"); } else { sb.append(i); } } return sb.toString(); } }

ここでは末尾スペースを取らなくて済むように、先頭判定 という別のテクニックを使っています。「i1 のときはスペースを足さない、i2 以上のときは要素の 前に スペースを足す」というロジックです。if (i > 1) sb.append(" "); の 1 行がそれを実現しています。最後に sb.toString()String に変換すれば完成です。

どちらの書き方でもテストは通りますが、n1 万以上になることがわかっているなら StringBuilder 一択 です。実務でも「文字列連結を for で大量に行うところは StringBuilder に置き換える」というのは定番の最適化テクニックなので、頭の隅に置いておきましょう。

末尾のスペースを取り除く 2 つのアプローチ

気付いた人もいるかもしれませんが、この課題の地味なポイントは 末尾の空白問題 です。"1 2 Fizz 4 Buzz " のように最後にスペースが残ると、テストの期待値 "1 2 Fizz 4 Buzz" と一致せず fail します。アプローチは大きく 2 つに分かれます。

  • 後始末派 ─ 毎回末尾にスペースを足しておいて、最後に result.trim() でまとめて取り除く。シンプル版で使った書き方
  • 先頭判定派 ─ 2 番目以降の要素の にスペースを足す。StringBuilder 版で使った書き方

どちらも有効ですが、StringBuilder のときは trim() で内部バッファ全体を走査するコストがあるので、先頭判定 のほうが効率的だ、というのが一般的な意見です。String+= でやるときは、もうコピーが大量に走っているので trim() の差は誤差なので気にしなくて大丈夫です。

動きを追ってみる ─ fizzbuzzList(5)

試しに fizzbuzzList(5) を呼び出したときの動きを、シンプル版で追ってみましょう。

  1. result = "" でスタート
  2. i = 1 どの条件にも当てはまらず → result = "1 "
  3. i = 2 どの条件にも当てはまらず → result = "1 2 "
  4. i = 3 3 % 150 ではなく、3 % 30 なので Fizzresult = "1 2 Fizz "
  5. i = 4 どの条件にも当てはまらず → result = "1 2 Fizz 4 "
  6. i = 5 5 % 150 ではなく、5 % 30 ではなく、5 % 50 なので Buzzresult = "1 2 Fizz 4 Buzz "
  7. ループ終了。result.trim() で末尾スペースを取って "1 2 Fizz 4 Buzz" を返す

判定ロジックが前章で身についていれば、追加で考えないといけないのは「for でどう回すか」「区切り文字をどう扱うか」のたった 2 点だけです。

よくある間違い

FizzBuzz の完成版を書くときに、ほぼ全員が一度はハマる落とし穴を 3 つ紹介します。

  • 判定順序を逆にする ─ 前章でも触れた話ですが、if (i % 3 == 0) を最初に書いてしまうと 15 のときも "Fizz" が返ってしまい、"FizzBuzz" が一度も出てきません。必ず i % 15 == 0 を最初に判定する か、i % 3 == 0 && i % 5 == 0 を最初に書きましょう
  • 末尾のスペースを取り忘れる"1 2 Fizz 4 Buzz " のように末尾にスペースが残ると、テストの期待値 "1 2 Fizz 4 Buzz" と異なり fail します。trim() で取り除くか、最初から先頭判定方式で書きましょう
  • n0 のときに何が返るかを考えていない ─ もし fizzbuzzList(0) が呼ばれたら、for は 1 回も回らず result"" のまま trim() を経て空文字列が返ります。今回のテストでは n >= 1 しか使いませんが、業務コードなら エッジケース として「0 のときに何を返すべきか」を意識しておくとバグが減ります

もうひとつ、ループの条件を i < n と書き間違える人がたまにいます。これは n 自身が処理されない バグで、fizzbuzzList(5)"1 2 Fizz 4" で止まるので、テストの 2 ケース目以降がガッツリ fail します。<=< か、書く前に「n を含めるかどうか」を必ず意識しましょう。

ループの境界条件 (fencepost error) は、ベテランプログラマでもうっかり間違えるバグの代表格です。for を書いたら必ず「最初の i は?」「最後の i は?」を声に出して確認するクセを付けると、99% のオフバイワンエラーは防げます。

やってみよう

それでは課題に挑戦してみましょう。

  1. 右側のエディタを開く
  2. String result = ""; で結果用の変数を初期化する
  3. for (int i = 1; i <= n; i++)1 から n までループを回す
  4. ループの中で i % 15 i % 3 i % 5 の順番で判定し、対応する文字列を result に足していく
  5. 各要素のあとに半角スペースを足す
  6. 最後に result.trim()return する

テストでは fizzbuzzList(5) fizzbuzzList(15) fizzbuzzList(3) fizzbuzzList(1) のような複数のケースを実行します。n = 1 の最小ケースで末尾スペースが残らないこと、n = 15 でちゃんと最後に "FizzBuzz" が登場すること、の両方が大事です。

慣れてきたら、StringBuilder 版に書き換えてみる、trim() を使わない先頭判定方式に書き換えてみる、と挑戦してみてください。同じ仕様を別の書き方で実現できるようになると、コードの引き出しが一気に増えます。これで FizzBuzz 問題は完答です。次は配列やコレクションを使った、もう一段大きな処理に進んでいきましょう。

よくある質問

Q. Java と他言語の文法はどう違いますか?

A. Java は静的型付けで、変数宣言時に型を明示します(int x = 0;)。中括弧でブロックを表し、文末にセミコロンが必要です。Python の動的型付けや JavaScript の柔軟さに慣れていると最初は窮屈ですが、コンパイル時にバグが発見できる安全性がメリットです。

Q. コードが動かないときに最初に見るべき場所は?

A. コンパイルエラーは行番号とエラー種別(cannot find symbol、incompatible types など)が表示されます。実行時例外はスタックトレースの最初の at ... が原因行です。IDE(IntelliJ / VS Code)の警告も丁寧に潰すと、半分のバグは未然に防げます。

Q. Java の習得後に学ぶべき技術は何ですか?

A. 基本構文を抑えたら java-intermediate(コレクション / ジェネリクス / Stream / 例外)に進み、Spring Boot や Web 開発に展開するのが王道です。クラウド時代は Kotlin / Scala への展開も視野に入りますが、まずは Java 標準を固めるのが効率的です。

次のレッスン

次は 繰り返しクイズ で、前章の FizzBuzz 判定と for ループを組み合わせて、1 から n までの FizzBuzz を空白区切りで返す完成版を作ろう を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. FizzBuzz の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. FizzBuzz とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. クラス名は Solution、メソッド名は fizzbuzzList、引数は int n 1 つ、戻り値の型は String
  2. 1 から n までを for ループで回し、各値を FizzBuzz ルールで変換した文字列を 半角スペース区切り でつなげること
  3. 末尾に余分なスペースを残さないこと (trim() を使うか、先頭判定方式でスペースを 2 番目以降の前に置く)

入出力例

test-cases.txt

fizzbuzzList(5)"1 2 Fizz 4 Buzz" fizzbuzzList(15)"1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz" fizzbuzzList(3)"1 2 Fizz" fizzbuzzList(1)"1" fizzbuzzList(10)"1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz"

ヒント

main.java
main.java
学習モード

メモ

FizzBuzz 完成

⌘S で保存