Kotlinで多次元配列:意外な落とし穴

投稿者: | 2018年12月26日

KotlinのArrayを使って多次元配列を実装する

Kotlinには魅力的な要素がたくさんある。

一方でJavaはと言えば、新しいAndroid APIを実装したデバイスだけしかJava 9以降の機能を使えない。デベロッパーとしてはパイの小さなそれら限定のアプリを作りたいとは思わないだろう。

OracleのJavaに対する方針が変わったことから、Googleが旧機種のために後方互換性のためのライブラリを作るとも思えない。

その点、Kotlinなら幅広いデバイスでサポートされており、先進的でメンテナンス性の高いコードが書ける。

そんなKotlinでAndroidアプリケーション開発をしようとして、はたと困ったのが多次元配列だった。

Javaの多次元配列は

String [][] abcArr = {{abc,def},{ghi,jkl}};

のように簡単に初期化してインスタンス化できる。既に中身が決まっている、何らかの定数的な配列を作るにはこのやり方が最も手っ取り早い。

KotlinにもArrayクラスがあって機能的にはJavaのものと同等であるが、使い方が大きく異なる。上に書いたJavaのサンプルはある種「静的」に内容を詰めているがKotlinではarrayOfなどの関数を使ってある種「動的」に中身を詰めていくのである。

さらに注意すべき点がいくつかあった。

要注意点

色々調べてみて感じた注意すべき点は以下のようなものであった。

  1. 用意されている関数などは殆どが1次元用
    なので2次元配列を扱う際には少なくとも一つのインデックスを指定して操作していくことになる。
    ただし操作するための関数は豊富にある。

  2. 増加方向のみだが実質的に可変長
    1次元配列を作るのに要素数0,空の配列を作っておいて後から中身を「足していく」作り方もできる。
    2次元配列を実現する場合は1次元配列をどんどんくっつけていくイメージ。
    3次元の場合はそうやって作った2次元のものに1次元配列をくっつけていく。
    もちろん固定長配列として宣言、実装することもできる。パフォーマンスを考えれば、その方がよいだろう。

  3. Arrayから継承されていないPrimitive Number用のArrayクラスがある
    IntやDoubleはプリミティブ型ではないので、パフォーマンス向上のためかプリミティブ型専用配列であるIntArray、ShortArray、LongArray等がある。

  4. 少なくとも最初は分かりにくい(C++、Javaプログラマーにとって)
    正直に言って、最初は訳が分からず、ArrayのためにKotlinを諦めようかと思ったくらい。というのも今回Kotlinで実装したいアプリケーションが多次元配列を多用するものだったからだ。(あるいはそこだけJavaにしようかとも思った。)

これらの注意点を踏まえて具体的なコードをある程度体系的に見ていこう。

サンプルはKotlin バージョン1.3.11 JDKはOracleの1.8.0_191という環境で実行した。

初期化の方法

Arrayを作るには関数を使う方法と、コンストラクタを使う方法がある。

まずは関数で1次元配列を作る方法

たとえば1から5までのIntが入っている配列を作るとしよう。

  1. 空で長さ0の配列を作って中身を詰めていくパターン
    var arr1d = emptyArray<Int>()
    arr1d += 1
    arr1d += 2
    arr1d += 3
    arr1d += 4
    arr1d += 5

    for (i in arr1d){
        println("$i")
    }
  1. 空の固定長配列を作って中身を詰めていくパターン
    val length = 5
    var arr1d1 = arrayOfNulls<Int>(length)

    for (i in 0..(length-1)){
        arr1d1[i] = i+1
    }

    for (i in arr1d1){
        println("$i")
    }

当然ながらレンジを越えて要素追加しようとすればランタイムエラーになる。さらに要注意なのはこのarrayOfNullsで生成される配列はArrayではなくArray<Int?>であることである。Nullableなのでイテレーターが回せなかったりする。それについては2次元の時に明らかになる。

  1. 最初から中身を詰めて作る
    var arr1d2 = arrayOf(1,2,3,4,5)

    for (i in arr1d2){
        println("$i")
    }
  1. コンストラクタで作る
    fun func(i : Int) : Int{
        return i+1
    }
    var arr1d3 = Array<Int>(5, {func(it)})
    arr1d3.forEach { println("${it}") }

