Kotlin
KotlinDay 21

Kotlinの型を知る ~前編~

はじめに

この記事は 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では、どのように表されるでしょうか?

Kotlin
fun foo(): Int = 1

fun bar(): Int? = 1

Bytecodeを見るには、IntelliJ IDEAの Tools > Kotlin > Show Kotlin Bytecode で確認することが可能です。
上記のコードは、このようになります

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というメソッドがあります。

Java
"one.two.".replaceAll(".", "*")

実行結果はどうなるでしょうか?
結果は ******** となります。
意図した結果になりましたか?恐らく思った結果とは異なる結果が出力されたと思います。
JavaのString.replaceAllの第一引数は正規表現をとるため、注意が必要です。

Kotlin
"one.two.".replace(".", "*")

一方、KotlinのStringクラスには replaceAll メソッドはありません。
代わりにreplaceメソッドが用意さています。
こちらの実行結果は、one*two*になります。
JavaのStringクラスと比較してメソッドが直感的になりました。
ちなみに、Javaの様に正規表現で変換するには、kotlin.text.RegexExtensions.ktクラスにある拡張関数 toRegex()メソッドを用いて以下のように書けます。

Kotlin
"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の機能が働いています。

Kotlin
log(2017)

fun log(any: Any) {
    println("Value: $any")
}

fun log(i: Int) {
    println("Value: $i”)
}

前章のNull同様、Bytecodeを見てみましょう。

  • log(Any)
Bytecode
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)
Bytecode
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は、全てのクラスのスーパクラスになると言いましたが、厳密には違います。
以下のコードを見てみましょう。

Kotlin
val hoge: Int? = 2017
log(hoge) // compile error

fun log(any: Any) {
    println("Value: $any")
}

このコードは、コンパイルエラーになります。
つまり、Null許容型のスーパクラスはAnyではありません。

Kotlin
val hoge: Int? = 2017
log(hoge)

fun log(any: Any?) { // <= Any?に変更
    println("Value: $any")
}

先程のコードを Any? に変更するとコンパイルする事が出来ます。
つまり厳密にクラスの階層構造を書くとこのようになります。

class_figure_1

ちなみに、引数の型をAny?にするとBytecodeはこのようになります。

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 the void 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する時を考えます。

Kotlin
val foo = if (bar) {
   123
} else {
   fail()
}

fun fail() {
    throw IllegalStateException()
}

Kotlinでは、戻り値を指定していない関数の型はUnitになります。

Kotlin
// コンパイラの解釈
fun fail(): Unit {
    throw IllegalStateException()
}

つまり、コンパイラはbarがTrueの時はInt型、Falseの時はUnit型として認識しています。
Int型とUnit型のスーパクラスはAny型になるので、foo の型はAny型になります。

Kotlin
// コンパイラの解釈
val foo: Any = if (bar) {
   123
} else {
   fail()
}

恐らくこれは、みなさんの意図した結果では無いと思います。
fooはInt型としてコードを書いていると思います。

この問題を解決するにはどのようにしたら良いでしょうか。
そこで、先程のNothingの登場です。
最初は省略をしていた、failメソッドの戻り値に Nothing を指定します。

Kotlin
fun fail(): Nothing {
    throw IllegalStateException()
}

failメソッドにNothingの戻り値を指定すると、コンパイラはbarがTrueの時はInt型、Falseの時はNothing型として認識します。
Nothingは全てのクラスのサブクラスになるので、fooはInt型としてコンパイラに認識され、意図したコードになります。

Kotlin
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非許容型のスーパクラスではありません。
図にするとこのようになります。

class_figure_2

全てのスーパクラスがAny?になり、全てのサブクラスがNothingになるのがわかると思います。
Nothingを頻繁に使う事は、あまりないかと思いますが型の階層図は覚えておきましょう。

まとめ

  • Kotlinを使う場合も、JavaのBytecodeを見る癖をつけましょう
  • Anyは全てのクラスのスーパータイプになるが厳密にはAny?が最上位に位置する
  • Nothingは全てのクラスのサブクラスになる。Nothing?Nothingのスーパータイプに位置する

2018年はKotlin🐤にとって更なる飛躍の年になりますように!
Have a nice Kotlin!