Android
Kotlin
googleio

Google I/O 2017 : Introduction to Kotlin (和訳/要約)

More than 1 year has passed since last update.


Keynote&Developer Keynote


Keynote

KeynoteにおいてKotlinはAndroid開発における公式言語としてサポートされることが発表されました。

合わせて、


  • JetBrains社とパートーナーとしてKotlin財団の設立

  • 既存のコード(Java)にもさらなる投資をする

ことが発表されています。

Kotlinが公式言語としてサポートされるに至った背景についてKeynote内では以下のような項目があげられています。


  • KotlinはAndroidの開発者コミュニティが既に求めていた言語で、開発者の生産性を高めることが出来る

  • Androidのランタイムとの完全な互換性があり、既存のコードとの相互運用が可能である

  • 素晴らしいIDEによるサポート

  • 言語が成熟し、製品版としてリリース可能な状態である


Developer Keynote

Developer KeynoteではAndroid/Google Playにおいて大きな4つのテーマについて話がされました。


  • Kotlin

  • Android Studio & Libraries

  • App Quality & Success

  • Android Instant App

そのうちの一つとしてKotlinは取り上げられ、Kotlinは「first-class language」としてサポートされ、Android StudioにKotlinが組み込まることになりました。

ただし、Java、C++への投資は引き続き変わらず行われ、追加としてKotlinはサポートされます。

Developer Keynote内で2つのKotlinに関するセッションが発表されました。


Introduction to Kotlin


概要

前半はJetBrains社Hadi Hariri氏によるライブコーディング、後半は同じくJetBrains社のKotlin言語リード開発者であるAndrey Breslav氏によるプレゼンテーションでした。


Keynoteと同じ会場が8割ほど埋まるほど大盛況のセッションとなりました。

特に前半のライブコーディングはこれからKotlinでAndroid開発を始める人にとっては必見の内容となっています。

セッションの内容はYoutubeで見ることが出来ます。

https://www.youtube.com/watch?v=X1RVYt2QKQE

本記事では特に前半のライブコーディングの部分について和訳・要約をしました。

動画を見る上での参考にしていただければ幸いです。


ライブコーディング by Hadi Hariri

Kotlinを使って、どう書くか?どういう利点があるかの紹介をします。

まずはdata classの紹介です。

amountcurrencyという2つのread only propertyを持ったdata classを定義します。

data class Money(val amount: Int, val currency: String)

これをJavaで書くと以下のようになります。

public class JavaMoney {

@Override
public int hashCode() {
return super.hashCode();
}

@Override
public boolean equals(Object obj) {
return super.equals(obj);
}

@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}

@Override
public String toString() {
return "JavaMoney{" +
"amount=" + amount +
"currency='" + currency + '\'' +
'}';
}

public JavaMoney(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}

private int amount;
private String currency;

public int getAmount() {
return amount;
}

public String getCurrency() {
return currency;
}

}

read only propertyなのでJavaの場合はgetterしかありません。

toStringclone(Kotlinの場合はcopy)、equalshashCodeと4つのメソッドも定義されています。

これらのメソッドはIDEが自動生成してくれたりしますが、メンテンナンス時に変更が必要になったときには変更に関連するコードを全て変更していく必要があります。

次にエントリポイントとなるfunctionを定義します。

fun main(args: Array<String>) {

}

Javaではstaticなメソッドをclass内に定義する必要がありますが、Kotlinはその必要はありません。

JavaScriptのように(もっと良い形で)、トップレベルに定義することが出来ます。

これはfunctionに限らず、propertyやclassもいかなる場所に定義することが出来ます。

val tickets = Money(100, "$")

ticketsという名のインスタンスを定義します。

Kotlinは型推論があるので、型を明記する必要はありません。

val popcorn = tickets.copy(500, "EUR")

copyを使ってpopcornという新しいインスタンスを定義します。

同じpropertyを持つ場合はcopyには引数が必要ありませんが、違う値をpropetryに代入したい場合はcopyの引数に渡すことが出来ます。

if (tickets != popcorn) {

println("They are different!")
}

!=を使ってインスタンスの持つpropertyそれぞれが全て等しいかの比較が出来ます。

同じオブジェクトかどうかの比較は!==を用います

val javaMoney = JavaMoney(100, "$")

javaMoney.amount

KotlinからJavaを呼ぶことが出来ます。

JavaでのgetterはKotlinからはpropertyのように変換されて呼び出すことが出来ます。

Money money = new Money(100, "$");

money.getAmount();

JavaからのKotlinを呼ぶことが出来、KotlinのpropertyはJavaからはgetterのように見えます。

