VavrのコレクションAPIガイド

VavrのコレクションAPIガイド

1. 概要

以前はJavaslangとして知られていたVavrライブラリは、Javaの機能的なライブラリです。 この記事では、その強力なコレクションAPIについて説明します。

このライブラリの詳細については、this articleをお読みください。

2. 永続的なコレクション

変更された永続コレクションは、現在のバージョンを保持しながら、コレクションの新しいバージョンを生成します。

同じコレクションの複数のバージョンを維持すると、CPUとメモリの使用効率が低下する可能性があります。 ただし、Vavrコレクションライブラリは、コレクションの異なるバージョン間でデータ構造を共有することでこれを克服しています。

これは、基礎となるコレクションのラッパーを提供するだけのCollectionsユーティリティクラスのJavaのunmodifiableCollection()とは根本的に異なります。

このようなコレクションを変更しようとすると、新しいバージョンが作成されるのではなく、UnsupportedOperationExceptionになります。 さらに、基礎となるコレクションは、その直接参照を通じて変更可能です。

3. Traversable

Traversableは、すべてのVavrコレクションの基本タイプです。このインターフェイスは、すべてのデータ構造間で共有されるメソッドを定義します。

これは、size()get()filter()isEmpty()など、サブインターフェイスに継承されるいくつかの便利なデフォルトメソッドを提供します。

コレクションライブラリをさらに詳しく見ていきましょう。

4. Seq

シーケンスから始めましょう。

Seqインターフェースは、順次データ構造を表します。 これは、ListStreamQueueArrayVector、およびCharSeqの親インターフェイスです。 これらのデータ構造にはすべて、以下で説明する独自のプロパティがあります。

4.1. List

Listは、LinearSeqインターフェイスを拡張する要素の熱心に評価されたシーケンスです。

永続的なListsは、ヘッドとテールから再帰的に形成されます。

  • ヘッド-最初の要素

  • テール–残りの要素を含むリスト(そのリストはヘッドとテールからも形成されます)

Listの作成に使用できるListAPIには静的ファクトリメソッドがあります。 静的of()メソッドを使用して、1つ以上のオブジェクトからListのインスタンスを作成できます。

静的empty()を使用して空のListを作成し、ofAll()を使用してIterableタイプからListを作成することもできます。

List list = List.of(
  "Java", "PHP", "Jquery", "JavaScript", "JShell", "JAVA");

リストを操作する方法の例をいくつか見てみましょう。

drop()とそのバリアントを使用して、最初のN要素を削除できます。

List list1 = list.drop(2);
assertFalse(list1.contains("Java") && list1.contains("PHP"));

List list2 = list.dropRight(2);
assertFalse(list2.contains("JAVA") && list2.contains("JShell"));

List list3 = list.dropUntil(s -> s.contains("Shell"));
assertEquals(list3.size(), 2);

List list4 = list.dropWhile(s -> s.length() > 0);
assertTrue(list4.isEmpty());

drop(int n)は、最初の要素から始まるnの要素数をリストから削除しますが、dropRight()は、リストの最後の要素から始まる同じことを行います。

dropUntil()は、述語がtrueと評価されるまでリストから要素を削除し続けますが、dropWhile()は、述語がtrueの間、要素を削除し続けます。

右側から要素の削除を開始するdropRightWhile()dropRightUntil()もあります。

次に、take(int n)を使用してリストから要素を取得します。 リストからn個の要素を取得して停止します。 リストの最後から要素の取得を開始するtakeRight(int n)もあります。

List list5 = list.take(1);
assertEquals(list5.single(), "Java");

List list6 = list.takeRight(1);
assertEquals(list6.single(), "JAVA");

List list7 = list.takeUntil(s -> s.length() > 6);
assertEquals(list7.size(), 3);

最後に、takeUntil()は、述語が真になるまでリストから要素を取得し続けます。 述語引数も取るtakeWhile()バリアントがあります。

さらに、APIには他にも便利なメソッドがあります。たとえば、実際には、重複しない要素のリストを返すdistinct()や、同等性を判断するためにComparatorを受け入れるdistinctBy()などです。

非常に興味深いことに、リストのすべての要素の間に要素を挿入するintersperse()もあります。 String操作には非常に便利です。

List list8 = list
  .distinctBy((s1, s2) -> s1.startsWith(s2.charAt(0) + "") ? 0 : 1);
assertEquals(list8.size(), 2);

String words = List.of("Boys", "Girls")
  .intersperse("and")
  .reduce((s1, s2) -> s1.concat( " " + s2 ))
  .trim();
assertEquals(words, "Boys and Girls");

