はじめに
この記事は Kotlin Advent Calendar 21日目の記事です。
Kotlin Advent Calendarは3年連続3回目の参加になります。
今年はGoogle I/OでKotlinがAndroidに正式採用されて、酉年の2017年はKotlin🐤にとっても飛躍の年になりました!
11月にはサンフランシスコにてKotlin Confが開催され、更なる盛り上がりを見せています!
また、レビュアーという形ですがKotlin In Actionの翻訳に携わりKotlinの盛り上げに少しばかりは貢献できたのではないかと思います。
本記事ではそのKotlin In Actionの著者の1人であるSvetlana Isakova氏のKotlin Confでのセッション 「Kotlin Types: Exposed」の内容と、Kotlin In Actionの6章の内容を元に「Kotlinの型」についてまとめたものになります。
アジェンダは以下のとおりです。
- KotlinとJavaの型
- Any, Unit, Nothing, void
- Null許容型/Null非許容型とプラットフォーム型
- Collection型
「Null許容型/Null非許容型とプラットフォーム型」、「Collection型」については「Kotlinの型を知る ~後編~」をご覧ください。
本記事は前半の「KotlinとJavaの型」、「Any, Unit, Nothing, void」について解説していきます。
KotlinとJavaの型
空前絶後のKotlinブームにより、新しくAndroidを始める方やRuby on Railsから静的型付けを求めてSpringに移行する方などが増え、これからはJavaを一度も書いたこと無い方がKotlinを書く事も増えていくのではないでしょうか?
Kotlinで書かれたコードはJVMで動作するJavaのバイトコードへコンパイルされるため、Javaの知識は避けて通れません。
この章では、JavaのBytecodeを見ながらKotlinとJavaの型の違いを解説していきます。
Null非許容型, Null許容型
KotlinのNull非許容型、Null許容型について見ていきましょう。
以下の様な、Kotlinのコードがあります。
Javaでは、どのように表されるでしょうか?
fun foo(): Int = 1
fun bar(): Int? = 1
Bytecodeを見るには、IntelliJ IDEAの Tools
> Kotlin
> Show Kotlin Bytecode
で確認することが可能です。
上記のコードは、このようになります
public static final int foo() {
return 1;
}
@Nullable
public static final Integer bar() {
return Integer.valueOf(1);
}
JavaにDecompileすると、KotlinのIntはプリミティブ型のint
、KotlinのInt?はObject型のjava.lang.Integer
になります。
KotlinとJavaの型比較
同様に、他の型もDecompileしてみるとこのようになります。
Kotlin | Java |
---|---|
Int | int |
Double | double |
Boolean | boolean |
Int? | java.lang.Integer |
Double? | java.lang.Double |
Boolean? | java.lang.Boolean |
List | java.util.List |
Array | Integer[] |
kotlin.String | java.lang.String |
String型だけはKotlinのStringクラスが存在します。
KotlinのStringは、Javaの分かりづらい処理をラップしています。
KotlinのStringクラス
KotlinのStringクラスは、単純なJavaのStringクラスではなく、Kotlinの拡張関数の機能を利用してJavaには無いメソッドを幾つも提供しています。
例えば、JavaのreplaceAll
というメソッドがあります。
"one.two.".replaceAll(".", "*")
実行結果はどうなるでしょうか?
結果は ********
となります。
意図した結果になりましたか?恐らく思った結果とは異なる結果が出力されたと思います。
JavaのString.replaceAll
の第一引数は正規表現をとるため、注意が必要です。
"one.two.".replace(".", "*")
一方、KotlinのStringクラスには replaceAll
メソッドはありません。
代わりにreplace
メソッドが用意されています。
こちらの実行結果は、one*two*
になります。
JavaのStringクラスと比較してメソッドが直感的になりました。
ちなみに、Javaの様に正規表現で変換するには、kotlin.text.RegexExtensions.kt
クラスにある拡張関数 toRegex()
メソッドを用いて以下のように書けます。
"one.two.".replace(".".toRegex(), "*")
Any, Unit, Nothing, void
次に、KotlinのAny, Unit, Nothing, Javaのvoidについてです。
Any
クラスのコメントを見てみると、Anyはすべてのクラスのスーパクラスになると書いてあります。
The root of the Kotlin class hierarchy.
Every Kotlin class has [Any] as a superclass.
つまり、JavaのObject同様、引数にAnyを指定した場合でもInt型の変数を渡して実行することが可能です。
これはJavaのAuto boxingの機能が働いています。
log(2017)
fun log(any: Any) {
println("Value: $any")
}
fun log(i: Int) {
println("Value: $i”)
}
前章のNull同様、Bytecodeを見てみましょう。
- log(Any)
public final static log(Ljava/lang/Object;)V
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 0
LDC "any"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 30 L1
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "Value: "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 1
L2
...略
- log(Int)
public final static log(I)V
L0
LINENUMBER 34 L0
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "Value: "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ILOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 1
L1
...略
Anyの方はL0でAnyをNull非許容型か確認して内部ではObject型として扱ったまま、L1でStringBuilderに渡しています。
StringBuilderの内部では、String.valueOf(Object)
が呼ばれて、変換されます。
Anyは、全てのクラスのスーパクラスになると言いましたが、厳密には違います。
以下のコードを見てみましょう。
val hoge: Int? = 2017
log(hoge) // compile error
fun log(any: Any) {
println("Value: $any")
}
このコードは、コンパイルエラーになります。
つまり、Null許容型のスーパクラスはAnyではありません。
val hoge: Int? = 2017
log(hoge)
fun log(any: Any?) { // <= Any?に変更
println("Value: $any")
}
先程のコードを Any?
に変更するとコンパイルする事が出来ます。
つまり厳密にクラスの階層構造を書くとこのようになります。
ちなみに、引数の型をAny?
にするとBytecodeはこのようになります。
public final static log(Ljava/lang/Object;)V
@Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0
L0
LINENUMBER 30 L0
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "Value: "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 1
L1
...略
Nullableアノテーションが付いただけで、あとは内部での処理はInt型と同じになります。(型の取り扱いはObject)
Unit, void
Unitのクラスコメントには、このように書いてあります。
The type with only one value: the Unit object.
This type corresponds to thevoid
type in Java.
つまり、KotlinのUnitとJavaのvoidは同じ事を表していて、これら2つの値は意味のない値を返す事を示しています。
Nothing
一方、Nothingのコメントには、このように書いてあります。
Nothing has no instances.
You can use Nothing to represent "a value that never exists”:
for example, if a function has the return type of Nothing, it means that it never returns (always throws an exception).
Nothingはインスタンスがありません。またNothingの場合は、関数が返されないので常にExceptionがthrowされます。
まとめると、Nothingは値を返す事は無く、存在しない事を表すことが出来ます。
このことから、KotlinのNothingは、KotlinのUnit, Javaのvoidとは異なる事が分かります。
Unit, Nothing, voidの具体例
具体例を見ていきましょう。
bar
のBoolean値がTrueの時、foo
にInt型の数字をいれて、Falseの時にExceptionをthrowする時を考えます。
val foo = if (bar) {
123
} else {
fail()
}
fun fail() {
throw IllegalStateException()
}
Kotlinでは、戻り値を指定していない関数の型はUnit
になります。
// コンパイラの解釈
fun fail(): Unit {
throw IllegalStateException()
}
つまり、コンパイラはbar
がTrueの時はInt型、Falseの時はUnit型として認識しています。
Int型とUnit型のスーパクラスはAny型になるので、foo
の型はAny型になります。
// コンパイラの解釈
val foo: Any = if (bar) {
123
} else {
fail()
}
恐らくこれは、みなさんの意図した結果では無いと思います。
foo
はInt型としてコードを書いていると思います。
この問題を解決するにはどのようにしたら良いでしょうか。
そこで、先程のNothingの登場です。
最初は省略をしていた、failメソッド
の戻り値に Nothing
を指定します。
fun fail(): Nothing {
throw IllegalStateException()
}
failメソッド
にNothingの戻り値を指定すると、コンパイラはbar
がTrueの時はInt型、Falseの時はNothing型として認識します。
Nothingは全てのクラスのサブクラスになるので、foo
はInt型としてコンパイラに認識され、意図したコードになります。
val foo: Int = if (bar) { // Int型として定義
123 // Int
} else {
fail() // Nothing
}
fun fail(): Nothing { // 戻り値にNothing
throw IllegalStateException()
}
Nothing?
AnyにもNull許容型があったので、NothingにもNull許容型があるのか疑問になる方もいるでしょう。
もちろんNothingにもNull許容型が存在します。
Nothing?
はNothingのスーパクラスではありますが、Null非許容型のスーパクラスではありません。
図にするとこのようになります。
全てのスーパクラスがAny?
になり、全てのサブクラスがNothing
になるのがわかると思います。
Nothingを頻繁に使う事は、あまりないかと思いますが型の階層図は覚えておきましょう。
まとめ
- Kotlinを使う場合も、JavaのBytecodeを見る癖をつけましょう
-
Any
は全てのクラスのスーパータイプになるが厳密にはAny?
が最上位に位置する -
Nothing
は全てのクラスのサブクラスになる。Nothing?
はNothing
のスーパータイプに位置する
2018年はKotlin🐤にとって更なる飛躍の年になりますように!
Have a nice Kotlin!