このように相互に呼び出せることによって同じプロジェクトでJavaとKotlinが共存出来ます。

fun sendPayment(money: Money) {

println("Sending ${money.amount}")
}

sendPaymentというfunctionをトップレベルに定義します。

Kotlinのfunctionは戻り値が特にない場合はvoidのように定義する必要がなく、デフォルトはUnitというシングルトンのクラスが返されます。

fun sendPayment(money: Money, message: String = "") {

println("Sending ${money.amount}")
}

functionの引数にはデフォルト値を設定することが出来ます。

また呼び出し時には名前をつけて呼び出すことが出来ます。

sendPayment(message = "Good luck!", money = tickets)

どの引数に何を渡してるかがわかりやすくなります。

fun sum(x: Int, y: Int) = x + y

一行でfunctionを定義することが出来、戻り値も暗黙的に定義されるので明記する必要はありません。

fun convertToDollars(money: Money): Money {

when (money.currency) {
"$" -> return money
"EUR" -> return Money(money.amount * BigDecimal(1.10), "$")
else -> throw IllegalArgumentException("not the currency you're interested")
}
}

上記のようなfunctionも以下のように一行で書くことが出来ます。

fun convertToDollars(money: Money) = when (money.currency) {

"$" -> money
"EUR" -> Money(money.amount * BigDecimal(1.10), "$")
else -> throw IllegalArgumentException("not the currency you're interested")
}

ここ以降、data classを以下のように変更しています。

data class Money(val amount: BigDecimal, val currency: String)

拡張関数を用いればBigDecimalのような既存のclassにfunctionを増やすことが出来ます。

fun BigDecimal.percent(percentage: Int) = this.multiply(BigDecimal(percentage)).divide(BigDecimal(100))

以下のように呼び出します。

val bd1 = BigDecimal(100)

bd1.percent(7)

infix fun Int.percentOf(money: Money) = money.amount.multiply(BigDecimal(this)).divide(BigDecimal(100))

Intなどのクラスも拡張することが出来、infixを用いることで、以下のように呼び出すことが出来ます。

7 percentOf popcorn

private val Int.bd: BigDecimal

get() = BigDecimal(this)

拡張関数と似たような形で拡張propertyを用いれば以下のようにlongと似た形でBigDecimalのインスタンスをつくることが出来ます。

val long = 100L

val bd2 = 100.bd

Kotlinでは+,-,*などのオペレーターをオーバーライドすることが出来ます。

operator fun Money.plus(money: Money) =

if (currency == money.currency) {
Money(amount + money.amount, currency)
} else
throw IllegalArgumentException("We're gonna have a problem here!")

plusという名前のoperator functionをオーバーライドすれば以下のように呼び出すことが出来ます。

val costs = tickets + popcorn

val train: Money = Money(100.bd, "$")

train = null // error!

新しいインスタンスを定義してnullを代入しようとするとエラーとなります。

これは正確には2つの意味でエラーとなっています。

valはimmutable(不変)を表現していて、一度値を代入しようとすることは出来ません。

以下のようにvarに置き換えることでmutableにすることが出来ます。

var train: Money = Money(100.bd, "$")

train = null // error!

Kotlinにおいてはデフォルトの型がnullableではないので、varに置き換えてもエラーが出ます。

Money??をつけることでnullableな型を宣言出来ます。

Kotlinの場合はnullableな型を持ちたくないので、ほとんどこうする必要がないでしょう。

しかし、KotlinはJavaとの互換性があり、Javaの型はnullableです。

fun javaMoney(money: JavaMoney?) {

println("${money.amount} is valid") // error!
}

nullableな型を引数にとるfunctionを定義して、その引数のpropertyを呼びだそうとするとエラーとなります。

これを解決するには2つの方法があります。

1つ目はnullチェックをすることです。

fun javaMoney(money: JavaMoney?) {

if (money != null) {
println("${money.amount} is valid")
}
}

(動画では述べられてませんがここには後述のsmart castが使われています。)

2つ目は?を使ってpropertyを呼び出すことです。

fun javaMoney(money: JavaMoney?) {

println("${money?.amount} is valid")
}

楽しみたいのなら別の方法もあります。

fun javaMoney(money: JavaMoney?) {

println("${money!!.amount} is valid")
}

!!というオペレーターはnullであることを知りながらも墓穴を掘りたい時に使います。

次はhigher-order function(高階関数)についての話をします。

fun findEmails(users: List<User>, predicate: (String) -> (Boolean)): List<User> {

TODO("LATER!")
}

上記のようなfunctionを定義します。

余談ですがTODOは呼び出すとNotImplementedErrorを投げる関数として用意されています。

