🎯 最終演習

Kotlinで歩数の記録から週のふりかえりを作ってみよう(総合演習)

ここまでの Kotlin レッスンで学んだことを全部つないで、動くものを1つ完成させます。新しい構文は出てきません。data class・null安全・when・リスト・filter——すでに練習したものだけで組み立てます。

作るのは1週間の歩数記録のふりかえりです。合計と平均を出し、目標を達成した日を数え、体重の増減を計算して、表にまとめます。

この演習には仕掛けがあります。——記録し忘れた日が入っています。水曜は何も記録せず、土曜は体重だけ量りました。

ここがKotlinを学ぶ意味のいちばん出るところです。「値が無いかもしれない」をnullで表し、型の上でそれを扱わせる——だから?.?:が、覚えるべき記法ではなく必要だから使う道具として出てきます。

このページではコードを実行していません。かわりにお手本と同じコードを書けたかを、その場で判定します(判定はあなたのブラウザの中だけで行われ、どこにも送信されません)。

表示している実行結果は本物です。このページを作るときに、実際のKotlinでコンパイルして走らせた出力をそのまま載せています。

完成イメージ

最後まで進むと、このコードが完成します。下の実行結果は、このページを作るときに開発機で実際にコードを走らせて得たものです(このページ上では実行していません)。

===== 今週のふりかえり =====
月   8,420歩  62.4kg  あと少し
火  11,350歩  62.1kg  達成
水       ——     ——  記録なし
木   6,980歩  62.6kg  不足
金  13,240歩  62.0kg  達成
土       ——  61.8kg  記録なし
日   9,510歩  61.9kg  あと少し
--------------------------------------
記録できた日: 5日 / 7日
合計歩数    : 49,500歩
1日平均      : 9,900歩
目標達成    : 2日(火・金)
体重の変化  : -0.5kg

STEP 11日の記録をdata classで表す

🎯 このステップのゴール: 値を持つだけのクラスを1行で作り、null許容の型を使い分ける

まずデータの形を決めます。1日が持つのは「日付・歩数・体重」の3つです。

data class DayLog(val date: String, val steps: Int?, val weightKg: Double?)——これだけで完成です

dataを付けると、Kotlinが3つのものを自動で用意します。中身を並べたtoString()、中身が同じなら等しいと判定するequals()、そして一部だけ変えた複製を作るcopy()Javaで同じことをすると、数十行の決まりきったコードを書くことになります

ここからが本題です。型に付いている?を見てください。

date: Stringには?がありません——日付は必ずありますsteps: Int?には付いています——記録し忘れた日があるからです

この?が Kotlin の中心にある考え方です。?の付かない型にはnullを入れられません。試しにDayLog("月", null, 62.4)の第1引数をnullにしてみてください——コンパイルが通りません

多くの言語では、どの変数にもnullが入りえます。だから「ここはnullにならないはず」という思い込みが、実行時に崩れます。Kotlinはその思い込みを型に書かせて、コンパイラに見張らせます

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
記録した日数: 7日
1日目: DayLog(date=月, steps=8420, weightKg=62.4)
水曜日: DayLog(date=水, steps=null, weightKg=null)

STEP 2記録のない日を飛ばして合計する

🎯 このステップのゴール: エルビス演算子で「無いときの代わり」を決める

歩数を合計します。ところがday.stepsInt?——そのままでは足せません

total += day.stepsと書くとコンパイルエラーになります。nullかもしれないものを足そうとしているからです。

ここが Kotlin の親切なところです。他の言語なら、たいてい実行時まで気づきません。夜中にサーバーが落ちてから「あの日だけデータが無かった」と分かる——Kotlinはそれを書いた瞬間に止めます

解決するのがエルビス演算子?:です。day.steps ?: 0と書くと、stepsnullなら0を使う」という意味になります。

名前の由来は見た目です。横に倒すと歌手のエルビス・プレスリーの前髪に見える、というところから来ています。覚えやすい名前ですが、やっていることは「無かったときの代わりを決める」——それだけです。

ここで大事なのは「0でよいのか」を考えることです。歩数の合計なら、記録の無い日を0扱いしても合計は変わりません。ですが平均を出すときは違います——記録の無い日を0として割ると、平均が実際より低く出ます。

だからステップ6の平均は記録のある日数で割ります?: 0と書いた瞬間、「無い」が「0」に化けます。それでよいかは、毎回考えてください。

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
合計歩数: 49500歩
記録のない日も0として足しています

STEP 3whenで評価を振り分ける

🎯 このステップのゴール: 条件を並べたwhenを式として使い、returnを省く

