[はじめようApache Groovy] #07 - コレクションその2(マップ編)

こんにちは!なるーらぼです。前回はコレクションのうちのリストについてお話ししました。かなりrubyに影響を受けていることがわかりますね。今回はマップをみていきたいと思います。


マップ

マップは「連想配列」と呼ばれることの方が多いと思います。配列との違いはインデックスの代わりにキーでアクセスすることができ、キーと値というペアになっています。キーはマップの中ではユニークになっているというのも特徴です。


リテラル

マップはリストと同様に角かっこを使うのですが、キーと値をコロン記号でつなぎます。

それ以外の使い方はリストとほぼ同じです。

def map = [name: 'nalulabo', url: 'https://nalu-labo.amebaownd.com']
assert map.get('name') == 'nalulabo'
assert map['name'] == 'nalulabo'

マップは「java.util.Map」のインスタンスです。空のマップを作るには角かっことコロンのみで記述します。上記のコードで示したように、マップの要素を取得するには配列のインデックスの代わりにキー名を指定します。

また、マップに要素を追加するには同様にするか「put」メソッドを使います。

def map = [:]
assert map.size() == 0
map.put('age', 37)

assert map.size() == 1

assert map.get('age') == 37

マップのキーには文字列を指定しますが、[name: 'nalulabo']と['name': 'nalulabo']は同等な表現になります。もしもリテラル中のキーへ変数で指定したいときは必ず丸括弧で囲んでエスケープするようにしてください

def name = 'nalulabo'
def map = [name: 'm0t0k1']
assert map[name] == 'nalulabo'

上記のコードではマップのキー「name」で得られる値を「m0t0k1」だと期待していますが、実際にはnullになります。

ERROR org.codehaus.groovy.runtime.powerassert.PowerAssertionError:

というのも、マップ中には「nalulabo」というキーは存在しないからです。

上記のコードでは「name」というキーに値として「m0t0k1」が指定されています。ということは、期待したとおりにリテラルに書くには丸括弧をつかってエスケープしなければなりません。

def map2 = [(name): 'm0t0k1']
assert map2[name] == 'm0t0k1'


マップをコピーするにはリスト同様に「clone」メソッドを使います。コピーされたマップはキーと値をコピー元から引き継ぎますが、値がマップとそれ以外で動作が異なります。

次の例を見てください。まず単純なマップのコピーではコピー先に加えた変更は元のマップと無関係です。

def map = [a: 12, b: 24]
def map2 = map.clone()
map2.put('c', 35)

// map = [a: 12, b: 24]

// map2 = [a: 12, b:24, c: 35]

ただ、マップに含まれるリストやマップについては別なことが起こります。

def map = [a: [1,2,3]]
map2 = map.clone()
map2['a'].remove(2)

// map = [a: [1,2,]]

// map2 = [a: [1,2]]

上記ではリストに含まれる要素を1つ削除しましたが、その影響がコピー元のマップにも及んでいます

def map = [nalulabo:m0t0k1, url:nalu-labo, complex:[a:12]]
def map2 = map.clone()
map2['complex'].put('b', 24)

// map = [nalulabo:m0t0k1, url:nalu-labo, complex:[a:12, b:24]]

// map2 = [nalulabo:m0t0k1, url:nalu-labo, complex:[a:12, b:24]]

マップでも同様です。つまりコピーされるのは値でありオブジェクトそのものではないということです。コピーしておいて、コピー元とは関係なくなったので…と思って加工すると元のマップにも影響する場合があることに注意してください。


プロパティ記述で要素アクセスする

Groovyのマップではマップのキーについて、配列のようにアクセスするこれまでご覧いただいた方法と、プロパティのようにドット記号でキー名を指定する方法の2種類があります。

def map = [name: 'nalulabo', age: 37]
assert map.name == 'nalulabo'
assert map.age == 37

同じように、マップへキーと値のセットを追加するときにもプロパティのように記述することができます。

def map = [:]
map.name = 'nalulabo'
assert map.name == 'nalulabo'

ただ、この機能のため犠牲になっていることがあります。Groovyではインスタンスの元になっているクラス名を取得するプロパティとして「class」というものがあります。

ですが、マップの場合はその仕様のためマップ中のキーとして「class」を探してしまうことになります。ですから、マップのクラス名を取得するには「getClass」メソッドを使います

同様に、利用することのできないキーがあります。例えば「true」「false」はキーワードでもあるため、キー名として利用するにはエスケープが必須となります。使うかどうかは・・・というところではありますが。

// この場合は文字列の「true」「false」になるので問題ない
def map = [true: 1, false: 0]
assert map.true == 1

// この場合はtrue、falseとなるのでnullになる

def map2 = [(true): 1, (false): 0]

assert !map2.true == 1

// これはnullにならない

assert map2.get(true) == 1

同様なものとして、nullがあります。


マップの反復