リストをカテゴリに分けたいですか? そのためのAPIもあります。

Iterator> iterator = list.grouped(2);
assertEquals(iterator.head().size(), 2);

Map> map = list.groupBy(e -> e.startsWith("J"));
assertEquals(map.size(), 2);
assertEquals(map.get(false).get().size(), 1);
assertEquals(map.get(true).get().size(), 5);

group(int n)は、Listをそれぞれn要素のグループに分割します。 groupdBy()は、リストを分割するロジックを含むFunctionを受け入れ、truefalseの2つのエントリを持つMapを返します。

trueキーは、Function;で指定された条件を満たす要素のListにマップされます。falseキーは、満たさない要素のListにマップされます。

予想どおり、Listを変更する場合、元のListは実際には変更されません。 代わりに、Listの新しいバージョンが常に返されます。

スタックセマンティクス(要素の後入れ先出し(LIFO)取得)を使用してListと対話することもできます。 この点で、peek()pop()push()などのスタックを操作するためのAPIメソッドがあります。

List intList = List.empty();

List intList1 = intList.pushAll(List.rangeClosed(5,10));

assertEquals(intList1.peek(), Integer.valueOf(10));

List intList2 = intList1.pop();
assertEquals(intList2.size(), (intList1.size() - 1) );

pushAll()関数は、整数の範囲をスタックに挿入するために使用され、peek()は、スタックの先頭を取得するために使用されます。 結果をOptionオブジェクトにラップできるpeekOption()もあります。

Java docsにきちんと文書化されている、Listインターフェースには他にも興味深く本当に便利なメソッドがあります。

4.2. Queue

不変のQueueは、先入れ先出し(FIFO)検索を可能にする要素を格納します。

Queueは、内部的に2つのリンクリスト(フロントListとリアList)で構成されています。 前部のListにはデキューされた要素が含まれ、後部のListにはエンキューされた要素が含まれます。

これにより、enqueueおよびdequeue操作をO(1)で実行できます。 前部のListが要素を使い果たすと、前部と後部のList’sが交換され、後部のListが逆になります。

キューを作成しましょう:

Queue queue = Queue.of(1, 2);
Queue secondQueue = queue.enqueueAll(List.of(4,5));

assertEquals(3, queue.size());
assertEquals(5, secondQueue.size());

Tuple2> result = secondQueue.dequeue();
assertEquals(Integer.valueOf(1), result._1);

Queue tailQueue = result._2;
assertFalse(tailQueue.contains(secondQueue.get(0)));

dequeue関数は、ヘッド要素をQueueから削除し、Tuple2<T, Q>を返します。 タプルには、最初のエントリとして削除されたhead要素と、2番目のエントリとしてQueueの残りの要素が含まれています。

combination(n)を使用して、Queue内の要素の可能なすべてのNの組み合わせを取得できます。

Queue> queue1 = queue.combinations(2);
assertEquals(queue1.get(2).toCharSeq(), CharSeq.of("23"));

ここでも、要素のエンキュー/デキュー中に元のQueueが変更されていないことがわかります。

4.3. Stream

Streamは、遅延リンクリストの実装であり、java.util.streamとはまったく異なります。 java.util.streamとは異なり、VavrStreamはデータを格納し、次の要素を遅延評価しています。

整数のStreamがあるとしましょう:

Stream s = Stream.of(2, 1, 3, 4);

s.toString()の結果をコンソールに出力すると、Stream(2, ?)のみが表示されます。 これは、テールが評価されていないのに、評価されたのはStreamのヘッドのみであることを意味します。

s.get(3)を呼び出し、続いてs.tail()の結果を表示すると、Stream(1, 3, 4, ?)が返されます。 逆に、s.get(3)の最初のを呼び出さないと、Streamは最後の要素を評価します–s.tail()の結果はStream(1, ?)のみになります。 これは、テールの最初の要素のみが評価されたことを意味します。

この動作により、パフォーマンスが向上し、Streamを使用して(理論的には)無限に長いシーケンスを表すことができます。

VavrStreamは不変であり、EmptyまたはConsの場合があります。 Consは、ヘッド要素とレイジー計算されたテールStreamで構成されます。 Listとは異なり、Streamの場合、head要素のみがメモリに保持されます。 テール要素はオンデマンドで計算されます。

10個の正の整数のStreamを作成し、偶数の合計を計算してみましょう。

Stream intStream = Stream.iterate(0, i -> i + 1)
  .take(10);

assertEquals(10, intStream.size());

long evenSum = intStream.filter(i -> i % 2 == 0)
  .sum()
  .longValue();