その日の歩数から「達成」「あと少し」「不足」を決めます。

使うのはwhenです。when { 条件 -> 値 }と書くと、上から順に条件を見て、最初に当たったものの値になります

順番が結果を決めます。steps == nullを最初に置いているのは、nullを先に片付けないと、その先の比較ができないからです。null >= GOALは書けません。

そしてwhenより前に、この関数の形を見てください。

fun rank(steps: Int?): String = when { ... }——波括弧もreturnもありません=のあとに式を1つ書くだけの単一式関数という書き方です。

これが成り立つのは、Kotlinのwhenが「文」ではなく「式」だからです。値を返すので、そのまま関数の戻り値にできます。Javaのswitchのように、変数を用意して各分岐で代入する必要がありません

elseを書いているのにも意味があります。戻り値の型をStringと決めた以上、どの道を通っても必ず文字列を返さなければなりませんelseが無いとコンパイラが「返らない場合がある」と止めます。

GOAL * 3 / 4は7500です。掛け算を先に書いています——GOAL / 4 * 3でも同じ値になりますが、割り算を先にすると端数が落ちる場面があるので、掛けてから割るのを習慣にしてください。

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
月: あと少し
火: 達成
水: 記録なし
木: 不足
金: 達成
土: 記録なし
日: あと少し

STEP 4letで「値があるときだけ」整形する

🎯 このステップのゴール: ?.let を使い、nullのときの表示を別に用意する

歩数を8,420歩のように整形して表示します。ただし記録の無い日は——と出したい

ここでString.format("%,6d歩", day.steps)とは書けません。day.stepsInt?だからです。

使うのが?.letです。

day.steps?.let { String.format("%,6d歩", it) } ?: " ——"——長く見えますが、読み方は素直です

?.は「nullでなければ、その先へ進む」nullならそこで止まって、全体がnullになります。

let { }は「受け取った値をブロックに渡して、結果を返す」。ブロックの中ではitという名前でその値が使えます。しかもitの型はInt——?が外れていますnullのときはletまで来ないからです。

そして最後の?: " ——"が、nullだったときの表示です。

この3つが組み合わさると「値があるときだけ加工し、無ければ別のものを出す」が1行で書けます。if (day.steps != null) { ... } else { ... }と書いても同じですが、変数を増やさずに済むぶん、読む負担が軽くなります

%,6d,は3桁区切り、66文字ぶんの幅で右揃えという意味です。幅を揃えるので、表の列がきれいに並びます。

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
月   8,420歩  62.4kg
火  11,350歩  62.1kg
水       ——     ——
木   6,980歩  62.6kg
金  13,240歩  62.0kg
土       ——  61.8kg
日   9,510歩  61.9kg

STEP 5filterで絞り込み、体重の増減を出す

🎯 このステップのゴール: 条件に合う要素を集め、両端が揃わないときはnullを返す

目標を達成した日を数えます。week.filter { (it.steps ?: 0) >= GOAL }——条件に合う要素だけを集めた新しいリストが返ります。

中で?: 0を使っているのは、記録の無い日を「達成していない」扱いにするためです。nullのまま>=で比べることはできません。

日付を並べるのがjoinToString("・") { it.date }です。区切り文字と、各要素から何を取り出すかを渡すだけ火・金という文字列になります。

次が体重の増減です。ここは少し込み入っています

week.firstOrNull { it.weightKg != null }?.weightKg——体重を記録した最初の日を探し、その体重を取り出すという意味です。

firstOrNullは「見つからなければnull」を返します。firstだと見つからないときに例外で落ちるので、「無いかもしれない」場面ではこちらを使います

そのあとの?.weightKgが効いています。見つからなければnullのまま先へ進まない——nullに対してプロパティを取ろうとして落ちる、ということが起きません。

そして戻り値をDouble?にしているのが、この関数の要点です。体重を1日も記録していなければ、増減は「0」ではなく「計算できない」0と「不明」を混ぜないでください——混ぜた瞬間、「体重が変わらなかった週」と「量らなかった週」が区別できなくなります。

if (first == null || last == null) return nullと早めに返しておくと、そこから先はnullを気にせず計算できます。Kotlinのコンパイラも、この行より下ではfirstDoubleとして扱ってくれます。

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
目標達成: 2日(火・金)
体重の変化: -0.5kg
記録なしの週: null

STEP 6ひとつの関数にまとめて仕上げる

🎯 このステップのゴール: reportにまとめ、平均を「記録のある日数」で割る

最後の仕上げです。ここまでの部品をreportという1つの関数にまとめます

