はじめに
年が明けて、コーディングの基礎体力を鍛え直したいと思い、AOJ を始めました。
以前、牛尾さんの本で「LeetCode から始めた」という話を読んで、問題を解きながらコーディング力を上げる方法に興味を持っていました。
LeetCode に挑戦したい気持ちはあるものの、英語に抵抗感があったため、まずは日本語で解ける AOJ からスタートすることに。
AOJ の Introduction to Programming I(ITP1)は、入出力や基本的な演算から体系的に学べる構成になっていて、基礎固めにちょうど良さそうです。
この記事では、Getting Started(1_A〜1_D)を Kotlin で解いた記録と、環境で詰まったポイントをまとめます。
動作環境
ローカル環境
この記事で使用している環境は以下の通りです:
- 開発環境: IntelliJ IDEA 2025.3.1
- 言語: Kotlin 2.2.20
- JDK: Amazon Corretto 21.0.9
- OS: macOS Sequoia 15.7.2
AOJのKotlin環境
AOJのKotlin 環境ではreadln()が使えませんでした。
IntelliJ で書いていると「readLine() は readln() に置き換えられます」という警告が出ますが、AOJ では readln() を使うとビルドエラーになります。
これは、readln() が Kotlin 1.6.0 から導入された関数で、AOJ の環境が 1.6.0 より前のバージョンを使っているためと思われます。
解決法
readLine() を使えば問題なく動作します:
fun main() {
val n = readLine()!!.toInt()
}
readLine() は String?(nullable な文字列)を返すため、!!(非 null assertion)を付けて String 型として扱っています。
AOJ では入力が必ず存在するため、!! を使っても問題ありません。
Getting Started
1_A: Hello World
"Hello World" と一行で出力する問題です。
提出したコード
fun main() {
println("Hello World")
}
1_B: X Cubic
x の 3 乗を計算し結果を出力する問題です。
提出したコード
fun main() {
val n = readLine()!!.toInt()
val result = n * n * n
println(result)
}
1_C: Rectangle
長方形の面積と周囲の長さを計算する問題です。
提出したコード
fun main() {
val (a, b) = readLine()!!.split(" ").map { it.toInt() }
val area = a * b
val perimeter = 2 * (a + b)
println("$area $perimeter")
}
1_D: Watch
秒を 時間:分:秒 に変換する問題です。
提出したコード
fun main() {
val S = readLine()!!.toInt()
val h = S / 3600 // 1時間は3600秒なので、商を求めて「時間」を出す
val m = (S % 3600) / 60 // 3600で割った余り(1時間に満たない秒数)から「分」を出す
val s = S % 60 // 最後に60で割った余りが、繰り上がらなかった「秒」
println("$h:$m:$s")
}
番外編
今回の目的は競プロではなく、Web アプリ開発の基礎体力をつけること。
なので、実務を意識して「読みやすさ」と「変更しやすさ」を重視したコードも書いてみます。
Web アプリ開発を意識した書き方
1_C : Rectangle
/**
* 長方形のデータとビジネスルールを管理するドメインモデル
*/
data class Rectangle(val width: Int, val height: Int) {
companion object {
const val MIN_SIZE = 1
const val MAX_SIZE = 100
}
init {
// インスタンス生成時に仕様を満たしているかチェック(ドメインの保護)
require(width in MIN_SIZE..MAX_SIZE) { "Width must be between $MIN_SIZE and $MAX_SIZE" }
require(height in MIN_SIZE..MAX_SIZE) { "Height must be between $MIN_SIZE and $MAX_SIZE" }
}
// 計算ロジックをプロパティとして隠蔽(外部は計算式を知らなくて良い)
val area = width * height
val perimeter = 2 * (width + height)
}
/**
* 外部の入力(String)をバリデーションし、ドメインモデルへ変換する(変換層)
*/
fun parseRectangle(input: String): Rectangle? {
val parts = input.trim().split(" ")
if (parts.size != 2) return null
return try {
val a = parts[0].toInt()
val b = parts[1].toInt()
// 変換と同時にドメインの制約チェックも行う
Rectangle(a, b)
} catch (_: Exception) {
// 数値変換失敗や制約違反を安全に処理
null
}
}
/**
* アプリケーションのエントリーポイント
*/
fun main() {
// 1. 入力の取得
val input = readLine() ?: return
// 2. 変換とバリデーション(早期リターンをせず、else でエラーハンドリングを明示)
val rectangle = parseRectangle(input)
if (rectangle != null) {
// 3. 正常系の処理:ビジネスロジックの結果を出力
println("${rectangle.area} ${rectangle.perimeter}")
} else {
// 4. 異常系の処理:ユーザーに具体的な修正アクションを提示(UX 向上)
System.err.println("Error: 「3 5」のように、1〜100 の数値を半角スペース 1 つで区切って入力してください。")
}
}
なぜこう書くのか:
競プロとしては冗長ですが、「チーム開発で変更に強く、壊れにくいコード」 にするために、以下のWeb開発のプラクティスを取り入れています。
関心の分離(Separation of Concerns)
-
main関数(司令塔)から「パースの仕方」や「面積の計算式」などの詳細を切り離しました。 - これにより、
mainを読むだけで 「入力を取る → モデルに変換する → 結果を出す」という全体の流れが一目で理解できる ようになります。
ドメインモデルによるカプセル化
-
Rectangleクラスにデータと計算ロジックを閉じ込め、外部から計算式を隠蔽しています(DRY 原則)。 -
initブロックでバリデーションを行い、「不正な状態のオブジェクトは存在すらさせない」 という堅牢な設計を担保しています。
マジックナンバーの排除(保守性の向上)
-
1や100といった仕様上の数値を定数(MIN_SIZE,MAX_SIZE)として定義しました。 - これにより、将来の仕様変更時に 「一箇所直すだけで全てのバリデーションとメッセージが更新される」 変更に強い構造になります。
境界(Boundary)の意識と UX
- 外部からの「汚いデータ」を「綺麗なオブジェクト」に変換する層(
parseRectangle)を設けています。 - 入力ミスに対して 「具体例(3 5)付きのエラーメッセージ」 を返すことで、システムを落とさずにユーザーを正しい操作へ導く実務的な工夫をしています。
テスト容易性(Unit Testability)
- 「標準入力がなくても、パース関数や計算ロジックが正しいか」を個別に検証できるため、自動テストが書きやすく、大規模開発でも壊れないコードになります。
おわりに
AOJ では入力形式が保証されているため、シンプルなコードで十分ですが、実務では「壊れにくい(想定外の入力にも耐えられる)コード」を意識して書くことが大切だと感じました。
また、Kotlin の data class をドメインモデルとして使うことで、計算ロジックをきれいに整理できることも学べました。
1_D の時間計算は苦手で Gemini に頼ってしまいましたが、ひとまず最初の 4 問を PASS できました🙌
問題に PASS すると Score が表示されるので、なんだか達成感がありますね。
コードは GitHub で公開しています。
Here’s to another year of happy coding!✨
