76
84

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Kotlin 勉強メモ

Last updated at Posted at 2016-05-28

Kotlin のコードを書いててよくわからなかったところや、ハマったところとかのメモ。
今後適宜追記していくかも。

基本的な文法とかは、 @k5n さんが以下の翻訳記事を書かれているのでそちらを参照ください。

Kotlin とは

JetBrains というチェコの会社が開発したプログラミング言語。
JetBrains は、 IntelliJ IDEA を開発している会社でもある。

  • JVM 上で動作する。
  • 静的型付け言語。
  • オープンソース(Apache License 2)。

環境

OS

Windows 7

Java

1.8.0_74

Hello World

JDK のインストールは割愛。

コンパイラをインストールする

GitHub の Release からコンパイラを落とせる。

kotlin-compiler-x.x.x.zip をダウンロードしてきて、任意のフォルダに解凍する。
bin の下にパスを通したら、インストールが上手くいっているか確認するため、以下のコマンドを実行する。

> kotlin -version
Kotlin version 1.0.0 (JRE 1.8.0_74-b02)

実装する

hello.kt
fun main(args : Array<String>) {
    println("Hello Kotlin!!")
}
  • 拡張子は .kt で作成する。

コンパイルする

> kotlinc hello.kt
  • kotlinc コマンドでコンパイルできる。

コンパイルで出力されるファイル

> dir /b
hello.kt
HelloKt.class

> javap HelloKt.class
Compiled from "hello.kt"
public final class HelloKt {
  public static final void main(java.lang.String[]);
}
  • コンパイルが完了すると、 class ファイルが出力される。
  • HelloKt というクラスと main メソッドが生成されている。

実行する

> kotlin HelloKt
Hello Kotlin!!
  • kotlin コマンドで実行する。
  • 実行するメインクラスとして HelloKt を指定する。
    • -classpath(-cp) など、 java コマンドと同じ感じで実行する。

いろいろな実行方法

jar にまとめる

> kotlinc -d hello.jar hello.kt

> kotlin -cp hello.jar HelloKt
Hello Kotlin!!
  • -d オプションで出力先の jar ファイル名を指定すると、コンパイル結果が jar ファイルにまとめられる。

Kotlin のランタイムも jar にまとめる

> kotlinc -d hello.jar -include-runtime hello.kt

> dir
2016/02/28  13:30    <DIR>          .
2016/02/28  13:30    <DIR>          ..
2016/02/28  13:30           799,490 hello.jar
2016/02/28  13:23                68 hello.kt

> java -jar hello.jar HelloKt
Hello Kotlin!!
  • -include-runtime オプションを指定すると、 Kotlin のランタイムも一緒にまとめた jar ファイルが生成される。
    • サイズは 800KB 弱ほど(結構軽いと思う) 。
  • なので、普通に java コマンドで実行できる(Kotlin をインストールしていない環境でも動かせる)。

スクリプトファイルとして実行する

hello.kts
println("Hello Kotlin!!")
> kotlinc -script hello.kts
Hello Kotlin!!
  • スクリプトファイルとして実行することもできる。
  • ファイルの拡張子を .kts にする。
  • kotlinc コマンドに -script オプションを指定して実行する。

Gradle でコンパイルする

build.gradle
buildscript {
    repositories {
        mavenCentral()
    }
 
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.0.0"
    }
}

apply plugin: 'kotlin'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:1.0.0"
}
  • Gradle 用のプラグインとして org.jetbrains.kotlin:kotlin-gradle-plugin:1.0.0 を読み込む。
  • kotlin プラグインを使用する。
  • 依存関係として org.jetbrains.kotlin:kotlin-stdlib:1.0.0 を追加する。
フォルダ構成
|-build.gradle
`-src/main/kotlin/
  `-hello.kt
  • src/main/kotlin の下にソースコードを入れる。
hello.kt
fun main(args : Array<String>) {
    println("Hello Kotlin!!")
}
コンパイル
> gradle jar

> kotlin -cp build\libs\hello.jar HelloKt
Hello Kotlin!!
  • とりあえず、 jar コマンドで jar ファイルを生成できる。

文法とか

プライマリコンストラクタを private にする

class Hoge private constructor() {
}

オブジェクトの比較

fun main(args: Array<String>) {

    val a = Hoge("a")
    val b = Hoge("a")

    println(a === b)
    println(a == b)
}

class Hoge(val name: String) {

    override fun equals(o: Any?): Boolean {
        if (this === o) return true
        if (!(o is Hoge)) return false

        println("equals()!!")

        return this.name == o.name
    }
}
実行結果
false
equals()!!
true
  • 参照するオブジェクトが同じかどうかは === で調べられる。(Java でいう ==)。
  • Kotlin の == は、 equals() メソッドが裏で呼びだされている。

