うるう年を判定する

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

うるう年を判定する とは

&&|| を組み合わせて、実用的な条件「うるう年かどうか」を 1 行で判定するメソッドを書いてみよう。

うるう年は「条件分岐の総合演習」

このレッスンでは、論理演算子 && || と比較演算子 == != をフル活用して、ある年が うるう年 (leap year) かどうかを判定するメソッドを書きます。たった 1 行で書ける問題ですが、その 1 行のなかに Java で条件分岐を扱うエッセンスがぎゅっと詰まっています。if 文・else 文・論理演算子の知識を、ここで実用レベルにまとめあげていきましょう。

うるう年判定は、FizzBuzz と並んで「プログラミング入門の鉄板課題」と呼ばれることが多い問題です。MicrosoftGoogle のコーディング面接でも、ジュニア向けのウォームアップとして出題された記録が残っています。一見シンプルでも、ルールの優先順位を取り違えると意外と間違える、奥が深い題材なのです。

うるう年のルールをおさらい

まずはアルゴリズムの前に、現実世界のルールを確認します。私たちが今使っているグレゴリオ暦のうるう年判定は、次の 3 つの条件を組み合わせたものです。

  • 基本ルール4 で割り切れる年はうるう年の 候補 になる
  • 例外 1 — ただし 100 で割り切れる年はうるう年では ない
  • 例外 2 — ただし 400 で割り切れる年は、やっぱりうるう年

言葉だけだと「ただし、ただし」が多くてこんがらがるので、具体例で確かめます。

  • 20244 で割り切れる、100 で割り切れない → うるう年 (true)
  • 20234 で割り切れない → うるう年ではない (false)
  • 21004 で割り切れる、でも 100 で割り切れる、400 では割り切れない → うるう年ではない (false)
  • 20004 で割り切れる、100 で割り切れる、400 でも割り切れる → うるう年 (true)

なぜこんなややこしい例外があるかというと、地球が太陽を 1 周する時間は 約 365.2422 日 で、365.25 日ピッタリではないからです。4 年に 1 回うるう年を入れるだけだとずれていくので、100 年に 1 回減らし、400 年に 1 回また戻す、という微調整をしているのです。

素直に if で書いてみる

まずは思考を整理するために、長めの if 文で書いてみます。

Java

public class Solution { public static boolean isLeapYear(int year) { if (year % 400 == 0) { return true; } else if (year % 100 == 0) { return false; } else if (year % 4 == 0) { return true; } else { return false; } } }

ここで使っているのは % (剰余演算子) です。year % 4 は「year4 で割った余り」、それが 0 なら「ちょうど割り切れた」という意味になります。%Java整数の割り算の余り を取る基本演算子で、うるう年・偶数奇数・FizzBuzz・配列の循環など、本当によく登場します。

上のコードのポイントは、特殊なルールから順番にチェックしている ことです。先に 400 で割り切れる年を true 確定にし、その後で 100 で割り切れる年を false で除外し、最後に 4 で割り切れる年を true にする。順番を入れ替えると正しい結果にならないので注意してください。

条件分岐を書くときは「特殊で強いルール」を上に、「ゆるいルール」を下に並べるのがコツです。else if は上から順番に評価されて、最初に当てはまったブロックだけが実行される、というルールを思い出しましょう。

&&|| で 1 行にまとめる

同じロジックを、論理演算子で 1 行に圧縮することもできます。これがこのレッスンの主役の書き方です。

Java

public class Solution { public static boolean isLeapYear(int year) { return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; } }

読み下すと「(4 で割り切れる かつ 100 で割り切れない) または (400 で割り切れる)」となります。日本語のうるう年の定義をほぼそのまま Java の式に直訳したような形です。それぞれのパーツの意味は次の通りです。

  • year % 4 == 0year4 で割った余りが 0、つまり 4 で割り切れる
  • year % 100 != 0year100 で割った余りが 0 ではない、つまり 100 で割り切れない
  • year % 400 == 0year400 で割った余りが 0、つまり 400 で割り切れる

この 3 つを && (かつ) と || (または) で結ぶと、if else if else if else で 4 ブロック書いたのとまったく同じ判定が、たった 1 行で表現できます。boolean を返すメソッドでは、こうやって式そのものを return する書き方がイディオムです。

if で書くべきか、&& || で書くべきか」は永遠の議論ですが、戻り値が boolean でロジックが 3〜4 要素までなら 1 行式 が読みやすいことが多いです。要素が増えてきたら if 文や、後で学ぶ switch 式で分解しましょう。

判定フローを Mermaid で見る

うるう年判定の分岐を流れ図にすると、こんな構造になります。

diagram (will load when visible)

この図と先ほどの if-else if 版のコードを見比べてみてください。図のひし形 (条件) がそのまま if の条件式に、矢印の先がそのまま return 文に対応していることがわかります。フローチャートを描いてからコードに翻訳するのは、複雑な条件分岐を組むときの強い味方になります。

