覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Kotlin - 文字列の分割

Java で 文字列分割する時は、 String クラスの split メソッドを利用することになります。
分割した文字列を配列にして返してくれます。

例えば、"100-123-A500-10" という文字列があったとして、"-" で区切りたいと思った時は以下のようになります

String str = "100-123-A500-10";
String[] array = str.split("-");

System.out.println(java.util.Arrays.toString(array));

実行結果は以下の通りです。予定通り "-"(ハイフン)で区切られている事が分かります。

[100, 123, A500, 10]


次に、 "100.123.A-500.10" という文字列があったとして、"." で区切りたいと思った時に以下のようなコードを書いたとします。

String str = "100.123.A-500.10";
String[] array = str.split(".");

System.out.println(java.util.Arrays.toString(array));

この場合の実行結果は以下の通りで、空の配列です。なんだか予定したものとは感覚的にずれます。

[]

正直、Java経験者ならこれはほとんどの人が一度は通る道なような気がします。
String#split メソッドは、引数として正規表現をとります。
"."(ドット)を引数に指定してしまうと、任意の文字を表現する正規表現 となってしまい、こんな結果になります。

例えば以下のように書く事で、 望む結果を得ることができます。

String str = "100.123.A-500.10";
String[] array = str.split("\\.");

System.out.println(java.util.Arrays.toString(array));

実行結果は以下の通りです。

[100, 123, A-500, 10]


Kotlinの場合

Kotlinでは、上記のような紛らわしいメソッドを隠蔽して、異なる引数を持つ split という名前のオーバーロードされた拡張関数で置き換わっています。

split に対して正規表現を渡すためには、String ではなく Regax 型の値が必要になります。
これによって、メソッドに渡される値が、プレーンテキストとして解釈されるのか、正規表現として解釈されるのかが明確になります。

正規表現として渡す場合のコードは以下のようになります。文字列を toRegax 関数で正規表現に変換しています。

val str = "100.123.A-500.10"
val array = str.split("\\.".toRegex())  // 正規表現として渡している事が明確

println(array)

実行結果は以下の通りです

[100, 123, A-500, 10]


"."(ドット) だけではなく "-"(ハイフン)でも分解したい場合は以下のようになります

val str = "100.123.A-500.10"
val array = str.split("\\.|-".toRegex())  // 正規表現として渡している事が明確

println(array)

実行結果は以下の通りです

[100, 123, A, 500, 10]


toRegax関数を使わずに、そのまま "." (ドット)を渡すとプレーンテキストとして扱われ、その文字列がデリミタとなります。

val str = "100.123.A-500.10"
val array = str.split(".")  // プレーンテキストのデリミタとして扱われる

println(array)

実行結果は以下の通りです

[100, 123, A-500, 10]


プレーンテキストを引数に渡すパターンで、 "."(ドット) だけではなく "-"(ハイフン)でも分解したい場合は以下のように書けます

val str = "100.123.A-500.10"
val array = str.split(".", "-")  // プレーンテキストのデリミタとして扱われる

println(array)

実行結果は以下の通りです

[100, 123, A, 500, 10]


まとめ

文字列の分割という基礎的な処理に対しても、Javaに比べて直感的で分かりやすい操作(関数)が提供されていることが分かりました。

tail -f した内容をgrepしてリアルタイムで出力する

Linuxでログファイルの状態をリアルタイムで確認する場合 tailコマンドに -f オプションをつけて tail -f {ログファイル名} という使い方をすることが多々あります。

例えば、今まさにログが追記されている application.log というログファイルの内容を
リアルタイムで確認するには以下のように実行します。

tail -f application.log


そして、こういう時には往々にして、出力される内容を特定の文字列が含むものだけに絞り込みたい場合があります。
例えば ERROR というキーワードが付いた行だけ出力したい。となった場合は以下のようにパイプで繋いで grep するようにします。

tail -f application.log | grep "ERROR"

こうすると ERROR という文字列が含まれる行だけ出力されるようになります。


さらに絞りこんだ内容に test-user が含まれる行は除外したい となった場合は、さらにパイプで繋いで以下のようにします。

tail -f application.log | grep "ERROR" | grep -v "user-test"

しかし、こうすると結果がリアルタイムで出力されません。
これは、grep がバッファリングしているためで、バッファが一定量になるまで出力されません。

