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)
実装する
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 をインストールしていない環境でも動かせる)。
スクリプトファイルとして実行する
println("Hello Kotlin!!")
> kotlinc -script hello.kts
Hello Kotlin!!
- スクリプトファイルとして実行することもできる。
- ファイルの拡張子を
.kts
にする。 -
kotlinc
コマンドに-script
オプションを指定して実行する。
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
の下にソースコードを入れる。
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
で修飾すると、演算子オーバーロードができるようになる
メソッドと演算子の対応は公式ドキュメントの こちら を参照。
ラムダ・関数
関数インターフェースにラムダ式を渡す
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 ソースで定義されたものの場合は、ラムダ式の前に型名を明示しなければならない。
- メソッドが Java ソースで定義されたものの場合は、引数と戻り値の型が一致するラムダ式を渡すことができる。
- ただし、インターフェースが Kotlin ソースで定義されたものだと、ラムダ式は渡せないっぽい。
- おとなしく関数型で型を宣言するしかなさそう。
fun main(args: Array<String>) {
val f : Runnable = Runnable {println("Hello")}
}
- 変数に代入する場合も同様。
参考
- java - Lambda expressions in Kotlin - Stack Overflow
- Kotlinでリスナーやコールバックをスッキリと書く【関数リテラルとSAM変換】 - Qiita
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
「型引数 T
は out
(出力)で宣言しているのに、 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
の定義とかに問題がありそうだが、実際は KotlinEjb
の hello()
メソッドが 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 の場合)。
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
の方でコンパイルエラーになる。
以下のように親クラスの 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 用のプロジェクトにする
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 のランタイムが重複しているのが少し気になる。
-