Kotlin の「型変換」には、性質の違う2つの操作があります。1つは as・as?・is による型のキャストで、オブジェクトはそのままに「どの型として扱うか」だけを変えます。もう1つは toInt()・toIntOrNull()・toDouble() などによる値の変換で、別の型の新しい値を作ります。 "123" を数値にしたいときに as Int と書いても変換はされず、ClassCastException になります。使うのは toInt() です。

この記事では2つを分けて整理し、変換元と変換先の早見表、ClassCastException と NumberFormatException の実際のメッセージ、スマートキャストが効かない場面のコンパイルエラー文と直し方、Long から Int への桁あふれや Double から Int への切り捨てを、実行出力つきで説明します。

確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc)/ OpenJDK 21.0.10。実行は kotlinc A.kt -include-runtime -d a.jar && java -Dstdout.encoding=UTF-8 -jar a.jar で行いました(-Dstdout.encoding=UTF-8 は日本語の出力を UTF-8 にそろえる指定です)。

型変換とキャストの早見表

キャストと変換の違い

項目 型のキャスト 値の変換
書き方 x as String、x as? String、x is String x.toInt()、x.toIntOrNull()、x.toString()
行うこと 同じオブジェクトを別の型として扱う 別の型の新しい値を作る
使える組み合わせ 実際の型が、指定した型かそのサブタイプのとき 変換関数が用意されている型どうし
うまくいかないとき as は ClassCastException、as? は null toInt() は NumberFormatException、toIntOrNull() は null
代表的な用途 Any や親の型で受け取った値を元の型に戻す 文字列を数値に、Int を Long に、Double を Int に

キャストの演算子

書き方 型が合うとき 型が合わないとき null のとき
x is T true(以降 x を T として使える) false false
x !is T false true true
x as T T 型の値 ClassCastException NullPointerException
x as T? T? 型の値 ClassCastException null
x as? T T? 型の値 null null

変換元と変換先の早見表

変換元 → 変換先 書き方 実行結果の例 気をつける点
String → Int "123".toInt() 123 数字以外があると NumberFormatException
String → Int(安全に) "abc".toIntOrNull() null ?: 0 で既定値を付けられる
String → Long "2147483648".toLong() 2147483648 Int に入らない桁数のとき
String → Double "12.5".toDouble() 12.5 toDoubleOrNull() もある
String → Boolean "TRUE".toBoolean() true "yes" は false。厳密に見るなら toBooleanStrict()
String → BigDecimal "0.1".toBigDecimal() 0.1 小数を誤差なく扱いたいとき
String → Char "A".single() A 2文字以上は IllegalArgumentException
Int → Long / Double 123.toLong() / 123.toDouble() 123 / 123.0 自動では変換されない
Int → String 255.toString() / 255.toString(16) 255 / ff 引数は基数
Int → Byte 200.toByte() -56 下位8ビットだけ残る
Int → Char 65.toChar() A 文字コードとして扱う
Long → Int 3000000000L.toInt() -1294967296 下位32ビットだけ残る(例外は出ない)
Double → Int 3.7.toInt() 3 0 の方向へ切り捨て
Double → Int(四捨五入) 3.7.roundToInt() 4 import kotlin.math.roundToInt
Double → String "%.2f".format(3.14159) 3.14 桁数を指定して文字列にする
Char → Int(数字の値) '7'.digitToInt() 7 '7'.code は文字コードの 55
Any → String など x as? String 合わなければ null 値は変換されない

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Kotlin 型変換とキャスト as・as?・is と toIntOrNull」

型のキャスト:is とスマートキャスト

is は「実行時の型がその型か」を調べる演算子です。調べたあとの範囲では、キャストを書かなくてもその型として使えます。 これをスマートキャストと呼びます。

fun describe(x: Any?): String = when (x) {
    null -> "null"
    is String -> "String 長さ=${x.length}"          // x は String として使える
    is Int -> "Int 2倍=${x * 2}"
    is List<*> -> "List 要素数=${x.size}"
    !is Number -> "数値ではない: $x"
    else -> "その他の数値: $x"
}

fun main() {
    for (v in listOf("Kotlin", 21, listOf(1, 2), null, 'c', 3.5)) {
        println(describe(v))
    }

    val any: Any = "hello"
    if (any is String && any.length > 3) println("&& の右側でも String: ${any.uppercase()}")
    if (any !is String) return
    println("return のあとは String: ${any.length}")
}
String 長さ=6
Int 2倍=42
List 要素数=2
null
数値ではない: c
その他の数値: 3.5
&& の右側でも String: HELLO
return のあとは String: 5