マップの反復も基本的にリストとほぼ同じです。「each」メソッドを使うとキーと値のペアか、キー、値それぞれをパラメータで受け取るかを選択して利用します。「eachWithIndex」メソッドを使うとほぼ同様ですがインデックスも得ることができます。

def map = [りんご: 100, みかん: 120, バナナ: 170]
map.each {
 println it.value

}

map.each { item ->

 println item.value

}

map.each { key, value ->

 println "$key -> $value"

}

map.eachWithIndex { key, value, i ->

 println "$i $key -> $value"

}


マップの操作

マップの操作もリストと似ています。1点異なるのはキーがユニークということです。つまり別のマップをputAllメソッドで書き込むと、別のマップとキーが重なるものについてはその値が上書きされてしまいます

マップは「java.util.LinkedHashMap」のインスタンスです。ですからリストのときと同様にコンストラクタへマップを渡すと別のインスタンスで同じ内容のものを作成します。

def map = [1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e']
def map2 = new LinkedHashMap(map)
map2[17] = 'v'

def map3 = [2: 'z', 4: "aaa"]

map2.putAll(map3)

// [1:a, 2:z, 3:c, 4:aaa, 5:e, 17:v]

マップの中身を空に戻したくなったら、「clear」メソッドを使ってください。これもリストと同じですね。

def map = [1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e']
assert map.size() == 5
map.clear()

assert map.size() == 0

ところで、文字列のところでお話ししたことを覚えていらっしゃいますか?マップのキーにGStringを使った変数展開で指定してはいけませんでしたよね?これは変数展開されたときと単なるStringではハッシュ値が異なるためでした。ということで、使わないほうがいいでしょう。


マップのキー、値、エントリ

マップのキー、値、エントリ(キーと値のペア)をチェックすることができます。それぞれ、「keySet()」メソッド、「values」メソッド、「entrySet()」メソッドでキーだけ、値だけ、エントリという一式にすることができます。

def map = [1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e']
def es = map.entrySet()
es.each { entry ->

 assert entry.key in [1,2,3,4,5]

 assert entry.value in ['a','b','c','d','e']

}

def ks = map.keySet()

assert ks == [1,2,3,4,5] as Set

def vs = map.values()

vs.each { println it }

なお、こうしたメソッドを使った表現型で値を変更するのは避けた方がいいです。というのも、GroovyはJavaのJDKに依存していますし、そのJDKはこうした表現用のメソッドを介して安全にコレクションを操作することができる保証が一切ありません。見る分には、という感じですね。


マップ内の検索、フィルタリング

これもリストとほぼ同じような感じですが、マップの場合はキーがユニークになるのでデータベース操作のような気分を味わうことができるでしょう。特定のエントリを検索するには「find」メソッドを使います。条件に合致するエントリすべてを返すには「findAll」メソッドを使いましょう。

def map = [1: 'a', 2: 'b',3: 'c',4: 'd',5: 'e']
def e = map.find { it.value == 'c' } // 3=c
def ea = map.findAll { it.key > 3 } // [4:d, 5:e]

findで返される値は「java.util.LinkedHashMap$Entry」ですが、findAllで返される値は「java.util.LinkedHashMap」ですので注意してください。この差はfindでは単一のペアが返されるから、ということになるでしょう。

findAllで返された値のみのリストがほしい場合は「collect」メソッドを使うとリストを作ることができます。

def values = ea.collect { it.value }
// [d, e]

またリストのときと同様に、すべての要素が条件に合致した場合にtrueを返す「every」メソッド、一部の要素が条件に合致していればtrueを返す「any」メソッドも利用することができます。

def map = [1: 'a', 2: 'b',3: 'c',4: 'd',5: 'e']
map.every { it.key > 0 } // true
map.any { it.key > 3 } // true


グループ化

マップはそのキーやプロパティを使ってグループ化することができます。「groupBy」メソッドを使って波括弧の中にグループ化の条件を記述します。

def map = [[name: 'motoki', city: 'kurashiki'],[name: 'we-i', city: 'nagayama'],[name: 'onigiri', city: 'tokio'],[name: 'nigai', city: 'kurashiki']]
map.groupBy { it.city }
===> [kurashiki:[[name:motoki, city:kurashiki], [name:nigai, city:kurashiki]], nagayama:[[name:we-i, city:nagayama]],

tokio:[[name:onigiri, city:tokio]]]

こうしてgroupByメソッドを使うと新たなマップが返されます。グループ化された値がキーとなります。


最後に

今回はコレクションその2としてマップを見てきました。

キーがユニークになるので便利に使うことができるのでぜひマスターしましょう。

もんが(なるーらぼ)

個人でプログラミングの学習サイト「なるーらぼ」を運営しています。
https://nalu-labo.amebaownd.com
PowerShell入門の電子書籍2冊も出版しています。
http://www.amazon.co.jp/dp/B017LJOCJ2

なるーらぼ

ごく自然にプログラミングを楽しむことができる世界をつくるお手伝いをしています

0コメント

  • 1000 / 1000