itは要素のインデックスを表すキーワードで、0から始まる通常の配列インデックスを与える。上の例ではこれに対してfuncという関数で加工を施した上で中身を詰めている。
foreach内におけるitは作成後の配列のインデックス。{}は必要ないが、これをつけておくと例えば{it*it}のような演算処理を追加できる。上の例ではfuncを使わなくても{it+1}とすれば出力は同じになる。(もちろん中身は変更されていない)

いずれも出力は

1  
2  
3  
4  
5  

次は2次元

まずは関数で詰めるパターン

  1. 空の配列を作って中身の1次元配列を詰めていくパターン
    var twoDarr = arrayOf<Array<Int>>()
    var arr1d1 = arrayOf(1,2,3,4,5)
    var arr1d2 = arrayOf(6,7,8,9,10)
    var arr1d3 = arrayOf(11,12,13,14,15)
    twoDarr +=arr1d1
    twoDarr +=arr1d2
    twoDarr +=arr1d3
    twoDarr +=arr1d1

    twoDarr.dropLast(1).forEach {
        it.forEach {
            print("${it} ")
        }
        println()
    }

出力は

1 2 3 4 5 
6 7 8 9 10 
11 12 13 14 15 

のようになる。
 これは少なくとも行に関しては実質的に可変長の配列である。注意すべきなのはdropでは配列そのものを削ってはいないと言うこと。

であるから、

    twoDarr.forEach {
        it.forEach {
            print("${it} ")
        }
        println()
    }

とやると

1 2 3 4 5 
6 7 8 9 10 
11 12 13 14 15 
1 2 3 4 5 

最後に付け加えたarr1d1もしっかり出力されるのである。

  1. 行列のサイズを決めて初期化、一次元配列を代入する方法
    val x = 5
    val y = 3

    var twoDarr2: Array<Array<Int>> = arrayOf<Array<Int>>()

    for (i in 0..(y-1)) {
        var array = arrayOf<Int>()
        for (j in 0..(x-1)) {
            array += 0
        }
        twoDarr2 += array
    }

    twoDarr2[0] = arrayOf(1,2,3,4,5)
    twoDarr2[1] = arrayOf(6,7,8,9,10)
    twoDarr2[2] = arrayOf(11,12,13,14,15)

    twoDarr2.forEach {
        it.forEach {
            print("${it} ")
        }
        println()
    }

出力は

1 2 3 4 5 
6 7 8 9 10 
11 12 13 14 15 

こちらは固定長でさらに配列を付け加えようとするとエラーになる。

  1. コンストラクターを使って作成、中身を詰める方法

基本的に固定長

    val intArray1 = intArrayOf(1,2,3,4,5)
    val intArray2 = intArrayOf(6,7,8,9,10)
    val intArray3 = intArrayOf(11,12,13,13,15)

    val twoDarr3 = Array(3, {IntArray(5)})
    twoDarr3.set(0, intArray1)
    twoDarr3.set(1, intArray2)
    twoDarr3.set(2, intArray3)

    twoDarr3.forEach {
        it.forEach {
            print("${it} ")
        }
        println()
    }

出力は

1 2 3 4 5 
6 7 8 9 10 
11 12 13 14 15 

上の例で2次元配列をval宣言していることに注意。にもかかわらず、中身を後からsetできている。どうもkotlinではimmutableな多次元配列は作成できないようだ。(要調査)

作成済みの2次元配列をコピーして作成する方法は

    var twoDarr4 = Array<Array<Int>>(3, {i->twoDarr3[i]})
  1. 作成済みの1次元配列を束ねて作成する方法
    var arrconst1 = arrayOf(1,2,3,4,5)
    var arrconst2 = arrayOf(6,7,8,9,10)
    var arrconst3 = arrayOf(11,12,13,13,15)

    val twoDarrC: Array<Array<Int>> = arrayOf(arrconst1, arrconst2, arrconst3)