スマートキャストが働く場所は、if や when の分岐の中だけではありません。&& の右側、!is で早めに return したあとの行でも、その型として扱えます。

型のキャスト:as と as? の違いと ClassCastException

open class Animal(val name: String)
class Dog(name: String) : Animal(name) { fun bark() = "$name: ワン" }
class Cat(name: String) : Animal(name)

fun main() {
    val a: Animal = Dog("ポチ")
    val dog = a as Dog                 // ダウンキャスト
    println(dog.bark())

    val c: Animal = Cat("タマ")
    try {
        (c as Dog).bark()
    } catch (e: ClassCastException) {
        println("as   : ${e::class.simpleName}: ${e.message}")
    }
    println("as?  : ${(c as? Dog)?.bark()}")
    println("as? と ?: : ${(c as? Dog)?.bark() ?: "${c.name} は Dog ではありません"}")

    val any: Any = 42
    try {
        val s = any as String
        println(s)
    } catch (e: ClassCastException) {
        println("Int を String に: ${e.message}")
    }
    try {
        val l = any as Long
        println(l)
    } catch (e: ClassCastException) {
        println("Int を Long に  : ${e.message}")
    }
    println("数値は変換関数で: ${(any as Int).toLong()}")

    val nothing: Any? = null
    try {
        val s = nothing as String
        println(s)
    } catch (e: NullPointerException) {
        println("null as String : ${e::class.simpleName}: ${e.message}")
    }
    println("null as String?: ${nothing as String?}")
    println("null as? String: ${nothing as? String}")
}
ポチ: ワン
as   : ClassCastException: class Cat cannot be cast to class Dog (Cat and Dog are in unnamed module of loader 'app')
as?  : null
as? と ?: : タマ は Dog ではありません
Int を String に: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
Int を Long に  : class java.lang.Integer cannot be cast to class java.lang.Long (java.lang.Integer and java.lang.Long are in module java.base of loader 'bootstrap')
数値は変換関数で: 42
null as String : NullPointerException: null cannot be cast to non-null type kotlin.String
null as String?: null
null as? String: null

ここから分かることを整理します。

  • as は型が合わないと ClassCastException を投げます。 メッセージは class 実際の型 cannot be cast to class 指定した型 の形で、どの型をどの型にしようとしたかが読み取れます。
  • as? は型が合わないと null を返します。 ?. や ?: と組み合わせると、分岐を1行で書けます。
  • Any に入った Int を as Long にはできません。 Int と Long は親子関係にない別の型で、キャストでは値が変換されないためです。いったん as Int で取り出してから toLong() を呼びます。
  • null を as String にすると NullPointerException になります。null を許すなら as String?、型が合わない場合も null にしたいなら as? String です。

公式ドキュメントでも、as は合わないと実行時に ClassCastException を投げるので安全でない(unsafe)演算子と呼ばれること、as String? は null を受け入れるだけで型が合わなければやはり例外になり、as? String は null を返すことが説明されています。

状況 選ぶ書き方
型を調べて分岐したい is と when
合わなければ null にして先へ進みたい as? と ?. / ?:
合わないのは想定外で、すぐ気づきたい as(例外で止まる)
数値の型を変えたい、文字列を数値にしたい キャストではなく toXxx()

コンパイル時の警告も手がかりになる

明らかに成り立たないキャストや、ジェネリクスの型引数へのキャストは、コンパイル時に警告が出ます。

fun main() {
    val mixed: List<Any> = listOf(1, "two")
    val strings = mixed as List<String>
    if (mixed is List<String>) println("x")
    val n: Int = 1
    val s = n as String
    println(strings.size.toString() + s)
}
GenericNg.kt:3:25: warning: unchecked cast of 'List<Any>' to 'List<String>'.
    val strings = mixed as List<String>
                        ^^^^^^^^^^^^^^^
GenericNg.kt:4:9: warning: check for instance is always 'true'.
    if (mixed is List<String>) println("x")
        ^^^^^^^^^^^^^^^^^^^^^
GenericNg.kt:6:15: warning: this cast can never succeed.
    val s = n as String
              ^^

警告のままコンパイルは通り、実行すると6行目で ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String になりました。this cast can never succeed が出たら、キャストではなく変換関数(この場合は n.toString())を使う合図です。

スマートキャストが効かない場面とエラー文、直し方

スマートキャストは、「調べてから使うまでの間に値が変わらない」とコンパイラが判断できるときだけ働きます。判断できない場面では、次のコンパイルエラーになります。

class Session {
    var user: Any? = null

    fun show() {
        if (user is String) {
            println(user.length)
        }
    }
}

open class Base { open val label: Any = "base" }

class Holder(val item: Any) {
    val computed: Any get() = "x"