Java で定義されたメソッドなどの名前が Kotlin の予約語だった場合

public class Hoge {
    public boolean is(Hoge hoge) {
        return false;
    }
}
fun main(args: Array<String>) {
    val hoge = Hoge()
    val fuga = Hoge()

    hoge.`is`(fuga)
}
  • is は Kotlin では予約語なので、そのままだとコンパイルエラーになる。
  • メソッド名を ` (バッククォート)で括ることで、 Kotlin でも is などの予約語になっているメソッドを使えるようになる。

文字列

文字列内で $ を出力する

fun main(args : Array<String>) {
    val a = "hoge"
    
    println("$")
    println("$a")
    println("${'$'}a")
}
実行結果
$
hoge
$a
  • 文字列の中に $<変数名> で、変数の値を埋め込むことができる。
  • しかし単純に $ を出力したい場合で、かつその後ろに文字があると変数と誤認識されてしまう。
  • 単純に $ と出力したい場合は、ちょっと面倒だけど ${'$'} とする。

Class オブジェクトの取得

変数から取得

fun main(args: Array<String>) {
    val clazz: Class<*> = "text".javaClass

    println("clazz = $clazz")
}
実行結果
clazz = class java.lang.String

クラス名から取得

fun main(args: Array<String>) {
    val clazz: Class<*> = String::class.java

    println("clazz = $clazz")
}
実行結果
clazz = class java.lang.String

参考

可変長引数

デフォルト値を指定する

fun function(vararg arguments : String = arrayOf("a", "b", "c")) {
    println("arguments=${arguments.joinToString(", ")}")
}

fun main(args : Array<String>) {
    function()
}
実行結果
arguments=a, b, c
  • arrayOf() でデフォルト値を設定できる。

名前付き引数で値を渡す

fun function(vararg arguments : String) {
    println("arguments=${arguments.joinToString(", ")}")
}

fun main(args : Array<String>) {
    function(arguments = "foo")
    function(arguments = *arrayOf("foo", "bar"))
    
    // 以下はコンパイルエラー
//    function(arguments = "foo", "bar")
//    function(arguments = ("foo", "bar"))
//    function(arguments = arrayOf("foo", "bar"))
}
実行結果
arguments=foo
arguments=foo, bar
  • 単一なら、その値をそのまま渡す。
  • 複数の値を渡したい場合は、先頭にアスタリスク * を付ける。

配列

サイズを指定して初期化

import java.util.Arrays

fun main(args: Array<String>) {
    val intArray = IntArray(3)
    println("intArray=${Arrays.toString(intArray)}")
    
    val array = Array(3, {i -> "init-$i"})
    println("array=${Arrays.toString(array)}")
}
実行結果
intArray=[0, 0, 0]
array=[init-0, init-1, init-2]
  • プリミティブ配列の場合は、コンストラクタ引数でサイズを指定すれば、そのサイズの配列が生成される
  • オブジェクト配列(Array)の場合は、第二引数に各要素の初期値を生成して返すラムダ式を渡す
    • ラムダ式にはその要素のインデックスが順次渡される

演算子オーバーロード

class Hoge(private val i: Int) {
    operator fun plus(other: Hoge) = Hoge(this.i + other.i)
    override fun toString() = "Hoge(${this.i})"
}

fun main(args: Array<String>) {
    println(Hoge(10) + Hoge(21))
}
実行結果
Hoge(31)
  • 所定の名前のメソッドを定義して operator で修飾すると、演算子オーバーロードができるようになる

メソッドと演算子の対応は公式ドキュメントの こちら を参照。

ラムダ・関数

関数インターフェースにラムダ式を渡す

Foo.java
public class Foo {

    public static void method(Runnable runnable) {
    }
}
fun main(args: Array<String>) {
    Foo.method { println("Hello") }
    function(Runnable {println("Hello")})
}

fun function(runnable: Runnable) {
}
  • 引数が関数型インターフェース(抽象メソッドを1つだけ持つインターフェース)の場合、
    • メソッドが Java ソースで定義されたものの場合は、引数と戻り値の型が一致するラムダ式を渡すことができる。
      • SAM(Single Abstract Method) 変換と呼ばれる。
    • メソッドが Kotlin ソースで定義されたものの場合は、ラムダ式の前に型名を明示しなければならない。
  • ただし、インターフェースが Kotlin ソースで定義されたものだと、ラムダ式は渡せないっぽい。
    • おとなしく関数型で型を宣言するしかなさそう。
fun main(args: Array<String>) {
    val f : Runnable = Runnable {println("Hello")}
}
  • 変数に代入する場合も同様。

参考

Kotlin のオブジェクトに定義したメソッドなどを Java から参照する

object Hoge {
    fun method() {}
}
public class Main {

    public static void main(String[] args) {
        Hoge.INSTANCE.method();
    }
}
  • 単純なクラスメソッドではなかった。
  • INSTANCE とかいうフィールドを経由するっぽい。
  • コンパニオンオブジェクトの場合は、 Companion というフィールドを経由する。
class Hoge {
    companion object {
        fun hello() = println("Hello")
    }
}
public class Main {

    public static void main(String[] args) {
        Hoge.Companion.hello();
    }
}

関数(ラムダ式)のレシーバーの型を指定する

fun func(lambda : String.() -> Unit) {
    "Receiver".lambda()
}

fun main(args : Array<String>) {
    func {
        println("this=$this")
        println(substring(0, 3))
    }
}
実行結果
this=Receiver
Rec
  • 関数の型の前に <レシーバーの型>. をつけると、その関数を実行するときのレシーバーの型を指定できる。
  • その関数を実行している間、その中の this はレシーバーを指すようになる。
  • Groovy の delegate みたいな感じ。
  • DSL 作るときに使いそう。

アノテーション

@Deprecated

Kotlin の @Deprecated は、独自のアノテーションが用意されている。

fun main(args: Array<String>) {
    hoge()
}

@Deprecated("使うな!!")
fun hoge() {
}

Kotlin の @Deprecated は、 message 属性が必須になっており、なんらかのメッセージを渡す必要がある。

また、 level という属性が用意されており、その API を使った場合にコンパイルエラーにさせることができる。

fun main(args: Array<String>) {
    hoge() ★コンパイルエラ
}

@Deprecated(message = "使うな!!", level = DeprecationLevel.ERROR)
fun hoge() {
}

level を指定しない場合はデフォルトの DeprecationLevel.WARNING になるので、コンパイルエラーにはならない。
DeprecationLevel.ERROR を指定すると、その API を使用した箇所がコンパイルエラーになる。

DeprecationLevel.HIDDEN を指定すると、その API は他のコードから一切参照できなくなる。
ERROR との使い分けは、よくわからない。

参考

型引数

共変

Java は非変なので、型引数が継承関係になっていても変数の代入はできない。

import java.util.Arrays;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        List<Integer> integerList = Arrays.asList(1, 2, 3);
        List<Number> numberList = integerList; // ★コンパイルエラー!!

        numberList.forEach(n -> System.out.println(n));
    }
}

一方、 Kotlin は読み取り専用なコレクションなら共変になる。

fun main(args: Array<String>) {
    val integerList = listOf(1, 2, 3)
    val numberList: List<Number> = integerList // ★コンパイルが通る!

    numberList.forEach { println(it) }
}
実行結果
1
2
3

読み書き可能なコレクションだと、コンパイルエラーになる。

fun main(args: Array<String>) {
    val integerList = mutableListOf(1, 2, 3)
    val numberList: MutableList<Number> = integerList // ★コンパイルエラー!!!

    numberList.forEach { println(it) }
}

コンパイラはどうやって読み書き可能か読み取り専用かを判断している?

ところで、コンパイラはどうやってその型が読み書き可能なのか、それとも読み取り専用なのかを判断しているのだろうか?

kotlin.collections.List の定義を見ると、以下のようになっている。

public interface List<out E> : kotlin.collections.Collection<E> {
...

何やら、型引数に out というキーワードが使用されている。

試しに、自作のクラスで out を使った型引数を定義してみる。

interface Base<out T> {

    fun get(): T

    fun set(t: T)
}

このインターフェースは、以下のようなエラーになってコンパイルできない。

Type parameter T is declared as 'out' but occurs in 'in' position in type T

「型引数 Tout (出力)で宣言しているのに、 in(入力)の場所に現れているよ!」というエラー内容になっている。

つまり、型引数を out で宣言することで、その型を出力専用にできる。
この結果、その型は共変扱いにできるとコンパイラが判断できる、というカラクリになっている(スマートで良いなぁ)。

ちなみに、 <in T> とすれば入力専用にもできる。

参考

JavaEE

EJB に DI しようとしたら UninitializedPropertyAccessException が出た場合の対処

package sample.ejb

import sample.cdi.KotlinCdiBean
import javax.ejb.Stateless
import javax.inject.Inject

@Stateless
open class KotlinEjb {

    @Inject
    lateinit private var bean: KotlinCdiBean

    fun hello() {
        this.bean.hello()
    }
}

EJB (Stateless Session Bean) に CDI 管理の Bean を @Inject でインジェクションしようとしている。
これを実行すると、以下のようなエラーが発生する。

エラーのスタックトレース
StandardWrapperValve[sample.web.SampleApplication]: Servlet.service() for servlet sample.web.SampleApplication threw exception
kotlin.UninitializedPropertyAccessException: lateinit property bean has not been initialized
        at sample.ejb.KotlinEjb.hello(KotlinEjb.kt:14)

lateinit 指定したプロパティが初期化できなくてエラー、というメッセージが出力されている。

一見すると bean フィールドの書きっぷりとか KotlinCdiBean の定義とかに問題がありそうだが、実際は KotlinEjbhello() メソッドが open になっていないことが原因になっている


@Stateless
open class KotlinEjb {

    @Inject
    lateinit private var bean: KotlinCdiBean

-   fun hello() {
+   open fun hello() {
        this.bean.hello()
    }
}

hello()open にして再実行すると、エラーは発生しなくなる。

ちなみに、インジェクション先のクラスが EJB ではなく CDI 管理 Bean だった場合は、起動時に以下のようなエラーが発生する(Payara の場合)。

CDI管理Beanのメソッドがopenになっていなかった場合のエラー
org.jboss.weld.exceptions.DeploymentException: WELD-001410: The injection point [BackedAnnotatedField] @Inject private sample.ejb.KotlinEjb.bean has non-proxyable dependencies
        at org.jboss.weld.bootstrap.Validator.validateInjectionPointForDeploymentProblems(Validator.java:380)
...
Caused by: org.jboss.weld.exceptions.UnproxyableResolutionException: WELD-001437: Normal scoped bean class sample.cdi.KotlinCdiBean is not proxyable because the type is final or it contains a final method public final void sample.cdi.KotlinCdiBean.hello() - Managed Bean [class sample.cdi.KotlinCdiBean] with qualifiers [@Any @Default].

ちゃんと 「final にするな!」というメッセージを出力してくれる。
(EJB だと、起動はできて実行時にエラーになるうえ、メッセージが直接的でないので原因が分かりにくい!)

その他

親クラスがアクセサメソッドを持つ場合で、サブクラスでフィールドを定義する

open class Parent {
    fun getValue() = "parent"
}

class Child: Parent() {
    private val value = "child"
}

こんな実装をすると、 Child の方でコンパイルエラーになる。

kotlin.jpg

以下のように親クラスの getValue()open にして、サブクラスで明示的にオーバーライドすればコンパイルエラーは外れる。

open class Parent {
    open fun getValue() = "parent"
}

class Child: Parent() {
    private val value = "child"

    override fun getValue() = this.value
}

参考

Eclipse で開発する(おまけ)

IntelliJ で開発するのがオススメですが、どうしても Eclipse で Kotlin したい場合のメモ。。。。

プラグインをインストールする

  • マーケットプレイスで Kotlin で検索すれば「Kotlin Plugin for Eclipse」というプラグインが出てくるので、これをインストールする。
  • インストールが完了したら、「新規作成」から Kotlin 用のプロジェクトを作れるようになる。
  • 実行は、 main 関数があるファイルを開いて Ctrl + F11 をするか、ソースファイルを右クリックして「実行」→「Kotlin Application」を選択。

Gradle の Eclipse プラグインで Kotlin 用のプロジェクトにする

build.gradle
buildscript {
    repositories {
        mavenCentral()
    }
 
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.0.0"
    }
}

apply plugin: 'kotlin'
apply plugin: 'eclipse'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:1.0.0"
}

eclipse {
    project {
        name = 'kotlin-sample'
        buildCommand 'org.jetbrains.kotlin.ui.kotlinBuilder'
        natures 'org.jetbrains.kotlin.core.kotlinNature'
        linkedResource(
            name: 'kotlin_bin',
            type: '2',
            locationUri: "org.jetbrains.kotlin.core.filesystem:/${name}/kotlin_bin"
        )
    }
    classpath.containers 'org.jetbrains.kotlin.core.KOTLIN_CONTAINER'
}
  • Eclipse で普通に Kotlin プロジェクトを作ったときの .project.classpath を参考に、必要な設定を追加しただけ。
  • とりあえず、これで eclipse コマンドを実行して Eclipse に取り込めば、 Kotlin プロジェクトとして処理してくれる。
    • Kotlin Runtime Library と依存関係で指定している Kotlin のランタイムが重複しているのが少し気になる。
76
84
0

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
76
84

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?