ここで、ステップ2で保留にした話に決着をつけます。平均歩数を何で割るかです。

total / week.size——7で割ると、記録の無い2日を「0歩歩いた日」として数えることになります。実際より低い平均が出ます。

total / recorded.size——記録のある5日で割れば、「歩いた日の平均」になります。こちらが知りたい数字のはずです。

どちらが正しいかは、何を知りたいかで決まります。「毎日どれだけ歩いたか」なら7で割る、「歩いた日はどれだけ歩いたか」なら5で割る。プログラムは黙って計算するだけなので、決めるのは書く人です。

if (recorded.isNotEmpty())で囲んでいるのは、0で割らないためです。1日も記録していない週を渡されたら、ここで落ちてしまいます。割る前に0を確かめる——習慣にしてください。

体重の増減はif (change == null)で分けます。「記録が足りません」と正直に出す——0kgと表示して「変わらなかった」と誤解させるより、ずっと親切です。

増えたときに+を付けるのはif (change < 0) "" else "+"です。マイナスは自動で付きますが、プラスは付きません。増減を見せるときは、この一手間で読みやすさが変わります。

完成です。weekの水曜に歩数を入れてみてください。合計も平均も達成日数も、全部連動して変わります。

逆に全部のweightKgnullにしてみてください。体重の欄が「記録が足りません」に変わり、ステップ5でDouble?を返すようにしておいた意味が確かめられます。

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
===== 今週のふりかえり =====
月   8,420歩  62.4kg  あと少し
火  11,350歩  62.1kg  達成
水       ——     ——  記録なし
木   6,980歩  62.6kg  不足
金  13,240歩  62.0kg  達成
土       ——  61.8kg  記録なし
日   9,510歩  61.9kg  あと少し
--------------------------------------
記録できた日: 5日 / 7日
合計歩数    : 49,500歩
1日平均      : 9,900歩
目標達成    : 2日(火・金)
体重の変化  : -0.5kg

よくある質問

このページではコードを実行しないのですか?

していません。Kotlinはコンパイルが必要な言語で、ブラウザの中では走らせられないためです。このページではお手本と同じコードを書けたかどうかを判定する形にしています(判定はあなたのブラウザの中だけで行われ、コードはどこにも送信されません)。

ただし表示している実行結果は本物です。このページを作るときに実際のKotlinでコンパイルして走らせた出力を、そのまま載せています。

?. と !! は、どう使い分けますか

!!は使わないでください、というのが基本の答えです。

!!は「nullではないと私が保証する」という意味で、外れるとその場で例外が飛びます。つまりKotlinがせっかく用意した仕組みを、自分から捨てていることになります。

?.で先へ進めないようにするか、?:で代わりの値を決めるか、if (x != null)で早めに分けるか——たいていはこの3つのどれかで書けます

どうしても!!を書きたくなったら、その値がなぜnullになりうる型なのかを見直すほうが早いことが多いです。

平均を出すとき、記録のない日はどう扱えばいいですか

何を知りたいかで変わります。プログラムは決めてくれません。

「毎日どれだけ歩いたか」を知りたいなら、記録のない日を0として7日で割ります

「歩いた日はどれだけ歩いたか」を知りたいなら、記録のある日数で割ります。この演習ではこちらを採りました。

大事なのはどちらにしたかをコードから読み取れるようにしておくことです。total / recorded.sizeと書いてあれば、意図が伝わります。

data class は普通の class と何が違いますか

「値を持つのが仕事」のクラス向けに、決まりきったコードを自動で用意してくれます。

toString()——中身を並べた文字列。ステップ1でDayLog(date=月, steps=8420, weightKg=62.4)と表示できたのはこれのおかげです。

equals()——中身が同じなら等しいと判定します。普通のクラスは「同じ実物かどうか」で比べるので、中身が同じでも別物と判定されます。

copy()——一部だけ変えた複製を作ります。day.copy(steps = 9000)のように書けます。

逆に振る舞いが主役のクラスには向きません。継承させたいクラスや、内部の状態を隠したいクラスは普通のclassで書きます。

🚩 次はこのカテゴリへ

KotlinはJavaと同じ土台の上で動きます。この演習で1行で書いたdata classを、Javaで書くとどうなるか——比べてみると、Kotlinが何を自動でやってくれているかがはっきりします。Javaカテゴリの最終演習では、図書館の貸出管理を作ります。null安全が無い世界でどう気をつけるかも、あわせて見えてきます。

Javaのレッスンへ進む
広告スペース(バナー)
広告スペース(記事内)