はじめに
TRIAL&RetailAI Advent Calendar 2025の15日目の記事になります。
昨日は @taquangku さんの「モバイルアプリにおけるJWT認証の実装について」という記事でした。
自分は不定期でブログを書いているのですが、その内容が業務において気になった部分や詰まった部分を調べた時の情報を備忘録的に残すというものになっています。
今回のアドベントカレンダーにおいてもその内容の延長ということで書いていきます。
Kotlin にて特定のデータをInt型に変換する際に、いくつかの選択肢の中からなぜ"それ"を選んだのかをうまく言語化できていなかったので、Int型への変換処理とそれによるメリット・デメリットを調べたのでまとめます。
3つの変換処理 (toInt,intValue,intValueExact)
Kotlinにおいて、Int型への変換に使われる処理としてメジャーなものは以下の3つです。
- toInt()
- intValue()
- intValueExact()
どれを使用したとしても最終的にはint型になっているのですが、では何を基準にして使い分けるのかということで、それぞれの仕様について理解していきましょう。
toInt()
Kotlinにおける汎用的な変換手段になります。
固有の特徴として、String型のデータ、つまり文字列をInt型に変換することが可能です。
変換元のデータによって挙動が変わるのでそれぞれ記述します。
String.toInt()
Int型への変換が可能かどうかを解析してから変換処理を実行します。
変換不可能な値(数値ではない文字列やIntの範囲外の数値)だった場合は例外がスローされます。
※Stringに対しては toIntOrNull() を使うことで変換不可能な値を null で受け取ることができます。
fun main() {
println("--- String.toInt() の例 ---")
// ケース1: 正常系
val normal = "123"
println("Success: ${normal.toInt()}") // 出力: 123
// ケース2: 数字ではない文字列 (NumberFormatException)
try {
val notNumber = "abc"
println(notNumber.toInt())
} catch (e: NumberFormatException) {
println("Error 1: 数字ではありません -> ${e.message}")
}
// ケース3: Intの範囲を超える文字列 (NumberFormatException)
try {
val tooBig = "2147483648" // Int.MAX_VALUE + 1
println(tooBig.toInt())
} catch (e: NumberFormatException) {
println("Error 2: 範囲外です -> ${e.message}")
}
}
Double.toInt() Float.toInt()
小数点以下の値は全て切り捨てて変換します。
Intの範囲より大きいサイズだった場合、自動的に範囲内になるよう丸められます。
※小数の値に対しては roundToInt() を使うことで小数部分を四捨五入しながら変換できます。
fun main() {
println("--- Double/Float.toInt() の例 ---")
// ケース1: 小数点以下切り捨て
val d1 = 1.99
println("Truncation: ${d1.toInt()}") // 出力: 1 (四捨五入ではない!)
// ケース2: 範囲外(巨大な数) -> クランプ (Clamping)
val hugeDouble = 1.0e20 // 非常に大きな数
val hugeInt = hugeDouble.toInt()
// エラーにはならず、Intの最大値になる
println("Overflow Max: $hugeInt") // 出力: 2147483647 (Int.MAX_VALUE)
// ケース3: 範囲外(巨大な負の数)
val hugeNegative = -1.0e20
println("Overflow Min: ${hugeNegative.toInt()}") // 出力: -2147483648 (Int.MIN_VALUE)
}
Long.toInt()
下位32ビット分を切り出して変換します。
範囲外の値だった場合、ラップアラウンドが発生します。
例:(Intの最大値のLong型 + 1).toInt() -> Intの最低値にラップアラウンドする
fun main() {
println("--- Long.toInt() の例 ---")
val maxInt = Int.MAX_VALUE.toLong() // 2147483647
// ケース1: 正常系
println("Normal: ${maxInt.toInt()}") // 出力: 2147483647
// ケース2: 1だけオーバーフロー
val overflow1 = maxInt + 1L
println("Overflow (+1): ${overflow1.toInt()}")
// 出力: -2147483648 (突然、最小値になる!)
// ケース3: 大幅なオーバーフロー
val overflowBig = 3000000000L
println("Overflow (30億): ${overflowBig.toInt()}")
// 出力: -1294967296 (全く意味不明な値になる)
}
BigInteger.toInt()
内部的にintValueを呼び出すだけなのでそちらで説明します。
以上をまとめると
メリット
- String型に対して使用できる
- Kotlinにおいて汎用的な処理なのでわかりやすい
デメリット
- Int型の範囲外の値が渡されてもほとんどの場合で例外がスローされず、数値を書き換えて変換処理を完了させるので、想定していないデータになってしまう場合がある
intValue()
Javaの数値型(Number)が共通して可能な処理です。
Int型の範囲外の値の場合、範囲内になるように自動的に丸められます。
使用可能な範囲で使っている限り、どんな値を変換してもエラーは発生しません。
基本的にはtoInt()とほとんど変わりませんがまとめると
メリット
- Javaとの互換性がある
- 数値型にしか使用できないので例外のスローが発生しない
デメリット
- Int型の範囲外の値が渡されても例外がスローされず、数値を書き換えて変換処理を完了させるので、想定していないデータになってしまう場合がある
intValueExact()
BigInteger・BigDecimal専用の変換手段になります。
変換対象の値がIntの範囲内かどうかを先に判定し、範囲外だった場合は例外をスローします。
それ以外はtoInt()やintValue()と変わらず変換が行われます。
import java.math.BigInteger
fun main() {
println("--- BigInteger 安全な変換 (intValueExact) の例 ---")
val storeCode = BigInteger("2147483648") // Intの範囲外
try {
// ここで厳密にチェックを行う
val id = storeCode.intValueExact()
println("変換成功: $id")
} catch (e: ArithmeticException) {
// バグになる前にここで止まる
println("🛑 エラー捕捉: storeCodeがIntの範囲を超えています!処理を中断します。")
// 必要に応じてログ出力や、呼び出し元へのエラー通知を行う
}
}
以上をまとめると
メリット
- 範囲外の数値はエラーで弾くので想定外の値になるという問題が発生しない
デメリット
- 使用できる値の幅が狭い
新たな疑問
ここまで調べていてちょっとした疑問が湧いてきました
「あれ、Long型を安全にInt型に変換する方法って存在しない…?」
ここまで取り上げた3つを使う場合、
- toInt() -> 勝手に下位32ビットを抜き出して変換
- intValue() -> 上記と同じ
- intValueExact() -> Long型には使用できない
変換するという意味では可能ですが、安全に変換する方法はこの中にはないようです。
ということで、追加で調べてみました。
Long -> Int への変換を安全に実行する
結論から言うと、Long -> Int を安全に変換するメソッドは存在しませんでした。
感覚的にありそうだと思った Long.toIntOrNull() はありません。
そもそもこのような無理矢理な変換が発生するような設計の方に問題があると言うことなんだろうと思いますが、それでも方法は知っておきたいと言うことで、Geminiに聞いてみました。
安全な変換方法 (手動チェック)
残念ながら、Long には BigInteger.intValueExact() のような、例外を投げてくれる便利なメソッドが標準で用意されていません。
そのため、安全に変換するには手動でチェックする必要があります。
val longValue = 3000000000L // 30億(Intの範囲外)
// 変換前に Int の範囲内にあるかチェックする
if (longValue < Int.MIN_VALUE || longValue > Int.MAX_VALUE) {
throw ArithmeticException("Long値 $longValue はIntの範囲外です")
}
// チェック後、安全に変換
val intValue = longValue.toInt()
このような感じであらかじめ変換できるかをチェックするという方法が最適のようです。
とてもシンプルではありますが、最終手段にしたいくらいには書きたくないコードでもありますね。
追加の疑問
ここまで調べている段階でもう1つ気になってきました。
「Char型のInt変換ってどんな挙動してたっけ…?」
最初の3つの変換について調べていたときは、String型と大して変わらないだろうと思っていたので、全く触れずに書いていたのですが、ここまでの調査中に独自の挙動があることを思い出したのでそれらについてもまとめてみます。
Char -> Int への変換で何が起きるか
なんとなくString型と同じ扱いでいいんじゃないかと思ってしまうChar型ですが、これに対して toInt() を使用することは非推奨となっています。
理由については以下の変換メソッドを見ていってもらえればわかるかと思います。
Char型に使われる変換メソッドは主に2つです。
Char.digitToInt()
シンプルに数値に変換する処理です。
val c = '5'
val i = c.digitToInt() // 結果: 5
安全に取得したい場合は digitToIntOrNull() も使うことができます。
これだけであれば toInt() が使えてもいいような気がしますが、問題は次の変換です。
Char.code()
対象の文字コードを取得します。
val c = 'A'
val code = c.code // 結果: 65
「コード値を取得したい」と明示したい時に使う処理になります。
このように、toInt() だとどちらの情報が欲しいのかわからないので非推奨になっているということですね。
まとめ
3つのInt型変換手段を見てきましたが、それぞれにちゃんと使うべき時と注意点が存在しています。
どんな時に例外がスローされ、小数や範囲外の値はどのように変換されるのか、最終的に想定した値になっているかを知っておくことで、どの変換手段を使うべきかを明確に決めることができるようになります。
何よりも、変換時に無理なことをしないでいいように設計することが一番大切ですね。
明日のアドベントカレンダーは、@tech_tuna さんの「マグネットポンプをつかって自動灌水機をつくりたかった」です。
ぜひそちらもご覧いただければと思います。
Retail AI と TRIAL ではエンジニアを募集しています。
この記事を見て興味を持ったという方がいらっしゃいましたら、ぜひご連絡ください。