    fun show(b: Base) {
        if (computed is String) println(computed.length)
        if (b.label is String) println(b.label.length)
        if (item is String) println(item.length)   // これは通る
    }
}

fun main() {
    var local: Any = "abc"
    val change = { local = 1 }
    if (local is String) {
        println(local.length)
    }
    change()
}
SmartNg.kt:6:21: error: smart cast to 'String' is impossible, because 'user' is a mutable property that could be mutated concurrently.
            println(user.length)
                    ^^^^
SmartNg.kt:17:41: error: smart cast to 'String' is impossible, because 'computed' is a property that has an open or custom getter.
        if (computed is String) println(computed.length)
                                        ^^^^^^^^
SmartNg.kt:18:40: error: smart cast to 'String' is impossible, because 'label' is a property that has an open or custom getter.
        if (b.label is String) println(b.label.length)
                                       ^^^^^^^
SmartNg.kt:27:17: error: smart cast to 'String' is impossible, because 'local' is a local variable that is mutated in a capturing closure.
        println(local.length)
                ^^^^^
対象 スマートキャスト エラー文の理由の部分
ローカルの val 効く ―
val プロパティ(item) 効く ―
var プロパティ(user) 効かない is a mutable property that could be mutated concurrently
独自の getter を持つプロパティ(computed) 効かない is a property that has an open or custom getter
open なプロパティ(label) 効かない 同上
ラムダの中で書き換えられるローカルの var(local) 効かない is a local variable that is mutated in a capturing closure

var プロパティは、is で調べた直後に別の処理から書き換えられる可能性があるため、対象外です。公式ドキュメントの表でも、var プロパティは「ほかのコードからいつでも変更されうるので、スマートキャストされない」とされています。

直し方は3通りあります。どれも「いったん変わらない変数に取り出す」という考え方です。

class Session {
    var user: Any? = null

    // 直し方1: ローカルの val に取り出す
    fun show1() {
        val u = user
        if (u is String) println("show1: ${u.length}")
    }

    // 直し方2: as? と ?.let
    fun show2() {
        (user as? String)?.let { println("show2: ${it.length}") }
    }

    // 直し方3: when の主語で変数に束縛する
    fun show3() = when (val u = user) {
        is String -> println("show3: ${u.length}")
        is Int -> println("show3: 数値 $u")
        else -> println("show3: その他")
    }
}

fun main() {
    val s = Session()
    s.user = "takahashi"
    s.show1(); s.show2(); s.show3()
    s.user = 7
    s.show1(); s.show2(); s.show3()
}
show1: 9
show2: 9
show3: 9
show3: 数値 7
直し方 書き方 向いている場面
ローカルの val に取り出す val u = user のあと if (u is String) 何行かにわたって使う
as? と ?.let (user as? String)?.let { ... } 1つの型だけを短く扱う
when の主語で束縛する when (val u = user) { is String -> ... } 型ごとに分岐する

値の変換:文字列を数値にする toInt と toIntOrNull

文字列から数値への変換は、入力値を扱うプログラムで最もよく使う変換です。さまざまな文字列で toInt() を呼んだ結果を並べます。

fun <T> attempt(label: String, f: () -> T) {
    try {
        println("%-28s -> %s".format(label, f()))
    } catch (e: Exception) {
        println("%-28s -> %s: %s".format(label, e::class.simpleName, e.message))
    }
}