最後に3次元

一番簡単なパターンは作成済みの2次元配列を繋げる方法

    var threeDarr = arrayOf(twoDarr2, twoDarr4)

    threeDarr.forEach { it.forEach { it.forEach { print("${it} ") }
    println()} }

出力は

1 2 3 4 5 
6 7 8 9 10 
11 12 13 14 15 
1 2 3 4 5 
6 7 8 9 10 
11 12 13 13 15 

繋げ方は2次元配列を1次元配列から作るときのものと同じ。

要素にアクセスする方法

こちらは豊富なメソッドが利用できる。

  1. Javaでも使うインデックス
    これはJavaと同じ
    twoDarr[0][1] = 1
  1. get, set
    Arrayクラスのメソッドとして登録されている。
    twoDarr3.set(0,intArray3)
    intArray4 = twoDarr3.get(0)
  1. アクセス関数やforeachのようなモダンなアクセス手段を用いる

 次の例で上は2次元配列の行数を下は列数を返す。

    println(twoDarr3.size)
    println(twoDarr3[0].size)

よって出力は

3
5

foreachの例は上に示した中に入っている。イテレーターは最初のものが縦(行)、二番目のものが横(列)のものに対応する。

forEachIndexedの場合

    for (j in 0..2) {
        twoDarrC[j].forEachIndexed { i, e -> println("($j $i) -> $e") }
    }

出力は

(0 0) -> 1
(0 1) -> 2
(0 2) -> 3
(0 3) -> 4
(0 4) -> 5
(1 0) -> 6
(1 1) -> 7
(1 2) -> 8
(1 3) -> 9
(1 4) -> 10
(2 0) -> 11
(2 1) -> 12
(2 2) -> 13
(2 3) -> 13
(2 4) -> 15

iはインデックス(index)、eは要素(element)の略。

その他様々なメソッドが公式ページにある。しかし殆ど1次元配列に適用するものばかりである。

プリミティブ型の配列作成方法

実行時のperformanceのためかNumber型ではなく、プリミティブ型の数を使った配列を作るための関数がある。

    var barr = byteArrayOf(1,2,3,4,5)
    var iarr = intArrayOf(1,2,3,4,5)
    var sarr = shortArrayOf(1,2,3,4,5)
    var larr = longArrayOf(1,2,3,4,5)
    var carr = charArrayOf('1','2','3','4','5')

定数の(immutableな)多次元配列を作る方法

今のところ成功していない。val宣言して初期化した配列でも

    var arrPrimitive2D = arrayOf(intArrayOf(1,2,3,4,5))
    arrPrimitive2D += intArrayOf(6,7,8,9,10)
    arrPrimitive2D += intArrayOf(11,12,13,13,15)

    val twoDarrC2: Array<IntArray> = arrPrimitive2D

    twoDarrC2.forEach {
        it.forEach {
            print(" ${it} ")
        }
        println()
    }

    twoDarrC2.set(0, intArrayOf(5,4,3,2,1))

    println("---------------!")
    twoDarrC2.forEach {
        it.forEach {
            print(" ${it} ")
        }
        println()
    }

出力は

 1  2  3  4  5 
 6  7  8  9  10 
 11  12  13  13  15 
---------------!
 5  4  3  2  1 
 6  7  8  9  10 
 11  12  13  13  15 

のようになってしっかり更新されてしまっている。

まとめ

Kotlinに限らず様々な情報や物事の状態を行列で表すことはよくある。しかるにKotlinで整備されているのは1次元配列のみで多次元配列はまだまだと言った印象である。

とりわけMLのような線形代数を扱うような分野での応用には今のところ厳しいかも知れないというのが正直な印象。

開発者は今後の整備を期待しつつ何らかの方策でKotlin化を進めるしかなさそうだ。

コメントを残す