1. 概述

Vavr 库(前身为 Javaslang)是一个为 Java 设计的函数式库。本文将深入探索其强大的集合 API。

想了解更多关于该库的信息,请阅读这篇文章

2. 持久化集合

持久化集合在修改时会产生新版本,同时保留当前版本。维护同一集合的多个版本可能导致 CPU 和内存使用效率低下,但 Vavr 集合库通过在不同版本间共享数据结构解决了这个问题。

这与 Java 中 Collections 工具类的 unmodifiableCollection() 完全不同——后者只是对底层集合的包装。尝试修改这样的集合会抛出 UnsupportedOperationException,而不是创建新版本。而且,底层集合仍然可以通过直接引用被修改。

3. Traversable

Traversable 是所有 Vavr 集合的基类型,定义了所有数据结构共享的方法。它提供了一些有用的默认方法,如 size()get()filter()isEmpty() 等,这些方法会被子接口继承。

让我们进一步探索集合库。

4. Seq

我们从序列开始。Seq 接口表示顺序数据结构,是 ListStreamQueueArrayVectorCharSeq 的父接口。这些数据结构各有独特特性,下面将逐一探索。

4.1. List

List 是一个急切求值的元素序列,继承自 LinearSeq 接口。持久化 List 由头(head)和尾(tail)递归构成:

  • :第一个元素
  • :包含剩余元素的列表(同样由头和尾构成)

List API 提供静态工厂方法创建实例:

  • of():从一个或多个对象创建 List
  • empty():创建空 List
  • ofAll():从 Iterable 类型创建 List
List<String> 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():移除元素直到谓词为真
  • dropWhile():当谓词为真时持续移除

⚠️ 还有 dropRightWhile()dropRightUntil() 从右侧开始移除。

✅ 使用 take(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():当谓词为真时持续获取

✅ 其他实用方法:

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");
  • distinctBy():通过比较器去重
  • intersperse():在元素间插入分隔符(字符串操作利器)

✅ 分组操作:

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

Map<Boolean, List<String>> 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):将列表分成每组 n 个元素
  • groupBy():按条件分组,返回包含 truefalse 键的 Map

❌ 修改 List 时,原始列表不会被修改,而是返回新版本。

✅ 栈语义操作(LIFO):

List<Integer> intList = List.empty();

List<Integer> 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():获取栈顶元素
  • peekOption():将结果包装在 Option

更多方法详见 Java 文档

4.2. Queue

不可变 Queue 实现先进先出(FIFO)检索。内部由两个链表组成:前部列表(出队元素)和后部列表(入队元素)。这使得入队和出队操作时间复杂度为 O(1)。当前部列表耗尽时,前后列表会交换并反转后部列表。

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

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

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

Queue<Integer> tailQueue = result._2;
assertFalse(tailQueue.contains(secondQueue.get(0)));
  • dequeue():移除队头元素,返回包含移除元素和剩余队列的元组
  • combination(n):获取所有可能的 N 元素组合:
    Queue<Queue<Integer>> queue1 = queue.combinations(2);
    assertEquals(queue1.get(2).toCharSeq(), CharSeq.of("23"));
    

❌ 入队/出队操作不会修改原始队列。

4.3. Stream

Stream 是惰性链表的实现,与 java.util.stream 完全不同。Vavr Stream 存储数据并惰性求值后续元素。

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

打印 s.toString() 只显示 Stream(2, ?),表示仅头部被求值。调用 s.get(3) 后,s.tail() 返回 Stream(1, 3, 4, ?)。这种特性可提升性能,并支持表示无限序列。

Vavr Stream 不可变,可以是 EmptyCons(包含头部元素和惰性计算的尾部流)。与 List 不同,Stream 仅在内存中保留头部元素,尾部按需计算。

Stream<Integer> 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 8 Stream API 不同,Vavr Stream 是存储元素序列的数据结构,支持 get()append()insert() 等操作。

tabulate() 生成应用函数的流:

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

zip() 合并两个流:

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

Stream<Tuple2<Integer, Integer>> s2 = s.zip(List.of(7,8,9));
Tuple2<Integer, Integer> t1 = s2.get(0);
 
assertEquals(t1._1().intValue(), 2);
assertEquals(t1._2().intValue(), 7);

4.4. Array

Array 是不可变、可索引的序列,支持高效随机访问,由 Java 对象数组支持。本质上是 T 类型对象数组的 Traversable 包装器。

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

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