fun main() {
    println("--- toInt ---")
    attempt("\"123\".toInt()") { "123".toInt() }
    attempt("\"-45\".toInt()") { "-45".toInt() }
    attempt("\"+7\".toInt()") { "+7".toInt() }
    attempt("\"007\".toInt()") { "007".toInt() }
    attempt("\"abc\".toInt()") { "abc".toInt() }
    attempt("\"\".toInt()") { "".toInt() }
    attempt("\" 12 \".toInt()") { " 12 ".toInt() }
    attempt("\" 12 \".trim().toInt()") { " 12 ".trim().toInt() }
    attempt("\"12.5\".toInt()") { "12.5".toInt() }
    attempt("\"1,000\".toInt()") { "1,000".toInt() }
    attempt("\"12\".toInt()") { "12".toInt() }
    attempt("\"2147483648\".toInt()") { "2147483648".toInt() }
    attempt("\"2147483648\".toLong()") { "2147483648".toLong() }
    attempt("\"ff\".toInt(16)") { "ff".toInt(16) }
    attempt("\"0xff\".toInt(16)") { "0xff".toInt(16) }
    attempt("\"1010\".toInt(2)") { "1010".toInt(2) }

    println("--- toIntOrNull ---")
    attempt("\"123\".toIntOrNull()") { "123".toIntOrNull() }
    attempt("\"abc\".toIntOrNull()") { "abc".toIntOrNull() }
    attempt("\"12.5\".toIntOrNull()") { "12.5".toIntOrNull() }
    attempt("\"abc\".toIntOrNull() ?: 0") { "abc".toIntOrNull() ?: 0 }
    attempt("\"2147483648\".toIntOrNull()") { "2147483648".toIntOrNull() }

    println("--- toDouble ---")
    attempt("\"12.5\".toDouble()") { "12.5".toDouble() }
    attempt("\"12\".toDouble()") { "12".toDouble() }
    attempt("\" 12.5 \".toDouble()") { " 12.5 ".toDouble() }
    attempt("\"1e3\".toDouble()") { "1e3".toDouble() }
    attempt("\"NaN\".toDouble()") { "NaN".toDouble() }
    attempt("\"1,000.5\".toDouble()") { "1,000.5".toDouble() }
    attempt("\"abc\".toDouble()") { "abc".toDouble() }
    attempt("\"\".toDouble()") { "".toDouble() }
    attempt("\"abc\".toDoubleOrNull()") { "abc".toDoubleOrNull() }
    attempt("\"12.5\".toDouble().toInt()") { "12.5".toDouble().toInt() }

    println("--- そのほか ---")
    attempt("\"true\".toBoolean()") { "true".toBoolean() }
    attempt("\"TRUE\".toBoolean()") { "TRUE".toBoolean() }
    attempt("\"yes\".toBoolean()") { "yes".toBoolean() }
    attempt("\"yes\".toBooleanStrict()") { "yes".toBooleanStrict() }
    attempt("\"yes\".toBooleanStrictOrNull()") { "yes".toBooleanStrictOrNull() }
    attempt("\"0.1\".toBigDecimal()") { "0.1".toBigDecimal() }
    attempt("\"12345678901234567890\".toBigInteger()") { "12345678901234567890".toBigInteger() }
    attempt("\"200\".toByte()") { "200".toByte() }
    attempt("\"200\".toUByte()") { "200".toUByte() }
    attempt("\"-1\".toUInt()") { "-1".toUInt() }
    attempt("\"A\".single()") { "A".single() }
    attempt("\"AB\".single()") { "AB".single() }
}
--- toInt ---
"123".toInt()                -> 123
"-45".toInt()                -> -45
"+7".toInt()                 -> 7
"007".toInt()                -> 7
"abc".toInt()                -> NumberFormatException: For input string: "abc"
"".toInt()                   -> NumberFormatException: For input string: ""
" 12 ".toInt()               -> NumberFormatException: For input string: " 12 "
" 12 ".trim().toInt()        -> 12
"12.5".toInt()               -> NumberFormatException: For input string: "12.5"
"1,000".toInt()              -> NumberFormatException: For input string: "1,000"
"12".toInt()                 -> 12
"2147483648".toInt()         -> NumberFormatException: For input string: "2147483648"
"2147483648".toLong()        -> 2147483648
"ff".toInt(16)               -> 255
"0xff".toInt(16)             -> NumberFormatException: For input string: "0xff" under radix 16
"1010".toInt(2)              -> 10
--- toIntOrNull ---
"123".toIntOrNull()          -> 123
"abc".toIntOrNull()          -> null
"12.5".toIntOrNull()         -> null
"abc".toIntOrNull() ?: 0     -> 0
"2147483648".toIntOrNull()   -> null
--- toDouble ---
"12.5".toDouble()            -> 12.5
"12".toDouble()              -> 12.0
" 12.5 ".toDouble()          -> 12.5
"1e3".toDouble()             -> 1000.0
"NaN".toDouble()             -> NaN
"1,000.5".toDouble()         -> NumberFormatException: For input string: "1,000.5"
"abc".toDouble()             -> NumberFormatException: For input string: "abc"
"".toDouble()                -> NumberFormatException: empty String
"abc".toDoubleOrNull()       -> null
"12.5".toDouble().toInt()    -> 12
--- そのほか ---
"true".toBoolean()           -> true
"TRUE".toBoolean()           -> true
"yes".toBoolean()            -> false
"yes".toBooleanStrict()      -> IllegalArgumentException: The string doesn't represent a boolean value: yes
"yes".toBooleanStrictOrNull() -> null
"0.1".toBigDecimal()         -> 0.1
"12345678901234567890".toBigInteger() -> 12345678901234567890
"200".toByte()               -> NumberFormatException: Value out of range. Value:"200" Radix:10
"200".toUByte()              -> 200
"-1".toUInt()                -> NumberFormatException: Invalid number format: '-1'
"A".single()                 -> A
"AB".single()                -> IllegalArgumentException: Char sequence has more than one element.