data class User(val id: Int, val username: String, val email: String, val role: Role)

enum class Role {
Admin,
Regular
}

fun usersFromJSONfile(fileName: String): List<User> {
val gson = Gson()
return gson.fromJson<List<User>>(FileReader(fileName), object : TypeToken<List<UserResult>>() {}.type)
}

以上のようなdata classとfunctionを用意してfindEmailsを呼び出します。

val users = usersFromJSONfile("users.json")

findEmails(users, { value -> value.endsWith(".com") })

lambdaを第二引数にとり、emailが.comで終わるユーザーを見つけることが出来ます。

lambdaの引数が一つの場合、明示的に宣言する必要はなく、itを使って引数を呼び出せることが出来ます。

findEmails(users, { it.endsWith(".com") })

さらに、functionの最後の引数がlambdaの場合、()の中に入れる必要はありません。

findEmails(users) {

it.endsWith(".com")
}

以上のようなことは改めて実装する必要はなく、build-inのfunctionを使って実装することが出来ます。

val dotComUsers = users.filter { it.email.endsWith(".com") }

.sortedBy { it.id }
.map { Pair(it.email, it.username) }

email.が.comで終わるユーザを見つけ、idでソートし、emailとusernameのpairにしています。

これらのfunctionはとても小さいライブラリとしてリリースされており、Androidにおいても全く問題なく使うことが出来ます。

Pairはtoというfunctionを使うことでより良く書けます。

val dotComUsers = users.filter { it.email.endsWith(".com") }

.sortedBy { it.id }
.map { it.email to it.username }

data classとして定義されたclassはdestructuring-declarationを使うことが出来ます。

val (id, username, email) = users.filter { it.email.endsWith(".com") }

.sortedBy { it.id }
.first()
println(id)

使わない変数は_に置き換えましょう。

val (id, _, _) = ...

次にsealed classについて紹介します。

まず、Kotlinのclassはデフォルトでは継承できません。

openをつける必要があります。

class UserResult

data class Success(val users: List<User>) : UserResult() // error!
data class Failure(val message: String) : UserResult() // error!

open class UserResult

UserResultをsealed classとして宣言します。

sealed class UserResult

data class Success(val users: List<User>) : UserResult()
data class Failure(val message: String) : UserResult()

sealed classはclassのヒエラルキーを表現したクラスで、UserResultを継承したclassが上記以外にないことを示しています。

(enumの拡張のようなものです。)

継承したclassは同じfileに書く必要があります。

sealed class UserResult {

data class Success(val users: List<User>) : UserResult()
data class Failure(val message: String) : UserResult()
}

上記のようにネストして書くことも出来ますがUserResult.Successのように書く必要があるので、ここではしません。

(Kotlinの1.1までは上記のようにネストして書く必要があった。)

このclassは成功時と失敗時で違ったpropertyを持つclassを返したい時に使えます。

fun retrieveUsers(): UserResult {

return Success(usersFromJSONfile("users.json"))
}

val result = retrieveUsers()

when (result) {
is Success -> result.users.forEach { println(it.username) }
is Failure -> println(result.message)
}

whenの中のis Success以降のresultsmart castされており、Successという型にcastされています。

is Failure以降のresultも同様にFailureにcastされています。

ここまではeager evaluation(先行評価)の話でしたが、lazy evaluation(遅延評価)の例を紹介します。

val values = generateSequence(1) {

it * 10
}

values.take(10).forEach { println(it) }

generateSequenceで作られるsequenceは無限で終わりがありませんが、take(10)で最初から10件だけ取得し、それらを出力しています。

val users = usersFromJSONfile("users.json").asSequence()

asSequenceを用いることでlazy evaluationへ置換することが出来ます。


プレゼンテーション by Andrey Breslav

将来、Kotlinがどのようになっていくかの話をします。

1つめの大きな方向はKotlinが様々なプラットフォームをサポートする方向に向かっています。

JVM上、Android、JavaScript(ブラウザ、node.js)でKotlinは動きますが、テクニカルプレビュー版として出しているKotlin/NativeはiOS、Linux、MAC、Windowsといったプラットフォーム上のコードにコンパイルされ、動作するように作っています。

1つのウェブアプリケーションがサーバサイドもクライアントもKotlinで書かれて、共通化のコードがそれらの間でシェア出来るようにマルチプラットフォームを目指しています。

2つめはCoroutinesです。

非同期的なプログラミングから、コールバックや複雑なフローを取り除き、sequentialなコードで書けることを目指しています。

Kotlinを始めたい場合は以下のサイトからはじめてください。