Array<Integer> rArray3 = Array.rangeClosedBy(1,6,2);
assertEquals(rArray3.size(), 3);
  • range():生成 [start, end-1] 范围
  • rangeClosed():生成 [start, end] 范围
  • rangeClosedBy():带步长的范围生成

✅ 按索引操作:

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

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

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

4.5. Vector

Vector 是介于 ArrayList 之间的数据结构,提供常量时间的随机访问和修改:

Vector<Integer> intVector = Vector.range(1, 5);
Vector<Integer> 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 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 的核心特性是不允许重复值,但实现方式不同:

  • HashSet:基础实现
  • TreeSet:可排序且无重复
  • LinkedHashSet:维护插入顺序

5.1. HashSet

HashSet 通过静态工厂方法创建实例(如 of()ofAll()range() 变体):

HashSet<Integer> set0 = HashSet.rangeClosed(1,5);
HashSet<Integer> 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));
  • diff():集合差集
  • union():并集
  • intersect():交集

✅ 基本操作:

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

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

⚠️ HashSet哈希数组映射字典树(HAMT) 支持,性能优于普通 HashTable,适合持久化集合。

5.2. TreeSet

不可变 TreeSetSortedSet 的实现,使用二叉搜索树存储有序元素,所有操作时间复杂度为 O(log n)。默认按自然顺序排序:

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

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

✅ 自定义排序:

SortedSet<String> 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 实现,继承自 SortedSet。通过 BitSet.Builder 的静态方法实例化,不允许重复值:

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

⚠️ 与标准库的 java.util.BitSet 不同,Vavr BitSet 不能包含 String 值。

6. Map

Map 是键值对数据结构。Vavr 的 Map 不可变,提供 HashMapTreeMapLinkedHashMap 实现。键不允许重复,但值可以重复。

6.1. HashMap

HashMap 是不可变 Map 的实现,使用键的哈希码存储键值对。Vavr Map 使用 Tuple2 而非传统 Entry 表示键值对:

Map<Integer, List<Integer>> 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 支持,几乎所有操作都是常量时间。

✅ 按键或值过滤:

Map<String, String> map1
  = HashMap.of("key1", "val1", "key2", "val2", "key3", "val3");
        
Map<String, String> fMap
  = map1.filterKeys(k -> k.contains("1") || k.contains("2"));
assertFalse(fMap.containsKey("key3"));
        
Map<String, String> fMap2
  = map1.filterValues(v -> v.contains("3"));
assertEquals(fMap2.size(), 1);
assertTrue(fMap2.containsValue("val3"));

✅ 转换条目:

Map<String, Integer> 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

不可变 TreeMapSortedMap 的实现,使用 Comparator 自定义排序:

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

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

✅ 自定义排序:

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

⚠️ TreeMap 基于树实现,操作时间复杂度为 O(log n)。map.get(key) 返回包装值的 Option

7. 与 Java 互操作

集合 API 与 Java 集合框架完全互操作。

7.1. Java 转 Vavr

每个 Vavr 集合实现都有静态工厂方法 ofAll(),接受 java.util.Iterable 或 Java Stream

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

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

✅ 结合 Stream.collect() 使用 collector()

List<Integer> 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 接口提供 toJavaXXX() 方法转换类型:

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

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

✅ 使用 Java 8 Collectors

java.util.Set<Integer> javaSet = List.of(1, 2, 3)
  .collect(Collectors.toSet());
        
assertEquals(3, javaSet.size());
assertEquals(1, javaSet.toArray()[0]);

7.3. Java 集合视图

库提供集合视图,转换时性能更优。视图实现标准 Java 接口,将方法调用委托给底层 Vavr 集合。目前仅支持 List 视图,分为不可变和可变视图。

❌ 不可变视图的修改操作抛出 UnsupportedOperationException

@Test(expected = UnsupportedOperationException.class)
public void givenVavrList_whenViewConverted_thenException() {
    java.util.List<Integer> javaList = List.of(1, 2, 3)
      .asJava();
    
    assertEquals(3, javaList.get(2).intValue());
    javaList.add(4);
}

✅ 创建可变视图:

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

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

8. 总结

本文介绍了 Vavr 集合 API 提供的各种函数式数据结构。⚠️ 注意,库还定义了 TryOptionEitherFuture,它们扩展了 Value 接口并实现 Java 的 Iterable 接口,因此在某些情况下可表现为集合。

本文所有示例的完整源代码可在 GitHub 获取。


原始标题:Guide to Collections API in Vavr

« 上一篇: Java Weekly, 第193期
» 下一篇: JDeferred 实战指南