NumberFormatException になる文字列のパターン

入力 toInt() の結果 対処
前後に空白(" 12 ") 例外 trim() してから変換する
小数("12.5") 例外 toDouble() してから toInt() / roundToInt()
桁区切り("1,000") 例外 replace(",", "") してから変換する
空文字("") 例外 toIntOrNull() で null として受ける
Int の範囲外("2147483648") 例外 toLong() を使う
0x 付きの16進("0xff") 基数16でも例外 removePrefix("0x") してから toInt(16)
先頭の +、先頭の 0、全角数字 変換できる そのままでよい

toInt() と toDouble() では空白の扱いが違います。 " 12 ".toInt() は例外になり、" 12.5 ".toDouble() は 12.5 になりました。入力値は先に trim() しておくと、どちらでも同じ結果になります。

例外メッセージは For input string: "入力した文字列" の形です。そのまま表示させると、次のようなスタックトレースになります。

Exception in thread "main" java.lang.NumberFormatException: For input string: "12a"
    at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
    at java.base/java.lang.Integer.parseInt(Integer.java:662)
    at java.base/java.lang.Integer.parseInt(Integer.java:778)
    at InputKt.main(Input.kt:20)
    at InputKt.main(Input.kt)

入力値の検査:toIntOrNull でまとめて書く

toIntOrNull() は、変換できないときに例外ではなく null を返します。?: と組み合わせると、「読めない」「範囲外」を順番に検査する処理が短く書けます。上のスタックトレースは、このコードの最後の行で出したものです。

fun parseQuantity(text: String): Result<Int> {
    val n = text.trim().toIntOrNull()
        ?: return Result.failure(IllegalArgumentException("数値として読めません: \"$text\""))
    if (n !in 1..99) return Result.failure(IllegalArgumentException("1〜99 で入力してください: $n"))
    return Result.success(n)
}

fun main() {
    for (input in listOf("3", " 12 ", "0", "abc", "1.5", "")) {
        parseQuantity(input)
            .onSuccess { println("\"$input\" -> 数量 $it") }
            .onFailure { println("\"$input\" -> ${it.message}") }
    }

    // try-catch で書く場合
    val n = try { "12a".toInt() } catch (e: NumberFormatException) { -1 }
    println("try-catch: $n")

    // 例外をそのまま表示するとスタックトレースになる
    "12a".toInt()
}
"3" -> 数量 3
" 12 " -> 数量 12
"0" -> 1〜99 で入力してください: 0
"abc" -> 数値として読めません: "abc"
"1.5" -> 数値として読めません: "1.5"
"" -> 数値として読めません: ""
try-catch: -1
書き方 変換できないとき 向いている場面
toInt() NumberFormatException 必ず数値だと分かっている文字列
toIntOrNull() null 利用者の入力など、読めない場合も普通に起こる値
toIntOrNull() ?: 既定値 既定値 読めなければ決まった値でよいとき
try { toInt() } catch (e: NumberFormatException) catch の中の処理 例外の内容を記録したいとき

値の変換:数値どうしの変換と桁あふれ

Int は Long や Double へ自動では変換されない

Kotlin では、Int を Long の変数に入れるときも変換関数を書きます。書かないとコンパイルエラーになります。

fun area(width: Double, height: Double) = width * height

fun main() {
    val i: Int = 10
    val l: Long = i
    val d: Double = i
    println(area(i, 2))
    val any: Any = 1
    val s: String = any
    val n: Int = "5"
}
Implicit.kt:5:17: error: initializer type mismatch: expected 'Long', actual 'Int'.
    val l: Long = i
                ^
Implicit.kt:6:19: error: initializer type mismatch: expected 'Double', actual 'Int'.
    val d: Double = i
                  ^
Implicit.kt:7:18: error: argument type mismatch: actual type is 'Int', but 'Double' was expected.
    println(area(i, 2))
                 ^
Implicit.kt:7:21: error: argument type mismatch: actual type is 'Int', but 'Double' was expected.
    println(area(i, 2))
                    ^
Implicit.kt:9:19: error: initializer type mismatch: expected 'String', actual 'Any'.
    val s: String = any
                  ^
Implicit.kt:10:16: error: initializer type mismatch: expected 'Int', actual 'String'.
    val n: Int = "5"
               ^

area(i, 2) の 2 のように、整数リテラルも Double の引数には渡せません(2.0 と書きます)。公式ドキュメントにも、数値型は互いのサブタイプではなく、代入や関数の引数で暗黙の変換は行われないと書かれています。直した形は次のとおりです。