assertEquals(20, evenSum);

Java 8Stream APIとは対照的に、VavrのStreamは、要素のシーケンスを格納するためのデータ構造です。

したがって、要素を操作するためのget()append(),insert()などのメソッドがあります。 drop()distinct()、および前述のその他の方法も利用できます。

最後に、Streamtabulate()を簡単に示しましょう。 このメソッドは、長さnStreamを返します。これには、関数を適用した結果である要素が含まれています。

Stream s1 = Stream.tabulate(5, (i)-> i + 1);
assertEquals(s1.get(2).intValue(), 3);

zip()を使用して、Tuple2<Integer, Integer>Streamを生成することもできます。これには、2つのStreamsを組み合わせて形成される要素が含まれます。

Stream s = Stream.of(2,1,3,4);

Stream> s2 = s.zip(List.of(7,8,9));
Tuple2 t1 = s2.get(0);

assertEquals(t1._1().intValue(), 2);
assertEquals(t1._2().intValue(), 7);

4.4. Array

Arrayは、効率的なランダムアクセスを可能にする不変のインデックス付きシーケンスです。 これは、Javaarrayのオブジェクトによって支えられています。 基本的に、これはタイプTのオブジェクトの配列のTraversableラッパーです。

静的メソッドof()を使用して、Arrayをインスタンス化できます。 静的なrange()およびrangeBy()メソッドを使用して範囲要素を生成することもできます。 rangeBy()には、ステップを定義するための3番目のパラメーターがあります。

range()およびrangeBy()メソッドは、開始値から終了値から1を引いた値から始まる要素のみを生成します。 終了値を含める必要がある場合は、rangeClosed()またはrangeClosedBy()のいずれかを使用できます。

Array rArray = Array.range(1, 5);
assertFalse(rArray.contains(5));

Array rArray2 = Array.rangeClosed(1, 5);
assertTrue(rArray2.contains(5));

Array rArray3 = Array.rangeClosedBy(1,6,2);
assertEquals(rArray3.size(), 3);

インデックスで要素を操作してみましょう。

Array intArray = Array.of(1, 2, 3);
Array newArray = intArray.removeAt(1);

assertEquals(3, intArray.size());
assertEquals(2, newArray.size());
assertEquals(3, newArray.get(1).intValue());

Array array2 = intArray.replace(1, 5);
assertEquals(array2.get(0).intValue(), 5);

4.5. Vector

Vectorは、ArrayListの中間の一種であり、ランダムアクセスと一定時間の変更の両方を可能にする別のインデックス付き要素シーケンスを提供します。

Vector intVector = Vector.range(1, 5);
Vector newVector = intVector.replace(2, 6);

assertEquals(4, intVector.size());
assertEquals(4, newVector.size());

assertEquals(2, intVector.get(1).intValue());
assertEquals(6, newVector.get(1).intValue());

4.6. CharSeq

CharSeqは、プリミティブ文字のシーケンスを表現するためのコレクションオブジェクトです。 これは基本的に、収集操作が追加されたStringラッパーです。

CharSeqを作成するには:

CharSeq chars = CharSeq.of("vavr");
CharSeq newChars = chars.replace('v', 'V');

assertEquals(4, chars.size());
assertEquals(4, newChars.size());

assertEquals('v', chars.charAt(0));
assertEquals('V', newChars.charAt(0));
assertEquals("Vavr", newChars.mkString());

5. Set

このセクションでは、コレクションライブラリのさまざまなSetの実装について詳しく説明します。 Setデータ構造の独自の機能は、重複する値を許可しないことです。

ただし、Setにはさまざまな実装があります。HashSetが基本的な実装です。 TreeSetは要素の重複を許可せず、並べ替えることができます。 LinkedHashSetは、その要素の挿入順序を維持します。

これらの実装を1つずつ詳しく見ていきましょう。

5.1. HashSet

HashSetには、of()ofAll()、およびrange()メソッドのバリエーションなど、新しいインスタンスを作成するための静的ファクトリメソッドがあります。

diff()メソッドを使用して、2つのセットの違いを取得できます。 また、union()メソッドとintersect()メソッドは、2つのセットの和集合と共通集合を返します。

HashSet set0 = HashSet.rangeClosed(1,5);
HashSet set1 = HashSet.rangeClosed(3, 6);

assertEquals(set0.union(set1), HashSet.rangeClosed(1,6));
assertEquals(set0.diff(set1), HashSet.rangeClosed(1,2));
assertEquals(set0.intersect(set1), HashSet.rangeClosed(3,5));

要素の追加や削除などの基本的な操作も実行できます。