そして (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 という 1 行式は、この 3 つのひし形を 同時に 評価して、論理演算子で結合した形です。if で書く版が条件を上から順に 1 つずつ評価していくのに対し、1 行式は数学的に「真になる条件を集めた式」を書いている、と理解するとスッキリします。

動きを追ってみる

書いた 1 行式が、それぞれの年でどう評価されるかを手で追ってみましょう。Java の演算子の優先順位では % == != && || の順に評価されることを思い出してください。

  • isLeapYear(2024)(2024 % 4 == 0 && 2024 % 100 != 0) || 2024 % 400 == 0
    • (0 == 0 && 24 != 0) || 24 == 0
    • (true && true) || false
    • true || false
    • true
  • isLeapYear(2100)(2100 % 4 == 0 && 2100 % 100 != 0) || 2100 % 400 == 0
    • (true && false) || (100 == 0)
    • false || false
    • false
  • isLeapYear(2000)(2000 % 4 == 0 && 2000 % 100 != 0) || 2000 % 400 == 0
    • (true && false) || true
    • false || true
    • true

左側の (... && ...)false でも、右側の year % 400 == 0true なら全体が true になる、というのが || の働きです。2000 年がうるう年として救われるのは、この右側の節のおかげです。

よくある間違い

この 1 行式は、一見シンプルですが詰まりどころが多い式です。代表的なミスを 3 つ紹介します。

  • %== の優先順位を勘違いするyear % 4 == 0 を「year % (4 == 0)」と解釈してしまう人がいますが、実際は % のほうが == より優先順位が高いので (year % 4) == 0 と評価されます。心配なときは自分で () を付けて意図を明確にしてもよいです
  • &&|| の優先順位を取り違える&&|| より優先順位が高いです。a || b && ca || (b && c) の意味になります。うるう年式で (year % 4 == 0 && year % 100 != 0) のように カッコで囲んでいる のはこの理由で、可読性とミス防止のためにカッコを残すのがおすすめです
  • === の打ち間違いyear % 4 == 0year % 4 = 0 と書くと「year % 40 を代入」しようとしてコンパイルエラーになります。比較は イコール 2 個、代入は イコール 1 個、と暗唱して指に覚えさせてください

もうひとつ、ハマりやすいのが「!=! の取り違え」です。!= は「等しくない」を判定する 比較演算子!boolean を反転させる 論理演算子 で、似ているけど別物です。year % 100 != 0!year % 100 == 0 と書いてしまうと、!year の時点で型が合わずエラーになります。

エラーが出たら、まずは IDE が示してくれる赤い波線とエラーメッセージを冷静に読むのが鉄則です。incompatible types: int cannot be converted to boolean のような英文は、慣れれば「intboolean のつもりで使っている」というヒントだとパッとわかるようになります。

やってみよう

それでは課題に挑戦します。Solution.isLeapYear(int year) を完成させましょう。

  1. まずは if-else if-else を使った長めの版で書いて、テストが全部緑になることを確認する
  2. 動いたら、return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; という 1 行版に書き換えて、同じテスト結果になるか試す
  3. さらに余裕があれば、year < 1 のときに例外をスローしたり、isLeapYear(1582) のように歴史的に複雑な年を試したりして遊んでみる

書き終えたら、2024 2023 2100 2000 1900 2020 の 6 つのテストケースを必ず通してください。特に 21002000 の違いは、100 で割り切れるけれど 400 で割り切れるか割り切れないかで結果がひっくり返る重要なポイントです。自分の頭の中で 1 行式の評価をたどってみて、truefalse が正しく出ることを確かめてから「実行」ボタンを押すと、より理解が深まります。

条件分岐は、Java だけでなく すべてのプログラミング言語の土台 です。ここで if 文と論理演算子の合わせ技を体に染み込ませておくと、この先のループや例外処理、クラス設計でも必ず効いてきます。じっくり手を動かしてマスターしていきましょう。

よくある質問

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 標準を固めるのが効率的です。

次のレッスン

次は switch 文 で、&&|| を組み合わせて、実用的な条件「うるう年かどうか」を 1 行で判定するメソッドを書いてみよう を学びます。

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

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

理解度チェック (30 秒)

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

この章のポイント

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

関連レッスン

要件

  1. クラス名は Solution、メソッド名は isLeapYear にすること
  2. 引数は int year の 1 つ、戻り値の型は boolean
  3. 4 で割り切れる、100 で割り切れない、400 で割り切れる、の 3 つのルールを正しく組み合わせて判定すること

入出力例

test-cases.txt

isLeapYear(2024)true isLeapYear(2023)false isLeapYear(2100)false isLeapYear(2000)true isLeapYear(1900)false isLeapYear(2020)true

ヒント

main.java
main.java
学習モード

メモ

うるう年を判定する

⌘S で保存