fun area(width: Double, height: Double) = width * height

fun main() {
    val i: Int = 10
    val l: Long = i.toLong()
    val d: Double = i.toDouble()
    println(area(i.toDouble(), 2.0))
    println(i + 1L)            // 演算は広い方の型(Long)になる
    println((i + 1L)::class.simpleName)
    println(i * 1.5)
    val big = 3_000_000_000    // Int に収まらないリテラルは Long になる
    println(big::class.simpleName)
    println("$l $d")
}
20.0
11
Long
15.0
Long
10 10.0

代入と引数では変換関数が必要ですが、i + 1L のような演算では広い方の型(Long)で計算されます。 Int に収まらない整数リテラルは、最初から Long になります。

Long から Int の桁あふれ、Double から Int の切り捨て

import kotlin.math.*

fun main() {
    println("--- 広い型へ(値は変わらない) ---")
    val i = 123
    val l: Long = i.toLong()
    val d: Double = i.toDouble()
    println("Int 123 -> Long $l / Double $d / String \"${i.toString()}\"")

    println("--- Long -> Int(下位32ビットだけ残る) ---")
    for (v in listOf(2147483647L, 2147483648L, 4294967296L, 3000000000L, -2147483649L)) {
        println("%14d.toInt() = %d".format(v, v.toInt()))
    }
    println("Math.toIntExact(3000000000) -> " +
        runCatching { Math.toIntExact(3000000000L) }.exceptionOrNull())
    println("coerceIn で範囲に収める -> " +
        3000000000L.coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt())

    println("--- Int -> Byte / Short ---")
    println("200.toByte() = ${200.toByte()} / 70000.toShort() = ${70000.toShort()}")

    println("--- Double -> Int(0 の方向へ切り捨て) ---")
    for (v in listOf(3.7, -3.7, 2.5, 3.5, 0.999999)) {
        println("%9s  toInt=%2d  roundToInt=%2d  floor=%5s  ceil=%5s  round=%5s"
            .format(v, v.toInt(), v.roundToInt(), floor(v), ceil(v), round(v)))
    }
    println("1e10.toInt() = ${1e10.toInt()} / (-1e10).toInt() = ${(-1e10).toInt()}")
    println("Double.NaN.toInt() = ${Double.NaN.toInt()}")
    println("Double.NaN.roundToInt() -> " + runCatching { Double.NaN.roundToInt() }.exceptionOrNull())
    println("1e10.toLong() = ${1e10.toLong()}")

    println("--- Int / Long -> Float / Double(大きい値は近い値になる) ---")
    println("16777217.toFloat() = ${"%.1f".format(16777217.toFloat())}")
    println("9007199254740993L.toDouble() = ${"%.1f".format(9007199254740993L.toDouble())}")
    println("0.1f.toDouble() = ${0.1f.toDouble()}")

    println("--- 整数どうしの割り算 ---")
    println("7 / 2 = ${7 / 2} / 7 / 2.0 = ${7 / 2.0} / 7.toDouble() / 2 = ${7.toDouble() / 2}")

    println("--- Char と数値 ---")
    println("'A'.code = ${'A'.code} / 65.toChar() = ${65.toChar()} / Char(0x3042) = ${Char(0x3042)}")
    println("'7'.digitToInt() = ${'7'.digitToInt()} / '7'.code = ${'7'.code} / 7.digitToChar() = ${7.digitToChar()}")
    println("'x'.digitToIntOrNull() = ${'x'.digitToIntOrNull()}")
    println("'x'.digitToInt() -> " + runCatching { 'x'.digitToInt() }.exceptionOrNull())

    println("--- 数値 -> 文字列 ---")
    println("255.toString(16) = ${255.toString(16)} / 5.toString(2) = ${5.toString(2)} / padStart = ${7.toString().padStart(3, '0')}")
    println("\"%.2f\".format(3.14159) = ${"%.2f".format(3.14159)} / \"%,d\".format(1234567) = ${"%,d".format(1234567)}")
    println("1.0.toString() = ${1.0.toString()} / 1e7.toString() = ${1e7.toString()} / toBigDecimal().toPlainString() = ${1e7.toBigDecimal().toPlainString()}")

    println("--- 符号なし ---")
    println("(-1).toUInt() = ${(-1).toUInt()} / 3000000000u.toInt() = ${3000000000u.toInt()}")
}
--- 広い型へ(値は変わらない) ---
Int 123 -> Long 123 / Double 123.0 / String "123"
--- Long -> Int(下位32ビットだけ残る) ---
    2147483647.toInt() = 2147483647
    2147483648.toInt() = -2147483648
    4294967296.toInt() = 0
    3000000000.toInt() = -1294967296
   -2147483649.toInt() = 2147483647
