LoginSignup
128
118

More than 3 years have passed since last update.

Kotlinの型を知る ~前編~

Last updated at Posted at 2017-12-20

はじめに

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

128
118
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
128
118