HashSet set = HashSet.of("Red", "Green", "Blue");
HashSet newSet = set.add("Yellow");

assertEquals(3, set.size());
assertEquals(4, newSet.size());
assertTrue(newSet.contains("Yellow"));

HashSetの実装は、Hash array mapped trie (HAMT)によってサポートされています。これは、通常のHashTableと比較して優れたパフォーマンスを誇り、その構造により、永続的なコレクションのサポートに適しています。

5.2. TreeSet

不変のTreeSetは、SortedSetインターフェースの実装です。 ソートされた要素のSetを格納し、バイナリ検索ツリーを使用して実装されます。 すべての操作はO(log n)時間で実行されます。

デフォルトでは、TreeSetの要素は自然な順序で並べ替えられます。

自然順を使用してSortedSetを作成しましょう。

SortedSet set = TreeSet.of("Red", "Green", "Blue");
assertEquals("Blue", set.head());

SortedSet intSet = TreeSet.of(1,2,3);
assertEquals(2, intSet.average().get().intValue());

カスタマイズされた方法で要素を並べ替えるには、TreeSet.を作成するときにComparatorインスタンスを渡します。セット要素から文字列を生成することもできます。

SortedSet reversedSet
  = TreeSet.of(Comparator.reverseOrder(), "Green", "Red", "Blue");
assertEquals("Red", reversedSet.head());

String str = reversedSet.mkString(" and ");
assertEquals("Red and Green and Blue", str);

5.3. BitSet

Vavrコレクションには、不変のBitSet実装も含まれています。 BitSetインターフェースは、SortedSetインターフェースを拡張します。 BitSetは、BitSet.Builderの静的メソッドを使用してインスタンス化できます。

Setデータ構造の他の実装と同様に、BitSetでは重複エントリをセットに追加できません。

Traversableインターフェースから操作のメソッドを継承します。 標準のJavaライブラリのjava.util.BitSetとは異なることに注意してください。 BitSetデータにString値を含めることはできません。

ファクトリメソッドof()を使用してBitSetインスタンスを作成する方法を見てみましょう。

BitSet bitSet = BitSet.of(1,2,3,4,5,6,7,8);
BitSet bitSet1 = bitSet.takeUntil(i -> i > 4);
assertEquals(bitSet1.size(), 4);

takeUntil()を使用して、BitSet.の最初の4つの要素を選択します。操作によって新しいインスタンスが返されました。 takeUntil()は、BitSet.の親インターフェイスであるTraversableインターフェイスで定義されていることに注意してください。

Traversableインターフェースで定義されている、上記で示した他のメソッドと操作は、BitSetにも適用できます。

6. Map

マップはキーと値のデータ構造です。 VavrのMapは不変であり、HashMapTreeMap、およびLinkedHashMapの実装があります。

通常、マップコントラクトではキーの重複は許可されていませんが、異なるキーにマッピングされた値が重複している場合があります。

6.1. HashMap

HashMapは、不変のMapインターフェースの実装です。 キーのハッシュコードを使用してキーと値のペアを保存します。

VavrのMapは、従来のEntryタイプの代わりに、Tuple2を使用してキーと値のペアを表します。

Map> map = List.rangeClosed(0, 10)
  .groupBy(i -> i % 2);

assertEquals(2, map.size());
assertEquals(6, map.get(0).get().size());
assertEquals(5, map.get(1).get().size());

HashSetと同様に、HashMapの実装は、ハッシュ配列マップトライ(HAMT)によってサポートされているため、ほとんどすべての操作で一定の時間が得られます。

filterKeys()メソッドを使用してキーで、またはfilterValues()メソッドを使用して値でマップエントリをフィルタリングできます。 どちらのメソッドも、引数としてPredicateを受け入れます。

Map map1
  = HashMap.of("key1", "val1", "key2", "val2", "key3", "val3");

Map fMap
  = map1.filterKeys(k -> k.contains("1") || k.contains("2"));
assertFalse(fMap.containsKey("key3"));

Map fMap2
  = map1.filterValues(v -> v.contains("3"));
assertEquals(fMap2.size(), 1);
assertTrue(fMap2.containsValue("val3"));

map()メソッドを使用してマップエントリを変換することもできます。 たとえば、map1Map<String, Integer>に変換してみましょう。

Map map2 = map1.map(
  (k, v) -> Tuple.of(k, Integer.valueOf(v.charAt(v.length() - 1) + "")));
assertEquals(map2.get("key1").get().intValue(), 1);

6.2. TreeMap