Math.toIntExact(3000000000) -> java.lang.ArithmeticException: integer overflow
coerceIn で範囲に収める -> 2147483647
--- Int -> Byte / Short ---
200.toByte() = -56 / 70000.toShort() = 4464
--- Double -> Int(0 の方向へ切り捨て) ---
      3.7  toInt= 3  roundToInt= 4  floor=  3.0  ceil=  4.0  round=  4.0
     -3.7  toInt=-3  roundToInt=-4  floor= -4.0  ceil= -3.0  round= -4.0
      2.5  toInt= 2  roundToInt= 3  floor=  2.0  ceil=  3.0  round=  2.0
      3.5  toInt= 3  roundToInt= 4  floor=  3.0  ceil=  4.0  round=  4.0
 0.999999  toInt= 0  roundToInt= 1  floor=  0.0  ceil=  1.0  round=  1.0
1e10.toInt() = 2147483647 / (-1e10).toInt() = -2147483648
Double.NaN.toInt() = 0
Double.NaN.roundToInt() -> java.lang.IllegalArgumentException: Cannot round NaN value.
1e10.toLong() = 10000000000
--- Int / Long -> Float / Double(大きい値は近い値になる) ---
16777217.toFloat() = 16777216.0
9007199254740993L.toDouble() = 9007199254740992.0
0.1f.toDouble() = 0.10000000149011612
--- 整数どうしの割り算 ---
7 / 2 = 3 / 7 / 2.0 = 3.5 / 7.toDouble() / 2 = 3.5
--- Char と数値 ---
'A'.code = 65 / 65.toChar() = A / Char(0x3042) = あ
'7'.digitToInt() = 7 / '7'.code = 55 / 7.digitToChar() = 7
'x'.digitToIntOrNull() = null
'x'.digitToInt() -> java.lang.IllegalArgumentException: Char x is not a decimal digit
--- 数値 -> 文字列 ---
255.toString(16) = ff / 5.toString(2) = 101 / padStart = 007
"%.2f".format(3.14159) = 3.14 / "%,d".format(1234567) = 1,234,567
1.0.toString() = 1.0 / 1e7.toString() = 1.0E7 / toBigDecimal().toPlainString() = 10000000
--- 符号なし ---
(-1).toUInt() = 4294967295 / 3000000000u.toInt() = -1294967296

押さえておきたい点は次のとおりです。

  • Long → Int は例外を出さずに、下位32ビットだけを残します。 2147483648L.toInt() は -2147483648、3000000000L.toInt() は -1294967296 になります。範囲外を検出したいときは Math.toIntExact()(ArithmeticException: integer overflow)、上限・下限に収めたいときは coerceIn を使います。
  • Double → Int は 0 の方向への切り捨てです。 3.7 は 3、-3.7 は -3 になります(floor なら -4.0)。公式ドキュメントにも、浮動小数点数を整数型に変換すると小数部が捨てられると書かれています。
  • Long → Int と Double → Int では、範囲外の扱いが違います。 Long の 3000000000 は負の値に化けますが、Double の 1e10 は Int の最大値 2147483647 に張り付きます。NaN は 0 になります。
  • 四捨五入は roundToInt() です。2.5.roundToInt() は 3 ですが、round(2.5) は 2.0(ちょうど中間のときは偶数側)になり、結果が異なります。
  • 整数どうしの割り算は整数になります。 7 / 2 は 3 です。小数の結果が欲しいときは、どちらかを toDouble() してから割ります。
  • 数字の文字を数値にするのは digitToInt() です。'7'.code は文字コードの 55 を返します。
やりたいこと 書き方 3.7 のとき -3.7 のとき 2.5 のとき
小数部を捨てる toInt() 3 -3 2
四捨五入して Int に roundToInt() 4 -4 3
小さい方の整数へ floor() 3.0 -4.0 2.0
大きい方の整数へ ceil() 4.0 -3.0 3.0
近い整数へ(中間は偶数側) round() 4.0 -4.0 2.0

コレクションとジェネリクスのキャスト

ジェネリクスの型引数は実行時には残りません。そのため、List<Any> を List<String> にキャストしても、その場では中身が調べられません。

inline fun <reified T> Any?.castOrNull(): T? = this as? T