こういう時は、grep に --line-buffered のオプションを付ける事でバッファリングしなくなり、リアルタイムで出力されるよになります。


先ほどの例は以下のように書き換えできます。

tail -f application.log | grep --line-buffered "ERROR" | grep --line-buffered -v "user-test"

これでgrepを重ねても、リアルタイムで出力されるようになります。


(ただし、--line-buffered を使うとパフォーマンスが若干悪くなる場合があるようです。一応意識はしておいた方がいいかもしれません)



関連エントリ

Kotlin - 中置呼び出し・分解宣言

Kotlin で マップを生成するために mapOf という関数を利用します。

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

val map = mapOf(1 to "1yen", 5 to "5yen", 10 to "10yen", 50 to "50yen")


この mapOf 関数の宣言は以下のようになされています

public fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V>

Pair型 の 可変長引数を受け取る関数となっています。戻り値の関係性から キーと値のペアである必要があります。


mapOf を呼び出している引数から

1 to "1yen"

の部分が、Pair(1, "1yen") を作り出していることが分かります。


to というキーワードは組み込みの機能ではなく、メソッド呼び出し方法の特別な種類で 中置呼び出し(infix call)と呼ばれるものです。
中置呼び出しでは、メソッド名が対象オブジェクト名と引数の間に隣接して置かれます。余分なセパレータはありません。

以下2つの呼び出しは等価となります。

1.to("1yen")  // 普通の方法での to関数 の呼び出し
1 to "1yen"  // 中置記法を使った to関数 の呼び出し


といことなので冒頭の mapOf 関数でのマップ生成は以下のように書こうと思えば書けます

val map = mapOf(1.to("1yen"), 5.to("5yen"), 10.to("10yen"), 50.to("50yen"))


中置呼び出しは、1つの必須な引数を持つ通常のメソッドや拡張関数で使用可能です。
中置記法を使って関数を呼び出すためには、その関数に infix 修飾子を付ける必要があります。

ちなみに、to関数の宣言は以下のようになっています。

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

to関数は、Pairのインスンタスを返します。これは、Kotlinの標準ライブラリのクラスで要素のペアを表現しています。


Pair の中身を使って、以下のように二つの変数を直接初期化する事ができます。

val (num, yen) = 10 to "10yen"

この例では、num に 10 が yen に "10yen" が格納されます。

この機能を 分解宣言(destructuring declaration)と呼びます。


分解宣言は、ペアに対してだけ限定して利用されるものではなく、2つの変数keyとvalueをマップのエントリで初期化することにも利用できます。

例えば以下のようにループ内でマップのエントリをkeyとvalueで初期化しながら出力する事が可能です

val map = mapOf(1 to "1yen", 5 to "5yen", 10 to "10yen", 50 to "50yen")

for ((key, value) in map) {
    println("$key -> $value")
}

◾️実行結果

1 -> 1yen
5 -> 5yen
10 -> 10yen
50 -> 50yen


コレクションのループでwithIndex 関数を使う場合にも利用可能です

val list = listOf("1yen", "5yen", "10yne", "50yen", "100yne")

for ((index, element) in list.withIndex()) {
    println("list[$index] -> $element")
}

◾️実行結果

1 -> 1yen
5 -> 5yen
10 -> 10yen
50 -> 50yen

Kotlin - 可変長引数を扱う

Javaのメソッドで可変長引数を扱う場合は、 <型>... 仮引数名 という記述で引数の部分を記述します。

例えば、与えられた可変長の文字列群をList化して出力するメソッドなら以下の様になります。

import java.util.Arrays;

void convToListAndPrint(String... values) {
    System.out.println((Arrays.asList(values)));
}

呼び出し側のコードは以下の様になります

convToListAndPrint("Str1", "Str2", "Str3");

実行結果は以下の通りです

[Str1, Str2, Str3]

可変長引数で渡されたパラメータは 配列 として扱われています。


以下の様に可変長引数部分に 配列を渡してメソッドを呼び出すことも可能です

convToListAndPrint(new String[] {"Str1", "Str2", "Str3"});    // -> [Str1, Str2, Str3]


Kotlinにおける可変長引数

