Edited at
KotlinDay 21

Kotlinの型を知る ~前編~

More than 1 year has passed since last update.


はじめに

この記事は 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!