fun main() {
    val mixed: List<Any> = listOf(1, "two", 3, "four", 5.0)

    // 要素を型で絞り込む
    println(mixed.filterIsInstance<String>())
    println(mixed.filterIsInstance<Int>().sum())

    // 型引数は実行時に残らないので、List<String> へのキャストは中身を調べない
    @Suppress("UNCHECKED_CAST")
    val strings = mixed as List<String>
    println("キャスト自体は通る: size=${strings.size}")
    try {
        val first: String = strings[0]
        println(first)
    } catch (e: ClassCastException) {
        println("取り出すときに: ${e.message}")
    }

    println(mixed is List<*>)
    println("reified: " + mixed[1].castOrNull<String>() + " / " + mixed[0].castOrNull<String>())

    // 要素を1つずつ変換するなら map
    println(listOf("1", "2", "x").map { it.toIntOrNull() })
    println(listOf("1", "2", "x").mapNotNull { it.toIntOrNull() })
}
[two, four]
4
キャスト自体は通る: size=5
取り出すときに: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
true
reified: two / null
[1, 2, null]
[1, 2]

mixed as List<String> は例外にならず、要素を String として取り出した時点で ClassCastException になります。 原因の行と例外の行が離れるので、次の書き方を選ぶと追いやすくなります。

やりたいこと 書き方 結果
特定の型の要素だけ取り出す filterIsInstance<String>() [two, four]
要素を1つずつ値として変換する map { it.toIntOrNull() } [1, 2, null]
変換できた要素だけ残す mapNotNull { it.toIntOrNull() } [1, 2]
型引数を指定して安全にキャストする関数を作る inline fun <reified T> Any?.castOrNull(): T? = this as? T 合わなければ null

エラー文から引く対処表

メッセージ 起きていること 対処
class X cannot be cast to class Y as で型が合わなかった as? にする、または is で調べる
class java.lang.Integer cannot be cast to class java.lang.Long 数値をキャストで変えようとした as Int のあと toLong()
null cannot be cast to non-null type kotlin.String null を as String にした as String? か as? String
For input string: "..." 数値として読めない文字列 trim()、toIntOrNull()、小数なら toDouble()
Value out of range. toByte() などで範囲外 より広い型の変換関数を使う
initializer type mismatch: expected 'Long', actual 'Int'. 暗黙の変換を期待した toLong() を書く
smart cast to 'String' is impossible, because ... 値が変わらないと判断できない ローカルの val に取り出す
this cast can never succeed.(警告) 親子関係にない型へのキャスト 変換関数に置き換える
unchecked cast of ...(警告) 型引数は実行時に調べられない filterIsInstance や map を使う

よくある質問

Q. "123" as Int はなぜ使えないのですか? A. as は同じオブジェクトを別の型として扱う演算子で、値を作り変える働きはありません。String と Int は親子関係にないので、キャストは成り立ちません。文字列から数値を作るには "123".toInt() か "123".toIntOrNull() を使います。

Q. toInt() と toIntOrNull() はどちらを使えばよいですか? A. 読めない文字列が来ることを想定するなら toIntOrNull() です。null かどうかの検査をコンパイラが求めるので、対処を書き忘れにくくなります。必ず数値だと分かっている文字列なら toInt() で十分です。

Q. as? と is はどう使い分けますか? A. 型によって処理を分けたいなら is(特に when と組み合わせて)、1つの型として使えればよく、合わなければ null で進めたいなら as? です。var プロパティのようにスマートキャストが効かない値には、as? と ?.let の組み合わせが短く書けます。

Q. Double を Int にするときに四捨五入したい場合は? A. roundToInt() を使います(import kotlin.math.roundToInt)。toInt() は小数部を捨てるので、3.7.toInt() は 3 になります。NaN を roundToInt() に渡すと IllegalArgumentException: Cannot round NaN value. になる点も確認しておくと安心です。

まとめ

  • キャスト(as・as?・is)は型の見方を変えるだけ、変換(toInt() など)は新しい値を作る
  • as は合わないと ClassCastException、as? は null、null を as String にすると NullPointerException
  • is で調べたあとはスマートキャストが働く。var プロパティや独自 getter のプロパティには効かないので、ローカルの val に取り出す
  • 文字列から数値は toInt() / toIntOrNull()。前後の空白・小数・桁区切り・範囲外は NumberFormatException: For input string: "..." になる
  • Int から Long や Double へも変換関数が必要。代入や引数で暗黙の変換は行われない
  • Long → Int は下位32ビットだけ残り、例外は出ない。検出したいなら Math.toIntExact()
  • Double → Int は 0 の方向へ切り捨て。四捨五入は roundToInt()

参考・出典

  • Kotlin 公式ドキュメント「Type checks and casts」: https://kotlinlang.org/docs/typecasts.html
  • Kotlin 公式ドキュメント「Numbers」: https://kotlinlang.org/docs/numbers.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Kotlin 型変換とキャスト as・as?・is と toIntOrNull」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?