Kotlinでは可変長引数として取る引数には、vararg 修飾子をつけます。

さきほどJavaのメソッドとして書いた convToListAndPrint をKotlinで書き直すと以下の様になります。
関数のパラメータにvararg 修飾子が付いている部分も当然異なっていますが、
Arrays.asList に可変長引数として受け取ったパラメータに * を付与している部分も違います。

fun convToListAndPrint(vararg values: String) {
    println(Arrays.asList(*values))
}

Kotlinでは、可変長引数をとる関数に 配列を渡す事ができません。可変長引数で受け取った values は、配列として格納されています。
これを、可変長引数を必要とする Arrays.asList には直接渡せません。
引数の前に* を置く スプレット演算子 というもので、配列の全要素を展開した状態にします。
これにより受け渡しが可能となっています。


以下の呼び出しは可能ですが、

convToListAndPrint("Str1", "Str2", "Str3")

以下の呼び出しはできません。コンパイルエラーになります

// コンパイルエラー
val array: Array<String> = arrayOf("Str1", "Str2", "Str3", "Str4")
convToListAndPrint(array)


スプレット演算子を利用した以下の呼び出しなら可能です

val array: Array<String> = arrayOf("Str1", "Str2", "Str3", "Str4")
convToListAndPrint(*array)


また、Javaでは可変長引数のパラメータは、一番最後の引数としてしか指定できません。
以下の様なコードはコンパイルエラーになります

import java.time.LocalDate;
import java.util.Arrays;

// コンパイルエラー(可変長引数の位置が不適切)
void convToListAddDateAndPrint(String... values, LocalDate date) {
    System.out.println(Arrays.asList(values).add(date.toString()));
}


ただし、Kotlinではこの縛り無いようで?、以下のようなコードは問題ありません。

import java.time.LocalDate;
import java.util.Arrays;

fun convToListAddDateAndPrint(vararg values: String, date: LocalDate) {
    println(Arrays.asList(*values).add(date.toString()))
}

呼び出す場合は、以下のように可変長引数以外のパラメータには名前付き引数を利用する必要があるようです。
(この辺りは、私の調べが足りてないだけかも?)

convToListAddDateAndPrint( "Str1", "Str2", "Str3", date = LocalDate.now())


まとめ

KotlinでもJavaと同様に可変長引数を扱える事が分かりました。
ただし、いくつかJavaとは異なる部分があり、意識する必要があります。

Kotlin - 拡張関数・拡張プロパティ

前回のエントリ で、文字列の前後に prefixとpostfixの文字列を付与する decorate というしょうもない関数を定義してみました。

この関数の呼び出しは以下のようになるのですが

decorate({ベースの文字列}, {prefix}, {postfix})

Kotlinの 拡張関数(extention function)という機能により、Stringのメンバ関数の様にして扱う事ができます。 結果的に以下の様に呼び出せるようになります。

<ベースの文字列>.decorate(<prefix>, <postfix>)


もともと定義した decorate 関数は以下の様なものでした

fun decorate(src: String, prefix: String, postfix: String): String {
    return StringBuilder(prefix)
        .append(src)
        .append(postfix)
        .toString()
}

これを拡張関数として定義し直すと以下の様になります

fun String.decorate(prefix: String, postfix: String): String {
    return StringBuilder(prefix)
        .append(this)
        .append(postfix)
        .toString()
}


拡張関数の構文は、以下ようになります。

fun <レシーバ型>.関数名: <戻り値型> {
    // ここに処理を記述。 ここでの this は レシーバオブジェクトを意味する
}

追加する関数の名前の前に、クラスまたはインターフェースの名前を置くだけです。このクラス名を レシーバ型(receivcer type)と呼び、拡張関数を呼び出す値は、レシーバーオブジェクト(receiver object)と呼ばれます。


実際に 定義してみた拡張関数を呼び出してみると以下の様になります。(Stringのメンバの様に呼び出している事が分かります)

val decoratedStr = "Kotlin".decorate("<b>", "</b>")
println("Kotlin -> $decoratedStr")

実行結果は以下の通りです。

Kotlin -> <b>Kotlin</b>

この例では、 String がレーシバ型で、"Kotlin"という文字列がレシーバオブジェクト ということになります。