不変のTreeMapは、SortedMapインターフェースの実装です。 TreeSetと同様に、Comparatorインスタンスは、TreeMapの要素をカスタムソートするために使用されます。

SortedMapの作成をデモンストレーションしましょう:

SortedMap map
  = TreeMap.of(3, "Three", 2, "Two", 4, "Four", 1, "One");

assertEquals(1, map.keySet().toJavaArray()[0]);
assertEquals("Four", map.get(4).get());

デフォルトでは、TreeMapのエントリはキーの自然な順序でソートされます。 ただし、並べ替えに使用されるComparatorを指定することはできます。

TreeMap treeMap2 =
  TreeMap.of(Comparator.reverseOrder(), 3,"three", 6, "six", 1, "one");
assertEquals(treeMap2.keySet().mkString(), "631");

TreeSetと同様に、TreeMapの実装もツリーを使用してモデル化されるため、その操作はO(log n)時間です。 map.get(key)は、マップ内の指定されたキーの値をラップするOptionを返します。

7. Javaとの相互運用性

コレクションAPIは、Javaのコレクションフレームワークと完全に相互運用可能です。 これが実際にどのように行われるかを見てみましょう。

7.1. JavaからVavrへの変換

Vavrの各コレクション実装には、java.util.Iterableを取る静的ファクトリメソッドofAll()があります。 これにより、JavaコレクションからVavrコレクションを作成できます。 同様に、別のファクトリメソッドofAll()はJavaStreamを直接受け取ります。

JavaListを不変のListに変換するには:

java.util.List javaList = java.util.Arrays.asList(1, 2, 3, 4);
List vavrList = List.ofAll(javaList);

java.util.stream.Stream javaStream = javaList.stream();
Set vavrSet = HashSet.ofAll(javaStream);

もう1つの便利な関数は、Stream.collect()と組み合わせてVavrコレクションを取得できるcollector()です。

List vavrList = IntStream.range(1, 10)
  .boxed()
  .filter(i -> i % 2 == 0)
  .collect(List.collector());

assertEquals(4, vavrList.size());
assertEquals(2, vavrList.head().intValue());

7.2. VavrからJavaへの変換

Valueインターフェースには、Vavr型をJava型に変換するための多くのメソッドがあります。 これらのメソッドの形式はtoJavaXXX()です。

いくつかの例を取り上げましょう。

Integer[] array = List.of(1, 2, 3)
  .toJavaArray(Integer.class);
assertEquals(3, array.length);

java.util.Map map = List.of("1", "2", "3")
  .toJavaMap(i -> Tuple.of(i, Integer.valueOf(i)));
assertEquals(2, map.get("2").intValue());

Java 8Collectorsを使用して、Vavrコレクションから要素を収集することもできます。

java.util.Set javaSet = List.of(1, 2, 3)
  .collect(Collectors.toSet());

assertEquals(3, javaSet.size());
assertEquals(1, javaSet.toArray()[0]);

7.3. Javaコレクションビュー

また、ライブラリは、Javaコレクションへの変換時にパフォーマンスが向上する、いわゆるコレクションビューを提供します。 前のセクションの変換メソッドは、すべての要素を反復処理してJavaコレクションを構築します。

一方、ビューは標準Javaインターフェースを実装し、メソッド呼び出しを基礎となるVavrコレクションに委任します。

この記事の執筆時点では、Listビューのみがサポートされています。 各順次コレクションには2つのメソッドがあり、1つは不変ビューを作成し、もう1つは可変ビュー用です。

不変ビューでミューテーターメソッドを呼び出すと、UnsupportedOperationExceptionになります。

例を見てみましょう:

@Test(expected = UnsupportedOperationException.class)
public void givenVavrList_whenViewConverted_thenException() {
    java.util.List javaList = List.of(1, 2, 3)
      .asJava();

    assertEquals(3, javaList.get(2).intValue());
    javaList.add(4);
}

不変のビューを作成するには:

java.util.List javaList = List.of(1, 2, 3)
  .asJavaMutable();
javaList.add(4);

assertEquals(4, javaList.get(3).intValue());

8. 結論

このチュートリアルでは、VavrのCollectionAPIによって提供されるさまざまな機能データ構造について学習しました。 VavrのコレクションJavaDocおよびuser guideには、より便利で生産的なAPIメソッドがあります。

最後に、ライブラリはValueインターフェースを拡張し、結果としてJavaを実装するTryOptionEither、およびFutureも定義することに注意することが重要です。 Iterableインターフェース。 これは、状況によってはコレクションとして動作できることを意味します。

この記事のすべての例の完全なソースコードは、over on Githubにあります。