拡張関数を定義する時の制限事項の一つに、カプセル化を破れない。というのがあります。
拡張関数では、拡張しているクラスのメソッドやプロパティに、そのクラス自身に定義されているメソッドのように直接アクセス可能です。
しかし、拡張関数はクラスに定義されたメソッドとは異なり、クラスの private または protected なメンバにアクセスすることは許されません。

また、拡張関数を定義した時に、自動的にプロジェクト全体を横断して利用可能になるわけではありません。
他のクラスや関数と同様にインポートが必要になります。


メンバが優先される

Sample というクラスを定義してメンバに hello という関数を定義します。
その上で同名の hello という拡張関数を定義します。

package net.yyuki.sandbox.expand

class Sample {
    fun hello() {
        println("Hello World")
    }
}

fun Sample.hello() {
    println("Hello World Expand")
}

このSampleのhello関数を呼び出してみます

import net.yyuki.sandbox.expand.hello
import net.yyuki.sandbox.expand.Sample

fun main() {
    val sample = Sample()
    sample.hello()
}

実行結果は以下の通りです

Hello World

メンバの関数の実行が優先されている事が分かります


ただし、ここから私の理解が追いついていないだけなのですが、
String には文字列を大文字化する toUpperCase というメソッドが存在しています。

例えば、これと同名の toUpperCase という拡張関数を String に定義します。
文字列の先頭を取り出すという関数名とは全く無関係な処理内容にしています。

package net.yyuki.sandbox.expand

fun String.toUpperCase(): String = this.first().toString()

で、これを以下のように呼び出すと

import net.yyuki.sandbox.expand.toUpperCase

fun main() {
    println("Kotlin".toUpperCase())
}

実行結果は以下の通りです。(通常の String の toUpperCase が実行されていない)

K

元のメンバの関数が実行されずに、拡張関数が呼ばれます。
この動きはいまいち何でこうなのかは理解できていないです。


拡張プロパティ

関数の構文ではなく、プロパティの構文を使ってクラスを拡張する方法も提供されています。
ただし、いかなる状態も持つことはできません。状態を格納するための場所がないためです。
つまり、Javaオブジェクトの既存インスタンスに付加的なフィールドを追加するのは不可能ということになります。

文字列の末尾の文字を取り出す拡張プロパティ lastChar を定義すると以下の様になります

val String.lastChar: Char
    get() = get(lastIndex)

上記では読み取り専用のプロパティの例でしたが、ミュータブルな書き込み可能な拡張プロパティの定義も可能です

var StringBuilder.lastChar: Char
    get() = get(lastIndex)
    set(value) {
        this.setCharAt(lastIndex , value)
    }

拡張プロパティを呼び出すコードは以下の通りです

import net.yyuki.sandbox.expand.lastChar
import java.lang.StringBuilder

fun main() {
    val lastCh = StringBuilder()
        .append("Abcdefg")
        .lastChar

    println("Last char = $lastCh")


    val sb = StringBuilder()
        .append("Abcdefg")
    sb.lastChar = '@'

    println("StringBuilder = $sb")
}

実行結果は以下の通りです

Last char = g
StringBuilder = Abcdef@


Javaライブラリ拡張

Kotlinで利用されている コレクションは Javaのコレクションライブラリです。
それにも関わらず、以下の様なListの先頭要素の取得、最終要素の取得する関数や、Setの最大値を取得する関数 が用意されています

val list = listOf("1st", "2nd", "3rd", "4th")

val firstElement = list.first() // Listの先頭要素を取得
val lastElement = list.last()   // Listの最終要素を取得

println("$list -> first=$firstElement, last=$lastElement")    // [1st, 2nd, 3rd, 4th] -> first=1st, last=4th


val set = setOf(10, 30, -1, 100, 1000, 5)
val maxNumber = set.max()    // コレクションの最大値を取得

println("$set -> max=$maxNumber")    // [10, 30, -1, 100, 1000, 5] -> max=1000

これらのfirst, last, max といった関数は、まさに拡張関数として宣言されたものです。
これら以外にも多くの拡張関数がKotlinの標準ライブラリに宣言されています。


まとめ

Kotlin では、Javaと違ってクラスを継承したりしなくても、クラスを拡張する能力が提供されていることが分かりました。



関連エントリ

blog.y-yuki.net